摘要:不像其他屬性,描述符在類級別上創建。當所有者類被定義時,每個描述符對象都是被綁定到一個不同的類級別屬性的描述符類實例。這必須返回描述符的值。此外,描述符對有一個方便的響應和請求格式。
__getattribute__()方法注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python
__getattribute__()方法是一個更底層的屬性處理。它的默認實現試圖把一個屬性作為一個已經存在于內部__dict__(或__slots__)的屬性來定位值。如果沒有找到該屬性,它會調用__getattr__()。如果值被定位為描述符(參見下面《創建描述符》部分),則處理描述符;否則只是簡單的返回值。
通過重寫此方法,我們可以完成以下任何一個任務:
我們可以有效地防止對屬性的訪問。這種方法通過拋出異常來代替返回一個值,可以使一個屬性比我們僅僅使用下劃線(_)將一個命名標記為私有更私密。
我們可以發明新的屬性,類似于__getattr__()如何發明新的屬性。然而,在這種情況下,我們可以通過默認版本的__getattribute__()來繞過默認查找。
我們可以讓屬性執行唯一且不同的任務。這會使得程序非常難以理解和維護。這是一個糟糕的想法。
我們可以改變描述符的行為。雖然技術上可能,但改變一個描述符的行為是一個可怕的想法。
在我們實現__getattribute__()方法時,重要的是要注意在方法體中不能有任何的內部屬性訪問。如果我們試圖通過self.name獲取值,將導致無限遞歸。
__getattribute__()方法不能提供任何簡單的self.name屬性訪問,這將導致無限遞歸。
為了在__getattribute__()方法中獲取屬性值,我們必須顯式地訪問object定義的基礎方法,如以下所示聲明:
object.__getattribute__(self, name)
例如,我們可以使用__getattribute__()修改我們的不可變類以及防止訪問內部__dict__屬性。下面這個類,隱藏了所有以下劃線(_)開頭的命名:
class BlackJackCard3: """Abstract Superclass""" def __init__(self, rank, suit, hard, soft): super().__setattr__("rank", rank) super().__setattr__("suit", suit) super().__setattr__("hard", hard) super().__setattr__("soft", soft) def __setattr__(self, name, value): if name in self.__dict__: raise AttributeError("Cannot set {name}".format(name=name)) raise AttributeError(""{__class__.__name__}" has no attribute "{name}"".format(__class__= self.__class__, name= name)) def __getattribute__(self, name): if name.startswith("_"): raise AttributeError return object.__getattribute__(self, name)
我們已經覆寫了__getattribute__()的私有名稱以及Python內部名稱來拋出一個屬性錯誤。這前面的示例有一個微小的優勢:我們不再允許調整對象。我們將會看到該類的實例交互的示例。
下面示例是該類對象的變形:
>>> c = BlackJackCard3("A", "?", 1, 11) >>> c.rank = 12 Traceback (most recent call last): File "", line 1, in File " ", line 9, in __setattr__ File " ", line 13, in __getattribute__ AttributeError >>> c.__dict__["rank"]= 12 Traceback (most recent call last): File " ", line 1, in File " ", line 13, in __getattribute__ AttributeError
通常建議,搞混__getattribute__()不是一個好主意。默認方法相當復雜,而且幾乎所有我們需要的都是作為特性利用或作為__getattr__()的改變。
創建描述符描述符是調和屬性訪問的一個類。描述符類可用來獲取、設置或刪除屬性值。描述符對象是在類定義的時候構建在一個類中的。
描述符設計模式有兩個部分:一個所有者類和屬性描述符本身。所有者類給它的屬性使用一個或多個描述符。描述符類定義了獲取、設置和刪除方法的組合。描述符類的一個實例將會是所有者類的一個屬性。
特性是基于所有者類的方法函數。描述符不像特性,是一個類的實例,與所有者類不同。因此,描述符通常是可重用的通用屬性。所有者類可以有多個不同描述符類的實例來類管理具有相似行為的屬性。
不像其他屬性,描述符在類級別上創建。它們不是在__init()__初始化時創建。然而描述符的值可以在初始化期間設置,描述符通常是作為類的一部分,在任何方法函數之外來構建的。
當所有者類被定義時,每個描述符對象都是被綁定到一個不同的類級別屬性的描述符類實例。
被確認為一個描述符,一個類必須實現以下三個方法的任意組合。
Descriptor.__get__(self, instance, owner) -> object:在這個方法中,instance參數是即將被訪問的對象的self變量。owner參數是所有者類的對象。如果這個描述符在類的上下文中被調用,instance參數將得到一個None值。這必須返回描述符的值。
Descriptor.__set__(self, instance, value):在這個方法中,instance參數是即將被訪問的對象的self變量。value參數是描述符需要設置的新值。
Descriptor.__delete__(self, instance)在這個方法中,instance參數是即將被訪問的對象的self變量。該描述符的方法必須刪除這個屬性的值。
有時,一個描述符類還將需要一個__init__()方法函數來初始化描述符的內部狀態。
有兩種基于已定義方法的描述符,如下所示:
非數據描述符:這種描述符定義__set__()或__delete__()或兩者皆有。它不能定義__get__()。非數據描述符對象往往會被用作表達式的一部分。它可能是一個可調用對象,或者它可能有自己的屬性或方法。一個不可變的非數據描述符必須實現__set__(),但可能只是拋出AttributeError。這些描述符設計時很簡單,因為接口更靈活。
數據描述符:這種描述符至少定義__get__()。通常,它定義__get__()和__set__()來創建一個可變對象。鑒于描述符將在很大程度上是不可見的,則不能更進一步的再定義屬性或方法。屬性的引用有一個數據描述符的數據被委托給描述符的__get__()、__set__()或__delete__()方法。這些是很難設計的,所以我們稍后來再看。
描述符有各種各樣的用例。在內部,Python使用描述符有以下幾個原因:
在隱藏的內部,類的方法是作為描述符來實現。這些非數據描述符應用方法函數到對象以及不同的參數值。
property()函數通過給一個字段創建數據描述符來實現。
一個類方法或靜態方法被實現為一個描述符;這被應用到類中來代替類的實例。
當我們在第11章《通過SQLite存儲和檢索對象》看到對象-關系映射的時候,我們將看到許多ORM類定義大量使用描述符將Python類映射到SQL表和列。
當我們考慮一個描述符的目的,我們還必須為數據作為描述符可以正常工作來考察三種常見用例,如下所示:
描述符對象有數據或獲取到了數據。在這種情況下,描述符對象的self變量是有意義的且描述符是有狀態的。數據描述符的__get__()方法返回這個內部數據。非數據描述符,描述符有其他方法或屬性來訪問這些數據。
包含數據的所有者實例。在這種情況下,描述符對象必須使用instance參數來引用值到所有者對象中。數據描述符的__get__()方法從實例獲取數據。非數據描述符有其他方法訪問實例數據。
包含相關數據的所有者類。在這種情況下,描述符對象必須使用owner參數。這是常用的當描述符實現了應用于整個類的靜態方法或類方法。
我們將仔細看下第一種情況。我們看看創建帶有__get__()和__set__()方法的數據描述符。我們也會看看創建沒有__get__()方法的非數據描述符。
第二種情況(所有者實例中的數據)展示了@property裝飾器都做了些什么。可能的優勢是描述符有一個傳統的特性將計算從擁有者類移到描述符類中。這傾向于分片類設計且可能不是最好的方法。如果計算是真正史詩般的復雜,策略模式可能會更好。
第三種情況展示@staticmethod和@classmethod裝飾器是如何實現的。我們不需要重新發明輪子。
1、使用非數據描述符我們經常會有一些緊密綁定了屬性值的小對象。對于這個示例,我們將看看數值被綁定到單位的舉措。
下面是一個簡單的非數據描述符類,它缺少一個__get__()方法:
class UnitValue_1: """Measure and Unit combined.""" def __init__(self, unit): self.value = None self.unit = unit self.default_format = "5.2f" def __set__(self, instance, value): self.value = value def __str__(self): return "{value:{spec}} {unit}" .format(spec=self.default_format, **self.__dict__) def __format__(self, spec="5.2f"): #print( "formatting", spec ) if spec == "": spec = self.default_format return "{value:{spec}} {unit}".format(spec=spec, **self.__dict__)
這個類定義了一對簡單的值,一個可變的(值),另一個是有效的不可變對象(單位)。
當這個描述符被訪問時,描述符對象本身是可用的,且描述符的其他方法或屬性可以被使用。我們可以使用這個描述符來創建類去管理尺寸和其他與物理單位有關的數值。
下面是一個類,做速度-時間-距離的及早計算:
class RTD_1: rate = UnitValue_1("kt") time = UnitValue_1("hr") distance = UnitValue_1("nm") def __init__(self, rate=None, time=None, distance=None): if rate is None: self.time = time self.distance = distance self.rate = distance / time if time is None: self.rate = rate self.distance = distance self.time = distance / rate if distance is None: self.rate = rate self.time = time self.distance = rate * time def __str__(self): return "rate: {0.rate} time: {0.time} distance:{0.distance}".format(self)
一旦對象被創建且屬性被加載,丟失的值就已經被計算。一旦計算,描述符可以檢查獲取值或單位的名稱。此外,描述符對str()有一個方便的響應和請求格式。
下面是描述符和RTD_1類之間的交互:
>>> m1 = RTD_1(rate=5.8, distance=12) >>> str(m1) "rate: 5.80 kt time: 2.07 hr distance: 12.00 nm" >>> print("Time:", m1.time.value, m1.time.unit) Time: 2.0689655172413794 hr
我們創建了一個帶有rate和distance參數的RTD_1實例。這些都是用來計算rate和distance描述符的__set__()方法。
當我們請求str(m1),這會計算RTD_1的所有str()方法,轉而使用rate、time和distance描述符的__format__()方法。這為我們提供了數字和單位。
鑒于非數據描述符沒有__get__()且不返回其內部值,我們可以訪問描述符的單個元素。
2、使用數據描述符數據描述符設計要復雜一些,因為它對接口有限制。它必須有一個__get__()方法,且只能有__set__()或__delete__()。這是所有的接口:這些方法從一到三,沒有其他方法。引入一個額外的方法意味著Python不會把該類當作一個正確的數據描述符。
我們會使用描述符設計一個簡單的單位轉換模式,可以在__get__()和__set__()方法做適當的轉換。
下面是一個單位描述符的超類,它在其他單位和標準單位之間做轉換:
class Unit: conversion = 1.0 def __get__(self, instance, owner): return instance.kph * self.conversion def __set__(self, instance, value): instance.kph = value / self.conversion
該類用簡單的乘法和除法將標準單位轉換為其他非標準單位,反之亦然。
通過這個超類,我們可以從一個標準單位定義一些轉換。在前面的示例,標準單位是千米時(公里/小時)。
以下是這兩個轉換描述符
class Knots(Unit): conversion = 0.5399568 class MPH(Unit): conversion = 0.62137119
繼承方法非常有用。唯一改變的是轉換因子。這些類可用于處理涉及單位轉換的值。我們可以處理英里每小時或可交換的節點。下面是一個標準單位的單位描述符,公里每小時:
class KPH(Unit): def __get__(self, instance, owner): return instance._kph def __set__(self, instance, value): instance._kph = value
這個類代表一個標準,所以不做任何轉換。它使用一個私有變量實例保存速度千米每小時的標準值。避免任何算術轉換是一個簡單的技術優化。避免任何一個公共字段的引用是至關重要的,來規避無限遞歸。
下面這個類,它對于一個給定的尺寸提供了一組轉換:
class Measurement: kph = KPH() knots = Knots() mph = MPH() def __init__(self, kph=None, mph=None, knots=None): if kph: self.kph = kph elif mph: self.mph = mph elif knots: self.knots = knots else: raise TypeError def __str__(self): return "rate: {0.kph} kph = {0.mph} mph = {0.knots} knots".format(self)
對于不同的單位每個類級別的屬性都是描述符。各種描述符的獲取和設置方法會做適當的轉換。我們可以使用這個類在各種單位之間進行速度轉換。
以下是與Measurement類交互的一個例子:
>>> m2 = Measurement(knots=5.9) >>> str(m2) "rate: 10.92680006993152 kph = 6.789598762345432 mph = 5.9 knots" >>> m2.kph 10.92680006993152 >>> m2.mph 6.789598762345432
我們通過設置不同的描述符創建了一個Measurement類的對象。在第一個示例中,我們設置了節點描述符。
當我們顯示的值是一個大字符串,則每個描述符的__get__()都將被使用。這些方法從所有者對象獲取內部kph字段值,應用一個轉換因子,且返回一個結果值。
kph字段還使用了一個描述符。這個描述符不做任何轉換;然而,它只是返回了緩存在所有者對象的私有值。KPH和Knots描述符要求所有者類實現一個kph屬性。
總結,設計要素和權衡在這一章,我們研究了幾種使用一個對象屬性的方式。我們可以使用內置object類的特性以及獲取和設置屬性值。我們可以定義屬性來修改屬性的行為。
如果我們想要更復雜,我們可以調整底層__getattr__()、__setattr__()、__delattr__()或__getattribute__()特殊方法實現。這些讓我們可以更精細的控制字段的行為。當我們接觸到這些方法我們走的很順利,因為我們可以對Python的行為進行基本(和令人困惑的)改變。
在內部,Python使用描述符來實現特性,例如方法函數、靜態方法函數和屬性。描述符許多很酷的用例已經是語言的最好的特性。
來自其他語言(特別是Java和c++)的程序員通常有試圖讓所有屬性私有以及編寫大量的getter和setter函數的沖動。
在Python中,將所有屬性作當作公有的是相當簡單的。這意味著如下:
它們應該有良好的文檔記錄。
它們應該正確的反映對象的狀態,它們不應該是暫時的或臨時的值。
一個字段有令人困惑的(或易變的)字段值是非常罕見的,一個以下劃線(_)開頭的命名作為“不是已定義接口中的一部分”不是真的私有。
把私有字段看做麻煩事是很重要的。在語言中封裝并沒有因為缺乏復雜的私有機制而受損;而會因為糟糕的設計而受損。
1、特性對屬性在大多數情況下,字段可以設置在類之外且沒有不良后果。我們的Hand類的示例展示了這一點。對于許多版本的類,我們可以簡單地追加到hand.cards,以及完美的工作通過特性延遲計算total。
在改變屬性這種情況下,會導致相應的其他字段的變化,這需要一些更復雜的類設計:
一個方法函數可以闡明狀態變化。當需要多個參數值時這將是必要的。
一個特性setter可能比一個方法函數更清晰。當需要單個值時這將是一個明智的選擇。
我們還可以使用原地操作符。我們將到第七章《創造數字》看到這些。
沒有嚴格的規則。在這種情況下,我們需要設置一個參數值,方法函數和特性之間的區別完全是API語法和如何傳達意圖的區別。
為了計算值,特性允許延遲計算,而一個屬性需要及早計算。這是性能的問題。延遲計算對及早計算的優勢是基于對用例的預期。
2、使用描述符設計很多描述符的示例已經是Python的一部分。我們不需要重復特性、類方法或靜態方法。
創建新的描述符最引人注目的情況是在Python和一些非Python之間建立映射關系。例如,對象-關系數據庫映射,需要大量的維護以確保一個Python類有正確的屬性以正確的順序匹配一個SQL表和列。同樣,當映射到一些Python之外,一個描述符類可以處理數據編碼和解碼或從外部獲取數據來源。
當構建一個web服務端,我們可能會考慮使用描述符來做web服務請求。例如,__get__()方法可能會變成一個HTTP的GET請求,__set__()方法可能會變成一個HTTP的PUT請求。
在某些情況下,一個單一的請求可能填充幾個描述符的數據。在這種情況下,__get__()方法將檢查實例的緩存以及在一個HTTP請求之前返回一個值。
通過屬性來操作許多數據描述符操作會非常簡單。這給我們提供開始的機會:首先編寫屬性。如果屬性處理變得非常昂貴和復雜,則我們可以切換到描述符來對類進行重構。
3、展望未來在下一章,我們將密切關注將在5、6、7章探索的ABCs(抽象基類)。這些基礎知識將幫助我們定義類,它會與現有Python特性很好地集成。它們還將允許我們創建執行一致的設計和擴展的類層次結構。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/44197.html
摘要:許多程序員發現賦值語句比方法函數看起來更清晰。自從和屬性的創建來自,我們必須經常定義特性使用如下代碼這允許我們用一條簡單的語句添加一張牌到手中像下面這樣前面的賦值語句有一個缺點,因為它看起來像一張牌替代了所有的牌。 注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python 對象就是一些特性的集合,包括方法和屬性。object...
摘要:第一是在對象生命周期中初始化是最重要的一步每個對象必須正確初始化后才能正常工作。第二是參數值可以有多種形式。基類對象的方法對象生命周期的基礎是它的創建初始化和銷毀。在某些情況下,這種默認行為是可以接受的。 注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python __init__()方法意義重大的原因有兩個。第一是在對象生命...
摘要:所以搞清楚是理解對象屬性描述符的唯一途徑。是一個對象,對象里的屬性描述符有兩種類型數據描述符和存取描述符。描述符必須是這兩種形式之一不能同時是兩者。描述符中未顯示設置的特性使用其默認值。創建一個新屬性默認描述符的鍵值都是或者。 對象屬性描述符 當別人對你提及對象屬性描述符,可能會蒙逼。而如果提及對象屬性的 get/set 方法就秒懂了,標準描述和習慣表述在這里有些差別,但是指向的是同一...
閱讀 654·2021-11-15 11:39
閱讀 2890·2021-10-08 10:04
閱讀 3252·2019-08-30 10:57
閱讀 3014·2019-08-26 13:25
閱讀 1896·2019-08-26 12:14
閱讀 2626·2019-08-23 15:27
閱讀 2988·2019-08-23 15:18
閱讀 1766·2019-08-23 14:26