python 定製類

看到相似__slots__這種形如__xxx__的變量或者函數名就要注意,這些在Python中是有特殊用途的。html

__slots__咱們已經知道怎麼用了,__len__()方法咱們也知道是爲了能讓class做用於len()函數。python

除此以外,Python的class中還有許多這樣有特殊用途的函數,能夠幫助咱們定製類。api

__str__

咱們先定義一個Student類,打印一個實例:app

>>> 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,打印出來的實例仍是很差看:code

>>> s = Student('Michael')
>>> s
<__main__.Student object at 0x109afb310>

這是由於直接顯示變量調用的不是__str__(),而是__repr__(),二者的區別是__str__()返回用戶看到的字符串,而__repr__()返回程序開發者看到的字符串,也就是說,__repr__()是爲調試服務的。server

解決辦法是再定義一個__repr__()。可是一般__str__()__repr__()代碼都是同樣的,因此,有個偷懶的寫法:htm

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有個神奇的切片方法:

>>> 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 isinstance(n, slice):
            start = n.start
            stop = n.stop
            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

試試:

>>> 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()來調用。能不能直接在實例自己上調用呢?相似instance()?在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對象,好比函數和咱們上面定義的帶有__call()__的類實例:

>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('string')
False

經過callable()函數,咱們就能夠判斷一個對象是不是「可調用」對象。

小結

Python的class容許定義許多定製方法,可讓咱們很是方便地生成特定的類。

本節介紹的是最經常使用的幾個定製方法,還有不少可定製的方法,請參考Python的官方文檔

相關文章
相關標籤/搜索