文章轉載自廖雪峯老師Python課程博客,僅供學習參考使用
看到相似__slots__這種形如__xxx__的變量或者函數名就要注意,這些在Python中是有特殊用途的。
__slots__咱們已經知道怎麼用了,__len__()方法咱們也知道是爲了能讓class做用於len()函數。
除此以外,Python的class中還有許多這樣有特殊用途的函數,能夠幫助咱們定製類。
__str__
咱們先定義一個Student類,打印一個實例:
>>> class Student(object):
... def __init__(self, name):
... self.name = name
...
>>> print(Student('Michael'))
<__main__.Student object at 0x109afb190>
打印出一堆<__main__.Student object at 0x109afb190>,很差看。
怎麼才能打印得好看呢?只須要定義好__str__()方法,返回一個好看的字符串就能夠了:
>>> 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)
這樣打印出來的實例,不但好看,並且容易看出實例內部重要的數據。
可是細心的朋友會發現直接敲變量不用print,打印出來的實例仍是很差看:
>>> s = Student('Michael')
>>> s
<__main__.Student object at 0x109afb310>
這是由於直接顯示變量調用的不是__str__(),而是__repr__(),二者的區別是__str__()返回用戶看到的字符串,而__repr__()返回程序開發者看到的字符串,也就是說,__repr__()是爲調試服務的。
解決辦法是再定義一個__repr__()。可是一般__str__()和__repr__()代碼都是同樣的,因此,有個偷懶的寫法:
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name=%s)' % self.name
__repr__ = __str__
__iter__
若是一個類想被用於for ... in循環,相似list或tuple那樣,就必須實現一個__iter__()方法,該方法返回一個迭代對象,而後,Python的for循環就會不斷調用該迭代對象的__next__()方法拿到循環的下一個值,直到遇到StopIteration錯誤時退出循環。
咱們以斐波那契數列爲例,寫一個Fib類,能夠做用於for循環:
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1 # 初始化兩個計數器a,b
def __iter__(self):
return self # 實例自己就是迭代對象,故返回本身
def __next__(self):
self.a, self.b = self.b, self.a + self.b # 計算下一個值
if self.a > 100000: # 退出循環的條件
raise StopIteration()
return self.a # 返回下一個值
如今,試試把Fib實例做用於for循環:
>>> for n in Fib():
... print(n)
...
1
1
2
3
5
...
46368
75025
__getitem__
Fib實例雖然能做用於for循環,看起來和list有點像,可是,把它當成list來使用仍是不行,好比,取第5個元素:
>>> Fib()[5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'Fib' object does not support indexing
要表現得像list那樣按照下標取出元素,須要實現__getitem__()方法:
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
如今,就能夠按下標訪問數列的任意一項了:
>>> f = Fib()
>>> f[0]
1
>>> f[1]
1
>>> f[2]
2
>>> f[3]
3
>>> f[10]
89
>>> f[100]
573147844013817084101
可是list有個神奇的切片方法:
>>> list(range(100))[5:10]
[5, 6, 7, 8, 9]
對於Fib卻報錯。緣由是__getitem__()傳入的參數多是一個int,也多是一個切片對象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
如今試試Fib的切片:
>>> f = Fib()
>>> f[0:5]
[1, 1, 2, 3, 5]
>>> f[:10]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
可是沒有對step參數做處理:
>>> f[:10:2]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
也沒有對負數做處理,因此,要正確實現一個__getitem__()仍是有不少工做要作的。
此外,若是把對象當作dict,__getitem__()的參數也多是一個能夠做key的object,例如str。
與之對應的是__setitem__()方法,把對象視做list或dict來對集合賦值。最後,還有一個__delitem__()方法,用於刪除某個元素。
總之,經過上面的方法,咱們本身定義的類表現得和Python自帶的list、tuple、dict沒什麼區別,這徹底歸功於動態語言的「鴨子類型」,不須要強制繼承某個接口。
__getattr__
正常狀況下,當咱們調用類的方法或屬性時,若是不存在,就會報錯。好比定義Student類:
class Student(object):
def __init__(self):
self.name = 'Michael'
調用name屬性,沒問題,可是,調用不存在的score屬性,就有問題了:
>>> s = Student()
>>> print(s.name)
Michael
>>> print(s.score)
Traceback (most recent call last):
...
AttributeError: 'Student' object has no attribute 'score'
錯誤信息很清楚地告訴咱們,沒有找到score這個attribute。
要避免這個錯誤,除了能夠加上一個score屬性外,Python還有另外一個機制,那就是寫一個__getattr__()方法,動態返回一個屬性。修改以下:
class Student(object):
def __init__(self):
self.name = 'Michael'
def __getattr__(self, attr):
if attr=='score':
return 99
當調用不存在的屬性時,好比score,Python解釋器會試圖調用__getattr__(self, 'score')來嘗試得到屬性,這樣,咱們就有機會返回score的值:
>>> s = Student()
>>> s.name
'Michael'
>>> s.score
99
返回函數也是徹底能夠的:
class Student(object):
def __getattr__(self, attr):
if attr=='age':
return lambda: 25
只是調用方式要變爲:
>>> s.age()
25
注意,只有在沒有找到屬性的狀況下,才調用__getattr__,已有的屬性,好比name,不會在__getattr__中查找。
此外,注意到任意調用如s.abc都會返回None,這是由於咱們定義的__getattr__默認返回就是None。要讓class只響應特定的幾個屬性,咱們就要按照約定,拋出AttributeError的錯誤:
class Student(object):
def __getattr__(self, attr):
if attr=='age':
return lambda: 25
raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)
這實際上能夠把一個類的全部屬性和方法調用所有動態化處理了,不須要任何特殊手段。
這種徹底動態調用的特性有什麼實際做用呢?做用就是,能夠針對徹底動態的狀況做調用。
舉個例子:
如今不少網站都搞REST API,好比新浪微博、豆瓣啥的,調用API的URL相似:
http://api.server/user/friends
http://api.server/user/timeline/list
若是要寫SDK,給每一個URL對應的API都寫一個方法,那得累死,並且,API一旦改動,SDK也要改。
利用徹底動態的__getattr__,咱們能夠寫出一個鏈式調用:
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'
這樣,不管API怎麼變,SDK均可以根據URL實現徹底動態的調用,並且,不隨API的增長而改變!
還有些REST API會把參數放到URL中,好比GitHub的API:
GET /users/:user/repos
調用時,須要把:user替換爲實際用戶名。若是咱們能寫出這樣的鏈式調用:
Chain().users('michael').repos
就能夠很是方便地調用API了。有興趣的童鞋能夠試試寫出來。
__call__
一個對象實例能夠有本身的屬性和方法,當咱們調用實例方法時,咱們用instance.method()來調用。能不能直接在實例自己上調用呢?在Python中,答案是確定的。
任何類,只須要定義一個__call__()方法,就能夠直接對實例進行調用。請看示例:
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print('My name is %s.' % self.name)
調用方式以下:
>>> s = Student('Michael')
>>> s() # self參數不要傳入
My name is Michael.
__call__()還能夠定義參數。對實例進行直接調用就比如對一個函數進行調用同樣,因此你徹底能夠把對象當作函數,把函數當作對象,由於這二者之間原本就沒啥根本的區別。
若是你把對象當作函數,那麼函數自己其實也能夠在運行期動態建立出來,由於類的實例都是運行期建立出來的,這麼一來,咱們就模糊了對象和函數的界限。
那麼,怎麼判斷一個變量是對象仍是函數呢?其實,更多的時候,咱們須要判斷一個對象是否能被調用,能被調用的對象就是一個Callable對象,好比函數和咱們上面定義的帶有__call__()的類實例:
>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False
經過callable()函數,咱們就能夠判斷一個對象是不是「可調用」對象。