鍍金池/ 教程/ Python/ 定制類和魔法方法
基礎(chǔ)
itertools
HTTP 服務(wù)
hashlib
閉包
文件和目錄
單元測試
使用 @property
標(biāo)準(zhǔn)模塊
陌生的 metaclass
Base64
進(jìn)程、線程和協(xié)程
讀寫二進(jìn)制文件
匿名函數(shù)
輸入和輸出
Click
元組
字符編碼
partial 函數(shù)
參考資料
collections
協(xié)程
類和實(shí)例
Python 之旅
定制類和魔法方法
常用數(shù)據(jù)類型
繼承和多態(tài)
ThreadLocal
HTTP 協(xié)議簡介
Requests 庫的使用
讀寫文本文件
列表
os 模塊
迭代器 (Iterator)
正則表達(dá)式
集合
上下文管理器
異常處理
你不知道的 super
定義函數(shù)
datetime
資源推薦
字典
slots 魔法
hmac
第三方模塊
進(jìn)程
類方法和靜態(tài)方法
函數(shù)參數(shù)
高階函數(shù)
函數(shù)
re 模塊
高級特性
線程
argparse
生成器
結(jié)束語
字符串
map/reduce/filter
函數(shù)式編程
Celery
裝飾器

定制類和魔法方法

在 Python 中,我們可以經(jīng)常看到以雙下劃線 __ 包裹起來的方法,比如最常見的 __init__,這些方法被稱為魔法方法(magic method)或特殊方法(special method)。簡單地說,這些方法可以給 Python 的類提供特殊功能,方便我們定制一個(gè)類,比如 __init__ 方法可以對實(shí)例屬性進(jìn)行初始化。

完整的特殊方法列表可在這里查看,本文介紹部分常用的特殊方法:

  • __new__
  • __str__ , __repr__
  • __iter__
  • __getitem__ , __setitem__ , __delitem__
  • __getattr__ , __setattr__ , __delattr__
  • __call__

new

在 Python 中,當(dāng)我們創(chuàng)建一個(gè)類的實(shí)例時(shí),類會先調(diào)用 __new__(cls[, ...]) 來創(chuàng)建實(shí)例,然后 __init__ 方法再對該實(shí)例(self)進(jìn)行初始化。

關(guān)于 __new____init__ 有幾點(diǎn)需要注意:

  • __new__ 是在 __init__ 之前被調(diào)用的;
  • __new__ 是類方法,__init__ 是實(shí)例方法;
  • 重載 __new__ 方法,需要返回類的實(shí)例;

一般情況下,我們不需要重載 __new__ 方法。但在某些情況下,我們想控制實(shí)例的創(chuàng)建過程,這時(shí)可以通過重載 __new_ 方法來實(shí)現(xiàn)。

讓我們看一個(gè)例子:

class A(object):
    _dict = dict()

    def __new__(cls):
        if 'key' in A._dict:
            print "EXISTS"
            return A._dict['key']
        else:
            print "NEW"
            return object.__new__(cls)

    def __init__(self):
        print "INIT"
        A._dict['key'] = self

在上面,我們定義了一個(gè)類 A,并重載了 __new__ 方法:當(dāng) keyA._dict 中時(shí),直接返回 A._dict['key'],否則創(chuàng)建實(shí)例。

執(zhí)行情況:

>>> a1 = A()
NEW
INIT
>>> a2 = A()
EXISTS
INIT
>>> a3 = A()
EXISTS
INIT

str & repr

先看一個(gè)簡單的例子:

class Foo(object):
    def __init__(self, name):
        self.name = name

>>> print Foo('ethan')
<__main__.Foo object at 0x10c37aa50>

在上面,我們使用 print 打印一個(gè)實(shí)例對象,但如果我們想打印更多信息呢,比如把 name 也打印出來,這時(shí),我們可以在類中加入 __str__ 方法,如下:

class Foo(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Foo object (name: %s)' % self.name

>>> print Foo('ethan')      # 使用 print
Foo object (name: ethan)
>>>
>>> str(Foo('ethan'))       # 使用 str
'Foo object (name: ethan)'
>>>
>>> Foo('ethan')             # 直接顯示
<__main__.Foo at 0x10c37a490>

可以看到,使用 print 和 str 輸出的是 __str__ 方法返回的內(nèi)容,但如果直接顯示則不是,那能不能修改它的輸出呢?當(dāng)然可以,我們只需在類中加入 __repr__ 方法,比如:

class Foo(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Foo object (name: %s)' % self.name
    def __repr__(self):
        return 'Foo object (name: %s)' % self.name

>>> Foo('ethan')
'Foo object (name: ethan)'

可以看到,現(xiàn)在直接使用 Foo('ethan') 也可以顯示我們想要的結(jié)果了,然而,我們發(fā)現(xiàn)上面的代碼中,__str____repr__ 方法的代碼是一樣的,能不能精簡一點(diǎn)呢,當(dāng)然可以,如下:

class Foo(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Foo object (name: %s)' % self.name
    __repr__ = __str__

iter

在某些情況下,我們希望實(shí)例對象可被用于 for...in 循環(huán),這時(shí)我們需要在類中定義 __iter__next(在 Python3 中是 __next__)方法,其中,__iter__ 返回一個(gè)迭代對象,next 返回容器的下一個(gè)元素,在沒有后續(xù)元素時(shí)拋出 StopIteration 異常。

看一個(gè)斐波那契數(shù)列的例子:

class Fib(object):
    def __init__(self):
        self.a, self.b = 0, 1

    def __iter__(self):  # 返回迭代器對象本身
        return self      

    def next(self):      # 返回容器下一個(gè)元素
        self.a, self.b = self.b, self.a + self.b
        return self.a    

>>> fib = Fib()
>>> for i in fib:
...     if i > 10:
...         break
...     print i
...
1
1
2
3
5
8

getitem

有時(shí),我們希望可以使用 obj[n] 這種方式對實(shí)例對象進(jìn)行取值,比如對斐波那契數(shù)列,我們希望可以取出其中的某一項(xiàng),這時(shí)我們需要在類中實(shí)現(xiàn) __getitem__ 方法,比如下面的例子:

class Fib(object):
    def __getitem__(self, n):
        a, b = 1, 1
        for x in xrange(n):
            a, b = b, a + b
        return a

>>> fib = Fib()
>>> fib[0], fib[1], fib[2], fib[3], fib[4], fib[5]
(1, 1, 2, 3, 5, 8)

我們還想更進(jìn)一步,希望支持 obj[1:3] 這種切片方法來取值,這時(shí) __getitem__ 方法傳入的參數(shù)可能是一個(gè)整數(shù),也可能是一個(gè)切片對象 slice,因此,我們需要對傳入的參數(shù)進(jìn)行判斷,可以使用 isinstance 進(jìn)行判斷,改后的代碼如下:

class Fib(object):
    def __getitem__(self, n):
        if isinstance(n, slice):   # 如果 n 是 slice 對象
            a, b = 1, 1
            start, stop = n.start, n.stop
            L = []
            for i in xrange(stop):
                if i >= start:
                    L.append(a)
                a, b = b, a + b
            return L
        if isinstance(n, int):     # 如果 n 是 int 型
            a, b = 1, 1
            for i in xrange(n):
                a, b = b, a + b
            return a

現(xiàn)在,我們試試用切片方法:

>>> fib = Fib()
>>> fib[0:3]
[1, 1, 2]
>>> fib[2:6]
[2, 3, 5, 8]

上面,我們只是簡單地演示了 getitem 的操作,但是它還很不完善,比如沒有對負(fù)數(shù)處理,不支持帶 step 參數(shù)的切片操作 obj[1:2:5] 等等,讀者有興趣的話可以自己實(shí)現(xiàn)看看。

__geitem__ 用于獲取值,類似地,__setitem__ 用于設(shè)置值,__delitem__ 用于刪除值,讓我們看下面一個(gè)例子:

class Point(object):
    def __init__(self):
        self.coordinate = {}

    def __str__(self):
        return "point(%s)" % self.coordinate

    def __getitem__(self, key):
        return self.coordinate.get(key)

    def __setitem__(self, key, value):
        self.coordinate[key] = value

    def __delitem__(self, key):
        del self.coordinate[key]
        print 'delete %s' % key

    def __len__(self):
        return len(self.coordinate)

    __repr__ = __str__

在上面,我們定義了一個(gè) Point 類,它有一個(gè)屬性 coordinate(坐標(biāo)),是一個(gè)字典,讓我們看看使用:

>>> p = Point()
>>> p['x'] = 2    # 對應(yīng)于 p.__setitem__('x', 2)
>>> p['y'] = 5    # 對應(yīng)于 p.__setitem__('y', 5)
>>> p             # 對應(yīng)于 __repr__
point({'y': 5, 'x': 2})
>>> len(p)        # 對應(yīng)于 p.__len__
2
>>> p['x']        # 對應(yīng)于 p.__getitem__('x')
2
>>> p['y']        # 對應(yīng)于 p.__getitem__('y')
5
>>> del p['x']    # 對應(yīng)于 p.__delitem__('x')
delete x
>>> p
point({'y': 5})
>>> len(p)
1

getattr

當(dāng)我們獲取對象的某個(gè)屬性,如果該屬性不存在,會拋出 AttributeError 異常,比如:

class Point(object):
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

>>> p = Point(3, 4)
>>> p.x, p.y
(3, 4)
>>> p.z
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-547-6dce4e43e15c> in <module>()
----> 1 p.z

AttributeError: 'Point' object has no attribute 'z'

那有沒有辦法不讓它拋出異常呢?當(dāng)然有,只需在類的定義中加入 __getattr__ 方法,比如:

class Point(object):
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
    def __getattr__(self, attr):
        if attr == 'z':
            return 0

>>> p = Point(3, 4)
>>> p.z
0

現(xiàn)在,當(dāng)我們調(diào)用不存在的屬性(比如 z)時(shí),解釋器就會試圖調(diào)用 __getattr__(self, 'z') 來獲取值,但是,上面的實(shí)現(xiàn)還有一個(gè)問題,當(dāng)我們調(diào)用其他屬性,比如 w ,會返回 None,因?yàn)?__getattr__ 默認(rèn)返回就是 None,只有當(dāng) attr 等于 'z' 時(shí)才返回 0,如果我們想讓 __getattr__ 只響應(yīng)幾個(gè)特定的屬性,可以加入異常處理,修改 __getattr__ 方法,如下:

def __getattr__(self, attr):
    if attr == 'z':
        return 0
    raise AttributeError("Point object has no attribute %s" % attr)

這里再強(qiáng)調(diào)一點(diǎn),__getattr__ 只有在屬性不存在的情況下才會被調(diào)用,對已存在的屬性不會調(diào)用 __getattr__。

__getattr__ 一起使用的還有 __setattr__, __delattr__,類似 obj.attr = value, del obj.attr,看下面一個(gè)例子:

class Point(object):
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __getattr__(self, attr):
        if attr == 'z':
            return 0
        raise AttributeError("Point object has no attribute %s" % attr)

    def __setattr__(self, *args, **kwargs):  
        print 'call func set attr (%s, %s)' % (args, kwargs)
        return object.__setattr__(self, *args, **kwargs)

    def __delattr__(self, *args, **kwargs):  
        print 'call func del attr (%s, %s)' % (args, kwargs)
        return object.__delattr__(self, *args, **kwargs)

>>> p = Point(3, 4)
call func set attr (('x', 3), {})
call func set attr (('y', 4), {})
>>> p.z
0
>>> p.z = 7
call func set attr (('z', 7), {})
>>> p.z
7
>>> p.w
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in __getattr__
AttributeError: Point object has no attribute w
>>> p.w = 8
call func set attr (('w', 8), {})
>>> p.w
8
>>> del p.w
call func del attr (('w',), {})
>>> p.__dict__
{'y': 4, 'x': 3, 'z': 7}

call

我們一般使用 obj.method() 來調(diào)用對象的方法,那能不能直接在實(shí)例本身上調(diào)用呢?在 Python 中,只要我們在類中定義 __call__ 方法,就可以對實(shí)例進(jìn)行調(diào)用,比如下面的例子:

class Point(object):
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __call__(self, z):
        return self.x + self.y + z

使用如下:

>>> p = Point(3, 4)
>>> callable(p)     # 使用 callable 判斷對象是否能被調(diào)用
True
>>> p(6)            # 傳入?yún)?shù),對實(shí)例進(jìn)行調(diào)用,對應(yīng) p.__call__(6)
13                  # 3+4+6

可以看到,對實(shí)例進(jìn)行調(diào)用就好像對函數(shù)調(diào)用一樣。

小結(jié)

  • __new____init__ 之前被調(diào)用,用來創(chuàng)建實(shí)例。
  • __str__ 是用 print 和 str 顯示的結(jié)果,__repr__ 是直接顯示的結(jié)果。
  • __getitem__ 用類似 obj[key] 的方式對對象進(jìn)行取值
  • __getattr__ 用于獲取不存在的屬性 obj.attr
  • __call__ 使得可以對實(shí)例進(jìn)行調(diào)用

參考資料

上一篇:HTTP 協(xié)議簡介下一篇:裝飾器