Python基礎(定製類)

文章轉載自廖雪峯老師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()函數,咱們就能夠判斷一個對象是不是「可調用」對象。
相關文章
相關標籤/搜索