Python 方法調用機制


來源:http://blog.csdn.net/yuxinleipp/article/details/7869290

 

內容目錄

  1. 介紹
  2. 構建和初始化
  3. 使操作符在自定義類內工作
  4. 描述你的類
  5. 屬性訪問控制
  6. 制作自定義序列
  7. 反射
  8. 可調用對象
  9. 上下文管理
  10. 構建描述符對象
  11. Pickling你的對象
  12. 總結
  13. 附錄:如何調用神奇方法

 


 

1.介紹

這份指南是幾個月內最有價值的Blog投稿精華。它的主題是向大家講述Python中的神奇方法。
何為神奇方法呢?它們是面向Python中的一切,是一些特殊的方法允許在自己的定義類中定義增加“神奇”的功能。它們總是使用雙下划線(比如__init__或__lt__),但它們的文檔沒有很好地把它們表現出來。所有這些神奇方法都出現在Python的官方文檔中,但內容相對分散,組織結構也顯得松散。還有你會難以發現一個實例(雖然他們被設計很棒,在語言參考中被詳細描述,可之后就會伴隨着枯燥的語法描述等)。
所以,為了解決我認為在Python文檔中的一大敗筆,我打算用更多純英語,實例驅動的文檔來說明Python的神奇方法。然后我就開始花了幾周的時間來寫blog,而現在我已經完成了它們,並將它們合訂成一份指南。
我希望你喜歡它。把它當作一個教程,進階,或者使用參考;我希望它能夠成為一份針對Python方法的用戶友好指南。


2.構建和初始化
相信大家都熟悉這個最基礎的神奇方法__init__。它令你能自定義一個對象的初始化行為。而當我調用x=SomeClass()時,__init__並不是最先被調用的。實際上有一個叫做__new__的方法,事實上是它創建了實例,它傳遞任何參數給初始化程序來達到創建的目的。在對象生命周期結束時,調用__del__。讓我們更近地觀察下這3個神奇方法吧:

__new__(cls,[...)

一個對象的實例化時__new__是第一個被調用的方法。在類中傳遞其他任何參數到__init__。__new__很少被使用,這樣做確實有其目的,特別是當一個子類繼承一個不可改變的類型(一個元組或一個字符串)時。我不打算再繼續深入追求__new__的細節了,因為這不會產生多大用處,因為在Python Docs內已經涵蓋了一份巨詳細的說明了。



__init__(self,[...)
類的初始化。它會獲得初始構建調用傳過來的任何東西(舉例來說就是,當我們調用x=SomeClass(10,'foo'),__init__就會把傳過來的10和'foo'作為參數。__init__在Python的類定義中幾乎普遍被使用)


__del__(self)
如果__new__和__init__是對象的構造器,那么__del__就是析構器。它不實現聲明為del x(這樣的代碼不會解釋成x.__del__())的行為。相反,它定義為當一個對象被垃圾回收時的行為。這可能對可能需要額外清理的對象相當有用,比如sockets或文件對象。但要小心,如果對象仍處於存活狀態而當被解釋退出時,__del__沒有保證就會被執行,因此這樣的__del__不能作為良好的編碼規范的替代。(就像當你完成操作總是要關閉一次連接。但事實上,__del__幾乎永遠不會執行,就因為它處於不安全情況被調用了。使用時保持警惕!)


把上述這些內容合在一起,就成了一份__init__和__del__的實際使用用例:

 

1
2
3
4
5
6
7
8
9
10
11
from os.path import join
class FileObject:
    '''對文件對象的包裝,確保文件在關閉時得到刪除'''
                                                     
    def __init__(self, filepath='~', filename='sample.txt'):
        # 按filepath,讀寫模式打開名為filename的文件
        self.file=open(join(filepath,filename), 'r+')
                                                         
    def __del__(self):
        self.file.close()
        del self.file

 

 

 

3.使操作符在自定義類內工作

使用Python神奇方法的優勢之一就是它提供了一種簡單的方式能讓對象的行為像內建類型。這意味着你可以避免用丑陋,反直覺和非標准方法執行基本運算。在某些語言中,通常會這樣做:

 

1
2
if instance.equals(other_instance):
    # do something

 

 

 

 

你也應該在Python確實會這樣做,但同時它會增加用戶的疑惑以及不必要的冗長。不同的庫可能會對相同的運算采用不同的命名,這使得用戶比平常干了更多的事。依靠神奇方法的力量,你可以定義一個方法(比如__eq__),然后帶代替我們真實的意圖:

 

1
2
if instance == other_instance:
    # do something

 

現在你看到的是神奇方法力量的一部分。絕大多數都允許我們定義為運算符本身的意義,當用在我們自己定義的類上就像它們是內建類型。


3.1 神奇方法——比較
Python有一整套神奇方法被設計用來通過操作符實現對象間直觀的比較,而非別扭的方法調用。它們同樣提供了一套覆蓋Python對象比較的默認行為(通過引用)。以下是這些方法的列表以及做法:


__cmp__(self, other)
__cmp__是神奇方法中最基礎的一個。實際上它實現所有比較操作符行為(<,==,!=,等),但它有可能不按你想要的方法工作(例如,一個實例是否等於另一個這取決於比較的准則,以及一個實例是否大於其他的這也取決於其他的准則)。如果self < other,那__cmp__應當返回一個負整數;如果self == other,則返回0;如果self > other,則返回正整數。它通常是最好的定義,而不需要你一次就全定義好它們,但當你需要用類似的准則進行所有的比較時,__cmp__會是一個很好的方式,幫你節省重復性和提高明確度。


__eq__(self, other)
定義了相等操作符,==的行為。
__ne__(self, other)
定義了不相等操作符,!=的行為。
__lt__(self, other)
定義了小於操作符,<的行為。
__gt__(self, other)
定義了大於操作符,>的行為。
__le__(self, other)
定義了小於等於操作符,<=的行為。
__ge__(self, other)
定義了大於等於操作符,>=的行為。


舉一個例子,設想對單詞進行類定義。我們可能希望能夠按內部對string的默認比較行為,即字典序(通過字母)來比較單詞,也希望能夠基於某些其他的准則,像是長度或音節數。在本例中,我們通過單詞長度排序,以下給出實現:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Word(str):
    '''單詞類,比較定義是基於單詞長度的'''
                                            
    def __new__(cls, word):
        # 注意,我們使用了__new__,這是因為str是一個不可變類型,
        # 所以我們必須更早地初始化它(在創建時)
        if ' ' in word:
            print "單詞內含有空格,截斷到第一部分"
            word = word[:word.index(' ')] # 在出現第一個空格之前全是字符了現在
        return str.__new__(cls, word)
                                                
    def __gt__(self, other):
        return len(self) > len(other)
    def __lt__(self, other):
        return len(self) < len(other)
    def __ge__(self, other):
        return len(self) >= len(other)
    def __le__(self, other):
        return len(self) <= len(other)

 

 

現在,我們可以創建2個單詞(通過Word('foo')和Word('bar'))並基於它們的長度進行比較了。注意,我們沒有定義__eq__ 和 __ne__。這是因為這可能導致某些怪異的行為(特別是當比較Word('foo') == Word('bar')將會得到True的結果)。基於單詞長度的相等比較會令人摸不清頭腦,因此我們就沿用了str本身的相等比較的實現。


現在可能是一個好時機來提醒你一下,你不必重載每一個比較相關的神奇方法來獲得各種比較。標准庫已經友好地為我們在模板functools中提供了一個裝飾(decorator)類,定義了所有比較方法。你可以只重載__eq__和一個其他的方法(比如__gt__,__lt__,等)。這個特性只在Python2.7(后?)適用,但當你有機會的話應該嘗試一下,它會為你省下大量的時間和麻煩。你可以通過在你自己的重載方法在加上@total_ordering來使用。


3.2 神奇方法——數字
就像你可以通過重載比較操作符的途徑來創建你自己的類實例,你同樣可以重載數字操作符。系好你們的安全帶,朋友們,還有很多呢。處於本文組織的需要,我會把數字的神奇方法分割成5塊:一元操作符,常規算術操作符,反射算術操作符,增量賦值,類型轉換。


一元操作符
一元運算和函數僅有一個操作數,比如負數,絕對值等
__pos__(self)
實現一元正數的行為(如:+some_object)
__neg__(self)
實現負數的行為(如: -some_object)
__abs__(self)
實現內建abs()函數的行為
__invert__(self)
實現用~操作符進行的取反行為。你可以參考Wiki:bitwise operations來解釋這個運算符究竟會干什么


常規算術操作符
現在我們涵蓋了基本的二元運算符:+,-,*等等。其中大部分都是不言自明的。
__add__(self, other)
實現加法
__sub__(self, other)
實現減法
__mul__(self, other)
實現乘法
__floordiv__(self, other)
實現地板除法,使用//操作符
__div__(self, other)
實現傳統除法,使用/操作符
__truediv__(self, other)
實現真正除法。注意,只有當你from __future__ import division時才會有效
__mod__(self, other)
實現求模,使用%操作符
__divmod__(self, other)
實現內建函數divmod()的行為
__pow__(self, other)
實現乘方,使用**操作符
__lshift__(self, other)
實現左按位位移,使用<<操作符
__rshift__(self, other)
實現右按位位移,使用>>操作符
__and__(self, other)
實現按位與,使用&操作符
__or__(self, other)
實現按位或,使用|操作符
__xor__(self, other)
實現按位異或,使用^操作符


反射算術操作符
你知道我會如何解釋反射算術操作符?你們中的有些人或許會覺得它很大,很可怕,是國外的概念。但它實際上很簡單,下面給一個例子:

some_object + other

這是“常規的”加法。而反射其實相當於一回事,除了操作數改變了改變下位置:

other + some_object

因此,所有這些神奇的方法會做同樣的事等價於常規算術操作符,除了改變操作數的位置關系,比如第一個操作數和自身作為第二個。此外沒有其他的操作方式。在大多數情況下,反射算術操作的結果等價於常規算術操作,所以你盡可以在剛重載完__radd__就調用__add__。干脆痛快:
__radd__(self, other)
實現反射加法
__rsub__(self, other)
實現反射減法
__rmul__(self, other)
實現反射乘法
__rfloordiv__(self, other)
實現反射地板除,用//操作符
__rdiv__(self, other)
實現傳統除法,用/操作符
__rturediv__(self, other)
實現真實除法,注意,只有當你from __future__ import division時才會有效
__rmod__(self, other)
實現反射求模,用%操作符
__rdivmod__(self, other)
實現內置函數divmod()的長除行為,當調用divmod(other,self)時被調用
__rpow__(self, other)
實現反射乘方,用**操作符
__rlshift__(self, other)
實現反射的左按位位移,使用<<操作符
__rrshift__(self, other)
實現反射的右按位位移,使用>>操作符
__rand__(self, other)
實現反射的按位與,使用&操作符
__ror__(self, other)
實現反射的按位或,使用|操作符
__rxor__(self, other)
實現反射的按位異或,使用^操作符



增量賦值

Python也有各種各樣的神奇方法允許用戶自定義增量賦值行為。你可能已經熟悉增量賦值,它結合了“常規的”操作符和賦值。如果你仍不明白我在說什么,下面有一個例子:

 

1
2
x = 5
x += 1 # 等價 x = x + 1

 

這些方法都不會有返回值,因為賦值在Python中不會有任何返回值。反而它們只是改變類的狀態。列表如下:
__iadd__(self, other)
實現加法和賦值
__isub__(self, other)
實現減法和賦值
__imul__(self, other)
實現乘法和賦值
__ifloordiv__(self, other)
實現地板除和賦值,用//=操作符
__idiv__(self, other)
實現傳統除法和賦值,用/=操作符
__iturediv__(self, other)
實現真實除法和賦值,注意,只有當你from __future__ import division時才會有效
__imod__(self, other)
實現求模和賦值,用%=操作符
__ipow__(self, other)
實現乘方和賦值,用**=操作符
__ilshift__(self, other)
實現左按位位移和賦值,使用<<=操作符
__irshift__(self, other)
實現右按位位移和賦值,使用>>=操作符
__iand__(self, other)
實現按位與和賦值,使用&=操作符
__ior__(self, other)
實現按位或和賦值,使用|=操作符
__ixor__(self, other)
實現按位異或和賦值,使用^=操作符


類型轉換的神奇方法
Python也有一組神奇方法被設計用來實現內置類型轉換函數的行為,如float()
__int__(self)
實現到int的類型轉換
__long__(self)
實現到long的類型轉換
__float__(self)
實現到float的類型轉換
__complex__(self)
實現到復數的類型轉換
__oct__(self)
實現到8進制的類型轉換
__hex__(self)
實現到16進制的類型轉換
__index__(self)
實現一個當對象被切片到int的類型轉換。如果你自定義了一個數值類型,考慮到它可能被切片,所以你應該重載__index__
__trunc__(self)
當math.trunc(self)被調用時調用。__trunc__應當返回一個整型的截斷,(通常是long)
__coerce__(self, other)
該方法用來實現混合模式的算術。如果類型轉換不可能那__coerce__應當返回None。否則,它應當返回一對包含self和other(2元組),且調整到具有相同的類型



4.描述你的類

用一個字符串來說明一個類這通常是有用的。在Python中提供了一些方法讓你可以在你自己的類中自定義內建函數返回你的類行為的描述。

__str__(self)
當你定義的類中一個實例調用了str(),用於給它定義行為
__repr__(self)
當你定義的類中一個實例調用了repr(),用於給它定義行為。str()和repr()主要的區別在於它的閱讀對象。repr()產生的輸出主要為計算機可讀(在很多情況下,這甚至可能是一些有效的Python代碼),而str()則是為了讓人類可讀。
__unicode__(self)
當你定義的類中一個實例調用了unicode(),用於給它定義行為。unicode()像是str(),只不過它返回一個unicode字符串。警惕!如果用戶用你的類中的一個實例調用了str(),而你僅定義了__unicode__(),那它是不會工作的。以防萬一,你應當總是定義好__str__(),哪怕用戶不會使用unicode
__hash__(self)
當你定義的類中一個實例調用了hash(),用於給它定義行為。它必須返回一個整型,而且它的結果是用於來在字典中作為快速鍵比對。
__nonzero__(self)
當你定義的類中一個實例調用了bool(),用於給它定義行為。返回True或False,取決於你是否考慮一個實例是True或False的。


我們已經相當漂亮地干完了神奇方法無聊的部分(無示例),至此我們已經討論了一些基礎的神奇方法,是時候讓我們向高級話題移動了。



5.屬性訪問控制

有許多從其他語言陣營轉到Python來的人抱怨Python對類缺乏真正的封裝(比如,沒有辦法自定義private屬性,已經給出public的getter和setter)。這可不是真相喲:Python通過神奇的方法實現了大量的封裝,而不是通過明確的方法或字段修飾符。請看:

__getattr__(self, name)
你可以為用戶在試圖訪問不存在(不論是存在或尚未建立)的類屬性時定義其行為。這對捕捉和重定向常見的拼寫錯誤,給出使用屬性警告是有用的(只要你願意,你仍舊可選計算,返回那個屬性)或拋出一個AttributeError異常。這個方法只適用於訪問一個不存在的屬性,所以,這不算一個真正封裝的解決之道。
__setattr__(self, name, value)
不像__getattr__,__setattr__是一個封裝的解決方案。它允許你為一個屬性賦值時候的行為,不論這個屬性是否存在。這意味着你可以給屬性值的任意變化自定義規則。然而,你需要在意的是你要小心使用__setattr__,在稍后的列表中會作為例子給出。
__delattr__
這等價於__setattr__,但是作為刪除類屬性而不是set它們。它需要相同的預防措施,就像__setattr__,防止無限遞歸(當在__delattr__中調用del self.name會引起無限遞歸)。
__getattribute__(self, name)
__getattribute__良好地適合它的同伴們__setattr__和__delattr__。可我卻不建議你使用它。__getattribute__只能在新式類中使用(在Python的最新版本中,所有的類都是新式類,在稍舊的版本中你可以通過繼承object類來創建一個新式類。它允許你定規則,在任何時候不管一個類屬性的值那時候是否可訪問的。)它會因為他的同伴中的出錯連坐受到某些無限遞歸問題的困擾(這時你可以通過調用基類的__getattribute__方法來防止發生)。當__getattribute__被實現而又只調用了該方法如果__getattribute__被顯式調用或拋出一個AttributeError異常,同時也主要避免了對__getattr__的依賴。這個方法可以使用(畢竟,這是你自己的選擇),不過我不推薦它是因為它有一個小小的用例(雖說比較少見,但我們需要特殊行為以獲取一個值而不是賦值)以及它真的很難做到實現0bug。


你可以很容易地在你自定義任何類屬性訪問方法時引發一個問題。參考這個例子:

 

1
2
3
4
5
6
7
8
9
def __setattr__(self, name, value):
    self.name = value
    # 當每次給一個類屬性賦值時,會調用__setattr__(),這就形成了遞歸
    # 因為它真正的含義是 self.__setattr__('name', value)
    # 所以這方法不停地調用它自己,變成了一個無法退出的遞歸最終引發crash
                                     
def __setattr__(self, name, value):
    self.__dict__[name] = value # 給字典中的name賦值
    # 在此自定義行為

 

再一次,Python的神奇方法向我們展示了其難以置信的能力,同時巨大的力量也伴隨着重大的責任。重要的是讓你明白正確使用神奇方法,這樣你就不會破壞其他代碼。


那么,我們在關於定制類屬性訪問中學習了什么?不要輕易地使用,事實上它過於強大以及反直覺。這也是它為何存在的理由:Python尋求干壞事的可能性,但會把它們弄得很難。自由是至高無上的,所以你可以做任何你想做的事情。以下是一個關於特殊屬性訪問方法的實際例子(注意,我們使用super因為並非所有類都有__dict__類屬性):

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class AccessCounter:
    '''一個類包含一個值和實現了一個訪問計數器。
    當值每次發生變化時,計數器+1'''
                                
    def __init__(self, val):
        super(AccessCounter, self).__setattr__('counter',0)
        super(AccessCounter, self).__setattr__('value', val)
                                    
    def __setattr__(self, name, value):
        if name == 'value':
            super(AccessCounter, self).__setattr__('counter', self.counter + 1)
        # Make this unconditional.
        # 如果你想阻止其他屬性被創建,拋出AttributeError(name)異常
        super(AccessCounter, self).__setattr__(name, value)
                                
    def __delattr__(self, name)
        if name == 'value':
            super(AccessCounter, self).__setattr__('counter', self.counter + 1)
        super(AccessCounter, self).__delattr__(name)

 

 

 

6.制作自定義序列

很有多種方式可以讓你的類表現得像內建序列(字典,元組,列表,字符串等)。這些是我迄今為止最喜歡的神奇方法了,因為不合理的控制它們賦予了你一種魔術般地讓你的類實例整個全局函數數組漂亮工作的方式。在我們開始講解這個內容之前,讓我們先快速理清需求。


需求
現在我們正在談論如何創建你自己的序列。也是什么談一談protocol了。protocol在某些地方跟接口很相似。接口在其他語言中,是一組給定的方法,而你必須定義它們。然而,在Python中protocol是完全非正式的,而且不要求顯式聲明去實現。更進一步說,它們更像是准則。
為何我們現在要談論protocol?因為在Python中要實現自定義容器類型會涉及使用到這其中某些protocol。首先,有一個protocol是為定義不可變容器的:為了制作一個不可變容器,你只需要定義__len__和__getitem__(稍后詳述)。可變容器protocol要求所有不可變容器增加__setitem__和__delitem__。然后,如果你希望你的對象是可迭代的,那你還得定義一個會返回迭代器iterator的__iter__方法。並且這個迭代器必須遵守一個迭代protocol,也就是要求迭代器有回調方法__iter__(返回自身)和next。


隱藏在容器背后的魔法
已經迫不及待了?以下便是容器使用的神奇魔法:
__len__(self)
返回容器的長度。部分protocol同時支持可變和不可變容器
__getitem__(self, key)
定義當某一個item被訪問時的行為,使用self[key]表示法。這個同樣也是部分可變和不可變容器protocol。這也可拋出適當的異常:TypeError 當key的類型錯誤,或沒有值對應Key時。
__setitem__(self, key, value)
定義當某一個item被賦值時候的行為,使用self[key]=value表示法。這也是部分可變和不可變容器protocol。再一次重申,你應當在適當之處拋出KeyError和TypeError異常。
__delitem__(self, key)
定義當某一個item被刪除(例如 del self[key])時的行為。這僅是部分可變容器的protocol。在一個無效key被使用后,你必須拋出一個合適的異常。
__iter__(self)
應該給容器返回一個迭代器。迭代器會返回若干內容,大多使用內建函數iter()表示。當一個容器使用形如for x in container:的循環。迭代器本身就是其對象,同時也要定義好一個__iter__方法來返回自身。
__reversed__(self)
當定義調用內建函數reversed()時的行為。應該返回一個反向版本的列表。
__contains__(self, item)
__contains__為成員關系,用in和not in測試時定義行為。那你會問這個為何不是一個序列的protocol的一部分?這是因為當__contains__未定義,Python就會遍歷序列,如果遇到正在尋找的item就會返回True。
__concat__(self, other)
最后,你可通過__concat__定義你的序列和另外一個序列的連接。應該從self和other返回一個新構建的序列。當調用2個序列時__concat__涉及操作符+


一個例子

在我們的例子中,讓我們看一下一個list實現的某些基礎功能性的構建。可能會讓你想起你使用的其他語言(比如Haskell)。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class FunctionalList:
    '''類覆蓋了一個list的某些額外的功能性魔法,像head,
    tail,init,last,drop,and take'''
    def __init__(self, values=None):
        if values is None:
            self.values = []
        else:
            self.values = values
                                 
    def __len__(self):
        return len(self.values)
                             
    def __getitem__(self, key):
        # 如果key是非法的類型和值,那么list valuse會拋出異常
        return self.values[key]
                                 
    def __setitem__(self, key, value):
        self.values[key] = value
                                 
    def __delitem__(self, key):
        del self.values[key]
                             
    def __iter__(self):
        return iter(self.values)
                                 
    def __reversed__(self):
        return reversed(self.values)
                             
    def append(self, value):
        self.values.append(value)
    def head(self):
        # 獲得第一個元素
        return self.values[0]
    def tail(self):
        # 獲得在第一個元素后的其他所有元素
        return self.values[1:]
    def init(self):
        # 獲得除最后一個元素的序列
        return self.values[:-1]
    def last(last):
        # 獲得最后一個元素
        return self.values[-1]
    def drop(self, n):
        # 獲得除前n個元素的序列
        return self.values[n:]
    def take(self, n):
        # 獲得前n個元素
        return self.values[:n]

 

 

 

通過這個(輕量的)有用的例子你知道了如何實現你自己的序列。當然,還有很多更有用的應用,但是它們其中的很多已經被標准庫實現了,像Counter, OrderedDict, NamedTuple


7.反射

你也可以通過定義神奇方法來控制如何反射使用內建函數isinstance()和issubclass()的行為。這些神奇方法是:

__instancecheck__(self, instance)
檢查一個實例是否是你定義類中的一個實例(比如,isinstance(instance, class))
__subclasscheck__(self, subclass)
檢查一個類是否是你定義類的子類(比如,issubclass(subclass, class))
這對於神奇方法的用例情況來說可能較小,可它的確是真的。我並不想花費太多的時間在反射方法上面,因為他們不是那么地重要。不過它們反映了在Python中關於面對對象編程一些重要的東西,而且在Python中的普遍:總是在找一種簡單的方式來做某些事情,即使它能被用到的不多。這些神奇方法似乎看上去不那么有用,但當你需要使用它們的時候你會感激它們的存在(和你閱讀的這本指南!)。


8.可調用對象

正如你可能已經知道,在Python中函數是第一類對象。這就意味着它們可以被傳遞到函數和方法,就像是任何類型的對象。這真是一種難以置信強大的特性。

這是Python中一個特別的神奇方法,它允許你的類實例像函數。所以你可以“調用”它們,把他們當做參數傳遞給函數等等。這是另一個強大又便利的特性讓Python的編程變得更可愛了。
__call__(self, [args...])
允許類實例像函數一樣被調用。本質上,這意味着x()等價於x.__call__()。注意,__call__需要的參數數目是可變的,也就是說可以對任何函數按你的喜好定義參數的數目定義__call__


__call__可能對於那些經常改變狀態的實例來說是極其有用的。“調用”實例是一種順應直覺且優雅的方式來改變對象的狀態。下面一個例子是一個類表示一個實體在一個平面上的位置:

 

1
2
3
4
5
6
7
8
9
10
11
12
class Entity:
    '''描述實體的類,被調用的時候更新實體的位置'''
                          
    def __init__(self, size, x, y):
        self.x, self.y = x, y
        self.size = size
                              
    def __call__(self, x, y):
        '''改變實體的位置'''
        self.x, self.y = x, y
                              
    #省略...

 

 

 

 

9.上下文管理

在Python2.5里引入了一個新關鍵字(with)使得一個新方法得到了代碼復用。上下文管理這個概念在Python中早已不是新鮮事了(之前它作為庫的一部分被實現過),但直到PEP343(http://www.python.org/dev/peps/pep-0343/)才作為第一個類語言結構取得了重要地位而被接受。你有可能早就已經見識過with聲明:

 

1
2
with open('foo.txt') as bar:
    # 對bar執行某些動作

 

上下文管理允許對對象進行設置和清理動作,用with聲明進行已經封裝的操作。上下文操作的行為取決於2個神奇方法:
__enter__(self)
定義塊用with聲明創建出來時上下文管理應該在塊開始做什么。注意,__enter__的返回值必須綁定with聲明的目標,或是as后面的名稱。
__exit__(self,  exception_type, exception_value, traceback)
定義在塊執行(或終止)之后上下文管理應該做什么。它可以用來處理異常,進行清理,或行動處於塊之后某些總是被立即處理的事。如果塊執行成功的話,excepteion_type,exception_value,和traceback將會置None。否則,你可以選擇去處理異常,或者讓用戶自己去處理。如果你想處理,確保在全部都完成之后__exit__會返回True。如果你不想讓上下文管理處理異常,那就讓它發生好了。


__enter__和__exit__對那些已有良好定義和對設置,清理行為有共同行為的特殊類是有用。你也可以使用這些方法去創建封裝其他對象通用的上下文管理。看下面的例子:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Closer:
    '''用with聲明一個上下文管理用一個close方法自動關閉一個對象'''
                   
    def __init__(self, obj):
        self.obj = obj
                       
    def __enter__(self):
        return self.obj # 綁定目標
                   
    def __exit__(self, exception_type, exception_val, trace):
        try:
            self.obj.close()
        except AttributeError: #obj不具備close
            print 'Not closable.'
            return True # 成功處理異常

 

 

以下是一個對於Closer實際應用的一個例子,使用一個FTP連接進行的演示(一個可關閉的套接字):

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> from magicmethods import Closer
>>> from ftplib import :;;
>>> with Closer(FTP('ftp.somsite.com')) as conn:
...     conn.dir()
...
# 省略的輸出
>>> conn.dir()
# 一個很長的AttributeError消息, 不能關閉使用的一個連接
>>> with Closer(int(5)) as i:
...     i += 1
...
Not closeable.
>>> i
6

 

 

 

瞧見我們如何漂亮地封裝處理正確或不正確的用例了嗎?那就是上下文管理和神奇方法的威力。


10.構建描述符對象

描述符可以改變其他對象,也可以是訪問類中任一的getting,setting,deleting。描述符不意味着孤立;相反,它們意味着會被它們的所有者類控制。當建立面向對象數據庫或那些擁有相互依賴的屬性的類時,描述符是有用的。當描述符在幾個不同單元或描述計算屬性時顯得更為有用。

作為一個描述符,一個類必須至少實現__get__,__set__,和__delete__中的一個。讓我們快點看一下這些神奇方法吧:


__get__(self, instance, owner)
當描述符的值被取回時定義其行為。instance是owner對象的一個實例,owner是所有類。
__set__(self, instance, value)
當描述符的值被改變時定義其行為。instance是owner對象的一個實例,value是設置的描述符的值
__delete__(self, instance)
當描述符的值被刪除時定義其行為。instance是owner對象的一個實例。


現在,有一個有用的描述符應用例子:單位轉換策略

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Meter(object):
    '''米描述符'''
            
    def __init__(self, value=0.0):
        self.value = float(value)
    def __get__(self, instance, owner):
        return self.value
    def __set__(self, instance, value):
        self.value = float(value)
            
class Foot(object):
    '''英尺描述符'''
                
    def __get__(self, instance, owner):
        return instance.meter * 3.2808
    def __set__(self, instance, value):
        instance.meter = float(value) / 3.2808
            
class Distance(object):
    '''表示距離的類,控制2個描述符:feet和meters'''
    meter = Meter()
    foot = Foot()

 

 

 

 

11.Pickling你的對象

假如你花時間和其他Pythonistas打交道,那么你至少有可能聽到過Pickling這個詞。Pickling是一種對Python數據結構的序列化過程。如果你需要存儲一個對象,之后再取回它(通常是為了緩存)那么它就顯得格外地有用了。同時,它也是產生憂慮和困惑的主要來源。

Pickling是那么地重要以至於它不僅有自己專屬的模塊(pickle),還有自己的protocol和神奇方法與其相伴。但首先用簡要的文字來解釋下如何pickle已經存在的類型(如果你已經懂了可以隨意跳過這部分內容)


Pickling:鹽水中的快速浸泡

讓我們跳入pickling。話說你有一個詞典你想要保存它並在稍后取回。你可以把它的內容寫到一個文件中去,需要非常小心地確保你寫了正確的語法,然后用exec()或處理文件的輸入取回寫入的內容。但這是不穩定的:如果你你在純文本中保存重要的數據,它有可能被幾種方法改變,導致你的程序crash或在你的計算機上運行了惡意代碼而出錯。於是,我們准備pickle它:

 

1
2
3
4
5
6
7
8
import pickle
         
data = {'foo': [1,2,3],
        'bar': ('Hello','world!'),
        'baz': True}
jar = open('data.pk1', 'wb')
pickle.dump(data, jar) # 把pickled數據寫入jar文件
jar.close()

 

好了現在,已經過去了幾個小時。我們希望拿回數據,而我們需要做的事僅僅是unpickle它:

 

1
2
3
4
5
6
import pickle
      
pk1_file = open('data.pk1','rb') #連接pickled數據
data = pickle.load(pk1_file) #把數據load到一個變量中去
print data
pk1_file.close()

 

發生了什么事?正如你的預期,我們獲得了data。
現在,我要給你一些忠告:pickling並非完美。Pickle文件很容易因意外或出於故意行為而被損毀。Pickling可能比起使用純文本文件安全些,但它仍舊有可能會被用來跑惡意代碼。還有因為Python版本的不兼容問題,所以不要期望發布Pickled對象,也不要期望人們能夠打開它們。但是,它依然是一個強大的緩存工具和其他常見序列化任務。


Pickling你自定義的對象
Pickling不僅可用在內建類型上,還可以用於遵守pickle協議的任何類。pickle協議有4個可選方法用於定制Python對象如何運行(這跟C擴展有點不同,但那不在我們討論的范圍內):


__getinitargs__(self)
如果你想當你的類unpickled時調用__init__,那你可以定義__getinitargs__,該方法應該返回一個元組的參數,然后你可以把他傳遞給__init__。注意,該方法僅適用於舊式類。
__getnewargs__(self)
對於新式類,你可以影響有哪些參數會被傳遞到__new__進行unpickling。該方法同樣應該返回一個元組參數,然后能傳遞給__new__
__getstate__(self)
代替對象的__dict__屬性被保存。當對象pickled,你可返回一個自定義的狀態被保存。當對象unpickled時,這個狀態將會被__setstate__使用。
__setstate__(self, state)
對象unpickled時,如果__setstate__定義對象狀態會傳遞來代替直接用對象的__dict__屬性。這正好跟__getstate__手牽手:當二者都被定義了,你可以描述對象的pickled狀態,任何你想要的。


一個例子: 

我們的例子是Slate類,它會記憶它曾經的值和已經寫入的值。然而,當這特殊的slate每一次pickle都會被清空:當前值不會被保存。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import time
   
class Slate:
    '''存儲一個字符串和一個變更log,當Pickle時會忘記它的值'''
   
    def __init__(self, value):
        self.value = value
        self.last_change = time.asctime()
        self.history = {}
   
    def change(self, new_value):
        # 改變值,提交最后的值到歷史記錄
        self.history[self.last_change] = self.value
        self.value = new_value
        self.last_change = time.asctime()
   
    def print_changes(self):
        print 'Changelog for Slate object:'
        for k, v in self.history.items():
            print '%st %s' % (k, v)
   
    def __getstate__(self):
        # 故意不返回self.value 或 self.last_change.
        # 當unpickle,我們希望有一塊空白的"slate"
        return self.history
   
    def __setstate__(self, state):
        # 讓 self.history = state 和 last_change 和 value被定義
        self.history = state
        self.value, self.last_change = None, None

 

 

 

12.總結

這份指南的目標就是任何人讀一讀它,不管讀者們是否具備Python或面對對象的編程經驗。如果你正准備學習Python,那你已經獲得了編寫功能豐富,優雅,易用的類的寶貴知識。如果你是一名中級Python程序員,你有可能已經拾起了一些新概念和策略和一些好的方法來減少你和你的用戶編寫的代碼量。如果你是一名Pythonista專家,你可能已經回顧了某些你可能已經被你遺忘的知識點,或着你又學習到了一些新技巧。不管你的的經驗等級,我希望這次Python神奇方法的旅程達到了真正神奇的效果。(我無法控制自己在最后不用個雙關語)

 

附錄:如果調用神奇方法

Python中的一些神奇方法直接映射到內建函數;在這種情況下,調用它們的方法是相當明顯的。然而,在其他情況下,那些調用方法就不這么明顯了。本附錄致力於揭開能夠引導神奇方法被調用的非明顯語法。

神奇方法 調用方法 說明
__new__(cls [,...]) instance = MyClass(arg1, arg2) __new__ 在創建實例的時候被調用
__init__(self [,...]) instance = MyClass(arg1, arg2) __init__ 在創建實例的時候被調用
__cmp__(self, other) self == other, self > other, 等 在比較的時候調用
__pos__(self) +self 一元加運算符
__neg__(self) -self 一元減運算符
__invert__(self) ~self 取反運算符
__index__(self) x[self] 對象被作為索引使用的時候
__nonzero__(self) bool(self) 對象的布爾值
__getattr__(self, name) self.name      # name不存在 訪問一個不存在的屬性時
__setattr__(self, name, val) self.name = val 對一個屬性賦值時
__delattr__(self, name) del self.name 刪除一個屬性時
__getattribute(self, name) self.name 訪問任何屬性時
__getitem__(self, key) self[key] 使用索引訪問元素時
__setitem__(self, key, val) self[key] = val 對某個索引值賦值時
__delitem__(self, key) del self[key] 刪除某個索引值時
__iter__(self) for x in self 迭代時
__contains__(self, value) value in self, value not in self 使用 in 操作測試關系時
__concat__(self, value) self + other 連接兩個對象時
__call__(self [,...]) self(args) “調用”對象時
__enter__(self) with self as x:  with語句上下文管理
__exit__(self, exc, val, trace) with self as x: with語句上下文管理
__getstate__(self) pickle.dump(pkl_file, self) 序列化
__setstate__(self) data = pickle.load(pkl_file) 序列化







希望這張表格可以幫你掃清你有關語法涉及到神奇方法的問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM