Python 中的魔術方法詳解

旺醬在路上發表於2016-10-28

前言

在Python中,所有以”__”雙下劃線包起來的方法,都統稱為”魔術方法”。比如我們接觸最多的__init__.

有些魔術方法,我們可能以後一輩子都不會再遇到了,這裡也就只是簡單介紹下;

而有些魔術方法,巧妙使用它可以構造出非常優美的程式碼,比如將複雜的邏輯封裝成簡單的API。

本文編輯的思路借鑑自Rafe Kettler的這篇部落格: A Guide to Python Magic Methods,並補充了一些程式碼示例。

介紹的順序大概是:常見的先介紹,越少見的越靠後講。

本文中用到的程式碼示例,可以在我的github上下載到。

構造和初始化

__init__我們很熟悉了,它在物件初始化的時候呼叫,我們一般將它理解為”建構函式”.

實際上, 當我們呼叫x = SomeClass()的時候呼叫,__init__並不是第一個執行的, __new__才是。所以準確來說,是__new____init__共同構成了”建構函式”.

__new__是用來建立類並返回這個類的例項, 而__init__只是將傳入的引數來初始化該例項.

__new__在建立一個例項的過程中必定會被呼叫,但__init__就不一定,比如通過pickle.load的方式反序列化一個例項時就不會呼叫__init__

__new__方法總是需要返回該類的一個例項,而__init__不能返回除了None的任何值。比如下面例子:

class Foo(object):

    def __init__(self):
        print 'foo __init__'
        return None  # 必須返回None,否則拋TypeError

    def __del__(self):
        print 'foo __del__'

實際中,你很少會用到__new__,除非你希望能夠控制類的建立。
如果要講解__new__,往往需要牽扯到metaclass(元類)的介紹。
如果你有興趣深入,可以參考我的另一篇部落格: 理解Python的metaclass

對於__new__的過載,Python文件中也有了詳細的介紹。

在物件的生命週期結束時, __del__會被呼叫,可以將__del__理解為”構析函式”.
__del__定義的是當一個物件進行垃圾回收時候的行為。

有一點容易被人誤解, 實際上,x.__del__() 並不是對於del x的實現,但是往往執行del x時會呼叫x.__del__().

怎麼來理解這句話呢? 繼續用上面的Foo類的程式碼為例:

foo = Foo()
foo.__del__()
print foo
del foo
print foo  # NameError, foo is not defined

如果呼叫了foo.__del__(),物件本身仍然存在. 但是呼叫了del foo, 就再也沒有foo這個物件了.

請注意,如果直譯器退出的時候物件還存在,就不能保證 __del__ 被確切的執行了。所以__del__並不能替代良好的程式設計習慣。
比如,在處理socket時,及時關閉結束的連線。

屬性訪問控制

總有人要吐槽Python缺少對於類的封裝,比如希望Python能夠定義私有屬性,然後提供公共可訪問的getter和 setter。Python其實可以通過魔術方法來實現封裝。

__getattr__(self, name)

該方法定義了你試圖訪問一個不存在的屬性時的行為。因此,過載該方法可以實現捕獲錯誤拼寫然後進行重定向, 或者對一些廢棄的屬性進行警告。

__setattr__(self, name, value)

__setattr__ 是實現封裝的解決方案,它定義了你對屬性進行賦值和修改操作時的行為。
不管物件的某個屬性是否存在,它都允許你為該屬性進行賦值,因此你可以為屬性的值進行自定義操作。有一點需要注意,實現__setattr__時要避免”無限遞迴”的錯誤,下面的程式碼示例中會提到。

__delattr__(self, name)

__delattr____setattr__很像,只是它定義的是你刪除屬性時的行為。實現__delattr__是同時要避免”無限遞迴”的錯誤。

__getattribute__(self, name)

__getattribute__定義了你的屬性被訪問時的行為,相比較,__getattr__只有該屬性不存在時才會起作用。

因此,在支援__getattribute__的Python版本,呼叫__getattr__前必定會用 __getattribute____getattribute__同樣要避免”無限遞迴”的錯誤。

需要提醒的是,最好不要嘗試去實現__getattribute__,因為很少見到這種做法,而且很容易出bug。

例子說明__setattr__的無限遞迴錯誤:

def __setattr__(self, name, value):
    self.name = value
    # 每一次屬性賦值時, __setattr__都會被呼叫,因此不斷呼叫自身導致無限遞迴了。

因此正確的寫法應該是:

def __setattr__(self, name, value):
    self.__dict__[name] = value

__delattr__如果在其實現中出現del self.name 這樣的程式碼也會出現”無限遞迴”錯誤,這是一樣的原因。

下面的例子很好的說明了上面介紹的4個魔術方法的呼叫情況:

class Access(object):

    def __getattr__(self, name):
        print '__getattr__'
        return super(Access, self).__getattr__(name)

    def __setattr__(self, name, value):
        print '__setattr__'
        return super(Access, self).__setattr__(name, value)

    def __delattr__(self, name):
        print '__delattr__'
        return super(Access, self).__delattr__(name)

    def __getattribute__(self, name):
        print '__getattribute__'
        return super(Access, self).__getattribute__(name)

access = Access()
access.attr1 = True  # __setattr__呼叫
access.attr1  # 屬性存在,只有__getattribute__呼叫
try:
    access.attr2  # 屬性不存在, 先呼叫__getattribute__, 後呼叫__getattr__
except AttributeError:
    pass
del access.attr1  # __delattr__呼叫

描述器物件

我們從一個例子來入手,介紹什麼是描述符,並介紹__get____set____delete__ 的使用。(放在這裡介紹是為了跟上一小節介紹的魔術方法作對比)

我們知道,距離既可以用單位”米”表示,也可以用單位”英尺”表示。現在我們定義一個類來表示距離,它有兩個屬性: 米和英尺。

class Meter(object):
    '''Descriptor for a meter.'''
    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):
    '''Descriptor for a foot.'''
    def __get__(self, instance, owner):
        return instance.meter * 3.2808
    def __set__(self, instance, value):
        instance.meter = float(value) / 3.2808

class Distance(object):
    meter = Meter()
    foot = Foot()

d = Distance()
print d.meter, d.foot  # 0.0, 0.0
d.meter = 1
print d.meter, d.foot  # 1.0 3.2808
d.meter = 2
print d.meter, d.foot  # 2.0 6.5616

在上面例子中,在還沒有對Distance的例項賦值前, 我們認為meter和foot應該是各自類的例項物件, 但是輸出卻是數值。這是因為__get__發揮了作用.

我們只是修改了meter,並且將其賦值成為int,但foot也修改了。這是__set__發揮了作用.

描述器物件(Meter、Foot)不能獨立存在, 它需要被另一個所有者類(Distance)所持有。

描述器物件可以訪問到其擁有者例項的屬性,比如例子中Foot的instance.meter

在物件導向程式設計時,如果一個類的屬性有相互依賴的關係時,使用描述器來編寫程式碼可以很巧妙的組織邏輯。在Django的ORM中, models.Model中的InterField等欄位, 就是通過描述器來實現功能的。

一個類要成為描述器,必須實現__get____set____delete__ 中的至少一個方法。下面簡單介紹下:

__get__(self, instance, owner)

引數instance是擁有者類的例項。引數owner是擁有者類本身。__get__在其擁有者對其讀值的時候呼叫。

__set__(self, instance, value)

__set__在其擁有者對其進行修改值的時候呼叫。

__delete__(self, instance)

__delete__在其擁有者對其進行刪除的時候呼叫。

構造自定義容器(Container)

在Python中,常見的容器型別有: dict, tuple, list, string。

其中tuple, string是不可變容器,dict, list是可變容器。

可變容器和不可變容器的區別在於,不可變容器一旦賦值後,不可對其中的某個元素進行修改。

比如定義了l = [1, 2, 3]t = (1, 2, 3)後, 執行l[0] = 0是可以的,但執行t[0] = 0則會報錯。

如果我們要自定義一些資料結構,使之能夠跟以上的容器型別表現一樣,那就需要去實現某些協議。

這裡的協議跟其他語言中所謂的”介面”概念很像,一樣的需要你去實現才行,只不過沒那麼正式而已。

如果要自定義不可變容器型別,只需要定義__len__ 和 __getitem__方法;

如果要自定義可變容器型別,還需要在不可變容器型別的基礎上增加定義__setitem__ 和 __delitem__

如果你希望你的自定義資料結構還支援”可迭代”, 那就還需要定義__iter__

__len__(self)

需要返回數值型別,以表示容器的長度。該方法在可變容器和不可變容器中必須實現。

__getitem__(self, key)

當你執行self[key]的時候,呼叫的就是該方法。該方法在可變容器和不可變容器中也都必須實現。
呼叫的時候,如果key的型別錯誤,該方法應該丟擲TypeError;
如果沒法返回key對應的數值時,該方法應該丟擲ValueError。

__setitem__(self, key, value)

當你執行self[key] = value時,呼叫的是該方法。

__delitem__(self, key)

當你執行del self[key]的時候,呼叫的是該方法。

__iter__(self)

該方法需要返回一個迭代器(iterator)。當你執行for x in container: 或者使用iter(container)時,該方法被呼叫。

__reversed__(self)

如果想要該資料結構被內建函式reversed()支援,就還需要實現該方法。

__contains__(self, item)

如果定義了該方法,那麼在執行item in container 或者 item not in container時該方法就會被呼叫。
如果沒有定義,那麼Python會迭代容器中的元素來一個一個比較,從而決定返回True或者False。

__missing__(self, key)

dict字典型別會有該方法,它定義了key如果在容器中找不到時觸發的行為。
比如d = {'a': 1}, 當你執行d[notexist]時,d.__missing__['notexist']就會被呼叫。

下面舉例,使用上面講的魔術方法來實現Haskell語言中的一個資料結構。

# -*- coding: utf-8 -*-
class FunctionalList:
    ''' 實現了內建型別list的功能,並豐富了一些其他方法: head, tail, init, last, drop, 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):
        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 FunctionalList(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(self):
        # 獲取最後一個元素
        return self.values[-1]
    def drop(self, n):
        # 獲取所有元素,除了前N個
        return self.values[n:]
    def take(self, n):
        # 獲取前N個元素
        return self.values[:n]

我們再舉個例子,實現Perl語言的autovivification,它會在你每次引用一個值未定義的屬性時為你自動建立陣列或者字典。

class AutoVivification(dict):
    """Implementation of perl's autovivification feature."""
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

weather = AutoVivification()
weather['china']['guangdong']['shenzhen'] = 'sunny'
weather['china']['hubei']['wuhan'] = 'windy'
weather['USA']['California']['Los Angeles'] = 'sunny'
print weather

結果輸出:{'china': {'hubei': {'wuhan': 'windy'}, 'guangdong': {'shenzhen': 'sunny'}}, 'USA':    {'California': {'Los Angeles': 'sunny'}}}

在Python中,關於自定義容器的實現還有更多實用的例子,但只有很少一部分能夠整合在Python標準庫中,比如Counter, OrderedDict等

上下文管理

with宣告是從Python2.5開始引進的關鍵詞。你應該遇過這樣子的程式碼:

with open('foo.txt') as bar:
    # do something with bar

在with宣告的程式碼段中,我們可以做一些物件的開始操作和清除操作,還能對異常進行處理。

這需要實現兩個魔術方法: __enter__ 和 __exit__

__enter__(self)

__enter__會返回一個值,並賦值給as關鍵詞之後的變數。在這裡,你可以定義程式碼段開始的一些操作。

__exit__(self, exception_type, exception_value, traceback)

__exit__定義了程式碼段結束後的一些操作,可以這裡執行一些清除操作,或者做一些程式碼段結束後需要立即執行的命令,比如檔案的關閉,socket斷開等。如果程式碼段成功結束,那麼exception_type, exception_value, traceback 三個引數傳進來時都將為None。如果程式碼段丟擲異常,那麼傳進來的三個引數將分別為: 異常的型別,異常的值,異常的追蹤棧。

如果__exit__返回True, 那麼with宣告下的程式碼段的一切異常將會被遮蔽。

如果__exit__返回None, 那麼如果有異常,異常將正常丟擲,這時候with的作用將不會顯現出來。

舉例說明:

這該示例中,IndexError始終會被隱藏,而TypeError始終會丟擲。

class DemoManager(object):

    def __enter__(self):
        pass

    def __exit__(self, ex_type, ex_value, ex_tb):
        if ex_type is IndexError:
            print ex_value.__class__
            return True
        if ex_type is TypeError:
            print ex_value.__class__
            return  # return None

with DemoManager() as nothing:
    data = [1, 2, 3]
    data[4]  # raise IndexError, 該異常被__exit__處理了

with DemoManager() as nothing:
    data = [1, 2, 3]
    data['a']  # raise TypeError, 該異常沒有被__exit__處理

輸出:
<type 'exceptions.IndexError'>
<type 'exceptions.TypeError'>
Traceback (most recent call last):
  ...

物件的序列化

Python物件的序列化操作是pickling進行的。pickling非常的重要,以至於Python對此有單獨的模組pickle,還有一些相關的魔術方法。使用pickling, 你可以將資料儲存在檔案中,之後又從檔案中進行恢復。

下面舉例來描述pickle的操作。從該例子中也可以看出,如果通過pickle.load 初始化一個物件, 並不會呼叫__init__方法。

# -*- coding: utf-8 -*-
from datetime import datetime
import pickle

class Distance(object):

    def __init__(self, meter):
        print 'distance __init__'
        self.meter = meter

data = {
    'foo': [1, 2, 3],
    'bar': ('Hello', 'world!'),
    'baz': True,
    'dt': datetime(2016, 10, 01),
    'distance': Distance(1.78),
}
print 'before dump:', data
with open('data.pkl', 'wb') as jar:
    pickle.dump(data, jar)  # 將資料儲存在檔案中

del data
print 'data is deleted!'

with open('data.pkl', 'rb') as jar:
    data = pickle.load(jar)  # 從檔案中恢復資料
print 'after load:', data

值得一提,從其他檔案進行pickle.load操作時,需要注意有惡意程式碼的可能性。另外,Python的各個版本之間,pickle檔案可能是互不相容的。

pickling並不是Python的內建型別,它支援所有實現pickle協議(可理解為介面)的類。pickle協議有以下幾個可選方法來自定義Python物件的行為。

__getinitargs__(self)

如果你希望unpickle時,__init__方法能夠呼叫,那麼就需要定義__getinitargs__, 該方法需要返回一系列引數的元組,這些引數就是傳給__init__的引數。

該方法只對old-style class有效。所謂old-style class,指的是不繼承自任何物件的類,往往定義時這樣表示: class A:, 而非class A(object):

__getnewargs__(self)

__getinitargs__很類似,只不過返回的引數元組將傳值給__new__

__getstate__(self)

在呼叫pickle.dump時,預設是物件的__dict__屬性被儲存,如果你要修改這種行為,可以在__getstate__方法中返回一個state。state將在呼叫pickle.load時傳值給__setstate__

__setstate__(self, state)

一般來說,定義了__getstate__,就需要相應地定義__setstate__來對__getstate__返回的state進行處理。

__reduce__(self)

如果pickle的資料包含了自定義的擴充套件類(比如使用C語言實現的Python擴充套件類)時,就需要通過實現__reduce__方法來控制行為了。由於使用過於生僻,這裡就不展開繼續講解了。

令人容易混淆的是,我們知道, reduce()是Python的一個內建函式, 需要指出__reduce__並非定義了reduce()的行為,二者沒有關係。

__reduce_ex__(self)

__reduce_ex__ 是為了相容性而存在的, 如果定義了__reduce_ex__, 它將代替__reduce__ 執行。

下面的程式碼示例很有意思,我們定義了一個類Slate(中文是板岩的意思)。這個類能夠記錄歷史上每次寫入給它的值,但每次pickle.dump時當前值就會被清空,僅保留了歷史。

# -*- coding: utf-8 -*-
import pickle
import time

class Slate:
    '''Class to store a string and a changelog, and forget its value when pickled.'''
    def __init__(self, value):
        self.value = value
        self.last_change = time.time()
        self.history = []

    def change(self, new_value):
        # 修改value, 將上次的valeu記錄在history
        self.history.append((self.last_change, self.value))
        self.value = new_value
        self.last_change = time.time()

    def print_changes(self):
        print 'Changelog for Slate object:'
        for k, v in self.history:
            print '%s    %s' % (k, v)

    def __getstate__(self):
        # 故意不返回self.value和self.last_change,
        # 以便每次unpickle時清空當前的狀態,僅僅保留history
        return self.history

    def __setstate__(self, state):
        self.history = state
        self.value, self.last_change = None, None

slate = Slate(0)
time.sleep(0.5)
slate.change(100)
time.sleep(0.5)
slate.change(200)
slate.change(300)
slate.print_changes()  # 與下面的輸出歷史對比
with open('slate.pkl', 'wb') as jar:
    pickle.dump(slate, jar)
del slate  # delete it
with open('slate.pkl', 'rb') as jar:
    slate = pickle.load(jar)
print 'current value:', slate.value  # None
print slate.print_changes()  # 輸出歷史記錄與上面一致

運算子相關的魔術方法

運算子相關的魔術方法實在太多了,也很好理解,不打算多講。在其他語言裡,也有過載運算子的操作,所以我們對這些魔術方法已經很瞭解了。

比較運算子

__cmp__(self, other)

如果該方法返回負數,說明self < other; 返回正數,說明self > other; 返回0說明self == other

強烈不推薦來定義__cmp__, 取而代之, 最好分別定義__lt__等方法從而實現比較功能。
__cmp__在Python3中被廢棄了。

__eq__(self, other)

定義了比較操作符==的行為.

__ne__(self, other)

定義了比較操作符!=的行為.

__lt__(self, other)

定義了比較操作符<的行為.

__gt__(self, other)

定義了比較操作符>的行為.

__le__(self, other)

定義了比較操作符<=的行為.

__ge__(self, other)

定義了比較操作符>=的行為.

下面我們定義一種型別Word, 它會使用單詞的長度來進行大小的比較, 而不是採用str的比較方式。
但是為了避免 Word('bar') == Word('foo') 這種違背直覺的情況出現,並沒有定義__eq__, 因此Word會使用它的父類(str)中的__eq__來進行比較。

下面的例子中也可以看出: 在程式語言中, 如果a >=b and a <= b, 並不能推匯出a == b這樣的結論。

# -*- coding: utf-8 -*-
class Word(str):
    '''儲存單詞的類,定義比較單詞的幾種方法'''
    def __new__(cls, word):
        # 注意我們必須要用到__new__方法,因為str是不可變型別
        # 所以我們必須在建立的時候將它初始化
        if ' ' in word:
            print "Value contains spaces. Truncating to first space."
            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)

print 'foo < fool:', Word('foo') < Word('fool')  # True
print 'foolish > fool:', Word('foolish') > Word('fool')  # True
print 'bar >= foo:', Word('bar') >= Word('foo')  # True
print 'bar <= foo:', Word('bar') <= Word('foo')  # True
print 'bar == foo:', Word('bar') == Word('foo')  # False, 用了str內建的比較方法來進行比較
print 'bar != foo:', Word('bar') != Word('foo')  # True

一元運算子和函式

__pos__(self)

實現了’+'號一元運算子(比如+some_object)

__neg__(self)

實現了’-'號一元運算子(比如-some_object)

__invert__(self)

實現了~號一元運算子(比如~some_object)

__abs__(self)

實現了abs()內建函式.

__round__(self, n)

實現了round()內建函式. 引數n表示四舍五進的精度.

__floor__(self)

實現了math.round(), 向下取整.

__ceil__(self)

實現了math.ceil(), 向上取整.

__trunc__(self)

實現了math.trunc(), 向0取整.

算術運算子

__add__(self, other)

實現了加號運算.

__sub__(self, other)

實現了減號運算.

__mul__(self, other)

實現了乘法運算.

__floordiv__(self, other)

實現了//運算子.

__div__(self, other)

實現了/運算子. 該方法在Python3中廢棄. 原因是Python3中,division預設就是true division.

__truediv__(self, other)

實現了true division. 只有你宣告瞭from __future__ import division該方法才會生效.

__mod__(self, other)

實現了%運算子, 取餘運算.

__divmod__(self, other)

實現了divmod()內建函式.

__pow__(self, other)

實現了**操作. N次方操作.

__lshift__(self, other)

實現了位操作<<.

__rshift__(self, other)

實現了位操作>>.

__and__(self, other)

實現了位操作&.

__or__(self, other)

實現了位操作|

__xor__(self, other)

實現了位操作^

反算術運算子

這裡只需要解釋一下概念即可。假設針對some_object這個物件:

some_object + other

上面的程式碼非常正常地實現了some_object的__add__方法。那麼如果遇到相反的情況呢?

other + some_object

這時候,如果other沒有定義__add__方法,但是some_object定義了__radd__, 那麼上面的程式碼照樣可以執行。
這裡的__radd__(self, other)就是__add__(self, other)的反算術運算子。

所以,類比的,我們就知道了更多的反算術運算子, 就不一一展開了:

  • __rsub__(self, other)
  • __rmul__(self, other)
  • __rmul__(self, other)
  • __rfloordiv__(self, other)
  • __rdiv__(self, other)
  • __rtruediv__(self, other)
  • __rmod__(self, other)
  • __rdivmod__(self, other)
  • __rpow__(self, other)
  • __rlshift__(self, other)
  • __rrshift__(self, other)
  • __rand__(self, other)
  • __ror__(self, other)
  • __rxor__(self, other)

增量賦值

這也是隻要理解了概念就容易掌握的運算。舉個例子:

x = 5
x += 1  # 這裡的+=就是增量賦值,將x+1賦值給了x

因此對於a += b__iadd__ 將返回a + b, 並賦值給a。
所以很容易理解下面的魔術方法了:

  • __iadd__(self, other)
  • __isub__(self, other)
  • __imul__(self, other)
  • __ifloordiv__(self, other)
  • __idiv__(self, other)
  • __itruediv__(self, other)
  • __imod__(self, other)
  • __ipow__(self, other)
  • __ilshift__(self, other)
  • __irshift__(self, other)
  • __iand__(self, other)
  • __ior__(self, other)
  • __ixor__(self, other)

型別轉化

__int__(self)

實現了型別轉化為int的行為.

__long__(self)

實現了型別轉化為long的行為.

__float__(self)

實現了型別轉化為float的行為.

__complex__(self)

實現了型別轉化為complex(複數, 也即1+2j這樣的虛數)的行為.

__oct__(self)

實現了型別轉化為八進位制數的行為.

__hex__(self)

實現了型別轉化為十六進位制數的行為.

__index__(self)

在切片運算中將物件轉化為int, 因此該方法的返回值必須是int。用一個例子來解釋這個用法。

class Thing(object):
    def __index__(self):
        return 1

thing = Thing()
list_ = ['a', 'b', 'c']
print list_[thing]  # 'b'
print list_[thing:thing]  # []

上面例子中, list_[thing]的表現跟list_[1]一致,正是因為Thing實現了__index__方法。

可能有的人會想,list_[thing]為什麼不是相當於list_[int(thing)]呢? 通過實現Thing的__int__方法能否達到這個目的呢?

顯然不能。如果真的是這樣的話,那麼list_[1.1:2.2]這樣的寫法也應該是通過的。
而實際上,該寫法會丟擲TypeError: slice indices must be integers or None or have an __index__ method

下面我們再做個例子,如果對一個dict物件執行dict_[thing]會怎麼樣呢?

dict_ = {1: 'apple', 2: 'banana', 3: 'cat'}
print dict_[thing]  # raise KeyError

這個時候就不是呼叫__index__了。雖然listdict都實現了__getitem__方法, 但是它們的實現方式是不一樣的。
如果希望上面例子能夠正常執行, 需要實現Thing的__hash__ 和 __eq__方法.

class Thing(object):
    def __hash__(self):
        return 1
    def __eq__(self, other):
        return hash(self) == hash(other)

dict_ = {1: 'apple', 2: 'banana', 3: 'cat'}
print dict_[thing]  # apple

__coerce__(self, other)

實現了混合模式運算。

要了解這個方法,需要先了解coerce()內建函式: 官方文件上的解釋是, coerce(x, y)返回一組數字型別的引數, 它們被轉化為同一種型別,以便它們可以使用相同的算術運算子進行操作。如果過程中轉化失敗,丟擲TypeError。

比如對於coerce(10, 10.1), 因為10和10.1在進行算術運算時,會先將10轉為10.0再來運算。因此coerce(10, 10.1)返回值是(10.0, 10.1).

__coerce__在Python3中廢棄了。

其他魔術方法

還沒講到的魔術方法還有很多,但有些我覺得很簡單,或者很少見,就不再累贅展開說明了。

__str__(self)

對例項使用str()時呼叫。

__repr__(self)

對例項使用repr()時呼叫。str()repr()都是返回一個代表該例項的字串,
主要區別在於: str()的返回值要方便人來看,而repr()的返回值要方便計算機看。

__unicode__(self)

對例項使用unicode()時呼叫。unicode()str()的區別在於: 前者返回值是unicode, 後者返回值是str。unicode和str都是basestring的子類。

當你對一個類只定義了__str__但沒定義__unicode__時,__unicode__會根據__str__的返回值自動實現,即return unicode(self.__str__());
但返回來則不成立。

class StrDemo2:
    def __str__(self):
        return 'StrDemo2'

class StrDemo3:
    def __unicode__(self):
        return u'StrDemo3'

demo2 = StrDemo2()
print str(demo2)  # StrDemo2
print unicode(demo2)  # StrDemo2

demo3 = StrDemo3()
print str(demo3)  # <__main__.StrDemo3 instance>
print unicode(demo3)  # StrDemo3

__format__(self, formatstr)

"Hello, {0:abc}".format(a)等價於format(a, "abc"), 等價於a.__format__("abc")

這在需要格式化展示物件的時候非常有用,比如格式化時間物件。

__hash__(self)

對例項使用hash()時呼叫, 返回值是數值型別。

__nonzero__(self)

對例項使用bool()時呼叫, 返回True或者False。
你可能會問, 為什麼不是命名為__bool__? 我也不知道。
我只知道該方法在Python3中改名為__bool__了。

__dir__(self)

對例項使用dir()時呼叫。通常實現該方法是沒必要的。

__sizeof__(self)

對例項使用sys.getsizeof()時呼叫。返回物件的大小,單位是bytes。

__instancecheck__(self, instance)

對例項呼叫isinstance(instance, class)時呼叫。 返回值是布林值。它會判斷instance是否是該類的例項。

__subclasscheck__(self, subclass)

對例項使用issubclass(subclass, class)時呼叫。返回值是布林值。它會判斷subclass否是該類的子類。

__copy__(self)

對例項使用copy.copy()時呼叫。返回”淺複製”的物件。

__deepcopy__(self, memodict={})

對例項使用copy.deepcopy()時呼叫。返回”深複製”的物件。

__call__(self, [args...])

該方法允許類的例項跟函式一樣表現:

class XClass:
    def __call__(self, a, b):
        return a + b

def add(a, b):
    return a + b

x = XClass()
print 'x(1, 2)', x(1, 2)
print 'callable(x)', callable(x)  # True
print 'add(1, 2)', add(1, 2)
print 'callable(add)', callable(add)  # True

Python3中的差異

  • Python3中,str與unicode的區別被廢除了,因而__unicode__沒有了,取而代之地出現了__bytes__.
  • Python3中,division預設就是true division, 因而__div__廢棄.
  • __coerce__因存在冗餘而廢棄.
  • __cmp__因存在冗餘而廢棄.
  • __nonzero__改名為__bool__.

相關文章