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允許定義許多訂製方法,可以讓我們非常方便的生成特製類。

Tags: