python-復盤-面向對象高級編程-各種高級函數

使用 __slots__

限制實例屬性, 比如,只允許對Student實例添加name和age屬性。

class Student(object):             # slot英文意思 位置 插槽
    __slots__ = ('name', 'age')    # 用tuple定義允許綁定的屬性名稱

然后,我們試試:

>>> s = Student()          # 創建新的實例
>>> s.name = 'Michael'     # 綁定屬性'name'
>>> s.age = 25             # 綁定屬性'age'
>>> s.score = 99           # 綁定屬性'score'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

由于'score'沒有被放到__slots__中,所以不能綁定score屬性,試圖綁定score將得到AttributeError的錯誤。

使用__slots__要注意,__slots__定義的屬性僅對當前類實例起作用,對繼承的子類是不起作用的


使用@property

還記得裝飾器(decorator)可以給函數動態加上功能嗎?對于類的方法,裝飾器一樣起作用。Python內置的@property裝飾器就是負責把一個方法變成屬性調用的:

class Student(object):

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

@property的實現比較復雜,我們先考察如何使用。把一個getter方法變成屬性,只需要加上@property就可以了,此時,@property本身又創建了另一個裝飾器@score.setter,負責把一個setter方法變成屬性賦值,于是,我們就擁有一個可控的屬性操作

>>> s = Student()
>>> s.score = 60 # OK,實際轉化為s.set_score(60)
>>> s.score # OK,實際轉化為s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

注意到這個神奇的@property,我們在對實例屬性操作的時候,就知道該屬性很可能不是直接暴露的,而是通過gettersetter方法來實現的。

還可以定義只讀屬性,只定義getter方法,不定義setter方法就是一個只讀屬性:

class Student(object):

    @property
    def birth(self):
        return self._birth

    @birth.setter
    def birth(self, value):
        self._birth = value

    @property
    def age(self):
        return 2015 - self._birth

上面的birth是可讀寫屬性,而age就是一個只讀屬性,因為age可以根據birth和當前時間計算出來

練習
請利用@property給一個Screen對象加上widthheight屬性,以及一個只讀屬性resolution

class Screen(object):    # 自己做的,仔細對照上方,就是一個套路,照著套路來就像
    @property
    def width(self):
        return self._width

    @width.setter     # 設置value
    def width(self,value):
        if not isinstance(value, int):
            raise ValueError('width must be an int!')
        if value <= 0:
            raise ValueError('width must be positive number')
        self._width = value

    @property
    def height(self):
        return self._width

    @height.setter
    def height(self, value):
        if not isinstance(value, int):
            raise ValueError('height must be an int!')
        if value <= 0:
            raise ValueError('height must be positive number')
        self._height = value

    @property
    def resolution(self):
        return self._height * self._width

s = Screen()
s.width = 24
s.height = 3
print(s.resolution)
assert s.resolution == 72, '1024 * 768 = %d ?' % s.resolution


多重繼承

對于需要Flyable功能的動物,就多繼承一個Flyable,例如Bat:

class Bat(Mammal, Flyable):     #   蝙蝠(哺乳動物,飛行動物)
    pass   #  部分代碼 敘述已略,明白意思就行,簡單

通過多重繼承,一個子類就可以同時獲得多個父類的所有功能。
MixIn
在設計類的繼承關系時,通常,主線都是單一繼承下來的,例如,Ostrich繼承自Bird。但是,如果需要“混入”額外的功能,通過多重繼承就可以實現,比如,讓Ostrich除了繼承自Bird外,再同時繼承Runnable。這種設計通常稱之為MixIn

為了更好地看出繼承關系,我們把RunnableFlyable改為RunnableMixInFlyableMixIn。類似的,你還可以定義出肉食動物CarnivorousMixIn和植食動物HerbivoresMixIn,讓某個動物同時擁有好幾個MixIn

class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
    pass   # 哺乳動物,可跑的,食肉動物

MixIn的目的就是給一個類增加多個功能,這樣,在設計類的時候,我們優先考慮通過多重繼承來組合多個MixIn的功能,而不是設計多層次的復雜的繼承關系。
關聯: MRO算法


定制類

__str__

>>> 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):   #  就是 a = b , b = a + b,二者不斷相加,所以開始會出現1,1
        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
...
46368

__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

廖神官網這里還有個用法,這里省略,研究可去官網
__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類似:
.........
如果要寫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()函數,我們就可以判斷一個對象是否是“可調用”對象。
還有很多可定制的方法,請參考Python的官方文檔


使用枚舉類型

當我們需要定義常量時,一個辦法是用大寫變量通過整數來定義,例如月份:

JAN = 1
FEB = 2
MAR = 3
...

好處是簡單,缺點是類型是int,并且仍然是變量。

更好的方法是為這樣的枚舉類型定義一個class類型,然后,每個常量都是class的一個唯一實例。Python提供了Enum類來實現這個功能:

from enum import Enum

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

這樣我們就獲得了Month類型的枚舉類,可以直接使用Month.Jan來引用一個常量,或者枚舉它的所有成員:

for name, member in Month.__members__.items():
    print(name, '=>', member, ',', member.value)

value屬性則是自動賦給成員的int常量,默認從1開始計數。

如果需要更精確地控制枚舉類型,可以從Enum派生出自定義類:

from enum import Enum, unique

@unique
class Weekday(Enum):
    Sun = 0 # Sun的value被設定為0
    Mon = 1
    Tue = 2
    Wed = 3

@unique裝飾器可以幫助我們檢查保證沒有重復值

Enum可以把一組相關常量定義在一個class中,且class不可變,而且成員可以直接比較


使用元類

type()

動態語言和靜態語言最大的不同,就是函數和類的定義,不是編譯時定義的,而是運行時動態創建的。

比方說我們要定義一個Hello的class,就寫一個hello.py模塊:

class Hello(object):
    def hello(self, name='world'):
        print('Hello, %s.' % name)

當Python解釋器載入hello模塊時,就會依次執行該模塊的所有語句,執行結果就是動態創建出一個Hello的class對象,測試如下:

>>> from hello import Hello
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class 'hello.Hello'>

type()函數可以查看一個類型或變量的類型,Hello是一個class,它的類型就是type,而h是一個實例,它的類型就是class Hello。

我們說class的定義是運行時動態創建的,而創建class的方法就是使用type()函數。
要創建一個class對象,type()函數依次傳入3個參數:

1.class的名稱;
2.繼承的父類集合,注意Python支持多重繼承,如果只有一個父類,別忘了tuple的單元素寫法;
3.class的方法名稱與函數綁定,這里我們把函數fn綁定到方法名hello上。

通過type()函數創建的類和直接寫class是完全一樣的,因為Python解釋器遇到class定義時,僅僅是掃描一下class定義的語法,然后調用type()函數創建出class。

正常情況下,我們都用class Xxx...來定義類,但是,type()函數也允許我們動態創建出類來,也就是說,動態語言本身支持運行期動態創建類,這和靜態語言有非常大的不同,要在靜態語言運行期創建類,必須構造源代碼字符串再調用編譯器,或者借助一些工具生成字節碼實現,本質上都是動態編譯,會非常復雜。

metaclass 元類
本質上,type()可以創建一個class的實例對象,而class是由metaclass元類創建了。

所以,metaclass允許你創建類或者修改類。換句話說,你可以把類看成是metaclass創建出來的“實例”。






最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1. 使用__slots__ 正常情況下,當我們定義了一個class,創建了一個class的實例后,我們可以給該實...
    時間之友閱讀 298評論 0 1
  • 要點: 函數式編程:注意不是“函數編程”,多了一個“式” 模塊:如何使用模塊 面向對象編程:面向對象的概念、屬性、...
    victorsungo閱讀 1,562評論 0 6
  • 定義類并創建實例 在Python中,類通過 class 關鍵字定義。以 Person 為例,定義一個Person類...
    績重KF閱讀 3,971評論 0 13
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,765評論 18 399