Python 5.4 訂製類
- 2020 年 1 月 14 日
- 筆記
訂製類
看到類似的__slots__這種形如__xx__的變數或者函數名就要注意,這些在Python中有特殊用途。
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'))
Studnet object (name: Michael)
但是直接敲命令不使用print,列印出來的實例還是不好看:
>>>s =Student('Michael')
>>>s
<__main__.Student object at 0x109afb190>
這是因為直接顯示變數調用的不是__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__()方法。該方法返回一個迭代對象,然後for循環就會不斷調用該迭代對象的__next__()方法拿到循環的下一個值,直到遇到StopIteration錯誤時退出循環。
我們以斐波那契數列為例寫一個Fib,可以作用於for循環: class Fib(object): def __init__(self): self.a,self.b = 0,1
def __iter__(self): return self
def __next__(self): self.a,self.b =self.b,self.a + self.b
if self.a >10000:
raise StopIteration()
return self.a
>>>for n in Fib(): print(n)
__getitem__
Fib實例雖然能作用於for循環,看起來和list有點像,但是把它當list還是不行的。比如,取第五個元素: >>>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
現在,就可以像list那樣按下標訪問數列的任一項了: >>>Fib()[0]
1
>>>Fib()[1]
1
>>>Fib()[2]
2
>>>Fib()(10)
89
但是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):
a,b =1,1
for x in range(n): a,b =b,a +b
return a
if isinstane(n,slice): 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__
正常情況下,當我們調用類的方法或者屬性時,如果不存在,就會報錯。
要避免這個錯誤,就寫一個__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.socre
99
返回函數也是可以得:
class Student(object): def __getattr__(self,attr): if attr =='age':
return lambda:25
調用方式改為: >>>s.age()
25
注意,只有在沒有找到屬性的情況下,才會調用__getattr__,已有的屬性,比如name,不會在__getattr__中查找。
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類似:
如果要寫SDK,給每個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()
My name is Michael
__call__()還可以定義參數。對實例進行直接調用就好比對一個函數調用一樣,所以,完全可以把對象看成函數,把函數看成 對象,這兩者之間本來就沒什麼區別。
如果你把對象看成函數,那麼函數本身其實也可以在運行期間動態創建出來,因為類的實例都是運行期間創建的,這麼一來,我們就模糊了對象和函數的界限。
>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None) False >>> callable('str')
False
通過callable()函數,我們就可以判斷一個對象是否是「可調用」。
小結:
Python的class允許定義許多訂製方法,可以讓我們非常方便的生成特製類。