在 Python 中,我們可以經(jīng)??吹揭噪p下劃線 __ 包裹起來的方法,比如最常見的 __init__,這些方法被稱為魔法方法(magic method)或特殊方法(special method)。簡(jiǎn)單地說,這些方法可以給 Python 的類提供特殊功能,方便我們定制一個(gè)類,比如 __init__ 方法可以對(duì)實(shí)例屬性進(jìn)行初始化。
完整的特殊方法列表可在這里查看,本文介紹部分常用的特殊方法:
__new____str__ , __repr____iter____getitem__ , __setitem__ , __delitem____getattr__ , __setattr__ , __delattr____call__在 Python 中,當(dāng)我們創(chuàng)建一個(gè)類的實(shí)例時(shí),類會(huì)先調(diào)用 __new__(cls[, ...]) 來創(chuàng)建實(shí)例,然后 __init__ 方法再對(duì)該實(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) key 在 A._dict 中時(shí),直接返回 A._dict['key'],否則創(chuàng)建實(shí)例。
執(zhí)行情況:
>>> a1 = A()
NEW
INIT
>>> a2 = A()
EXISTS
INIT
>>> a3 = A()
EXISTS
INIT
先看一個(gè)簡(jiǎn)單的例子:
class Foo(object):
def __init__(self, name):
self.name = name
>>> print Foo('ethan')
<__main__.Foo object at 0x10c37aa50>
在上面,我們使用 print 打印一個(gè)實(shí)例對(duì)象,但如果我們想打印更多信息呢,比如把 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__ 方法的代碼是一樣的,能不能精簡(jiǎn)一點(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__
在某些情況下,我們希望實(shí)例對(duì)象可被用于 for...in 循環(huán),這時(shí)我們需要在類中定義 __iter__ 和 next(在 Python3 中是 __next__)方法,其中,__iter__ 返回一個(gè)迭代對(duì)象,next 返回容器的下一個(gè)元素,在沒有后續(xù)元素時(shí)拋出 StopIteration 異常。
看一個(gè)斐波那契數(shù)列的例子:
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1
def __iter__(self): # 返回迭代器對(duì)象本身
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
有時(shí),我們希望可以使用 obj[n] 這種方式對(duì)實(shí)例對(duì)象進(jìn)行取值,比如對(duì)斐波那契數(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è)切片對(duì)象 slice,因此,我們需要對(duì)傳入的參數(shù)進(jìn)行判斷,可以使用 isinstance 進(jìn)行判斷,改后的代碼如下:
class Fib(object):
def __getitem__(self, n):
if isinstance(n, slice): # 如果 n 是 slice 對(duì)象
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)在,我們?cè)囋囉们衅椒ǎ?/p>
>>> fib = Fib()
>>> fib[0:3]
[1, 1, 2]
>>> fib[2:6]
[2, 3, 5, 8]
上面,我們只是簡(jiǎn)單地演示了 getitem 的操作,但是它還很不完善,比如沒有對(duì)負(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 # 對(duì)應(yīng)于 p.__setitem__('x', 2)
>>> p['y'] = 5 # 對(duì)應(yīng)于 p.__setitem__('y', 5)
>>> p # 對(duì)應(yīng)于 __repr__
point({'y': 5, 'x': 2})
>>> len(p) # 對(duì)應(yīng)于 p.__len__
2
>>> p['x'] # 對(duì)應(yīng)于 p.__getitem__('x')
2
>>> p['y'] # 對(duì)應(yīng)于 p.__getitem__('y')
5
>>> del p['x'] # 對(duì)應(yīng)于 p.__delitem__('x')
delete x
>>> p
point({'y': 5})
>>> len(p)
1
當(dāng)我們獲取對(duì)象的某個(gè)屬性,如果該屬性不存在,會(huì)拋出 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í),解釋器就會(huì)試圖調(diào)用 __getattr__(self, 'z') 來獲取值,但是,上面的實(shí)現(xiàn)還有一個(gè)問題,當(dāng)我們調(diào)用其他屬性,比如 w ,會(huì)返回 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__ 只有在屬性不存在的情況下才會(huì)被調(diào)用,對(duì)已存在的屬性不會(huì)調(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}
我們一般使用 obj.method() 來調(diào)用對(duì)象的方法,那能不能直接在實(shí)例本身上調(diào)用呢?在 Python 中,只要我們?cè)陬愔卸x __call__ 方法,就可以對(duì)實(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 判斷對(duì)象是否能被調(diào)用
True
>>> p(6) # 傳入?yún)?shù),對(duì)實(shí)例進(jìn)行調(diào)用,對(duì)應(yīng) p.__call__(6)
13 # 3+4+6
可以看到,對(duì)實(shí)例進(jìn)行調(diào)用就好像對(duì)函數(shù)調(diào)用一樣。
__new__ 在 __init__ 之前被調(diào)用,用來創(chuàng)建實(shí)例。__str__ 是用 print 和 str 顯示的結(jié)果,__repr__ 是直接顯示的結(jié)果。__getitem__ 用類似 obj[key] 的方式對(duì)對(duì)象進(jìn)行取值__getattr__ 用于獲取不存在的屬性 obj.attr__call__ 使得可以對(duì)實(shí)例進(jìn)行調(diào)用