統一 Python 2.2 中的型別和類
統一 Python 2.2 中的型別和類
Python 版本:2.2
(有關本教程的較新版本,請參閱 Python 2.2.3)
Guido van Rossum
本文件是不完整的草案。我正在徵求反饋。如果您發現任何問題,請寫信給我:guido@python.org。
目錄
- 簡介
- 子類化內建型別
- 內建型別作為工廠函式
- 內省內建型別的例項
- 靜態方法和類方法
- 屬性:由 get/set 方法定義的屬性
- 方法解析順序
- 協作方法和 "super"
- 重寫 __new__ 方法
- 元類
- 向後不相容
- 參考文獻
簡介
Python 2.2 引入了“型別/類統一”的第一階段。 這是對 Python 進行的一系列更改,旨在消除內建型別和使用者定義的類之間的大部分差異。 最明顯的一個是限制在類語句中將內建型別(例如列表和字典的型別)用作基類。
這是對 Python 的最大更改之一,但它可以在很少的向後不相容的情況下完成。 這些更改在PEP(Python 增強提案)中進行了詳細描述。 PEP 不是為教程而設計的,並且描述型別/類統一的 PEP 有時很難閱讀。 它們也尚未完成。 這就是本文的作用所在:它為普通 Python 程式設計師介紹了型別/類統一的關鍵要素。
一些術語:“經典 Python”是指 Python 2.1(及其補丁版本,例如 2.1.1)或更早版本,而“經典類”是指使用 class 語句定義的類,該類在其基類中沒有內建物件:要麼因為它沒有基類,要麼因為其所有基類本身都是經典類 - 遞迴地應用定義。
經典類在 Python 2.2 中仍然是一個特殊的類別。 它們最終將與型別完全統一,但由於額外的向後不相容性,這將在 2.2 釋出之後完成(可能在 Python 3.0 之前不會)。 當我指的是內建型別時,我將嘗試說“型別”,而當我指的是經典類或可能是任何一個的類時,我將說“類”; 如果從上下文中不清楚應該使用哪個解釋,我將嘗試明確,使用“經典類”或“類或型別”。
子類化內建型別
讓我們從最精彩的部分開始:您可以子型別化內建型別,例如字典和列表。 您只需要一個作為內建型別的基類的名稱,您就可以開始了。
有一個新的內建名稱“dict”,用於表示字典的型別。(在 2.2b1 及更早版本中,這稱為“dictionary”;雖然總的來說我不喜歡縮寫,“dictionary”打字太長了,我們多年來一直在說“dict”。)
這實際上只是一種語法糖,因為已經有兩種其他方式來命名此型別:type({}) 和(在匯入 types 模組之後)types.DictType(以及第三種,types.DictionaryType)。 但是現在型別扮演著更核心的角色,為可能遇到的型別使用內建名稱似乎是合適的。
這是一個簡單的 dict 子類的示例,它提供了一個“預設值”,在請求缺少的鍵時返回該值
class defaultdict(dict): def __init__(self, default=None): dict.__init__(self) self.default = default def __getitem__(self, key): try: return dict.__getitem__(self, key) except KeyError: return self.default
此示例顯示了一些內容。 __init__() 方法擴充套件了 dict.__init__() 方法。 與 __init__() 方法一樣,它的引數列表與基類 __init__() 方法不同。 同樣,__getitem__() 方法擴充套件了基類 __getitem__() 方法。
__getitem__() 方法也可以如下編寫,使用 Python 2.2 中引入的新的“key in dict”測試
def __getitem__(self, key): if key in self: return dict.__getitem__(self, key) else: return self.default
我認為這個版本效率較低,因為它進行了兩次鍵查詢。 例外情況是,當我們預計所請求的鍵幾乎不會在字典中時:那麼設定 try/except 語句比失敗的“key in self”測試更昂貴。
為了完整起見,get() 方法也應該擴充套件,使其使用與 __getitem__() 相同的預設值
def get(self, key, *args): if not args: args = (self.default,) return dict.get(self, key, *args)
(儘管此函式是使用可變長度引數列表宣告的,但實際上應該只使用一個或兩個引數呼叫;如果傳遞了更多引數,則基類方法呼叫將引發 TypeError 異常。)
我們不限於擴充套件基類上定義的方法。 這是一個有用的方法,它執行類似於 update() 的操作,但如果鍵在兩個字典中都存在,則保留現有值而不是用新值覆蓋它們
def merge(self, other): for key in other: if key not in self: self[key] = other[key]
這使用了新的“key not in dict”測試以及新的“for key in dict:”,可以高效地(無需複製鍵列表)迭代字典中的所有鍵。 它不要求其他引數是 defaultdict 甚至是字典:任何支援“for key in other”和 other[key] 的對映物件都可以。
這是新型別的工作原理
>>> print defaultdict # show our type <class '__main__.defaultdict'> >>> print type(defaultdict) # its metatype <type 'type'> >>> a = defaultdict(default=0.0) # create an instance >>> print a # show the instance {} >>> print type(a) # show its type <class '__main__.defaultdict'> >>> print a.__class__ # show its class <class '__main__.defaultdict'> >>> print type(a) is a.__class__ # its type is its class 1 >>> a[1] = 3.25 # modify the instance >>> print a # show the new value {1: 3.25} >>> print a[1] # show the new item 3.25 >>> print a[0] # a non-existant item 0.0 >>> a.merge({1:100, 2:200}) # use a dictionary method >>> print a # show the result {1: 3.25, 2: 200} >>>
我們還可以將新型別用於經典型別僅允許“真實”字典的上下文,例如 exec 語句的 locals/globals 字典或內建函式 eval()
>>> print a.keys() [1, 2] >>> exec "x = 3; print x" in a 3 >>> print a.keys() ['__builtins__', 1, 2, 'x'] >>> print a['x'] 3 >>>
但是,我們的 __getitem__() 方法不會被直譯器用於變數訪問
>>> exec "print foo" in a Traceback (most recent call last): File "<stdin>", line 1, in ? File "<string>", line 1, in ? NameError: name 'foo' is not defined >>>
為什麼不列印 0.0? 直譯器使用內部函式訪問字典,該函式繞過了我們的 __getitem__() 重寫。 我承認這可能是一個問題(儘管這僅在這種上下文中是一個問題,即當 dict 子類用作 locals/globals 字典時); 我是否可以在不影響常見情況下的效能的情況下修復此問題還有待觀察。
現在我們將看到 defaultdict 例項具有動態例項變數,就像經典類一樣
>>> a.default = -1 >>> print a["noway"] -1 >>> a.default = -1000 >>> print a["noway"] -1000 >>> print a.__dict__.keys() ['default'] >>> a.x1 = 100 >>> a.x2 = 200 >>> print a.x1 100 >>> print a.__dict__.keys() ['default', 'x2', 'x1'] >>> print a.__dict__ {'default': -1000, 'x2': 200, 'x1': 100} >>>
這並不總是您想要的; 特別是,與使用常規字典相比,使用單獨的字典來儲存單個例項變數會使 defaultdict 例項使用的記憶體增加一倍! 有一種方法可以避免這種情況
class defaultdict2(dict): __slots__ = ['default'] def __init__(self, default=None): ...(like before)...
__slots__ 宣告採用例項變數列表,並在例項中為這些變數精確地保留空間。 使用 __slots__ 時,無法為其他例項變數賦值
>>> a = defaultdict2(default=0.0) >>> a[1] 0.0 >>> a.default = -1 >>> a[1] -1 >>> a.x1 = 1 Traceback (most recent call last): File "<stdin>", line 1, in ? AttributeError: 'defaultdict2' object has no attribute 'x1' >>>
關於 __slots__ 的一些值得注意的花絮和警告
- 未定義的槽變數將按預期引發 AttributeError。(請注意,在 Python 2.2b2 和更早版本中,槽變數預設值為 None,“刪除”它們會恢復此預設值。)
- 您不能使用類屬性來為由 __slots__ 定義的例項變數定義預設值; __slots__ 宣告為每個包含描述符的變數建立一個類屬性,並將類屬性設定為預設值將覆蓋此描述符。
- 沒有檢查可以阻止您使用 __slots__ 宣告覆蓋基類已定義的例項變數。 如果您這樣做,則基類定義的例項變數將不可訪問(除非直接從基類檢索其描述符;這可以用來重新命名它)。 這樣做會使程式的含義不確定; 將來可能會新增一個檢查以防止這種情況。
- 使用 __slots__ 的類的例項沒有 __dict__(除非基類定義了 __dict__); 但是它的派生類的例項確實有 __dict__,除非它們的類也使用 __slots__。
- 您可以透過使用 __slots__ = [] 定義一個沒有例項變數且沒有 __dict__ 的物件。
- 您不能將槽與“可變長度”內建型別用作基類。 可變長度的內建型別是 long、str 和 tuple。
- 使用 __slots__ 的類不支援對其例項的弱引用,除非 __slots__ 列表中的字串之一等於 "__weakref__"。(嗯,此功能可以擴充套件到 "__dict__" ...)
- __slots__ 變數不必是列表; 任何可以迭代的非字串都可以,並且迭代返回的值用作槽名稱。 特別是,可以使用字典。 您還可以使用單個字串來宣告單個槽。 但是,將來可能會為使用字典分配額外的含義,例如,字典值可用於限制例項變數的型別或提供文件字串; 使用不是列表的內容的效果會使程式的含義不確定。
請注意,雖然通常運算子過載的工作方式與經典類一樣,但仍存在一些差異。(最大的一個是沒有對 __coerce__ 的支援;新式類應始終使用新式數值 API,該 API 將未強制轉換的其他運算元傳遞給 __add__ 和 __radd__ 方法等。)
有一種新的覆蓋屬性訪問的方式。 __getattr__ 鉤子(如果定義)的工作方式與經典類相同:僅當搜尋屬性的常規方式未找到它時才呼叫它。 但是您現在也可以覆蓋 __getattribute__,這是一個新的操作,對於所有屬性引用都會呼叫它。
覆蓋 __getattribute__ 時,請記住很容易導致無限遞迴:每當 __getattribute__ 引用 self 的屬性(甚至是 self.__dict__!)時,都會遞迴呼叫它。(這類似於 __setattr__,它會被呼叫以進行所有屬性賦值;當 __getattr__ 被粗心編寫並引用 self 的不存在的屬性時,它也可能會遇到這種情況。)
在 `__getattribute__` 中從 `self` 獲取任何屬性的正確方法是呼叫基類的 `__getattribute__` 方法,就像任何覆蓋基類方法的方法可以呼叫基類方法一樣:`Base.__getattribute__(self, name)`。(如果您想在多重繼承的世界中保持正確,請參閱下面關於 super() 的討論。)
這是一個覆蓋 `__getattribute__` 的示例(實際上是擴充套件它,因為覆蓋方法會呼叫基類方法)
class C(object): def __getattribute__(self, name): print "accessing %r.%s" % (self, name) return object.__getattribute__(self, name)
關於 `__setattr__` 的說明:有時屬性不會儲存在 `self.__dict__` 中(例如,當使用 `__slots__` 或屬性,或使用內建基類時)。與 `__getattribute__` 相同的模式適用,您呼叫基類的 `__setattr__` 來完成實際工作。這是一個例子
class C(object): def __setattr__(self, name, value): if hasattr(self, name): raise AttributeError, "attributes are write-once" object.__setattr__(self, name, value)
C++ 程式設計師可能會發現,Python 中的這種子型別形式的實現方式與 C++ 中的單繼承子類非常相似,其中 `__class__` 的作用類似於虛擬函式表。
還有很多內容可以解釋(例如 `__metaclass__` 宣告和 `__new__` 方法),但其中大部分內容都相當深奧。如果您有興趣,請參閱下面的 __new__。
我將以一些注意事項作為結尾
- 您可以使用多重繼承,但不能從不同的內建型別多重繼承(例如,您不能建立既繼承自內建 `dict` 型別又繼承自 `list` 型別的型別)。這是一個永久性的限制;要取消這個限制,需要對 Python 的物件實現進行太多更改。但是,您可以透過繼承 "object" 來建立 mix-in 類。這是一個新的內建型別,命名了新系統下所有內建型別的無特徵基型別。
- 使用多重繼承時,您可以在基類列表中混合經典類和內建型別(或從內建型別派生的型別)。(這是 Python 2.2b2 中的新特性;在早期版本中您無法做到這一點。)
- 另請參閱 2.2 中的錯誤列表。
內建型別作為工廠函式
上一節表明,可以透過呼叫 `defaultdict()` 來建立內建子型別 `defaultdict` 的例項。這是預期的,因為這也適用於經典類。但這是一個新特性:內建基型別本身也可以透過直接呼叫該型別來例項化。
對於一些內建型別,在經典的 Python 中已經存在以型別命名的工廠函式,例如 `str()` 和 `int()`。我已經更改了這些內建函式,使它們現在成為相應型別的名稱。雖然這會將這些名稱的型別從內建函式更改為內建型別,但我預計這不會造成向後相容性問題:我已經確保這些型別可以使用與以前的函式完全相同的引數列表來呼叫。(它們通常也可以在不帶引數的情況下呼叫,生成一個具有適當預設值的物件,例如零或空;這是一個新特性。)
這些是受影響的內建型別
- int([數字或字串[, 基數]])
- long([數字或字串])
- float([數字或字串])
- complex([數字或字串[, 虛數]])
- str([物件])
- unicode([字串[, 編碼字串]])
- tuple([可迭代物件])
- list([可迭代物件])
- type(物件) 或 type(名稱字串, 基類元組, 方法字典)
`type()` 的簽名需要解釋:傳統上,`type(x)` 返回物件 `x` 的型別,並且仍然支援此用法。但是,`type(name, bases, methods)` 是一種新的用法,它會建立一個全新的型別物件。(這涉及到 元類程式設計,我不會在這裡進一步討論,只是指出這個簽名與 Don Beaudry 的著名元類鉤子使用的簽名相同。)
還有一些新的內建函式遵循相同的模式。這些已在上面描述或將在下面描述
- dict([對映或可迭代物件]) - 返回一個新的字典;可選引數必須是複製其項的對映,或者是一個長度為 2 的 2 元組序列,給出要插入到新字典中的(鍵,值)對
- object([...]) - 返回一個新的無特徵物件;引數將被忽略
- classmethod(函式) - 請參閱下面的 staticmethod
- staticmethod(函式) - 請參閱下面的 staticmethod
- super(類或型別[, 例項]) - 請參閱下面的 super()
- property([fget[, fset[, fdel[, doc]]]]) - 請參閱下面的 property
此更改的目的是雙重的。首先,這使得在類語句中方便地使用這些型別中的任何一個作為基類。其次,它使得測試特定型別變得更容易一些:與其編寫 `type(x) is type(0)`,您現在可以編寫 `isinstance(x, int)`。
這讓我想起來了。`isinstance()` 的第二個引數現在可以是類或型別的元組。例如,當 `x` 是 `int` 或 `long` 時(或者當是這些型別的子類的例項時),`isinstance(x, (int, long))` 返回 true,並且類似地,`isinstance(x, (str, unicode))` 測試是否是兩種字串中的任何一種。我們沒有對 `isclass()` 執行此操作。
內省內建型別的例項
對於內建型別的例項(以及通常的新式類),`x.__class__` 現在與 `type(x)` 相同
>>> type([]) <type 'list'> >>> [].__class__ <type 'list'> >>> list <type 'list'> >>> isinstance([], list) 1 >>> isinstance([], dict) 0 >>> isinstance([], object) 1 >>>
在經典的 Python 中,列表的方法名稱作為列表物件的 `__methods__` 屬性可用,其效果與使用內建的 `dir()` 函式相同
Python 2.1 (#30, Apr 18 2001, 00:47:18) [GCC egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)] on linux2 Type "copyright", "credits" or "license" for more information. >>> [].__methods__ ['append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort'] >>> >>> dir([]) ['append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
在新方案下,`__methods__` 屬性不再存在
Python 2.2c1 (#803, Dec 13 2001, 23:06:05) [GCC egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)] on linux2 Type "copyright", "credits" or "license" for more information. >>> [].__methods__ Traceback (most recent call last): File "<stdin>", line 1, in ? AttributeError: 'list' object has no attribute '__methods__' >>>
相反,您可以從 `dir()` 函式獲取相同的資訊,該函式提供更多資訊
>>> dir([]) ['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__eq__', '__ge__', '__getattribute__', '__getitem__', '__getslice__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__repr__', '__rmul__', '__setattr__', '__setitem__', '__setslice__', '__str__', 'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort'] >>>
新的 `dir()` 比舊的 `dir()` 提供更多資訊:除了例項變數和常規方法的名稱之外,它還顯示通常透過特殊符號呼叫的方法,例如 `__iadd__` (+=), `__len__` (len), `__ne__` (!=)。
更多關於新的 `dir()` 函式的資訊
- 對例項(經典或新式)呼叫 `dir()` 會顯示例項變數,以及例項的類及其所有基類定義的方法和類屬性。
- 對類(經典或新式)呼叫 `dir()` 會顯示類的 `__dict__` 及其所有基類的內容。它不顯示由元類定義的類屬性。
- 對模組呼叫 `dir()` 會顯示模組的 `__dict__` 的內容。(這沒有改變。)
- 不帶引數的 `dir()` 顯示呼叫者的區域性變數。(同樣,沒有改變。)
- 有一個新的 C API 實現了 `dir()` 函式:`PyObject_Dir()`。
- 還有更多細節;特別是,對於覆蓋 `__dict__` 或 `__class__` 的物件,這些將被遵守,並且為了向後相容,如果定義了 `__members__` 和 `__methods__`,也會被遵守。
您可以將內建型別的方法用作“未繫結的方法”
>>> a = ['tic', 'tac'] >>> list.__len__(a) # same as len(a) 2 >>> list.append(a, 'toe') # same as a.append('toe') >>> a ['tic', 'tac', 'toe'] >>>
這就像使用使用者定義類的未繫結方法一樣 - 類似地,它主要在子類方法內部有用,用於呼叫相應的基類方法。
與使用者定義的類不同,您不能更改內建型別:嘗試為內建型別分配屬性會引發 `TypeError`,並且它們的 `__dict__` 是隻讀代理物件。對於新式使用者定義的類(包括內建型別的子類),屬性賦值的限制被解除;但是,即使是那些類也有一個只讀的 `__dict__` 代理,您必須使用屬性賦值來替換或新增新式類的方法。示例會話
>>> list.append <method 'append' of 'list' objects> >>> list.append = list.append Traceback (most recent call last): File "<stdin>", line 1, in ? TypeError: can't set attributes of built-in/extension type 'list' >>> list.answer = 42 Traceback (most recent call last): File "<stdin>", line 1, in ? TypeError: can't set attributes of built-in/extension type 'list' >>> list.__dict__['append'] <method 'append' of 'list' objects> >>> list.__dict__['answer'] = 42 Traceback (most recent call last): File "<stdin>", line 1, in ? TypeError: object does not support item assignment >>> class L(list): ... pass ... >>> L.append = list.append >>> L.answer = 42 >>> L.__dict__['answer'] 42 >>> L.__dict__['answer'] = 42 Traceback (most recent call last): File "<stdin>", line 1, in ? TypeError: object does not support item assignment >>>
對於好奇的人:不允許更改內建類有兩個原因。首先,很容易破壞內建型別的一個在其他地方(無論是標準庫還是執行時程式碼)依賴的不變數。其次,當 Python 嵌入到另一個建立多個 Python 直譯器的應用程式中時,內建類物件(作為靜態分配的資料結構)在所有直譯器之間共享;因此,在一個直譯器中執行的程式碼可能會對另一個直譯器造成嚴重破壞,這是不允許的。
靜態方法和類方法
新的描述符 API 使得新增靜態方法和類方法成為可能。靜態方法很容易描述:它們的行為很像 C++ 或 Java 中的靜態方法。這是一個例子
class C: def foo(x, y): print "staticmethod", x, y foo = staticmethod(foo) C.foo(1, 2) c = C() c.foo(1, 2)
呼叫 `C.foo(1, 2)` 和呼叫 `c.foo(1, 2)` 都會使用兩個引數呼叫 `foo()`,並列印 "staticmethod 1 2"。在 `foo()` 的定義中沒有宣告 "self",並且在呼叫中不需要例項。如果使用例項,則僅用於查詢定義靜態方法的類。這適用於經典類和新類!
類語句中的 "foo = staticmethod(foo)" 這行是關鍵要素:這使得 `foo()` 成為靜態方法。內建的 `staticmethod()` 將其函式引數包裝在一種特殊的描述符中,該描述符的 `__get__()` 方法返回原始函式,不會發生更改。
更多關於 `__get__` 方法:在 Python 2.2 中,將方法繫結到例項的魔力(即使對於經典類!)是透過在類中找到的物件的 `__get__` 方法完成的。常規函式物件的 `__get__` 方法返回一個繫結方法物件;靜態函式物件的 `__get__` 方法返回底層函式。如果類屬性沒有 `__get__` 方法,則它永遠不會繫結到例項,換句話說,存在一個預設的 `__get__` 操作,該操作返回未更改的物件;這就是處理簡單類變數(例如數值)的方式。
類方法使用類似的模式來宣告接收一個隱式第一個引數的方法,該引數是它們被呼叫的 *類*。這沒有 C++ 或 Java 等價物,並且與 Smalltalk 中的類方法並不完全相同,但可能具有類似的目的。(Python 也有真正的 元類,並且在元類中定義的方法也許更有資格被稱為“類方法”;但我預計大多數程式設計師不會使用元類。)這是一個例子
class C: def foo(cls, y): print "classmethod", cls, y foo = classmethod(foo) C.foo(1) c = C() c.foo(1)
呼叫 `C.foo(1)` 和呼叫 `c.foo(1)` 最終都會使用 *兩個* 引數呼叫 `foo()`,並列印 "classmethod __main__.C 1"。`foo()` 的第一個引數是隱式的,它是類,即使該方法是透過例項呼叫的。現在讓我們繼續該示例
class D(C): pass D.foo(1) d = D() d.foo(1)
這會列印 "classmethod __main__.D 1" 兩次;換句話說,作為 `foo()` 的第一個引數傳遞的類是呼叫中涉及的類,而不是 `foo()` 定義中涉及的類。
但請注意這一點
class E(C): def foo(cls, y): # override C.foo print "E.foo() called" C.foo(y) foo = classmethod(foo) E.foo(1) e = E() e.foo(1)
在此示例中,從 `E.foo()` 呼叫 `C.foo()` 將會看到類 `C` 作為其第一個引數,而不是類 `E`。這是意料之中的,因為呼叫指定了類 `C`。但這強調了這些類方法與在 元類中定義的方法之間的區別,在元類中,對元方法的向上呼叫會將目標類作為顯式的第一個引數傳遞。(如果您不理解這一點,請不要擔心,您不是一個人。:-)
屬性:由 get/set 方法管理的屬性
屬性是一種簡潔的實現方式,用於實現那些使用方式類似於屬性訪問,但其實現使用方法呼叫的特性。這些有時被稱為“託管屬性”。在之前的 Python 版本中,你只能透過重寫 __getattr__ 和 __setattr__ 來實現這一點;但是重寫 __setattr__ 會顯著減慢所有屬性賦值的速度,而重寫 __getattr__ 總是有點難以正確實現。屬性讓你能夠輕鬆地完成這項工作,而無需重寫 __getattr__ 或 __setattr__。
我將首先展示一個示例。讓我們定義一個類,其中屬性 x 由一對方法 getx() 和 setx() 定義。
class C(object): def __init__(self): self.__x = 0 def getx(self): return self.__x def setx(self, x): if x < 0: x = 0 self.__x = x x = property(getx, setx)
這是一個小的演示。
>>> a = C() >>> a.x = 10 >>> print a.x 10 >>> a.x = -10 >>> print a.x 0 >>> a.setx(12) >>> print a.getx() 12 >>>
完整的簽名是 property(fget=None, fset=None, fdel=None, doc=None)。fget、fset 和 fdel 引數是在獲取、設定或刪除屬性時呼叫的方法。如果這三個中的任何一個未指定或為 None,則相應的操作將引發 AttributeError 異常。第四個引數是屬性的文件字串;它可以從類中檢索,如下例所示。
>>> class C(object): ... def getx(self): return 42 ... x = property(getx, doc="hello") ... >>> C.x.__doc__ 'hello' >>>
關於 property() 的注意事項(除了第一點之外,其他都是高階內容)
- 屬性不適用於經典類,但是當你嘗試這樣做時,你不會得到明確的錯誤。你的 get 方法將被呼叫,所以它看起來可以工作,但是在屬性賦值時,經典類例項將只是在它的 __dict__ 中設定值,而不會呼叫屬性的 set 方法,之後,也不會再呼叫屬性的 get 方法。(你可以重寫 __setattr__ 來修復這個問題,但這將是極其昂貴的。)
就 property() 而言,它的 fget、fset 和 fdel 引數是函式,而不是方法 - 它們被傳遞一個對物件的顯式引用作為它們的第一個引數。由於 property() 通常在類語句中使用,所以這是正確的(當呼叫 property() 時,這些方法實際上是函式物件),但你仍然可以將它們視為方法 - 只要你沒有使用對方法進行特殊處理的 元類。
- 當屬性作為類屬性 (C.x) 而不是作為例項屬性 (C().x) 訪問時,get 方法不會被呼叫。如果你想覆蓋屬性作為類屬性使用時的 __get__ 操作,你可以子類化 property - 它本身是一個新式型別 - 以擴充套件其 __get__ 方法,或者你可以透過建立一個定義了 __get__、__set__ 和 __delete__ 方法的新式類來從頭定義一個描述符型別。
方法解析順序
隨著多重繼承的出現,隨之而來的是方法解析順序的問題:在查詢給定名稱的方法時,搜尋類及其基類的順序。
在經典 Python 中,規則由以下遞迴函式給出,也稱為從左到右深度優先規則
def classic_lookup(cls, name): "Look up name in cls and its base classes." if cls.__dict__.has_key(name): return cls.__dict__[name] for base in cls.__bases__: try: return classic_lookup(base, name) except AttributeError: pass raise AttributeError, name
在 Python 2.2 中,我決定為新式類採用不同的查詢規則。(出於向後相容的考慮,經典類的規則保持不變;最終所有類都將成為新式類,然後這種區別將消失。)我將首先嚐試解釋經典規則的問題所在。
當我們考慮“菱形圖”時,經典規則的問題就顯而易見了。
class A: ^ ^ def save(self): ... / \ / \ / \ / \ class B class C: ^ ^ def save(self): ... \ / \ / \ / \ / class D
箭頭從子型別指向其基型別。這個特定的圖表示 B 和 C 派生自 A,而 D 派生自 B 和 C(因此也間接派生自 A)。
假設 C 覆蓋了基類 A 中定義的方法 save()。(C.save() 可能會呼叫 A.save(),然後儲存它自己的一些狀態。)B 和 D 不會覆蓋 save()。當我們在 D 例項上呼叫 save() 時,呼叫的是哪個方法?根據經典的查詢規則,呼叫的是 A.save(),忽略了 C.save()!
這不好。它可能會破壞 C(它的狀態沒有被儲存),從而破壞了從一開始就繼承 C 的整個目的。
為什麼這在經典 Python 中不是問題?菱形圖在經典 Python 類層次結構中很少見。大多數類層次結構使用單繼承,而多重繼承通常僅限於 mix-in 類。實際上,這裡顯示的問題很可能是多重繼承在經典 Python 中不受歡迎的原因!
為什麼這在新系統中會成為問題?型別層次結構頂部的“object”型別定義了許多可以被子型別有效擴充套件的方法,例如 __getattribute__() 和 __setattr__()。
(題外話:__getattr__() 方法實際上不是 get-attribute 操作的實現;它只是一個鉤子,只有在無法透過正常方式找到屬性時才會被呼叫。這經常被認為是一個缺點 - 一些類設計需要一個 get-attribute 方法,該方法會被呼叫以處理所有屬性引用,而這個問題現在透過使 __getattribute__() 可用來解決。但是,此方法必須能夠以某種方式呼叫預設實現。最自然的方法是將預設實現作為 object.__getattribute__(self, name) 提供。)
因此,像這樣的經典類層次結構
class B class C: ^ ^ __setattr__() \ / \ / \ / \ / class D
在新系統中將變為菱形圖
object: ^ ^ __setattr__() / \ / \ / \ / \ class B class C: ^ ^ __setattr__() \ / \ / \ / \ / class D
當在原始圖中呼叫 C.__setattr__() 時,在新系統中使用經典查詢規則時,將呼叫 object.__setattr__()!
幸運的是,有一種更好的查詢規則。它有點難以解釋,但它在菱形圖中做了正確的事情,並且當繼承圖中沒有菱形時(當它是樹時),它與經典的查詢規則相同。
新的查詢規則構造一個繼承圖中所有類的列表,並按照它們將被搜尋的順序排列。此構造在定義類時完成,以節省時間。為了解釋新的查詢規則,讓我們首先考慮一下對於經典的查詢規則,這樣一個列表會是什麼樣子。請注意,在存在菱形的情況下,經典的查詢會多次訪問某些類。例如,在上面的 ABCD 菱形圖中,經典的查詢規則會按以下順序訪問類
D, B, A, C, A
請注意,A 在列表中出現了兩次。第二次出現是多餘的,因為可以在那裡找到的任何東西在搜尋第一次出現時都已經找到了。但是它仍然被訪問(經典規則的遞迴實現不記得它已經訪問了哪些類)。
我們使用這個觀察來解釋我們的新查詢規則。使用經典的查詢規則,構造將要搜尋的類列表,包括重複項。現在,對於列表中多次出現的每個類,刪除除最後一個之外的所有出現。生成的列表包含每個祖先類恰好一次(包括最派生的類,例如示例中的 D):D、B、C、A。
按照此順序搜尋方法將為菱形圖做正確的事情。由於列表的構造方式,它永遠不會在不涉及菱形的情況下更改搜尋順序。
這是否向後不相容?這會不會破壞現有程式碼?如果更改所有類的方法解析順序,則會發生這種情況。但是,在 Python 2.2 中,新的查詢規則僅適用於從內建型別派生的型別,這是一個新功能。沒有基類的類語句建立“經典類”,基類本身是經典類的類語句也會建立“經典類”。對於經典類,將使用經典的查詢規則。我們還可以提供一個工具,用於分析類層次結構,查詢方法解析順序的更改會影響哪些方法。
順序不一致
本節僅適用於高階讀者。當前的實現使用了一個略有不同的演算法,這在極少數情況下可能會產生稍微不同的搜尋順序。僅當兩個給定的基類在兩個不同的派生類的繼承列表中以不同的順序出現,並且這些派生類又都被另一個類繼承時,才會顯示差異。這是我能想到的最小的例子class A(object): def meth(self): return "A" class B(object): def meth(self): return "B" class X(A, B): pass class Y(B, A): pass class Z(X, Y): pass
根據上面給出的演算法,Z 的 MRO(方法解析順序)應該是 [Z, X, Y, B, A, object]。但是,如果你在 Python 2.2 中嘗試這樣做(使用 Z.__mro__,請參閱下面的協作),你將得到 [Z, X, Y, A, B, object]!在將來的版本中,可能會發生兩件事:Z 的 MRO 可能會更改為 [Z, X, Y, B, A, object];或者類 Z 的宣告可能會變得非法,因為它引入了“順序不一致”:類 A 在 X 的繼承列表中位於 B 之前,但在 Y 的繼承列表中位於其之後。
書《讓元類發揮作用》啟發我更改 MRO,其中定義了當前實現的 MRO 演算法,但是其對演算法的描述非常難以理解 - 我什至沒有意識到直到 Tim Peters 找到了一個反例之前,上面的演算法並不總是計算相同的 MRO。幸運的是,反例只能在繼承圖中存在順序不一致時發生。本書禁止包含此類順序不一致的類,如果順序不一致是“嚴重的”。當兩個類至少定義一個具有相同名稱的方法時,這兩個類之間的順序不一致是嚴重的。在上面的示例中,順序不一致是嚴重的。在 Python 2.2 中,我選擇不檢查嚴重的順序不一致;但是包含嚴重順序不一致的程式的含義是未定義的,並且其效果在將來可能會更改。
協作方法和 "super"
新類最酷但也可能最不尋常的功能之一是編寫“協作”類的可能性。編寫協作類時要考慮多重繼承,並使用我稱之為“協作超級呼叫”的模式。這在其他一些多重繼承語言中稱為“call-next-method”,並且比單繼承語言(如 Java 或 Smalltalk)中的超級呼叫更強大。C++ 既沒有超級呼叫的形式,而是依賴於類似於經典 Python 中使用的顯式機制。(“協作方法”一詞來自“讓元類發揮作用”。)
作為回顧,讓我們首先回顧一下傳統的非協作超級呼叫。當類 C 派生自基類 B 時,C 通常會覆蓋 B 中定義的方法 m。“超級呼叫”發生在 C 的 m 定義呼叫 B 的 m 定義來完成其部分工作時。在 Java 中,C 中的 m 主體可以編寫 super(a, b, c) 以使用引數列表 (a, b, c) 呼叫 B 的 m 定義。在 Python 中,C.m 編寫 B.m(self, a, b, c) 來實現相同的效果。例如
class B: def m(self): print "B here" class C(B): def m(self): print "C here" B.m(self)我們說 C 的方法 m“擴充套件”了 B 的方法 m。只要我們使用單繼承,此處的模式就可以很好地工作,但是它會在多重繼承中崩潰。讓我們看一下其繼承圖形成“菱形”的四個類(該圖在上一節中以圖形方式顯示)
class A(object): .. class B(A): ... class C(A): ... class D(B, C): ...
假設 A 定義了一個方法 m,而 B 和 C 都擴充套件了這個方法。現在 D 該怎麼做呢?它繼承了 m 的兩個實現,一個來自 B,一個來自 C。按照傳統,Python 只是簡單地選擇第一個找到的實現,在本例中是來自 B 的定義。這並非理想之選,因為它完全忽略了 C 的定義。要了解忽略 C 的 m 會有什麼問題,假設這些類代表某種持久化容器層次結構,並考慮一個實現“將資料儲存到磁碟”操作的方法。可以推測,一個 D 例項同時擁有 B 的資料和 C 的資料,以及 A 的資料(後者的一個副本)。忽略 C 的 save 方法定義意味著,當請求 D 例項儲存自身時,只會儲存 A 和 B 部分的資料,而不會儲存由 C 類定義的資料部分!
C++ 注意到 D 繼承了方法 m 的兩個衝突定義,併發出錯誤訊息。然後,D 的作者應該重寫 m 以解決衝突。但是 D 的 m 定義應該做什麼呢?它可以先呼叫 B 的 m,然後呼叫 C 的 m,但是由於這兩個定義都呼叫了從 A 繼承的 m 定義,因此 A 的 m 最終會被呼叫兩次!根據操作的細節,這充其量是一種低效率(當 m 是冪等時),最壞情況下會導致錯誤。經典的 Python 也有同樣的問題,只不過它甚至不認為繼承一個方法的兩個衝突定義是錯誤:它只是簡單地選擇第一個。
解決這個難題的傳統方法是將每個派生的 m 定義拆分為兩部分:一個只儲存一個類特有的資料的部分實現 _m,以及一個完整的實現 m,它呼叫自身的 _m 和基類的 _m。例如
class A(object): def m(self): "save A's data" class B(A): def _m(self): "save B's data" def m(self): self._m(); A.m(self) class C(A): def _m(self): "save C's data" def m(self): self._m(); A.m(self) class D(B, C): def _m(self): "save D's data" def m(self): self._m(); B._m(self); C._m(self); A.m(self)
這種模式存在幾個問題。首先,存在額外的方法和呼叫的增殖。但也許更重要的是,它在派生類中建立了一個不希望有的對其基類依賴關係圖細節的依賴:A 的存在不再被視為 B 和 C 的實現細節,因為 D 類需要了解它。如果在程式的未來版本中,我們想從 B 和 C 中刪除對 A 的依賴,這將也會影響像 D 這樣的派生類;同樣,如果想在 B 和 C 中新增另一個基類 AA,則必須更新它們的所有派生類。
“呼叫下一個方法”模式很好地解決了這個問題,並結合了新的方法解析順序。如下所示:
class A(object): def m(self): "save A's data" class B(A): def m(self): "save B's data"; super(B, self).m() class C(A): def m(self): "save C's data"; super(C, self).m() class D(B, C): def m(self): "save D's data"; super(D, self).m()
請注意,super 的第一個引數始終是它所在的類;第二個引數始終是 self。另請注意,m 的引數列表中沒有重複 self。
現在,為了解釋 super 的工作原理,請考慮每個類的 MRO(方法解析順序)。MRO 由 __mro__ 類屬性給出
A.__mro__ == (A, object) B.__mro__ == (B, A, object) C.__mro__ == (C, A, object) D.__mro__ == (D, B, C, A, object)
表示式 super(C, self).m 應該只在 C 類中方法 m 的實現內部使用。請記住,雖然 self 是 C 的一個例項,但 self.__class__ 可能不是 C:它可能是 C 的派生類(例如 D)。然後,表示式 super(C, self).m 在 self.__class__.__mro__(用於在 self 中建立例項的類的 MRO)中搜索 C 的出現,然後開始查詢在該點之後方法 m 的實現。
例如,如果 self 是一個 C 例項,則 super(C, self).m 將找到 A 的 m 實現,如果 self 是一個 B 例項,則 super(B, self).m 也會找到 A 的 m 實現。但現在考慮一個 D 例項。在 D 的 m 中,super(D, self).m() 將找到並呼叫 B.m(self),因為 B 是 D.__mro__ 中 D 之後定義 m 的第一個基類。現在在 B.m 中,呼叫了 super(B, self).m()。由於 self 是一個 D 例項,MRO 是 (D, B, C, A, object),而 B 之後的類是 C。這就是繼續搜尋 m 定義的地方。這將找到 C.m,它被呼叫,並依次呼叫 super(C, self).m()。仍然使用相同的 MRO,我們看到 C 之後的類是 A,因此呼叫 A.m。這是 m 的原始定義,因此此時沒有進行 super 呼叫。
請注意,同一個 super 表示式如何根據 self 的類找到實現方法的不同類!這是合作 super 機制的關鍵。
上面顯示的 super 呼叫有些容易出錯:很容易將 super 呼叫從一個類複製貼上到另一個類,同時忘記將類名更改為目標類的類名,如果這兩個類都屬於同一個繼承圖,則不會檢測到此錯誤。(你甚至可以透過錯誤地傳入包含 super 呼叫的類的派生類的名稱來導致無限遞迴。)如果我們不必顯式命名類就好了,但這需要 Python 解析器提供比我們目前能獲得的更多幫助。我希望在未來的 Python 版本中透過讓解析器識別 super 來解決這個問題。
與此同時,這是一個你可以應用的技巧。我們可以建立一個名為 __super 的類變數,該變數具有“繫結”行為。(繫結行為是 Python 2.2 中的一個新概念,但它形式化了經典 Python 中眾所周知的概念:當透過例項上的 getattr 操作訪問未繫結方法時,未繫結方法轉換為繫結方法。它由上面討論的 __get__ 方法實現。)這是一個簡單的例子
class A: def m(self): "save A's data" class B(A): def m(self): "save B's data"; self.__super.m() B._B__super = super(B) class C(A): def m(self): "save C's data"; self.__super.m() C._C__super = super(C) class D(B, C): def m(self): "save D's data"; self.__super.m() D._D__super = super(D)
該技巧的部分在於使用名稱 __super,它(透過名稱修飾轉換)包含類名。這確保了 self.__super 在每個類中表示不同的含義(只要類名不同;不幸的是,在 Python 中可以重用基類的名稱作為派生類的名稱)。該技巧的另一部分是,可以使用單個引數呼叫 super 內建函式,然後建立一個未繫結的版本,該版本可以透過稍後的例項 getattr 操作進行繫結。
不幸的是,由於多種原因,此示例仍然相當醜陋:super 要求傳入類,但類在類語句執行完畢後才可用,因此必須在類外部分配 __super 類屬性。在類外部,名稱修飾不起作用(畢竟它旨在作為隱私功能),因此分配必須使用未修飾的名稱。幸運的是,可以編寫一個元類,該元類會自動向其類新增 __super 屬性;請參見下面的 autosuper 元類示例。
請注意,super(class, subclass) 也有效;這是 __new__ 和其他靜態方法所必需的。
示例:在 Python 中編寫 super 程式碼。
為了說明新系統的強大功能,這裡有一個用純 Python 實現的 super() 內建類的完整功能實現。透過詳細闡述搜尋過程,這也有助於闡明 super() 的語義。以下程式碼底部的 print 語句會列印 "DCBA"。
class Super(object): def __init__(self, type, obj=None): self.__type__ = type self.__obj__ = obj def __get__(self, obj, type=None): if self.__obj__ is None and obj is not None: return Super(self.__type__, obj) else: return self def __getattr__(self, attr): if isinstance(self.__obj__, self.__type__): starttype = self.__obj__.__class__ else: starttype = self.__obj__ mro = iter(starttype.__mro__) for cls in mro: if cls is self.__type__: break # Note: mro is an iterator, so the second loop # picks up where the first one left off! for cls in mro: if attr in cls.__dict__: x = cls.__dict__[attr] if hasattr(x, "__get__"): x = x.__get__(self.__obj__) return x raise AttributeError, attr class A(object): def m(self): return "A" class B(A): def m(self): return "B" + Super(B, self).m() class C(A): def m(self): return "C" + Super(C, self).m() class D(C, B): def m(self): return "D" + Super(D, self).m() print D().m() # "DCBA"
重寫 __new__ 方法
當對數字和字串等不可變的內建型別進行子類化時,有時在其他情況下,靜態方法 __new__ 會派上用場。__new__ 是例項構造的第一步,在 __init__ 之前呼叫。__new__ 方法被呼叫時,將類作為其第一個引數;它的職責是返回該類的新例項。將其與 __init__ 進行比較:__init__ 被呼叫時,將例項作為其第一個引數,並且不返回任何內容;它的職責是初始化例項。在不呼叫 __init__ 的情況下建立新例項的情況(例如,當例項從 pickle 載入時)。沒有辦法在不呼叫 __new__ 的情況下建立新例項(儘管在某些情況下你可以透過呼叫基類的 __new__ 來解決問題)。
回想一下,你是透過呼叫類來建立類例項的。當類是新式類時,呼叫它時會發生以下情況。首先,呼叫類的 __new__ 方法,將類本身作為第一個引數,然後是原始呼叫收到的任何(位置和關鍵字)引數。這將返回一個新的例項。然後,呼叫該例項的 __init__ 方法以進一步初始化它。(順便說一下,這一切都由元類的 __call__ 方法控制。)
這是一個重寫 __new__ 的子類的示例 - 這是你通常使用它的方式。
>>> class inch(float): ... "Convert from inch to meter" ... def __new__(cls, arg=0.0): ... return float.__new__(cls, arg*0.0254) ... >>> print inch(12) 0.3048 >>>
這個類不是很有用(它甚至不是進行單位轉換的正確方法),但它顯示瞭如何擴充套件不可變型別的建構函式。如果不是 __new__,而是試圖重寫 __init__,則它將不起作用
>>> class inch(float): ... "THIS DOESN'T WORK!!!" ... def __init__(self, arg=0.0): ... float.__init__(self, arg*0.0254) ... >>> print inch(12) 12.0 >>>
重寫 __init__ 的版本不起作用,因為 float 型別的 __init__ 是一個空操作:它立即返回,忽略其引數。
完成所有這些操作是為了讓不可變型別在允許子類化的同時保留其不可變性。如果 float 物件的值由其 __init__ 方法初始化,則你可以更改現有 float 物件的值!例如,這將起作用
>>> # THIS DOESN'T WORK!!! >>> import math >>> math.pi.__init__(3.0) >>> print math.pi 3.0 >>>
我可以以其他方式解決此問題,例如新增一個“已初始化”標誌,或者僅允許在子類例項上呼叫 __init__,但這些解決方案並不優雅。相反,我添加了 __new__,這是一個完全通用的機制,可以由內建類和使用者定義的類用於不可變物件和可變物件。
以下是關於 __new__ 的一些規則
- __new__ 是一個靜態方法。在定義它時,你不需要(但可以!)使用短語 "__new__ = staticmethod(__new__)",因為這是由其名稱暗示的(它由類建構函式特殊處理)。
- __new__ 的第一個引數必須是一個類;其餘引數是建構函式呼叫看到的引數。
- 重寫基類的 __new__ 方法的 __new__ 方法可以呼叫該基類的 __new__ 方法。呼叫基類的 __new__ 方法的第一個引數應該是重寫 __new__ 方法的類引數,而不是基類;如果你要傳入基類,則會獲得基類的例項。
- 除非你想玩遊戲,例如在接下來的兩個要點中描述的遊戲,否則 __new__ 方法必須呼叫其基類的 __new__ 方法;這是建立物件例項的唯一方法。子類 __new__ 可以執行兩項操作來影響結果物件:將不同的引數傳遞給基類 __new__,並在建立結果物件後對其進行修改(例如,初始化必要的例項變數)。
- __new__ 必須返回一個物件。雖然按照慣例,它應該返回一個新的物件,即其類引數的例項,但並沒有規定必須如此。如果你返回一個已存在的物件,建構函式呼叫仍然會呼叫它的 __init__ 方法。如果你返回一個不同類的物件,則會呼叫那個物件的 __init__ 方法。如果你忘記返回任何東西,Python 會不幫忙地返回 None,你的呼叫者可能會非常困惑。
- 對於不可變類,你的 __new__ 可能會返回對具有相同值的現有物件的快取引用;這就是 int、str 和 tuple 型別對小值所做的。這也是它們 __init__ 不執行任何操作的原因之一:快取的物件會被一遍又一遍地重新初始化。(另一個原因是 __init__ 沒有什麼可以初始化的:__new__ 返回一個完全初始化的物件。)
- 如果你子類化一個內建的不可變型別,並想要新增一些可變狀態(也許你新增一個到字串型別的預設轉換),最好在 __init__ 方法中初始化可變狀態,而讓 __new__ 保持不變。
- 如果你想要更改建構函式的簽名,你通常必須同時重寫 __new__ 和 __init__ 以接受新的簽名。但是,大多數內建型別會忽略它們不使用的方法的引數;特別是,不可變型別(int、long、float、complex、str、unicode 和 tuple)有一個虛擬的 __init__,而可變型別(dict、list、file 以及 super、classmethod、staticmethod 和 property)有一個虛擬的 __new__。內建型別 'object' 有一個虛擬的 __new__ 和一個虛擬的 __init__(其他的型別繼承了它們)。內建型別 'type' 在許多方面都很特殊;請參閱關於元類的部分。
- (這與 __new__ 無關,但知道一下總是有用的。)如果你子類化一個內建型別,額外的空間會自動新增到例項中,以容納 __dict__ 和 __weakrefs__。(__dict__ 在你使用它之前不會初始化,所以你不必擔心你建立的每個例項佔用一個空字典的空間。)如果你不需要這個額外的空間,你可以向你的類新增短語 "__slots__ = []"。(有關 __slots__ 的更多資訊,請參閱上面。)
- 事實:__new__ 是一個靜態方法,而不是類方法。我最初認為它必須是一個類方法,這就是我新增 classmethod 原語的原因。不幸的是,對於類方法,在這種情況下向上呼叫無法正常工作,所以我不得不將其設定為靜態方法,並以顯式類作為其第一個引數。具有諷刺意味的是,現在在 Python 發行版中(除了在測試套件中)沒有發現任何已知的類方法用法。如果找不到它的任何良好用途,我甚至可能會在未來的版本中刪除 classmethod!
作為 __new__ 的另一個示例,這裡有一種實現單例模式的方法。
class Singleton(object): def __new__(cls, *args, **kwds): it = cls.__dict__.get("__it__") if it is not None: return it cls.__it__ = it = object.__new__(cls) it.init(*args, **kwds) return it def init(self, *args, **kwds): pass
要建立單例類,你需要從 Singleton 子類化;無論其建構函式被呼叫多少次,每個子類都將只有一個例項。要進一步初始化子類例項,子類應該重寫 'init' 而不是 __init__ - __init__ 方法在每次呼叫建構函式時都會被呼叫。例如
>>> class MySingleton(Singleton): ... def init(self): ... print "calling init" ... def __init__(self): ... print "calling __init__" ... >>> x = MySingleton() calling init calling __init__ >>> assert x.__class__ is MySingleton >>> y = MySingleton() calling __init__ >>> assert x is y >>>
元類
過去,Python 中元類的主題曾引起人們的驚愕,甚至導致大腦爆炸(例如,參見Python 1.5 中的元類)。幸運的是,在 Python 2.2 中,元類更容易訪問,並且危害性更小。
從術語上講,元類只是“類的類”。任何例項本身就是類的類都是元類。當我們談論一個不是類的例項時,該例項的元類是其類的類:根據定義,x 的元類是 x.__class__.__class__。但是,當我們談論類 C 時,我們通常所說的元類是指 C.__class__(而不是 C.__class__.__class__,這將是一個元元類;它們沒有太多用途,儘管我們不排除它們)。
內建的 'type' 是最常見的元類;它是所有內建型別的元類。經典類使用不同的元類:稱為 types.ClassType 的型別。後者相對來說不那麼有趣;它是一個歷史遺蹟,需要它來賦予經典類其經典行為。你不能使用 x.__class__.__class__ 獲取經典例項的元類;你必須使用 type(x.__class__),因為經典類不支援類上的 __class__ 屬性(僅支援例項)。
當執行 class 語句時,直譯器首先確定適當的元類 M,然後呼叫 M(name, bases, dict)。所有這些都發生在 class 語句的末尾,在類的正文(定義方法和類變數的地方)已經執行之後。M 的引數是類名(從 class 語句中提取的字串)、基類元組(在 class 語句開頭評估的表示式;如果 class 語句中未指定基類,則為 ())以及包含 class 語句定義的方法和類變數的字典。此呼叫 M(name, bases, dict) 返回的任何內容都會被賦值給與類名對應的變數,這就是 class 語句的全部內容。
如何確定 M?
- 如果 dict['__metaclass__'] 存在,則使用它。
- 否則,如果至少有一個基類,則使用它的元類(首先查詢 __class__ 屬性,如果未找到,則使用其型別)。(在經典 Python 中,也存在此步驟,但僅在元類可呼叫時執行。這被稱為 Don Beaudry 鉤子 - 願它安息。)
- 否則,如果存在名為 __metaclass__ 的全域性變數,則使用它。
- 否則,使用經典元類 (types.ClassType)。
這裡最常見的結果是 M 是 types.ClassType(建立一個經典類),或 'type'(建立一個新式類)。其他常見的結果是自定義擴充套件型別(如 Jim Fulton 的 ExtensionClass),或 'type' 的子型別(當我們使用新式元類時)。但是,這裡可能會出現一些完全奇怪的東西:如果我們指定一個具有自定義 __class__ 屬性的基類,我們可以使用任何東西作為“元類”。這就是我最初的元類論文中令人頭疼的主題,我不會在這裡重複它。
總是有一個額外的難題。當你在基類列表中混合經典類和新式類時,使用第一個新式基類的元類而不是 types.ClassType(假設 dict['__metaclass__'] 未定義)。效果是,當你跨越經典類和新式類時,後代是一個新式類。
還有一個難題(我保證這是元類確定中的最後一個難題)。對於新式元類,有一個約束,即選擇的元類等於或屬於每個基類的元類的子類。考慮一個具有兩個基類 B1 和 B2 的類 C。假設 M = C.__class__,M1 = B1.__class__,M2 = B2.__class__。那麼我們需要 issubclass(M, M1) 和 issubclass(M, M2)。(這是因為 B1 的方法應該能夠在 self.__class__ 上呼叫 M1 中定義的元方法,即使 self 是 B1 的子類的例項。)
元類書籍描述了一種機制,其中在必要時,透過從 M1 和 M2 多重繼承自動建立一個合適的元類。在 Python 2.2 中,我選擇了一種更簡單的方法,如果元類約束不滿足,則會引發異常;程式設計師有責任透過 __metaclass__ 類變數提供合適的元類。但是,如果其中一個基元類滿足約束(包括顯式給出的 __metaclass__,如果有),則找到的滿足約束的第一個基元類將用作元類。
在實踐中,這意味著如果你有一個形狀像塔的退化元類層次結構(意味著對於兩個元類 M1 和 M2,至少有一個 issubclass(M1, M2) 或 issubclass(M2, M1) 始終為真),你無需擔心元類約束。例如
# Metaclasses class M1(type): ... class M2(M1): ... class M3(M2): ... class M4(type): ... # Regular classes class C1: __metaclass__ = M1 class C2(C1): __metaclass__ = M2 class C3(C1, C2): __metaclass__ = M3 class D(C2, C3): __metaclass__ = M1 class C4: __metaclass__ = M4 class E(C3, C4): pass
對於類 C2,滿足約束是因為 M2 是 M1 的子類。對於類 C3,滿足約束是因為 M3 是 M1 和 M2 的子類。對於類 D,顯式元類 M1 不是基元類(M2,M3)的子類,但選擇 M3 滿足約束,因此 D.__class__ 是 M3。但是,類 E 是一個錯誤:涉及的兩個元類是 M3 和 M4,並且它們都不是另一個的子類。我們可以按如下方式修復後一種情況
# A new metaclass class M5(M3, M4): pass # Fixed class E class E(C3, C4): __metaclass__ = M5
(元類書籍中的方法將根據類 E 的原始定義自動提供 M5 的類定義。)
元類示例
讓我們先複習一些理論。請記住,class 語句會導致呼叫 M(name, bases, dict),其中 M 是元類。現在,元類是一個類,我們已經確定,當呼叫一個類時,會依次呼叫其 __new__ 和 __init__ 方法。因此,會發生這樣的事情
cls = M.__new__(M, name, bases, dict) assert cls.__class__ is M M.__init__(cls, name, bases, dict)
我在這裡將 __init__ 呼叫寫成一個未繫結方法呼叫。這表明我們正在呼叫 M 定義的 __init__,而不是 cls 中定義的 __init__(這將是 cls 例項的初始化)。但它實際上呼叫了物件 cls 的 __init__ 方法;cls 碰巧是一個類。
我們的第一個示例是一個元類,它在類的方法中查詢名為 _get_<something> 和 _set_<something> 的方法,並自動新增名為 <something> 的屬性描述符。事實證明,重寫 __init__ 就足以完成我們想要的操作。該演算法進行了兩次傳遞:首先它收集屬性名稱,然後將其新增到類中。收集傳遞遍歷 dict,dict 是表示類變數和方法的字典(不包括基類變數和方法)。但是第二次傳遞,屬性構造傳遞,將 _get_<something> 和 _set_<something> 作為類屬性查詢。這意味著如果基類定義了 _get_x,而子類定義了 _set_x,則子類將具有從這兩種方法建立的屬性 x,即使子類的字典中只出現 _set_x。因此,你可以在子類中擴充套件屬性。請注意,我們使用了 getattr() 的三引數形式,因此,缺少 _get_x 或 _set_x 將被轉換為 None,而不是引發 AttributeError。我們還使用 super() 以協作方式呼叫基類的 __init__ 方法。
class autoprop(type): def __init__(cls, name, bases, dict): super(autoprop, cls).__init__(name, bases, dict) props = {} for name in dict.keys(): if name.startswith("_get_") or name.startswith("_set_"): props[name[5:]] = 1 for name in props.keys(): fget = getattr(cls, "_get_%s" % name, None) fset = getattr(cls, "_set_%s" % name, None) setattr(cls, name, property(fget, fset))
讓我們用一個愚蠢的例子來測試 autoprop。這是一個將屬性 x 儲存為其在 self.__x 下的反向值的類
class InvertedX: __metaclass__ = autoprop def _get_x(self): return -self.__x def _set_x(self, x): self.__x = -x a = InvertedX() assert not hasattr(a, "x") a.x = 12 assert a.x == 12 assert a._InvertedX__x == -12
我們的第二個例子建立了一個名為 'autosuper' 的類,它會新增一個名為 __super 的私有類變數,並將其值設定為 super(cls)。 (回想一下上面關於 self.__super 的討論。)現在,__super 是一個私有名稱(以雙下劃線開頭),但我們希望它是要建立的類的私有名稱,而不是 autosuper 的私有名稱。 因此,我們必須自己進行名稱修飾,並使用 setattr() 來設定類變數。 為了本例的目的,我將名稱修飾簡化為“在前面加上一個下劃線和類名”。 同樣,只需重寫 __init__ 即可完成我們想要的操作,並且再次,我們協作地呼叫基類的 __init__。
class autosuper(type): def __init__(cls, name, bases, dict): super(autosuper, cls).__init__(name, bases, dict) setattr(cls, "_%s__super" % name, super(cls))
現在讓我們用經典的菱形圖來測試 autosuper
class A: __metaclass__ = autosuper def meth(self): return "A" class B(A): def meth(self): return "B" + self.__super.meth() class C(A): def meth(self): return "C" + self.__super.meth() class D(C, B): def meth(self): return "D" + self.__super.meth() assert D().meth() == "DCBA"
(如果定義一個與基類同名的子類,我們的 autosuper 元類很容易被欺騙;它應該真正檢查這種情況,如果發生這種情況則丟擲錯誤。但這對於示例來說程式碼太多了,所以我將其留給讀者作為練習。)
現在我們有兩個獨立開發的元類,我們可以將兩者組合成一個繼承它們的第三個元類
class autosuprop(autosuper, autoprop): pass
簡單吧? 因為我們協作地編寫了兩個元類(意味著它們的方法使用 super() 來呼叫基類方法),這就是我們所需要的一切。 讓我們測試一下
class A: __metaclass__ = autosuprop def _get_x(self): return "A" class B(A): def _get_x(self): return "B" + self.__super._get_x() class C(A): def _get_x(self): return "C" + self.__super._get_x() class D(C, B): def _get_x(self): return "D" + self.__super._get_x() assert D().x == "DCBA"
今天就到此為止。 我希望你的大腦沒有太疼!
向後不相容
放鬆! 上面描述的大多數功能僅在您使用帶有內建物件作為基類的類語句時(或者當您使用顯式的 __metaclass__ 賦值時)才會被呼叫。
一些可能影響舊程式碼的事項
- 另請參見 2.2 列表中的錯誤。
- 內省的工作方式不同(請參閱 PEP 252)。 特別是,現在大多數物件都有一個 __class__ 屬性,並且 __methods__ 和 __members__ 屬性不再起作用,並且 dir() 函式的工作方式也不同。 另請參閱上面。
- 一些可以被視為強制轉換或建構函式的內建函式現在是型別物件,而不是工廠函式; 型別物件支援與舊工廠函式相同的行為。 受影響的有:complex、float、long、int、str、tuple、list、unicode 和 type。 (還有一些新的:dict、object、classmethod、staticmethod,但由於這些是新的內建函式,我看不出這會如何破壞舊程式碼。)另請參閱上面。
- 有一個非常具體(幸運的是不常見)的錯誤以前沒有被檢測到,但現在被報告為錯誤
class A: def foo(self): pass class B(A): pass class C(A): def foo(self): B.foo(self)
在這裡,C.foo 想要呼叫 A.foo,但錯誤地呼叫了 B.foo。在舊系統中,由於 B 沒有定義 foo,所以 B.foo 與 A.foo 相同,因此呼叫會成功。在新系統中,B.foo 被標記為需要 B 例項的方法,而 C 不是 B,因此呼叫失敗。 - 與舊擴充套件的二進位制相容性不保證。我們在 Python 2.2 的 alpha 和 beta 釋出週期中收緊了這一點。截至 2.2b1,Jim Fulton 的 ExtensionClass 可以工作(如 Zope 2.4 的測試所示),我期望其他基於 Don Beaudry hook 的擴充套件也可以工作。雖然 PEP 253 的最終目標是消除 ExtensionClass,但我相信 ExtensionClass 仍然可以在 Python 2.2 中工作,不會早於 Python 2.3 中斷它。
其他主題
這些主題也應該討論
- 描述符:__get__、__set__、__delete__
- 可子類化的內建型別的規範
- 'object' 型別及其方法
- <type 'foo'> vs. <type 'mod.foo'> vs. <class 'mod.foo'>
- 還有什麼?
參考文獻
- PEP 252 - 使型別看起來更像類
- PEP 253 - 子型別化內建型別
- Python 1.5 中的元類 - 又名:殺手笑話
- 讓元類發揮作用:面向物件程式設計的新維度,Ira R. Forman 和 Scott H. Danforth 著。 Addison-Wesley, 1999, ISBN 0-201-43305-2。