Python學習之面向對象高級編程

Python學習目錄python

  1. 在Mac下使用Python3
  2. Python學習之數據類型
  3. Python學習之函數
  4. Python學習之高級特性
  5. Python學習之函數式編程
  6. Python學習之模塊
  7. Python學習之面向對象編程
  8. Python學習之面向對象高級編程
  9. Python學習之錯誤調試和測試
  10. Python學習之IO編程
  11. Python學習之進程和線程
  12. Python學習之正則
  13. Python學習之經常使用模塊
  14. Python學習之網絡編程

數據封裝、繼承和多態只是面向對象程序設計中最基礎的3個概念。在Python中,面向對象還有不少高級特性,如:多重繼承、定製類、元類等概念。編程

_slots_

做用:限制實例的屬性。網絡

Python容許在定義class的時候,定義一個特殊的__slots__變量,來限制該class實例能添加的屬性:app

class Student(object):
    __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__定義的屬性僅對當前類實例起做用,對繼承的子類是不起做用的:函數

>>> class GraduateStudent(Student):
...     pass
...
>>> g = GraduateStudent()
>>> g.score = 9999
複製代碼

除非在子類中也定義__slots__,這樣,子類實例容許定義的屬性就是自身的__slots__加上父類的__slots__工具

@property

裝飾器(decorator)能夠給函數動態加上功能,對於類的方法,裝飾器同樣起做用,Python內置的@property裝飾器就是負責把一個方法變成屬性調用的:post

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
複製代碼

把一個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,咱們在對實例屬性操做的時候,就知道該屬性極可能不是直接暴露的,而是經過getter和setter方法來實現的。測試

還能夠定義只讀屬性,只定義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和當前時間計算出來。

多重繼承

在設計類的繼承關係時,一般,主線都是單一繼承下來的,例如,Ostrich繼承自Bird。可是,若是須要「混入」額外的功能,經過多重繼承就能夠實現,好比,讓Ostrich除了繼承自Bird外,再同時繼承Runnable。這種設計一般稱之爲MixIn。

爲了更好地看出繼承關係,咱們把RunnableFlyable改成RunnableMixInFlyableMixIn。相似的,你還能夠定義出肉食動物CarnivorousMixIn和植食動物HerbivoresMixIn,讓某個動物同時擁有好幾個MixIn:

class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
    pass
複製代碼

MixIn的目的就是給一個類增長多個功能,這樣,在設計類的時候,咱們優先考慮經過多重繼承來組合多個MixIn的功能,而不是設計多層次的複雜的繼承關係。

定製類

到相似__slots__這種形如__xxx__的變量或者函數名就要注意,這些在Python中是有特殊用途的。__slots__咱們已經知道怎麼用了,__len__()方法咱們也知道是爲了能讓class做用於len()函數。除此以外,Python的class中還有許多這樣有特殊用途的函數,能夠幫助咱們定製類。

_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_

像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 
複製代碼

_getattr_

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)
複製代碼

_call_

一個對象實例能夠有本身的屬性和方法,當咱們調用實例方法時,咱們用instance.method()來調用。

一樣的,任何類,只須要定義一個__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
複製代碼

枚舉類

「枚舉類」Enum是爲枚舉類型定義一個class類型,而後,每一個常量都是class的一個惟一實例。

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
    Thu = 4
    Fri = 5
    Sat = 6
複製代碼

@unique裝飾器能夠幫助咱們檢查保證沒有重複值。

訪問這些枚舉類型能夠有若干種方法:

>>> day1 = Weekday.Mon
>>> print(day1)
Weekday.Mon
>>> print(Weekday.Tue)
Weekday.Tue
>>> print(Weekday['Tue'])
Weekday.Tue
>>> print(Weekday.Tue.value)
2
>>> print(day1 == Weekday.Mon)
True
>>> print(day1 == Weekday.Tue)
False
>>> print(Weekday(1))
Weekday.Mon
>>> print(day1 == Weekday(1))
True
>>> Weekday(7)
Traceback (most recent call last):
  ...
ValueError: 7 is not a valid Weekday
>>> for name, member in Weekday.__members__.items():
...     print(name, '=>', member)
...
Sun => Weekday.Sun
Mon => Weekday.Mon
Tue => Weekday.Tue
Wed => Weekday.Wed
Thu => Weekday.Thu
Fri => Weekday.Fri
Sat => Weekday.Sat
複製代碼

可見,既能夠用成員名稱引用枚舉常量,又能夠直接根據value的值得到枚舉常量。

元類

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()函數。

type()函數既能夠返回一個對象的類型,又能夠建立出新的類型,好比,咱們能夠經過type()函數建立出Hello類,而無需經過class Hello(object)...的定義:

>>> def fn(self, name='world'): # 先定義函數
...     print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 建立Hello class
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>
複製代碼

要建立一個class對象,type()函數依次傳入3個參數:

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

經過type()函數建立的類和直接寫class是徹底同樣的,由於Python解釋器遇到class定義時,僅僅是掃描一下class定義的語法,而後調用type()函數建立出class。

正常狀況下,咱們都用class Xxx...來定義類,可是,type()函數也容許咱們動態建立出類來,也就是說,動態語言自己支持運行期動態建立類,這和靜態語言有很是大的不一樣,要在靜態語言運行期建立類,必須構造源代碼字符串再調用編譯器,或者藉助一些工具生成字節碼實現,本質上都是動態編譯,會很是複雜。

metaclass

除了使用type()動態建立類之外,要控制類的建立行爲,還可使用metaclass。

metaclass,直譯爲元類,簡單的解釋就是:

當咱們定義了類之後,就能夠根據這個類建立出實例,因此:先定義類,而後建立實例。

可是若是咱們想建立出類呢?那就必須根據metaclass建立出類,因此:先定義metaclass,而後建立類。

鏈接起來就是:先定義metaclass,就能夠建立類,最後建立實例。

因此,metaclass容許你建立類或者修改類。換句話說,你能夠把類當作是metaclass建立出來的「實例」。

下一篇:Python學習之錯誤調試和測試

相關文章
相關標籤/搜索