注意: 雖然 JavaScript 不是本網站的必要組成部分,但您與內容的互動將會受到限制。請啟用 JavaScript 以獲得完整體驗。

Python 2.2 中統一型別和類

Python 版本: 2.2.3

Guido van Rossum

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

目錄

更改日誌

自最初的 Python 2.2 版本本教程以來的更改

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

簡介

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 語句的區域性/全域性字典或內建函式 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__() 重寫。我承認這可能是一個問題(儘管這在字典子類用作區域性/全域性字典時才是一個問題);如果我能在不影響常見情況下的效能的情況下解決這個問題,仍有待觀察。

現在我們將看到 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__ 的類的例項沒有 __dict__(除非基類定義了 __dict__);但它的派生類的例項確實有 __dict__,除非它們的類也使用 __slots__。

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

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

  • 使用 __slots__ 的類不支援對其例項的弱引用,除非 __slots__ 列表中的一個字串等於 "__weakref__"。(在 Python 2.3 中,此功能已擴充套件到 "__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__ 的作用類似於虛擬函式表(vtable)。

還有很多內容可以解釋(例如 __metaclass__ 宣告和 __new__ 方法),但大多數都相當深奧。 如果您感興趣,請參閱下面的 __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 部分。
  • staticmethod(function) - 請參閱下面的 staticmethod 部分。
  • super(class_or_type[, instance]) - 請參閱下面的 cooperation 部分。
  • 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)) 測試是否為任意型別的字串。我們沒有對 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__ 中設定值,而無需呼叫屬性的 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(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 類層次結構中很少見。大多數類層次結構使用單繼承,而多重繼承通常僅限於混入類。事實上,這裡顯示的問題可能就是多重繼承在經典 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, B, C, A

按此順序搜尋方法將為菱形圖做正確的事情。由於列表的構造方式,它永遠不會在不涉及菱形的情況下更改搜尋順序。

下一節(其中引用了一篇單獨的論文來了解最微妙的細節)將解釋所使用的確切規則。我在這裡只注意到查詢規則中單調性的重要特性:如果類 X 在類 D 的任何基類的查詢順序中先於類 Y,則類 X 也將在類 D 的查詢順序中先於類 Y。例如,由於 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 的單調性要求。這被稱為順序不一致。在未來的版本中,我們可能會決定在某些情況下禁止此類順序不一致,或針對它們發出警告。

這本書 "Putting Metaclasses to Work" 啟發我改變 MRO,定義了當前實現的 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 對 Python 的 C3 進行了非常易於理解的描述。

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

協作方法和“super”

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

作為複習,讓我們首先回顧一下傳統的非協作超級呼叫。當類 C 派生自基類 B 時,C 通常想要覆蓋 B 中定義的方法 m。“超級呼叫”發生在 C 的 m 定義呼叫 B 的 m 定義來完成其某些工作時。在 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,所有它們的派生類也必須更新。

“呼叫下一個方法”模式與新的方法解析順序結合,很好地解決了這個問題。如下所示:

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。另請注意,self 不會在 m 的引數列表中重複。

現在,為了解釋 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 解析器獲得的更多幫助。我希望透過使解析器識別 super 來在未來的 Python 版本中修復此問題。

在此期間,這是一個您可以應用的技巧。我們可以建立一個名為 __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__ 方法的類引數,而不是基類;如果要傳入基類,您將獲得基類的例項。(這實際上只是類似於將 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__']` 未定義)。其效果是,當你跨越一個經典類和一個新式類時,後代將是一個新式類。

還有一個曲折(我保證這是元類確定中的最後一個曲折)。對於新式元類,存在一個約束,即所選擇的元類等於或為基類的每個元類的子類。考慮一個具有兩個基類 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)` 之一始終為 true),則不必擔心元類約束。例如

# 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 版本中的 bug 列表

  • 內省機制的工作方式有所不同(請參閱 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'> vs. <type 'mod.foo'> vs. <class 'mod.foo'>
  • 還有什麼?

參考