Python屬性和方法

轉載出處:http://abyssly.com/2013/08/19/python_attributes_and_methods/

關於此文

解釋了Python新式對象的屬性訪問機制:

  • 函數是如何成爲方法的?

  • descriptor和property是如何工作的?

  • 如何確定方法解析順序(method resolution order)?

新式對象是從Python2.2版本開始引入的。在2.2及其以上的各版本之中存在一些差別,但這裏涉及到的所有概念都是OK的。

開始之前

你需要注意的幾點:

  • 本文講述的是新式對象(在很久之前的Python2.2中就已引入),下面的示例對於Python2.5及其以上版本適用。

  • 本文不適合什麼都不會的新手,如果要理解本文,至少需要知道一點Python,而且想知道更多。

  • 你應該對Python中的不同類別的對象較熟悉,當你期望類(class)而看到類型(type)時也不應該感到困惑。你可以先閱讀這個系列的第一部分Python Types and Objects,瞭解一下背景知識。

第一章 新的屬性訪問(Attribute Access)

動態的dict

屬性是什麼?很簡單,屬性是通過一個對象得到另一個對象的方式。藉助全能的點操作符——objectname.attributename——你就得到了另一個對象。你也能創造屬性,通過賦值操作符:objectname.attributename = notherobject

但是訪問一個屬性會返回哪個對象?屬性對應的對象在哪個地方?這些問題會在這章得到解答。

例1.1. 簡單的屬性訪問

>>> class C(object):
...     classattr = "attr on class" #1
...
>>> cobj = C()
>>> cobj.instattr = "attr on instance" #2
>>>
>>> cobj.instattr #3
'attr on instance' 
>>> cobj.classattr #4
'attr on class'
>>> C.__dict__['classattr'] #5
'attr on class'
>>> cobj.__dict__['instattr'] #6
'attr on instance'
>>>
>>> cobj.__dict__ #7
{'instattr': 'attr on instance'}
>>> C.__dict__ #8
{'classattr': 'attr on class', '__module__': '__main__', '__doc__': None}
  • 1 屬性可以在一個類上設置。

  • 2 或者甚至設在類的實例上。

  • 3,4 從一個實例可以同時訪問類的屬性和實例的屬性。

  • 5,6 屬性實際上包含在對象的一個類似字典的__dict__裏面。

  • 7,8 __dict__只包含了用戶提供的屬性。

好,我承認'用戶提供的屬性'是我造出來的術語,但我覺得它有助於你更好的理解。注意__dict__本身是一個屬性。我們並沒有自己去設置這個屬性,但是Python提供了它。我們的老朋友__class____bases__(儘管似乎沒有哪個在__dict__裏面)也類似。讓我們稱它們爲Python提供的屬性。一個屬性是否是Python提供的取決於所討論的對象(例如__bases__只是對於類來說是Python提供的)。

不過我們更感興趣的是用戶定義的屬性。它們由用戶提供的,而且通常(並非總是)會出現在所在對象__dict__中。

當一個屬性被訪問時(例如,print objectname.attributename),會依次在下面的對象上搜索它:

  1. 對象自身(objectname.__dict__或者objectname的任意一個Python提供的屬性)。

  2. 對象的類型(objectname.__class__.__dict__)。注意只有__dict__被搜索,這意味着只有用戶提供的類的屬性。換句話說,objectname.__bases__可能不會返回任何東西,即使objectname.__class__.__bases__存在。

  3. 對象的類的基類,基類的基類,一直下去。(objectname.__class__.__bases__中的每一個類的__dict__)。多個基類不會使Python混淆,我們也暫時不用考慮。重點是所有的基類都會被搜索直到屬性被找到。

如果所有的努力都沒有成功找到名字相符的屬性,Python會拋出異常AttributeError。訪問某對象(例子中的objectname)的屬性時,其類型的類型(objectname.__class__.__class__)永遠不會被搜索。

內建函數dir()返回一個對象的全部屬性的列表。也可以看看標準庫中的inspect module,裏面有更多的考察對象的函數。

上面的小節解釋了對所有對象通用的機制。甚至對於類也適用(例如訪問classname.attrname),只需要一點輕微的修改:類的基類先於類的類(即classname.__class__,對於大部分類型來說都是<type 'type'>)被搜索。

一些對象,比如內建類型及它們的實例(列表,元組等)沒有__dict__。因此它們不能接受用戶定義的屬性。

還沒完!這只是故事的濃縮版。當設置和獲取屬性時有更多的事情發生,下面的章節會介紹。

從函數到方法

繼續我們的Python實驗:

例1.1. 考察函數

>>> class C(object):
...     classattr = "attr on class"
...     def f(self):
...             return "function f"
...
>>> C.__dict__ #1
{'classattr': 'attr on class', '__module__': '__main__',
 '__doc__': None, 'f': <function f at 0x008F6B70>}
 >>> cobj = C()
 >>> cobj.classattr is C.__dict__['classattr'] #2
 True
 >>> cobj.f is C.__dict__['f'] #3
 False
 >>> cobj.f #4
 <bound method C.f of <__main__.C instance at 0x008F9850>>
 >>> C.__dict__['f'].__get__(cobj, C) #5
 <bound method C.f of <__main__.C instance at 0x008F9850>>
  • 1 兩個看起來正常的類屬性,字符串'classattr'和函數'f'

  • 2 訪問'classattr'屬性實際上是從類的__dict__中獲得,這在意料之中。

  • 3 對於函數卻不是這樣的!爲什麼?

  • 4 嗯,它確實看起來像一個不同的對象。(綁定方法是一個可調用(callable)的對象,它在被調用時,會將一個實例(例子中的cobj)作爲第一個參數添加到被提供的參數之前一起傳給一個函數(例子中的C.f),這是實例中的方法運行的機制。)

  • 5 劇透一下,Python就是這樣來創建綁定方法的。當查找一個實例的屬性時,如果Python在類的__dict__中找到的對象擁有__get__()方法,Python不會直接返回這個對象,而是返回它調用__get__()方法後的結果。注意__get__()方法被調用時傳的第一個參數是實例,第二個參數是類。

僅僅是由於__get__()方法的存在使得一個普通函數變爲一個綁定方法。函數對象真沒啥特別的,任何人都可以在類的__dict__中放置帶__get__()方法的對象。這樣的對象被叫做descriptor,有很多用處。

創建descriptor

任何對象,只要具有__get__()方法,以及可有可無的__set__()__delete__()方法,並接受特定的參數,就被認爲遵循descriptor協議。這樣的對象由此成爲descriptor,可以被放到一個類的__dict__之中。當相應的屬性被獲取、設置或者刪除時,descritor能夠做一些特別的事。一個空的descriptor如下例所示。

例1.3. 一個簡單的descriptor

class Desc(object):
    "A descriptor example that just demonstrates the protocol"

    def __get__(self, obj, cls=None): #1
            pass

    def __set__(self, obj, val): #2
            pass

    def __delete__(self, obj): #3
            pass
  • 1 讀屬性時被調用(例如print objectname.attrname)。這裏的obj是要訪問的屬性所在的對象(如果是直接訪問類的屬性,則爲None,例如print classname.attrname)。clsobj的類(當直接訪問類的屬性時,cls就是類本身,此時objNone)

  • 2 在實例上設置屬性時被調用(例如objectname.attrname = 12)。這裏的obj是要設置屬性的對象,val是作爲屬性值的對象。

  • 3 刪除實例的屬性時被調用(例如del objectname.attrname)。這裏的obj是要刪除的屬性所在的對象。

我們在上面定義了一個類,實例化這個類就可以創建一個descriptor。讓我們看看怎樣創建descriptor並將它附在一個類中以便讓它起作用。

例1.4. 使用descriptor

class C(object):
    "A class with a single descriptor"
    d = Desc() #1

cobj = C()

x = cobj.d #2
cobj.d = "setting a value" #3
cobj.__dict__['d'] = "try to force a value" #4
x = cobj.d #5
del cobj.d #6

x = C.d #7
C.d = "setting a value on class" #8
  • 1 現在屬性d就是一個descriptor。(使用了上一個例子中的Desc。)

  • 2 調用d.__get__(cobj, C),返回值被綁定到x。這裏的d指的是#1中定義的Desc的實例,即C.__dict__['d']

  • 3 調用d.__set__(cobj, "setting a value")

  • 4 直接在實例的__dict__中強行給d賦值,沒有問題,但是...

  • 5 沒有用,仍然調用d.__get__(cobj, C)

  • 6 調用d.__delete__(cobj)

  • 7 調用d.__get__(None, C)

  • 8 不會調用什麼,會將descriptor替換成一個新的字符串對象。在這之後,訪問cobj.d或者C.d只會返回字符串"setting a value on class",descriptor被踢出了C__dict__

注意當從類本身訪問時,只有__get__()方法會起作用,在類上直接設置或刪除屬性會替換或移除相應的descriptor。

只有附在類中,descriptors纔會起作用。將descriptor放在一個不是類的對象中沒有任何用處。

兩種descriptor

在前面的例子中,我們使用了一個同時具有__get__()__set__()方法的descriptor,這樣的descriptor叫做數據(data) descriptor。只帶有__get__()方法的descriptor要稍微弱一些,稱爲非數據(non-data) descriptor

重複我們的實驗,不過這次用非數據descriptor:

例1.5. 非數據descriptor

class GetonlyDesc(object):
    "Another useless descriptor"

    def __get__(self, obj, typ=None):
        pass

class C(object):
    "A class with a single descriptor"
    d = GetonlyDesc()

cobj = C()

x = cobj.d #1
cobj.d = "setting a value" #2
x = cobj.d #3
del cobj.d #4

x = C.d #5
C.d = "setting a value on class" #6
  • 1 調用d.__get__(cobj, C)(和以前一樣)。

  • 2 將"setting a value"放置在實例自己中(準確的說是在cobj.__dict__中)。

  • 3 很奇怪!返回的是"setting a value",即cobj.__dict__中的值。而之前對於數據descriptor,實例的__dict__被繞過。

  • 4 刪除實例的d屬性(準確的說是從cobj.__dict__中刪除)。

  • 5,6 和數據descriptor的表現一樣。

有趣的是,沒有__set__()不僅影響屬性設置,還影響屬性的獲取。Python在想什麼?如果設置屬性的時候,descriptor失去作用並把數據放在某個地方,那麼就只有descriptor知道如何取回數據。還有什麼必要改變實例的__dict__

數據descriptor對於提供某個屬性的全部控制非常有用。人們對於用來存儲數據的屬性,通常都會使用數據descriptor。舉個例子,如果一個屬性在設置時會被轉化並存儲在某個地方,那麼當我們讀取它時一般會還原並返回它。當你有一個數據descriptor時,它會接管實例上對相應屬性的所有訪問(讀和寫)。當然,你還是可以直接去中替換descriptor,但你不能從類的實例中做這件事。

相反的是,非數據descriptor,只會在實例自身沒有值的時候提供一個值。因此在一個實例上設置相應的屬性會隱藏descriptor。這對於函數(函數就是非數據descriptor)的情況特別有用,因爲它允許通過在實例上附上一個函數來隱藏定義在類中的同名函數。

例1.6. 隱藏一個方法

 class C(object):
     def f(self):
         return "f defined in class"

 cobj = C()

 cobj.f() #1

 def another_f():
     return "another f"

 cobj.f = another_f

 cobj.f() #2
  • 1 調用被f.__get__(cobj, C)返回的綁定方法。實質上最終調用的是C.__dict__['f'](cobj)

  • 2 調用another_f()。定義在C中的函數f()被隱藏了。

屬性搜索總結

這是屬性訪問故事的長版本,僅僅是爲了完整性而包含在這裏。

當從一個對象中獲取某個屬性(print objectname.attrname)時,Python會執行下列步驟:

  1. 如果attrname對於objectname來說一個特殊的(比如是Python提供的)屬性,返回它。

  2. objectname.__class__.__dict__中查找attrname,如果存在且是一個數據descriptor,返回descriptor結果。在objectname.__class__的全部基類中做同樣的搜索。

  3. objectname.__dict__中查找attrname,如果找到則返回。如果objectname是一個類,也搜索它的基類。如果它是一個類而且在它或它基類中存在descriptor,返回descriptor結果。

  4. objectname.__class__.__dict__中查找attrname,如果存在且是一個非數據descriptor,返回descriptor結果。如果存在且不是一個descriptor,返回它。如果存在且是一個數據descriptor,我們不應該遇到這種情況,因爲這種情況在第2步中就返回了。在objectname.__class__的全部基類中做同樣的搜索。

  5. 拋出AttributeError異常。

注意Python首先會在類(和類的基類)中查找數據descriptor,然後是在對象的__dict__中查找這個屬性,然後是在(和類的基類)中查找非數據descriptor。這分別對應於上述第2、3、4步。

上面所說的descriptor結果指的是通過合適的參數調用descriptor的__get__()方法的結果。另外,在__dict__中查找attrname指的是檢查__dict__["attrname"]是否存在。

現在,看看設置一個用戶定義的屬性時Python採取的步驟(objectname.attrname = something):

  1. objectname.__class__.__dict__中查找attrname。如果存在且是一個數據descriptor,使用descriptor去設置值。在objectname.__class__的全部基類中做同樣的搜索。

  2. objectname.__dict__中對鍵attrname插入值something

  3. 感覺“哇,這簡單得多了!”

當設置一個Python提供的屬性時會發生什麼取決於要設置的屬性。Python可能甚至不會允許某些屬性的設置。刪除屬性和上面所說的設置屬性非常類似。

Python提供的descriptor

在你創建自己的descriptor之前,注意Python已經自帶了一些非常有用的descriptor。

例1.7. 內建descriptor

class HidesA(object):

    def get_a(self):
        return self.b - 1

    def set_a(self, val):
        self.b = val + 1

    def del_a(self):
        del self.b

    a = property(get_a, set_a, del_a, "docstring") #1


    def cls_method(cls):
        return "You called class %s" % cls

    clsMethod = classmethod(cls_method) #2


    def stc_method():
        return "Unbindable!"

    stcMethod = staticmethod(stc_method) #3
  • property提供了一種簡單的方式去調用函數,當實例上的屬性被獲取、設置或刪除時。當屬性從類中直接獲取時,getter方法不會被調用,而是返回property對象本身。還可以提供一個docstring,通過HidesA.a.__doc__可以訪問到。

  • classmethod類似於常規的方法,只是它會將類(不是實例)作爲第一個參數傳給相應函數。剩餘的參數和通常一樣進行傳遞。它也能夠直接在類上調用,並且具有相同的行爲。第一個參數被命名爲cls而非傳統的的self,是爲了清楚地表達出它的含義。

  • staticmethod就像類外面的一個函數。它永遠不會綁定,意味着無論你怎麼訪問它(通過類或者實例),它都會原原本本地接收你傳的參數,不會有對象作爲第一個參數插入。

我們之前看到,Python的函數也是descriptor,它們在Python的早期版本中並不是descriptor(因爲那時根本就沒有descriptor),但是現在它們變成了一般的機制。

property總是數據descriptor,但在定義property是並非全部參數都是必需的。

Example 1.8. More on properties

class VariousProperties(object):

例1.8. 關於property的更多

class VariousProperties(object):

    def get_p(self):
        pass

    def set_p(self, val):
        pass

    def del_p(self):
        pass

    allOk = property(get_p, set_p, del_p) #1

    unDeletable = property(get_p, set_p) #2

    readOnly = property(get_p) #3
  • 1 可以被設置、獲取或刪除。

  • 2 嘗試從實例中刪除這個屬性會拋出AttributeError

  • 3 嘗試設置或從實例中刪除這個屬性會拋出AttributeError

getter和setter函數不一定要在class自身裏面定義,任何函數都可以被使用。任何情況下,這些函數調用時都會接受實例作爲第一個參數。注意在上例中這些函數被傳給property構造器的地方,它們還不是綁定函數。

另一個需要注意的有用的結論是,子類化這個類並重定義getter(或setter)函數並不會改變property。property始終指向剛開始被定義時提供的函數,它會說“嘿我會一直保存你給我的這個函數,我只會調用這個函數然後返回結果。”,而不是說“嗯,讓在當前的類中去查找一個叫'get_a'的方法然後利用那個方法”。如果那是你想要的,就自己去定義一個新的descriptor吧。怎麼弄?我們可以在它初始化時傳一個字符串(例如要調用的方法名),當被激活時,它通過getattr()去當前類中找這個方法名,然後利用所找到的方法。簡單吧!

classmetod和staticmethod是非數據descriptor,所以如果有一個同名的屬性在實例上直接被設置時它們就被隱藏。如果你在定義自己的descriptor(而且沒有使用property),你可以通過給它一個__set__()方法但是在方法中拋出AttributeError的方式使它變爲只讀,這就是property沒有setter函數時的行爲。

第二章 方法解析順序(Method Resolution Order)

問題(方法解析的混亂)

爲什麼我們需要方法解析順序?

  1. 我們很樂意進行面向對象編程並創建不同層次的類。

  2. 我們實現do_your_stuff()方法通常採用的技術是首先調用基類的do_your_stuff(),然後做我們自己的事情。

例2.1. 通常的基類調用技術

class A(object):
    def do_your_stuff(self):
        # do stuff with self for A
        return

class B(A):
    def do_your_stuff(self):
        A.do_your_stuff(self)
        # do stuff with self for B
        return

class C(A):
    def do_your_stuff(self):
        A.do_your_stuff(self)
        # do stuff with self for C
        return
  1. 我們從兩個類中繼承生成一個新的子類,於是就存在一個超類可以通過兩條路徑被訪問到。

例2.2. 基類調用技術失敗

class D(B,C):
    def do_your_stuff(self):
        B.do_your_stuff(self)
        C.do_your_stuff(self)
        # do stuff with self for D
        return

圖2.1 鑽石圖示

圖2.1 鑽石圖示

  1. 現在如果我們想實現do_your_stuff()就卡住了。使用我們通常用的技術,如果我們同時調用BC,我們就會調用A.do_your_stuff()兩次。我們當然知道,如果A只是準備被調用一次的話,調用A兩次就可能會有問題。另一個選擇是我們只調用B或只調用C,但這也不是我們想要的結果。

對於這個問題,有很麻煩的解決辦法,也有乾淨的。明顯Python實現了一個乾淨的,在下節中會解釋。

"誰是下一個"列表

比如說:

  1. 對每一個類,我們將全部的超類無重複地放到一個有序的列表裏,然後將這個類本身插入到這個列表的頭部。我們將這個列表放到一個叫做next_class_list的類屬性中供後面用。

例2.3. 創建"誰是下一個"列表

B.next_class_list = [B,A]

C.next_class_list = [C,A]

D.next_class_list = [D,B,C,A]
  1. 我們使用一種不同的技術來爲我們的類實現do_your_stuff()

例2.4. 調用下一個方法技術

class B(A):
    def do_your_stuff(self):
        next_class = self.find_out_whos_next()
        next_class.do_your_stuff(self)
        # do stuff with self for B

    def find_out_whos_next(self):
        l = self.next_class_list           # l depends on the actual instance
        mypos = l.index(B) #1             # Find this class in the list
        return l[mypos+1]                  # Return the next one

有意思的部分在於我們如何find_out_whos_next(),這依賴於我們正在操作哪個實例。注意:

  • 根據我們傳遞的是D還是B的實例,next_class會被解析成C或者A

  • 我們需要爲每個類實現find_out_whos_next(),因爲它需要將類名硬編碼在裏面(看上面代碼中的1處)。我們不能在這裏使用self.__class__,如果我們在D的一個實例上調用do_your_stuff(),這個調用會沿着層次上溯,那麼這裏的self.__class__會是D

使用這個技術,每個方法只會被調用一次。它顯得很乾淨,但似乎需要做太多的工作。對我們幸運的是,我們既不需要爲每個類實現find_out_whos_next(),也不需要去設置next_class_list,因爲Python幫我們把這兩件事都做了。

Super解決辦法

Python爲每個類提供了一個叫做__mro__的類屬性,還提供了一個叫做super的類型。__mro__屬性是一個元組,這個元組包含了類本身和它的全部超類,以一種可預測的沒有重複的方式。super對象被用來代替find_out_whos_next()方法。

例2.5. super技術

class B(A): #1
    def do_your_stuff(self):
        super(B, self).do_your_stuff() #2
        # do stuff with self for B
  • super()調用創建一個super對象。它在self.__class__.__mro__之中查找B之後的下一個類。在super對象上訪問的屬性在下一個類上搜索並返回,descriptors也會得到解析。這意味着訪問某個方法(像上面一樣)返回的是綁定方法(注意do_your_stuff()調用並沒有傳遞self)。當使用super()時,第一個參數應該始終和它所在的類相同(例中1處)。

如果我們使用的是一個類方法,我們就不會有一個叫self的實例傳給super調用。幸運的是,即使第二個參數是一個類,super依然可以照常工作。觀察上例,super使用self僅僅是爲了得到self.__class__.__mro__。類可以直接被傳給super,像下例中一樣。

例2.6. 通過類方法使用super

class A(object):
    @classmethod #1
    def say_hello(cls):
        print 'A says hello'

class B(A):
    @classmethod
    def say_hello(cls):
        super(B, cls).say_hello() #2
        print 'B says hello'

class C(A):
    @classmethod
    def say_hello(cls):
        super(C, cls).say_hello()
        print 'C says hello'

class D(B, C):
    @classmethod
    def say_hello(cls):
        super(D, cls).say_hello()
        print 'D says hello'

B.say_hello() #3
D.say_hello() #4
  • 1 這個例子說的是類方法(不是實例方法)。

  • 2 注意我們傳給super()的是cls(類而非實例)。

  • 3 這個會打印出:

    A says hello B says hello

  • 4 這個會打印出(注意每個方法只被調用了一次):

    A says hello C says hello B says hello D says hello

還有另一種使用super的方式:

例2.7. 另一個super技術

class B(A):

    def do_your_stuff(self):
        self.__super.do_your_stuff()
        # do stuff with self for B

B._B__super = super(B) #1

當只用類型來創建時,super實例表現就像descriptor。這意味着(如果dD的實例)super(B).__get__(d)super(B,d)返回的東西一樣。在上面的1處,我們改寫了一個屬性名,類似於Python對於類裏面的以雙下劃線開頭的名字所做的事。因此它在類中能夠以self.__super的形式被訪問。如果我們不使用一個特定於類的屬性名,通過實例self訪問屬性可能會返回一個定義在子類中的對象。

在使用super時,我們一般在一個方法中只會調用一次super,即使類有多個基類。而且,使用super代替直接調用基類上的方法是一個好的編程習慣。

如果do_your_stuff()CA分別接受的是不同的參數,一個可能的陷阱就會出現。因爲,如果我們在B中使用super來調用下一個類的do_your_stuff(),我們不知道它是會在A還是C上被調用。如果這種場景不可避免,那就需要特定問題特別對待了。

計算方法解析順序(MRO)

一個還未回答的問題是Python是如何決定一個類型的__mro__的?這節會解釋其算法後面的基本思想。對於使用super或者閱讀後面的章節這並不是必要的,所以你可以跳到下一節,如果你想的話。

Python通過兩種由用戶指定的限制來決定類型的優先級(或者說它們在任意_mro__中應該按何種順序放置):

  1. 如果AB的超類,則B的優先級高於A。或者說,在所有的__mro__(同時包含了兩者)中,B應該總是出現在A之前。爲了簡潔,我們將這個關係表示爲B > A

  2. 如果在類語句(例如class Z(C,D):)的基類列表中,C出現在D之前,則C > D

另外,爲了避免含混,Python堅持下面的原則:

  1. 如果在一個場景(或者一個__mro__)中E > F,那麼在所有場景(或者所有__mro__)中E > F

我們能夠滿足上述限制,如果在我們爲每一個我們引入的新的類C構建__mro__時,使得:

  1. C全部超類出現在C.__mro__(加上C本身,在最前面)之中,而且

  2. 對於C.__bases__裏面的每個BC.__mro__裏面的類型的優先級不會與B.__mro__中類型的優先級衝突。

這裏將這個問題轉換成一個遊戲。考慮一個類層次如下所示:

圖2.2 簡單的層次

圖2.2 簡單的層次

因爲只有單繼承,所以找出這些類的__mro__很容易。比如我們定義了一個新的類class N(A,B,C),爲了計算__mro__,考慮玩一個遊戲,這個遊戲使用串在一系列線上的像算盤一樣的珠子。

圖2.3 串在線上的珠子-不可解

圖2.3 串在線上的珠子-不可解

珠子能夠在線上自由移動,但是線不能被剪斷或扭曲。從左到右的的線按順序包含了每個基類的__mro__。最右邊的線包含着對應每個基類的小珠,按照類定義中基類聲明的順序。

目標是將珠子按行排列整齊,使得每行只包含同一種標籤的珠子(就像圖中的O珠一樣)。每根線都代表着一個順序限制,如果我們能達到目標,我們就能得到一種滿足全部限制的順序。那樣的話我們就只需要從底往上讀每行的標籤來得到N__mro__

不幸的是,我們不能解決這個問題。後兩根線上的CB有不同的順序。但是,如果我們改邊類定義爲class N(A,C,B),我們就有了希望。

圖2.4 串在線上的珠子-可解

圖2.4 串在線上的珠子-可解

我們可以發現N.__mro__(N,A,C,B,object)(注意我們將N插到了前面)。讀者可以在真正的Python裏面嘗試這個試驗(對於上述不可解的情形,Python會拋出異常)。注意我們甚至交換了兩根線的位置,以保持線的順序和類定義中指定的基類的順序一致。這樣做的用處在後面會看到。

有時,可能存在不止一種解法,就像下圖中所示的。考慮4個類class A(object)class B(A)class C(object)class D(C)。如果一個新類被定義爲class E(B, D),就存在多種可能的滿足所有限制的解法。

圖2.5 多種解法

圖2.5 多種解法

A的可能的位置在圖中以小珠子示出。如果採取下面的策略,則可以使順序變得唯一:

  1. 按照類定義中基類出現的順序從左到右排列各線。

  2. 嘗試按行從底往上、從左往右排列小珠。這意味着class E(B, D)的MRO會被設置成:(E,B,A,D,C,object)。這是因爲A,由於在C的左邊,於是在從底往上第二行時會先被考慮。

這本質上就是Python用於爲新類型產生__mro__的算法的背後思想。關於這個算法的正式的描述可以參見mro-algorithm

第三章 用法說明

這一章包括一些沒有在其他章節中提到的用法說明。

特殊方法

在Python中,我們可以使用一些帶有特殊名字的方法,例如__len__()__str__()__add__(),去使得對象便於被使用(例如,通過內建函數len()str(),或者+操作符,等等)。

例3.1. 特殊方法只能用在類型上

class C(object):
    def __len__(self): #1
        return 0

cobj = C()

def mylen():
    return 1

cobj.__len__ = mylen #2

print len(cobj) #3
  • 1 通常我們將特殊方法放在類中。

  • 2 我們可以試着將它們放到實例本身中,但不會起作用。

  • 3 這會直接去到類中(調用C.__len__()),而不是實例。

對於所有這樣的方法都是如此,將它們放在我們想要應用的實例上不會起作用。如果真的去實例中,那麼甚至像str(C)(類Cstr)都會調用C.__str__(),而它是爲C的實例定義的,並非爲C本身。

想允許爲每個實例分別定義這樣的方法,可以採用如下所示的簡單技術:

例3.2. 將特殊方法傳遞給實例

class C(object):
    def __len__(self): #1
        return self._mylen() 

    def _mylen(self): #2
        return 0

cobj = C()

def mylen():
    return 1

cobj._mylen = mylen #3

print len(cobj) #4
  • 1 我們調用實例上的另一個方法,

  • 2 在類中我們提供這個方法的一個默認實現。

  • 3 但是它能夠通過設置在實例上而被覆蓋(更確切地說是隱藏)。

  • 4 現在調用的是mylen()

子類化內建類型

子類化內建類型很簡單。實際上我們一直在這樣做(當我們子類化<type 'object'>時)。有一些內建類型(例如types.FunctionType)不可被子類化(至少目前還不能)。但是,我們這裏討論的是子類化<type 'list'>,<type 'tuple'>和其他的基本數據類型。

例3.3. 子類化<type 'list'>

>>> class MyList(list): #1
...     "A list that converts appended items to ints"
...     def append(self, item): #2
...         list.append(self, int(item)) #3
...
>>>
>>> l = MyList() 
>>> l.append(1.3) #4
>>> l.append(444) 
>>> l
[1, 444] #5
>>> len(l) #6
2
>>> l[1] = 1.2 #7
>>> l
[1, 1.2]
>>> l.color = 'red' #8
  • 1 常規的class定義語句

  • 2 定義要覆蓋的方法。在這個例子中我們將所有傳給append()的項都轉換成整數。

  • 3 如果有需要可以向上調用基類的方法。list.append()像一個未綁定的方法一樣工作,被傳入的第一個參數是實例。

  • 4 append一個浮點數,然後...

  • 5 看到它自動變成了整數。

  • 6 除此以外,它跟任何其他的list行爲一樣。

  • 7 這個操作不會經過append。我們需要在我們的類中定義__setitem__()去操縱我們的數據。相應向上的基類調用是list.__setitem__(self,item)。注意像__setitem__這樣的特殊方法存在於內建類型上。

  • 8 我們能夠在我們的實例上設置屬性,因爲它具有__dict__

基本的list沒有__dict__(因此沒有用戶定義的屬性),但是我們的有。這通常不是個問題而且甚至是我們想要的。但是,如果我們使用非常大量的MyList,我們可以優化我們的程序,通過告訴Python不爲MyList的實例創建__dict__

例3.4. 使用stots優化程序

class MyList(list):
    "A list subclass disallowing any user-defined attributes"

    __slots__ = [] #1

ml = MyList()
ml.color = 'red' # raises exception! #2

class MyListWithFewAttrs(list):
    "A list subclass allowing specific user-defined attributes"

    __slots__ = ['color'] #3

mla = MyListWithFewAttrs()
mla.color = 'red' #4
mla.weight = 50 # raises exception! #5
  • __slots__類屬性告訴Python不爲這個類型的實例創建__dict__

  • 2 在它上面設置任何一個屬性都會拋出異常。

  • __slots__可以包含一個字符串的列表。實例仍然不會爲__dict__分配一個真正的字典,而是得到一個代理。Python在實例中爲指定的屬性保留空間。

  • 4 現在,如果屬性有保留的空間,它就能被使用。

  • 5 否則,就不行,會拋出異常。

__slots__的目的和建議用法是優化。當一個類型被定義後,它的slots不能被改變。而且,每個子類都必須定義__slots__,否則,子類的實例仍然會有__dict__

我們甚至能夠像實例化任意其他類型一樣實例化list來創建它:list([1,2,3])。這意味着list.__init__()接受同樣的參數(比如任何iterable)然後初始化一個list。我們可以在子類中定製初始化過程,通過重定義__init__()然後向上調用基類的__init__()方法。

元組是不可變的,和列表不同。一旦一個實例被創建,它就不能被改變。注意當__init__()方法被調用時,類型的實例已經存在(事實上實例是被作爲第一人蔘數傳遞的)。類型的靜態方法__new__()被調用來創建類型的一個實例。類型本身被作爲第一個參數傳遞,然後是其他初始化參數(類似於__init__())。我們用此來定製像元組這樣的不可變類型。

例3.5. 定製子類的創建

class MyList(list):

    def __init__(self, itr): #1
        list.__init__(self, [int(x) for x in itr])

class MyTuple(tuple):

    def __new__(typ, itr): #2
        seq = [int(x) for x in itr]
        return tuple.__new__(typ, seq) #3
  • 1 對於列表,我們操縱參數然後將它們傳遞給list.__init__()

  • 2 以於元組,我們需要覆蓋__new__()

  • __new__()應該總是返回。它應該返回類型的一個實例。

__new__()方法不是爲不可變類型而特別存在的,它用來所有類型裏。而且它會被Python自動轉換成靜態方法(根據它的名字)。

相關資料

descrintro. Guido van Rossum.

pep-252. Guido van Rossum.

pep-253. Guido van Rossum.

descriptors-howto. Raymond Hettinger.

mro-algorithm. Michele Simionato.


發佈了144 篇原創文章 · 獲贊 40 · 訪問量 102萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章