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

在 Python 2.2 中統一型別和類

在 Python 2.2 中統一型別和類

Python 版本:2.2
(有關本教程的更新版本,請參閱 Python 2.2.3

吉多·範羅蘇姆

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

目錄

引言

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}
    >>>

我們還可以在經典程式碼只允許“真實”字典的上下文中使用新型別,例如用於 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__() 重寫。我承認這可能是一個問題(儘管它在這種情況下是一個問題,當 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__ 扮演了虛擬函式表 (vtable) 的角色。

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

我將以一份注意事項列表作為結尾

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

  • 使用多重繼承時,您可以在基類列表中混合經典類和內建型別(或從內建型別派生的型別)。 (這是 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 元組序列,給出要插入新字典的 (key, value) 對
  • object([...]) - 返回一個沒有任何功能的新物件;引數被忽略
  • classmethod(function) - 參見下文
  • staticmethod(function) - 參見下文
  • super(class_or_type[, instance]) - 參見下文
  • property([fget[, fset[, fdel[, doc]]]]) - 參見下文

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

這讓我想起。isinstance() 的第二個引數現在可以是類或型別的元組。例如,isinstance(x, (int, long)) 在 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 類層次結構中很少見。大多數類層次結構使用單繼承,而多重繼承通常僅限於混合類。事實上,這裡顯示的問題很可能是多重繼承在經典 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):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 的繼承列表中排在 B 之後。

啟發我改變 MRO 的書《將元類付諸實踐》定義了當前實現的 MRO 演算法,但其對演算法的描述很難理解——我甚至沒有意識到上面的演算法並非總是計算出相同的 MRO,直到Tim Peters找到了一個反例。幸運的是,反例只會在繼承圖中存在順序衝突時出現。該書禁止包含此類順序衝突的類,如果順序衝突是“嚴重”的。當兩個類定義至少一個同名方法時,它們之間的順序衝突是嚴重的。在上面的示例中,順序衝突是嚴重的。在 Python 2.2 中,我選擇不檢查嚴重的順序衝突;但包含嚴重順序衝突的程式的含義未定義,其效果將來可能會改變。

協作方法和“super”

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

作為回顧,讓我們首先回顧傳統的非協作 super 呼叫。當類 C 派生自基類 B 時,C 通常希望重寫 B 中定義的方法 m。“super 呼叫”發生在 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 和基類(es) 的 _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 也將找到 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 實現的 fully functional 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
    >>> 

這個類不是很實用(它甚至不是進行單位轉換的正確方法),但它展示瞭如何擴充套件不可變型別的建構函式。如果我們嘗試重寫 __init__ 而不是 __new__,它就不會起作用

    >>> 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__ 是一個空操作:它立即返回,忽略其引數。

所有這些都是為了讓不可變型別在允許子類化的同時保持其不可變性。如果浮點物件的值由其 __init__ 方法初始化,您就可以更改現有浮點物件的值!例如,這將起作用

    >>> # 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__ 屬性(只支援例項)。

當執行類語句時,直譯器首先確定合適的元類 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 的方法應該能夠呼叫在 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_<something> 和 _set_<something> 的方法,並自動新增名為 <something> 的屬性描述符。事實證明,重寫 __init__ 足以實現我們想要的功能。該演算法分兩步:首先收集屬性名稱,然後將它們新增到類中。收集步驟遍歷 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 鉤子的擴充套件也將工作。雖然 PEP 253 的最終目標是廢棄 ExtensionClass,但我相信 ExtensionClass 在 Python 2.2 中仍應工作,不早於 Python 2.3 中打破它。

其他主題

這些主題也應該討論

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

參考文獻