注意: 雖然 JavaScript 對於本網站不是必需的,但您與內容的互動將受到限制。請開啟 JavaScript 以獲得完整的體驗。

在 Python 2.2 中統一型別和類

Python 版本:2.2.3

吉多·範羅蘇姆

本文件是一個不完整的草稿。我正在徵求反饋意見。如果您發現任何問題,請傳送郵件至 guido@python.org 給我。

目錄

更改日誌

自本教程的原始 Python 2.2 版本以來的更改

  • 不要透過暗示類方法可能會消失來嚇唬人。(2002年4月4日)

引言

Python 2.2引入了“型別/類統一”的第一階段。這是一系列對Python的更改,旨在消除內建型別和使用者定義類之間的大部分差異。其中最明顯的一點可能是取消了在類語句中將內建型別(如列表和字典的型別)用作基類的限制。

這是Python有史以來最大的變化之一,但它的向後不相容性卻非常少。這些更改在一系列 PEP(Python增強提案)中詳細描述。PEP並非旨在作為教程,並且描述型別/類統一的PEP有時難以閱讀。它們也尚未完成。這就是本文的作用:它為普通Python程式設計師介紹了型別/類統一的關鍵元素。

一些術語:“經典Python”指的是Python 2.1(及其補丁版本如2.1.1)或更早的版本,而“經典類”指的是在類語句中定義的類,其基類中不包含內建物件:要麼因為它沒有基類,要麼因為它所有的基類本身都是經典類——遞迴地應用這個定義。

經典類在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}
>>>

我們還可以在只有經典Python才允許“真實”字典的上下文中使用新型別,例如用於exec語句或內建函式eval()的locals/globals字典。

>>> 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__()覆蓋。我承認這可能是一個問題(儘管它**只**在這種情況下是一個問題,即當一個字典子類被用作區域性/全域性字典時);是否能在不影響常見情況效能的情況下修復它,還有待觀察。

現在我們將看到 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__ 的一些值得注意的細節和警告

  • 未定義的 slot 變數將按預期引發 AttributeError。(請注意,在 Python 2.2b2 及更早版本中,slot 變數預設值為 None,“刪除”它們會恢復此預設值。)

  • 您不能使用類屬性來為由 __slots__ 定義的例項變數定義預設值。__slots__ 宣告會為每個槽建立一個包含描述符的類屬性,將類屬性設定為預設值會覆蓋此描述符。

  • 沒有檢查來防止類中定義的槽與其基類中定義的槽之間的名稱衝突。如果一個類定義了一個在基類中也定義的槽,那麼由基類槽定義的例項變數將無法訪問(除非直接從基類檢索其描述符;這可以用於重新命名它)。這樣做會使您的程式含義未定義;將來可能會新增檢查以防止這種情況發生。

  • 使用 __slots__ 的類的例項沒有 __dict__(除非基類定義了 __dict__);但是其派生類的例項有 __dict__,除非它們的類也使用了 __slots__。

  • 您可以使用 __slots__ = [] 定義一個沒有例項變數和沒有 __dict__ 的物件。

  • 您不能將槽與“變長”內建型別作為基類一起使用。變長內建型別包括 long、str 和 tuple。

  • 使用 __slots__ 的類不支援對其例項的弱引用,除非 __slots__ 列表中的一個字串等於 "__weakref__"。(在 Python 2.3 中,此功能已擴充套件到 "__dict__")

  • __slots__ 變數不必是列表;任何可迭代的非字串都可以,並且迭代返回的值將用作槽名。特別是,可以使用字典。您也可以使用單個字串來宣告單個槽。然而,將來可能會為使用字典賦予額外的含義,例如,字典值可能用於限制例項變數的型別或提供文件字串;使用非列表的東西會使您的程式含義未定義。

請注意,雖然通常運算子過載與經典類的工作方式相同,但仍有一些差異。(最大的不同是缺少對 __coerce__ 的支援;新式類應該始終使用新式數字 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__扮演著虛擬函式表(vtable)的角色。

還有很多可以解釋的內容(比如 __metaclass__ 宣告和 __new__ 方法),但其中大部分都相當深奧。如果你感興趣,請參閱下文

我將以一份注意事項列表結束

  • 您可以使用多重繼承,但不能從不同的內建型別多重繼承(例如,您不能建立一個同時繼承自內建 dict 和 list 型別的型別)。這是一個永久性限制;要解除它,需要對 Python 的物件實現進行太多更改。但是,您可以透過繼承自“object”來建立 mix-in 類。這是一個新的內建型別,命名了新系統下所有內建型別的功能缺失基型別。

  • 當使用多重繼承時,您可以在基類列表中混合經典類和內建型別(或從內建型別派生的型別)。 (這在 Python 2.2b2 中是新增的;在早期版本中則不行。)

  • 另請參閱 2.2 錯誤列表

作為工廠函式的內建型別

上一節展示了內建子型別 defaultdict 的例項可以透過呼叫 defaultdict() 建立。這是預期的,因為這對於經典類也適用。但這裡有一個新功能:內建基型別本身也可以透過直接呼叫型別來例項化。

對於幾個內建型別,經典 Python 中已經存在以型別命名的工廠函式,例如 str() 和 int()。我已將這些內建函式更改為現在是相應型別的名稱。雖然這會將這些名稱的型別從內建函式更改為內建型別,但我預計這不會造成向後相容性問題:我已確保這些型別可以用與舊函式完全相同的引數列表呼叫。(它們通常也可以無引數呼叫,生成具有合適預設值(例如零或空)的物件;這是新功能。)

這些是受影響的內建函式

  • int([number_or_string[, base_number]])
  • long([number_or_string])
  • float([number_or_string])
  • complex([number_or_string[, imag_number]])
  • str([object])
  • unicode([string[, encoding_string]])
  • tuple([iterable])
  • list([iterable])
  • type(object) 或 type(name_string, bases_tuple, methods_dict)

type() 的簽名需要解釋:傳統上,type(x) 返回物件 x 的型別,這種用法仍然受支援。然而,type(name, bases, methods) 是一種新的用法,它建立一個全新的型別物件。(這涉及 元類程式設計,我在此不再深入討論,只指出這個簽名與 Don Beaudry 的元類鉤子使用的簽名相同。)

還有一些遵循相同模式的新內建函式。這些已在上面或將在下面描述

  • dict([mapping_or_iterable]) - 返回一個新的字典;可選引數必須是一個其項被複制的對映,或一個包含 2 元組(或長度為 2 的序列)的序列,用於給出要插入到新字典中的 (鍵,值) 對。
  • object([...]) - 返回一個不帶任何特徵的新物件;引數被忽略
  • classmethod(function) - 參見 下文
  • staticmethod(function) - 參見 下文
  • super(class_or_type[, instance]) - 參見 下文
  • property([fget[, fset[, fdel[, doc]]]]) - 參見 下文

這種改變的目的是雙重的。首先,這使得在類語句中方便地將這些型別中的任何一個用作基類。其次,它使得測試特定型別變得稍微容易一些:您現在可以編寫 isinstance(x, int),而不是編寫 type(x) is type(0)。

這提醒了我。isinstance() 的第二個引數現在可以是一個類或型別的元組。例如,當 x 是 int 或 long(或這些型別中任何一個的子類例項)時,isinstance(x, (int, long)) 返回 true,類似地,isinstance(x, (str, unicode)) 測試兩種字串型別。我們沒有對 issubclass() 進行此操作。(尚未。Python 2.3 中已對 issubclass() 進行了此操作。)

內省內建型別的例項

對於內建型別的例項(以及通常對於新式類),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__ 方法返回一個繫結方法物件;staticmethod 物件的 __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__ 中,而不會呼叫 property 的 set 方法,在那之後,property 的 get 方法也不會被呼叫。(你可以覆蓋 __setattr__ 來解決這個問題,但這會非常昂貴。)

  • 就 property() 而言,它的 fget、fset 和 fdel 引數是函式,而不是方法——它們被傳遞給物件的顯式引用作為它們的第一個引數。由於 property() 通常在類語句中使用,這是正確的(在呼叫 property() 時,方法確實是函式物件),但你仍然可以把它們看作方法——只要你不使用對方法做特殊處理的 元類

  • 當屬性作為類屬性(C.x)而不是例項屬性(C().x)訪問時,get 方法不會被呼叫。如果你想在屬性作為類屬性使用時覆蓋其 __get__ 操作,你可以子類化 property——它本身是一種新式型別——以擴充套件其 __get__ 方法,或者你可以透過建立定義 __get__、__set__ 和 __delete__ 方法的新式類來從頭開始定義一個描述符型別。

方法解析順序

隨著多重繼承的出現,方法解析順序(MRO)的問題也隨之而來:即在查詢具有給定名稱的方法時,搜尋類及其基類的順序。

在經典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(A):
    ...

class C(A):
    def save(self): ...

class D(B, C):
    ...
或用表示子類化關係的箭頭圖表示(解釋了名稱)。
              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__() 方法並非真正的獲取屬性操作的實現;它是一個只有在無法透過正常方式找到屬性時才會被呼叫的鉤子。這經常被認為是缺陷——一些類設計確實需要一個對**所有**屬性引用都會被呼叫的獲取屬性方法,而這個問題現在透過提供 __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, B, C, A

按照此順序搜尋方法將對菱形圖進行正確的操作。由於列表的構建方式,在不涉及菱形的情況下,它永遠不會改變搜尋順序。

所使用的確切規則將在下一節中解釋(其中引用了另一篇論文以獲取最細微的細節)。我在此僅指出查詢規則的**單調性**這一重要屬性:如果類 X 在類 D 的任何基類的查詢順序中先於類 Y,那麼類 X 也將先於類 Y 在類 D 的查詢順序中。例如,由於 B 在 B 的查詢列表中先於 A,它也在 D 的查詢列表中先於 A;C 先於 A 也是如此。例外:如果在類 D 的基類中,有一個 X 先於 Y,另一個 Y 先於 X,則演算法必須打破僵局。在這種情況下,一切都懸而未決;將來,這種情況可能會導致警告或錯誤。

(以前在此處描述的規則被證明不具有單調性屬性。請參閱 Samuele Pedroni 在 python-dev 上發起的帖子。)

這難道不是向後不相容嗎?它不會破壞現有程式碼嗎?如果我們更改所有類的方法解析順序,就會。然而,在 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, A, B, object],這不滿足上面提到的單調性要求:Y 的 MRO 是 [Y, B, A, object],這不是上述列表的子序列!事實上,在這裡沒有一個解決方案能同時滿足 X 和 Y 的單調性要求。這被稱為**順序不一致**。在未來的版本中,我們可能會決定在某些情況下禁止這種順序不一致,或者對此發出警告。

啟發我修改 MRO 的書籍 "Putting Metaclasses to Work" 定義了當前實現的 MRO 演算法,但其對演算法的描述相當難以理解——我最初記錄了一種不同的、樸素的演算法,甚至沒有意識到它並非總是計算出相同的 MRO,直到 Tim Peters 找到了一個反例。最近,Samuele Pedroni 找到了一個反例,表明樸素演算法未能保持單調性,所以我不再描述它。Samuele 說服我使用一種名為 C3 的新 MRO 演算法,該演算法在論文 "A Monotonic Superclass Linearization for Dylan" 中描述。該演算法將在 Python 2.3 中使用。C3 像書中的演算法一樣是單調的,但此外還保持了直接基類的順序,而書中的演算法並非總是如此。Michele Simionato 撰寫的 The Python 2.3 Method Resolution Order 對 C3 在 Python 中的應用進行了非常易懂的描述。

如果順序不一致“嚴重”,該書會禁止包含此類順序不一致的類。當兩個類定義至少一個同名方法時,它們之間的順序不一致是嚴重的。在上面的示例中,順序不一致是嚴重的。在 Python 2.2 中,我選擇不檢查嚴重的順序不一致;但包含嚴重順序不一致的程式的含義是未定義的,其效果將來可能會改變。但自從 Samuele 的反例以來,我們知道禁止順序不一致不足以避免 Python 2.2 演算法(來自書中)和 Python 2.3 演算法(C3,來自 Dylan 論文)之間出現不同的結果。

協作方法和“super”

新類最酷但也可能是最不尋常的特性之一是編寫“協作”類的可能性。協作類在設計時考慮了多重繼承,使用我稱之為“協作 super 呼叫”的模式。這在其他一些多重繼承語言中被稱為“call-next-method”,並且比 Java 或 Smalltalk 等單繼承語言中的 super 呼叫更強大。C++ 沒有這兩種形式的 super 呼叫,而是依賴於類似於經典 Python 中使用的顯式機制。(“協作方法”一詞來自 “Putting Metaclasses to Work”。)

作為回顧,我們首先回顧傳統的、非協作的 super 呼叫。當一個類 C 派生自基類 B 時,C 經常希望覆蓋 B 中定義的方法 m。當 C 對 m 的定義呼叫 B 對 m 的定義來完成其部分工作時,就會發生“super 呼叫”。在 Java 中,C 中 m 的主體可以寫入 super(a, b, c) 來呼叫 B 對 m 的定義,引數列表為 (a, b, c)。在 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,它們的所有派生類也必須更新。

“call-next-method”模式結合新的方法解析順序,很好地解決了這個問題。具體如下:

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 也會找到。但現在考慮一個 D 例項。在 D 的 m 中,super(D, self).m() 將找到並呼叫 B.m(self),因為 B 是 D.__mro__ 中定義 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() 的語義。以下程式碼底部列印的語句將列印“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__方法的類引數,而不是基類;如果你傳入基類,你將得到基類的一個例項。(這實際上類似於將self傳遞給被覆蓋的__init__呼叫。)

  • 除非你想玩接下來兩點中描述的遊戲,否則一個 __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 釋出版中沒有已知的類方法用法(除了測試套件中)。然而,類方法在其他地方仍然有用,例如,用於程式設計可繼承的替代建構函式。

作為 __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__ 屬性(僅在例項上)。

當一個類語句執行時,直譯器首先確定合適的元類 M,然後呼叫 M(name, bases, dict)。這一切都發生在類語句的**末尾**,在類的主體(其中定義了方法和類變數)已經執行之後。M 的引數是類名(取自類語句的字串)、一個基類元組(在類語句開始時評估的表示式;如果類語句中沒有指定基類,則為 ()),以及一個包含由類語句定義的方法和類變數的字典。這個呼叫 M(name, bases, dict) 返回的任何內容,然後被賦值給與類名對應的變數,這就是類語句的全部。

如何確定 M?

  • 如果 dict['__metaclass__'] 存在,則使用它。
  • 否則,如果至少有一個基類,則使用其元類(這會先查詢 __class__ 屬性,如果找不到,則使用其型別)。(在經典 Python 中,此步驟也存在,但只有在元類可呼叫時才執行。這被稱為 Don Beaudry 鉤子——願它安息。)
  • 否則,如果存在名為 __metaclass__ 的全域性變數,則使用它。
  • 否則,使用經典元類(types.ClassType)。

這裡最常見的結果是 M 是 types.ClassType(建立經典類)或 'type'(建立新式類)。其他常見結果是自定義擴充套件型別(如 Jim Fulton 的 ExtensionClass)或 'type' 的子型別(當我們使用新式元類時)。但這裡也可能出現完全出乎意料的情況:如果我們指定一個具有自定義 __class__ 屬性的基類,我們可以將任何東西用作“元類”。這曾是我最初的 元類論文 中令人頭疼的話題,我在此不再贅述。

總會有一個額外的細節。當您在基類列表中混合經典類和新式類時,將使用第一個新式基類的元類,而不是 types.ClassType(假設 dict['__metaclass__'] 未定義)。其效果是,當您將經典類與新式類結合時,其後代是新式類。

還有一點(我保證這是元類確定中的最後一個細節)。對於新式元類,有一個約束,即所選元類必須等於或是一個或多個基類的元類的子類。考慮一個類 C,它有兩個基類 B1 和 B2。假設 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 的類定義。)

元類示例

讓我們先回顧一些理論。記住,類語句會呼叫 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_ 和 _set_ 的方法,並自動新增名為 的屬性描述符。事實證明,覆蓋 __init__ 就足以實現我們想要的功能。該演算法分兩步進行:首先收集屬性名稱,然後將它們新增到類中。收集過程遍歷 dict,即表示類變數和方法的字典(不包括基類變數和方法)。但第二步,屬性構建過程,將 _get_ 和 _set_ 作為類屬性查詢。這意味著如果一個基類定義了 _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 member in dict.keys():
            if member.startswith("_get_") or member.startswith("_set_"):
		props[member[5:]] = 1
	for prop in props.keys():
            fget = getattr(cls, "_get_%s" % prop, None)
            fset = getattr(cls, "_set_%s" % prop, None)
            setattr(cls, prop, 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,但由於這些是新的內建函式,我看不出這會如何破壞舊程式碼。)另請參見 上面

  • 有一個非常特定(幸運的是不常見)的 bug,它過去沒有被檢測到,但現在被報告為錯誤
    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 鉤子的擴充套件也能正常工作。儘管 PEP 253 的最終目標是淘汰 ExtensionClass,但我相信 ExtensionClass 在 Python 2.2 中仍應能工作,不應早於 Python 2.3 停止支援。

附加主題

這些話題也應該討論

  • 描述符:__get__、__set__、__delete__
  • 可子類化內建型別的規範
  • “object”型別及其方法
  • <type 'foo'> 與 <type 'mod.foo'> 與 <class 'mod.foo'>
  • 還有什麼?

參考文獻