__str__
我們先定義一個(gè)Student類(lèi),打印一個(gè)實(shí)例:
>>> class Student(object): ... def __init__(self, name): ... self.name = name ...
>>> print(Student('Michael'))
<__main__.Student object at 0x109afb190>
打印出一堆<main.Student object at 0x109afb190>,不好看。怎么才能打印得好看呢?只需要定義好__str__()方法,返回一個(gè)好看的字符串就可以了:
>>> class Student(object): ... def __init__(self, name): ... self.name = name ... def __str__(self): ... return 'Student object (name: %s)' % self.name ...
>>> print(Student('Michael')) Student object (name: Michael)
這樣打印出來(lái)的實(shí)例,不但好看,而且容易看出實(shí)例內(nèi)部重要的數(shù)據(jù)。但是細(xì)心的朋友會(huì)發(fā)現(xiàn)直接敲變量不用print,打印出來(lái)的實(shí)例還是不好看:
>>> s = Student('Michael')
>>> s
<__main__.Student object at 0x109afb310>
這是因?yàn)橹苯语@示變量調(diào)用的不是__str__(),而是__repr__(),兩者的區(qū)別是__str__()返回用戶(hù)看到的字符串,而__repr__()返回程序開(kāi)發(fā)者看到的字符串,也就是說(shuō),__repr__()是為調(diào)試服務(wù)的。解決辦法是再定義一個(gè)__repr__()
__iter__
如果一個(gè)類(lèi)想被用于for ... in循環(huán),類(lèi)似list或tuple那樣,就必須實(shí)現(xiàn)一個(gè)__iter__()方法,該方法返回一個(gè)迭代對(duì)象,然后,Python的for循環(huán)就會(huì)不斷調(diào)用該迭代對(duì)象的__next__()方法拿到循環(huán)的下一個(gè)值,直到遇到StopIteration錯(cuò)誤時(shí)退出循環(huán)。
我們以斐波那契數(shù)列為例,寫(xiě)一個(gè)Fib類(lèi),可以作用于for循環(huán):
class Fib(object): def __init__(self): self.a, self.b = 0, 1 # 初始化兩個(gè)計(jì)數(shù)器a,b def __iter__(self): return self # 實(shí)例本身就是迭代對(duì)象,故返回自己 def __next__(self): self.a, self.b = self.b, self.a + self.b # 計(jì)算下一個(gè)值 if self.a > 100000: # 退出循環(huán)的條件 raise StopIteration(); return self.a # 返回下一個(gè)值
現(xiàn)在,試試把Fib實(shí)例作用于for循環(huán):
>>> for n in Fib(): ... print(n) ... 1 1 2
__getitem__
Fib實(shí)例雖然能作用于for循環(huán),看起來(lái)和list有點(diǎn)像,但是,把它當(dāng)成list來(lái)使用還是不行,比如,取第5個(gè)元素Fib()[5], 要表現(xiàn)得像list那樣按照下標(biāo)取出元素,需要實(shí)現(xiàn)__getitem__()方法:
class Fib(object): def __getitem__(self, n): a, b = 1, 1 for x in range(n): a, b = b, a + b return a
現(xiàn)在,就可以按下標(biāo)訪(fǎng)問(wèn)數(shù)列的任意一項(xiàng)了
但是list有個(gè)神奇的切片方法對(duì)于Fib卻報(bào)錯(cuò)。原因是__getitem__()傳入的參數(shù)可能是一個(gè)int,也可能是一個(gè)切片對(duì)象slice,所以要做判斷:
class Fib(object): def __getitem__(self, n): if isinstance(n, int): # n是索引 a, b = 1, 1 for x in range(n): a, b = b, a + b return a if isinstance(n, slice): # n是切片 start = n.start stop = n.stop if start is None: start = 0 a, b = 1, 1 L = [] for x in range(stop): if x >= start: L.append(a) a, b = b, a + b return L
但是沒(méi)有對(duì)step參數(shù)作處理,也沒(méi)有對(duì)負(fù)數(shù)作處理,所以,要正確實(shí)現(xiàn)一個(gè)__getitem__()還是有很多工作要做的
此外,如果把對(duì)象看成dict,__getitem__()的參數(shù)也可能是一個(gè)可以作key的object,例如str。與之對(duì)應(yīng)的是__setitem__()方法,把對(duì)象視作list或dict來(lái)對(duì)集合賦值。最后,還有一個(gè)__delitem__()方法,用于刪除某個(gè)元素。
總之,通過(guò)上面的方法,我們自己定義的類(lèi)表現(xiàn)得和Python自帶的list、tuple、dict沒(méi)什么區(qū)別,這完全歸功于動(dòng)態(tài)語(yǔ)言的“鴨子類(lèi)型”,不需要強(qiáng)制繼承某個(gè)接口。
__getattr__
正常情況下,當(dāng)我們調(diào)用類(lèi)的方法或?qū)傩詴r(shí),如果不存在,就會(huì)報(bào)錯(cuò)。要避免這個(gè)錯(cuò)誤,除了可以加上一個(gè)屬性外,Python還有另一個(gè)機(jī)制,那就是寫(xiě)一個(gè)__getattr__()方法,動(dòng)態(tài)返回一個(gè)屬性。
class Student(object): def __init__(self): self.name = 'Michael' def __getattr__(self, attr): if attr=='score': return 99
當(dāng)調(diào)用不存在的屬性時(shí),比如score,Python解釋器會(huì)試圖調(diào)用__getattr__(self, 'score')來(lái)嘗試獲得屬性,這樣,我們就有機(jī)會(huì)返回score的值
返回函數(shù)也是完全可以的:
class Student(object): def __getattr__(self, attr): if attr=='age': return lambda: 25
只是調(diào)用方式要變?yōu)椋?br>
>>> s.age() 25
注意,只有在沒(méi)有找到屬性的情況下,才調(diào)用__getattr__,已有的屬性,比如name,不會(huì)在__getattr__中查找。
這實(shí)際上可以把一個(gè)類(lèi)的所有屬性和方法調(diào)用全部動(dòng)態(tài)化處理了,不需要任何特殊手段。這種完全動(dòng)態(tài)調(diào)用的特性有什么實(shí)際作用呢?作用就是,可以針對(duì)完全動(dòng)態(tài)的情況作調(diào)用。
現(xiàn)在很多網(wǎng)站都搞REST API,比如新浪微博、豆瓣啥的,調(diào)用API的URL類(lèi)似:
http://api.server/user/friends
http://api.server/user/timeline/list
如果要寫(xiě)SDK,給每個(gè)URL對(duì)應(yīng)的API都寫(xiě)一個(gè)方法,那得累死,而且,API一旦改動(dòng),SDK也要改。利用完全動(dòng)態(tài)的__getattr__,我們可以寫(xiě)出一個(gè)鏈?zhǔn)秸{(diào)用:
class Chain(object): def __init__(self, path=''): self._path = path def __getattr__(self, path): return Chain('%s/%s' % (self._path, path)) def __str__(self): return self._path __repr__ = __str__
試試:
>>> Chain().status.user.timeline.list
'/status/user/timeline/list'
這樣,無(wú)論API怎么變,SDK都可以根據(jù)URL實(shí)現(xiàn)完全動(dòng)態(tài)的調(diào)用,而且,不隨API的增加而改變!
__call__
一個(gè)對(duì)象實(shí)例可以有自己的屬性和方法,當(dāng)我們調(diào)用實(shí)例方法時(shí),我們用instance.method()
來(lái)調(diào)用。能不能直接在實(shí)例本身上調(diào)用呢?在Python中,答案是肯定的。
任何類(lèi),只需要定義一個(gè)__call__()方法,就可以直接對(duì)實(shí)例進(jìn)行調(diào)用。請(qǐng)看示例:
class Student(object): def __init__(self, name): self.name = name def __call__(self): print('My name is %s.' % self.name)
調(diào)用方式如下:
>>> s = Student('Michael')
>>> s() # self參數(shù)不要傳入
My name is Michael.
__call__()還可以定義參數(shù)。對(duì)實(shí)例進(jìn)行直接調(diào)用就好比對(duì)一個(gè)函數(shù)進(jìn)行調(diào)用一樣,所以你完全可以把對(duì)象看成函數(shù),把函數(shù)看成對(duì)象,因?yàn)檫@兩者之間本來(lái)就沒(méi)啥根本的區(qū)別。
如果你把對(duì)象看成函數(shù),那么函數(shù)本身其實(shí)也可以在運(yùn)行期動(dòng)態(tài)創(chuàng)建出來(lái),因?yàn)轭?lèi)的實(shí)例都是運(yùn)行期創(chuàng)建出來(lái)的,這么一來(lái),我們就模糊了對(duì)象和函數(shù)的界限。
那么,怎么判斷一個(gè)變量是對(duì)象還是函數(shù)呢?其實(shí),更多的時(shí)候,我們需要判斷一個(gè)對(duì)象是否能被調(diào)用,能被調(diào)用的對(duì)象就是一個(gè)Callable對(duì)象,比如函數(shù)和我們上面定義的帶有__call__()的類(lèi)實(shí)例:
>>> callable(Student())
True
>>> callable(max)
True