面向對象的設計思想是從天然界中來的,由於在天然界中,類(Class)和實例(Instance)的概念是很天然的。Class是一種抽象概念,好比咱們定義的Class——Student,是指學生這個概念,而實例(Instance)則是一個個具體的Student,好比,Bart Simpson和Lisa Simpson是兩個具體的Student。html

面向對象的抽象程度又比函數要高,由於一個Class既包含數據,又包含操做數據的方法。python

數據封裝、繼承和多態是面向對象的三大特色,咱們後面會詳細講解。程序員

 

類和實例

類(Class)和實例(Instance)是面向對象最重要的概念。sql

類是指抽象出的模板。實例則是根據類建立出來的具體的「對象」,每一個對象都擁有從類中繼承的相同的方法,但各自的數據可能不一樣。編程

在python中定義一個類:安全

classStudent(object):
    pass

關鍵字class後面跟着類名,類名一般是大寫字母開頭的單詞,緊接着是(object),表示該類是從哪一個類繼承下來的。一般,若是沒有合適的繼承類,就使用object類,這是全部類最終都會繼承下來的類。網絡

定義好了 ,就能夠根據Student類建立實例:app

>>> classStudent(object):
...     pass
...
>>> bart = Student() # bart是Student()的實例
>>> bart
<__main__.Student object at 0x101be77f0>
>>> Student # Student 自己是一個類
<class'__main__.Student'>

能夠自由地給一個實例變量綁定屬性,好比,給實例bart綁定一個name屬性:函數

>>> bart.name = "diggzhang"
>>> bart.name
'diggzhang'

類同時也能夠起到模板的做用,咱們能夠在建立一個類的時候,把一些認爲公共的東西寫進類定義中去,在python中經過一個特殊的__init__方法實現:工具

classStudent(object):
    """__init__ sample."""
    def__init__(self,name,score):
        self.name = name
        self.score = score

__init__方法的第一個參數永遠都是self,表示建立實例自己,在__init__方法內部,能夠把各類屬性綁定到self,由於self指向建立的實例自己。

有了__init__方法,在建立實例的時候,就不能傳入空的參數了,必須傳入與__init__方法匹配的參數,但self不須要傳,Python解釋器本身會把實例變量傳進去。以下面的類,在新建實例的時候,須要把namescore屬性捆綁上去:

classStudent(object):
    """example for __init__ function passin args."""
    def__init__(self,name,score):
        self.name = name
        self.score = score

咱們直接看個實例,若是咱們老老實實傳name和score進去的時候,成功聲明瞭這個實例,可是隻傳一個值的時候,報錯:

In [1]: class Student(object):
   ...:     def __init__(self, name, score):
   ...:         self.name = name
   ...:         self.score = score
   ...:

In [2]: bart = Student('diggzhang', 99)

In [3]: bart.name
Out[3]: 'diggzhang'

In [4]: bart.score
Out[4]: 99

In [5]: bart_test = Student('max')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-97f4e2f67951> in <module>()
----> 1 bart_test = Student('max')

TypeError: __init__() takes exactly 3 arguments (2 given)

和普通函數相比,在類中定義的函數只有一點不一樣,就是第一個參數永遠是實例變量self,而且,調用時,不用傳遞該參數。除此以外,類的方法和普通函數沒有什麼區別。

面向對象編程的一個重要特色就是數據封裝。在上面的Student類中,每一個實例就擁有各自的namescore這些數據。咱們能夠經過函數來訪問這些數據,好比打印一個學生的成績:

defprint_socre(std):
    print("%s: %s" % (std.name, std.score))

print_socre(bart)

# 實際執行效果
In [7]: defprint_socre(std):
   ...:         print("%s: %s" % (std.name, std.score))
   ...:

In [8]: print_socre(bart)
diggzhang: 99

 

既然咱們建立的實例裏有自身的數據,若是想訪問這些數據,就不必從外面的函數去訪問,能夠在Student類內部去定義這樣一個訪問數據的函數,這樣就把「數據」給封裝起來了。這些封裝數據的函數和Student類自己關聯起來的,咱們稱之爲類的方法:

classStudent(object):
    def__init__(self,name,score):
        self.name = name
        self.score = score
    defprint_socre(self):
        print("%s: %s" % (self.name, self.score))

要定義一個類的方法,除了傳入的第一個參數是self外,其它和普通函數同樣。若是想調用這個方法,直接在實例變量上調用,除了self不用傳遞,其他參數正常傳入:

>>> bart.print_score()
Bart Simpson: 59

實際代碼,須要在Python3環境中測試,Python2.7會報錯(NameError: global name 'name' is not defined)

$ python3
Python 3.5.1 (v3.5.1:37a07cee5969, Dec  5 2015, 21:12:44)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> classStudent(object):
...     def__init__(self,name,score):
...         self.name = name
...         self.score = score
...     defprint_score(self):
...         print("%s: %s" % (self.name, self.score))
...
>>> bart = Student('zhang', 99)
>>> bart.print_score()
zhang: 99
>>>

數據和邏輯都被封裝起來,直接調用方法便可,但卻能夠不用知道內部的細節。

總結一下。

是建立實例的模板,而 實例 則是一個一個具體的對象,各個實例擁有的數據都互相獨立,互不影響;

方法 就是與實例綁定的函數,和普通函數不一樣,方法能夠直接訪問實例的數據;

經過在實例上調用方法,咱們就直接操做了對象內部的數據,但無需知道方法內部的實現細節。

和靜態語言不一樣,Python容許對實例變量綁定任何數據,也就是說,對於兩個實例變量,雖然它們都是同一個類的不一樣實例,但擁有的變量名稱均可能不一樣:

# 用相同類建立了兩個不一樣實例
>>> bart = Student('Bart Simpson', 59)
>>> lisa = Student('Lisa Simpson', 87)
# 給其中一個實例綁定了一個變量名age
>>> bart.age = 8
>>> bart.age
8
# 另外一個同類實例中是沒有age的
>>> lisa.age
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'age'
>>>

至此,總算搞明白了什麼是類,什麼是對象。如何定義類,如何定義類內的方法。同類建立出的不一樣實例的相同和不一樣。

 

封裝

Class內部,能夠有屬性和方法,而外部代碼能夠經過直接調用實例變量的方法來操做數據,這樣,就隱藏了內部的複雜邏輯。

可是,從前面Student類的定義來看,外部代碼仍是能夠自由地修改一個實例的name、score屬性:

>>> bart = Student('Bart Simpson', 98)
>>> bart.score
98
>>> bart.score = 59
>>> bart.score
59

若是想讓內部屬性不被外部訪問,能夠把屬性的名稱前加上兩個下劃線__,在Python中,實例的變量名若是以雙下劃線開頭,就變成了一個私有變量(private),只有內部能夠訪問,外部不能訪問:

classStudent(object):
    def__init__(self,name,score):
        self.__name = name
        self.__score = score
    defprint_score(self):
        print('%s: %s' % (self.__name, self.__score))

改完後,對於外部代碼來講,沒有什麼變更,可是已經沒法從外部訪問到實例變量.__name實例變量

>>> bart = Student('Bart Simpson', 98)
>>> bart.__name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'

這樣就確保了外部代碼不能隨意修改對象內部的狀態,這樣經過訪問限制的保護,代碼更加健壯。

若是外部還須要訪問到這兩個內部狀態的話,能夠給Student類增長get_nameget_score這樣的方法。若是外部還有修改需求的話,就給該類再增長set_scoreset_name方法。用這樣的方式去get set 一個內部保護量:

classStudent(object):
    defget_name(self):
        return self.__name
    defget_score(self):
        return self.__score
    defset_name(self,name):
        self.__name = name
    defset_score(self,score):
        self.__score = score
    # 對於set_score(self, score)咱們能夠藉由set方法順便作參數檢查,提升代碼安全性
    defset_safe_score(self,score):
        if score >= 0 and score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')

須要注意的是,Python中若是變量名以雙下劃線開頭和結尾的,是特殊變量__XXX__。特殊變量是能夠直接從類內部訪問的。

有些時候,你會看到以一個下劃線開頭的實例變量名,好比_name,這樣的實例變量外部是能夠訪問的,可是,按照約定俗成的規定,當你看到這樣的變量時,意思就是,「雖然我能夠被訪問,可是,請把我視爲私有變量,不要隨意訪問」。

雙下劃線開頭的實例變量是否是必定不能從外部訪問呢?其實也不是。不能直接訪問__name是由於Python解釋器對外把__name變量改爲了_Student__name,因此,仍然能夠經過_Student__name來訪問__name變量:

>>> bart._Student__name
'Bart Simpson'

可是強烈建議你不要這麼幹,由於不一樣版本的Python解釋器可能會把__name改爲不一樣的變量名。

Python的訪問限制其實並不嚴格,主要靠自覺。

 

繼承和多態

在OOP程序設計中,當咱們定義一個class的時候,能夠從某個現有的class繼承,新的class稱爲子類(Subclass),而被繼承的class稱爲基類、父類或超類(Base class、Super class)。

好比,咱們已經編寫了一個名爲Animal的class,有一個run()方法能夠直接打印一句話,而後新建一個叫Dog的類,繼承了Animal類:

classStudent(object):
    defget_name(self):
        return self.__name
    defget_score(self):
        return self.__score
    defset_name(self,name):
        self.__name = name
    defset_score(self,score):
        self.__score = score
    # 對於set_score(self, score)咱們能夠藉由set方法順便作參數檢查,提升代碼安全性
    defset_safe_score(self,score):
        if score >= 0 and score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')

對於Dog來講,Animal就是它的父類,對於Animal來講,Dog就是它的子類。

子類得到了父類的所有功能。Dog()裏繼承了run()函數,能夠給本身的實例裏直接用。

那麼問題來了,子類和父類若是定義的時候都有個run(),會發生什麼?

classAnimal(object):
    defrun(self):
        print('running...')

classDog(Animal):
    defrun(self):
        print("Dog running...")

classCat(Animal):
    defrun(self):
        print("Cat running...")

# 結果以下
Dog is running...
Cat is running...

子類的的方法若是和父類的方法重名,子類會覆蓋掉父類。由於這個特性,就得到了一個繼承的好處」多態」。

當咱們定義一個class的時候,實際上也就是定義了一種數據類型。跟list str dict一個意思。使用isinstance(待判斷值, 數據類型)能夠作數據類型斷定。

>>> a = list()
>>> b = Animal()
>>> c = Dog()
>>> isinstance(a, list)
True
>>> isinstance(a, dict)
False
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True

有意思的是,Dog繼承自Animal,那麼Dog的實例同事也是Animal數據類型:

>>>isinstance(c, Animal)
True

# 可是若是繼承自父類,想跟子類去作判斷的話返回False
>>>isinstance(b, Dog)
False

要理解多態的好處,咱們還須要再編寫一個函數,這個函數接受一個Animal類型的變量:

"""
    run_twice() 函數接收了一個`Animal`類型的變量
"""
defrun_twice(animal):
    animal.run()
    animal.run()

>>>defrun_twice(animal):
...    animal.run()
...    animal.run()
...
"""
    當咱們將Animal()的實例傳入run_twice中...
"""
>>>run_twice(Animal())
running...
running...
"""
    當咱們將Dog()的實例傳入run_twice中...
"""
>>>run_twice(Dog())
running...
running...
>>>

看上去沒啥意思,可是仔細想一想,如今,若是咱們再定義一個Tortoise類型,也從Animal派生:

>>>classTortoise(Animal):
...    defrun(self):
...        print("Tortoise is running slowly...")
...
"""
當咱們調用run_twice()時,傳入Tortoise的實例
"""
>>>run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...
>>>

Tortoise做爲Animal的子類,沒必要對run_twice()作任何修改。實際上,任何依賴Animal做爲參數的函數或者方法均可以不加修改地正常運行,緣由在於多態。

多態的好處就是,當咱們須要傳入Dog、Cat、Tortoise……時,咱們只須要接收Animal類型就能夠了,由於Dog、Cat、Tortoise……都是Animal類型,而後,按照Animal類型進行操做便可。因爲Animal類型有run()方法,所以,傳入的任意類型,只要是Animal類或者子類,就會自動調用實際類型的run()方法,這就是多態的意思:

對於一個變量,咱們只須要知道它是Animal類型,無需確切地知道它的子類型,就能夠放心地調用run()方法,而具體調用的run()方法是做用在Animal、Dog、Cat仍是Tortoise對象上,由運行時該對象的確切類型決定,這就是多態真正的威力:調用方只管調用,無論細節,而當咱們新增一種Animal的子類時,只要確保run()方法編寫正確,不用管原來的代碼是如何調用的。這就是著名的「開閉」原則:

  • 對擴展開放:容許新增Animal子類;
  • 對修改封閉:不須要修改依賴Animal類型的run_twice()等函數。

對於靜態語言(例如Java)來講,若是須要傳入Animal類型,則傳入的對象必須是Animal類型或者它的子類,不然,將沒法調用run()方法。

對於Python這樣的動態語言來講,則不必定須要傳入Animal類型。咱們只須要保證傳入的對象有一個run()方法就能夠了:

classTimer(object):
    defrun(self):
        print('Start...')

這就是動態語言的「鴨子類型」,它並不要求嚴格的繼承體系,一個對象只要「看起來像鴨子,走起路來像鴨子」,那它就能夠被看作是鴨子。

Python的「file-like object「就是一種鴨子類型。對真正的文件對象,它有一個read()方法,返回其內容。可是,許多對象,只要有read()方法,都被視爲「file-like object「。許多函數接收的參數就是「file-like object「,你不必定要傳入真正的文件對象,徹底能夠傳入任何實現了read()方法的對象。

總結一下:

繼承能夠把父類的全部功能都直接拿過來,這樣就沒必要重零作起,子類只須要新增本身特有的方法,也能夠把父類不適合的方法覆蓋重寫。

動態語言的鴨子類型特色決定了繼承不像靜態語言那樣是必須的。

 

獲取對象信息

當咱們拿到一個對象的引用時,如何知道這個對象是什麼類型、有哪些方法呢?

type() 能夠檢查類型。用法超級簡單:

>>> type(123)
<class'int'>>>> type('helloworld')<class 'str'>>>> type(None)<class 'NoneType'>>>> type(abs)<class 'builtin_function_or_method'>>>> type(a)<class 'list'>>>> type(Animal)<class 'type'>>>> type(Dog)<class 'type'>>>> type(Dog())<class '__main__.Dog'>>>>

type()常常被用來作類型比較:

>>>type(123) == type(456)
True
>>>type(123) == int
True
>>>type(123) == type('123')
False

判斷基本數據類型能夠直接寫intstr等,但若是要判斷一個對象是不是函數怎麼辦?可使用types模塊中定義的常量:

>>>import types
>>>deffn():
...    pass
...
>>>type(fn) == types.FunctionType
True
>>>type(abs) == types.BuiltinFunctionType
True
>>>type(lambda x: x)==types.LambdaType
True
>>>type((x for x in range(10)))==types.GeneratorType
True

還有大殺器isinstance()

對於class的繼承關係來講,使用type()就很不方便。咱們要判斷class的類型,可使用isinstance()函數。

咱們回顧上次的例子,若是繼承關係是:

object -> Animal -> Dog -> Husky

那麼,isinstance()就能夠告訴咱們,一個對象是不是某種類型。這玩意兒也是上手熟系列:

>>> a = Animal()
>>> b = Dog()

>>> isinstance(c, Animal)
True
>>> isinstance(c, Dog)
True

>>> isinstance(a, Animal)
True
>>> isinstance(a, Dog)
False

還能夠判斷一個變量是不是某些類型中的一種,好比下面的代碼就能夠判斷是不是list或者tuple:

>>>isinstance([1, 2, 3], (list, tuple))
True
>>>isinstance((1, 2, 3), (list, tuple))
True
>>>isinstance((1, 2, 3), (tuple))
True
>>>isinstance((1, 2, 3), (list))
False

最後一個大殺器dir()

若是要得到一個對象的全部屬性和方法,可使用dir()函數,它返回一個包含字符串的list,好比,得到一個str對象的全部屬性和方法:

dir('ABC')
[........,'__add__',.....,'__len__',...,'lower','upper'...]

相似__xxx__的屬性和方法在Python中都是有特殊用途的,好比__len__方法返回長度。在Python中,若是你調用len()函數試圖獲取一個對象的長度,實際上,在len()函數內部,它自動去調用該對象的__len__()方法,因此,下面的代碼是等價的:

>>> len('ABC')
3
>>> 'ABC'.__len__()
3

咱們本身寫的類,若是也想用len(myObj)的話,就本身寫一個__len__()方法:

>>> classMyDog(object):
...     def__len__(self):
...         return 100
...
>>> dog = MyDog()
>>> len(dog)
100

dir()返回的非雙下劃線樣子的,都是普通屬性或方法,好比lower:

>>> 'ABC'.lower()
'abc'

固然既然能列出這屬性和方法,也能夠相應的修改。python準備了getattr()、setattr()、hasattr(),能夠直接操做一個對象的狀態:

>>> classMyObject(object):
...     def__init__(self):
...         self.x = 9
...     defpower(self):
...         return self.x + self.x
...
>>> obj = MyObject()


>>> hasattr(obj, 'x') # 有屬性'x'嗎?
True
>>> obj.x
9
>>> hasattr(obj, 'y') # 有屬性'y'嗎?
False
>>> setattr(obj, 'y', 19) # 設置一個屬性'y'
>>> hasattr(obj, 'y') # 有屬性'y'嗎?
True
>>> getattr(obj, 'y') # 獲取屬性'y'
19
>>> obj.y # 獲取屬性'y'
19

>>> hasattr(obj, 'power') # 有屬性'power'嗎?
True
>>> getattr(obj, 'power') # 獲取屬性'power'
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>>>> fn = getattr(obj, 'power') # 獲取屬性'power'並賦值到變量fn
>>> fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>>>> fn() # 調用fn()與調用obj.power()是同樣的
81

實際編碼過程當中,能夠設置一個default值,若是屬性不存在,就返回默認值:

>>> getattr(obj, 'k', 404)
404

經過內置的一系列函數,咱們能夠對任意一個Python對象進行剖析,拿到其內部的數據。要注意的是,只有在不知道對象信息的時候,咱們纔會去獲取對象信息。若是能夠直接寫:

>>> getattr(obj, 'k', 404)
404

就不要寫:

sum = getattr(obj, 'x') + getattr(obj, 'y')

一個正確的用法以下:

defreadImage(fp):
    if hasattr(fp, 'read'):
        return readData(fp)
    return None

假設咱們但願從文件流fp中讀取圖像,咱們首先要判斷該fp對象是否存在read方法,若是存在,則該對象是一個流,若是不存在,則沒法讀取。hasattr()就派上了用場。

請注意,在Python這類動態語言中,根據鴨子類型,有read()方法,不表明該fp對象就是一個文件流,它也多是網絡流,也多是內存中的一個字節流,但只要read()方法返回的是有效的圖像數據,就不影響讀取圖像的功能。

若是你成功看到這部分,你能夠跟本身說:「來了,這份感受終於來了,個人人生開始贏了。」

 

實例屬性和類屬性

因爲Python是動態語言,根據類建立的實例能夠任意綁定屬性。那就會有這種狀況:

classStudent(object):
    name = 'Student'

類的名字是Student,類裏的屬性也叫Student。這會致使黑人問號臉。

>>> classStudent(object):
...     name = 'Student'
...
>>> s = Student() # 建立實例s
>>> print(s.name) # 打印name屬性,由於實例並無name屬性,因此會繼續查找class的name屬性
Student
>>> print(Student.name) # 打印類的name屬性
Student
>>> s.name = 'Michael' # 給實例綁定name屬性
>>> print(s.name) # 因爲實例屬性優先級比類屬性高,所以,它會屏蔽掉類的name屬性
Michael
>>> print(Student.name) # 可是類屬性並未消失,用Student.name仍然能夠訪問
Student
>>> del s.name # 若是刪除實例的name屬性
>>> print(s.name) # 再次調用s.name,因爲實例的name屬性沒有找到,類的name屬性就顯示出來了
Student

從上面的例子能夠看出,在編寫程序的時候,千萬不要把實例屬性和類屬性使用相同的名字,由於相同名稱的實例屬性將屏蔽掉類屬性,可是當你刪除實例屬性後,再使用相同的名稱,訪問到的將是類屬性


數據封裝、繼承和多態只是面向對象程序設計中最基礎的3個概念。在Python中,面向對象還有不少高級特性,容許咱們寫出很是強大的功能。

接下來咱們會討論多重繼承、定製類、元類等概念。

 

使用 slots

正常狀況下,當咱們定義了一個class,建立了一個class的實例後,咱們能夠給該實例綁定任何屬性和方法。可是,若是咱們想要限制實例的屬性怎麼辦?

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

classStudent(object):
    __slots__ = ('name', 'age') # 用tuple定義容許綁定的屬性名稱

"""實際執行效果"""
>>>classStudent(object):
...    __slots__ = ('name', 'age')
...
>>>s = Student()
>>>s.name = 'digg'
>>>s.age = '19'
>>>s.score = 99
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__定義的屬性僅對當前類實例起做用,對繼承的子類是不起做用的:

>>>classGraduateStudent(Student):
...    pass
...
>>>g = GraduateStudent()
>>>g.score = 9999

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

 

使用 @property

在綁定屬性時,若是咱們直接把屬性暴露出去,雖然寫起來很簡單,可是,沒辦法檢查參數,致使能夠把成績隨便改:

s = Student()
s.score = 9999

這顯然不合邏輯。爲了限制score的範圍,能夠經過一個set_score()方法來設置成績,再經過一個get_score()來獲取成績,這樣,在set_score()方法裏,就能夠檢查參數:

classStudent(object):
    defget_score(self):
        return self._socre
    defset_socre(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.')

如今,對任意的Student實例進行操做,就不能爲所欲爲地設置score了:

classStudent(object):
    defget_score(self):
        return self._socre
    defset_socre(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.')

有沒有既能檢查參數,又能夠用相似屬性這樣簡單的方式來訪問類的變量呢?對於追求完美的Python程序員來講,這是必需要作到的!

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

classStudent(object):
    @property
    defscore(self):
        return self._score

    @score.setter
    defscore(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方法變成屬性賦值,因而,咱們就擁有一個可控的屬性操做。看一下實際執行效果:

>>> classStudent(object):
...     @property
...     defscore(self):
...         return self._score
...     @score.setter
...     defscore(self,value):
...         if not isinstance(value, int):
...             raise ValueError('score must be integer!')
...         if value < 0 or value > 100:
...             raise ValueError('score must between 0 - 100!')
...         self._score = value
...
>>> s = Student()
>>> s.score = 60
>>> s.score
60
>>> s.score = 9999
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 10, in score
ValueError: score must between 0 - 100!
>>>

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

classStudent(object):

    @property
    defbirth(self):
        return self._birth

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

    @property
    defage(self):
        return 2015 - self._birth

上面的birth是可讀寫屬性,而age就是一個只讀屬性,由於age能夠根據birth和當前時間計算出來。

@property普遍應用在類的定義中,可讓調用者寫出簡短的代碼,同時保證對參數進行必要的檢查,這樣,程序運行時就減小了出錯的可能性。

廖老師給了一個做業:

利用@property給一個Screen對象加上width和height屬性,以及一個只讀屬性resolution。

"""
    做業解決方案
"""
>>>classScreen(object):
...    @property
...    defwidth(self):
...        return self._width
...    @width.setter
...    defwidth(self,value):
...        self._width = value
...    @property
...    defheight(self):
...        return self._height
...    @height.setter
...    defheight(self,value):
...        self._height = value
...    @property
...    defresolution(self):
...        return self._width * self._height
...
>>>s = Screen()
>>>s.width = 1024
>>>s.height = 768
>>>s.resolution
786432
>>>

 

多重繼承

繼承是面向對象編程的一個重要的方式,由於經過繼承,子類就能夠擴展父類的功能。

以前咱們的講的例子中有Animal類,以及繼承了Animal類的Dog類。這個繼承關係是單向的。咱們能夠再建立一個類,讓Dog繼承Animal同時,繼承新建的類:

classRunnable(object):
    defrun(self):
        print("I'm running...")

多重繼承:

classDog(Animal,Runnable):
    pass

經過多重繼承,一個子類就能夠同時得到多個父類的全部功能。

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

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

經過各類組合繼承類,不須要複雜而龐大的繼承鏈,只要選擇組合不一樣的類的功能,就能夠快速構造出所需的子類。因爲Python容許使用多重繼承,所以,MixIn就是一種常見的設計。

只容許單一繼承的語言(如Java)不能使用MixIn的設計。

 

定製類

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

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

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

  • __str__
>>> classStudent(object):
...     def__init__(self,name):
...         self.name = name
...
>>> print(Student('diggzhang'))
<__main__.Student object at 0x1016e4828> # 這裏打印了一堆醜東西
>>>

若是想改變這堆打印的的醜東西,就須要用到__str___,在類裏從新定義這個方法就能夠了:

>>> classStudent(object):
...     def__init__(self,name):
...         self.name = name
...     def__str__(self):
...         return "Student name is %s" % self.name
...
>>> print(Student('diggzhang'))
Student name is diggzhang
>>>    
# 可是去掉print
>>> Student('diggzhang')
<__main__.Student object at 0x1016e4828>

去掉print打印醜是由於直接顯示變量不歸__str__管了,由__repr__管,通常這倆類若是定製的話,處理辦法都同樣,因而能夠來個簡單的,在定製好__str__後直接從新賦值給__str__

classStudent(object):
    def__init__(self,name):
        self.name = name
    def__str__(self):
        return 'Student object (name=%s)' % self.name
    __repr__ = __str__
  • __iter____next__

若是一個類想被用於for ... in循環,相似listtuple那樣,就必須實現一個__iter__()方法,該方法返回一個迭代對象,而後,Python的for循環就會不斷調用該迭代對象的__next__()方法拿到循環的下一個值,直到遇到StopIteration錯誤時退出循環。

classFib(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 > 100000: # 退出循環條件
            raise StopIteration();
        return self.a

# 測試
for n in Fib():
    print(n)
  • __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__()方法:

classFib(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
  • __getattr__

還記得以前若是訪問實例中的屬性不存在就會拋出的no attribute錯誤嗎?

__getattr__能夠動態的返回一個屬性,當要訪問的屬性不存在的時候,Python解釋器會試圖調用__getattr__(XXX)來嘗試得到須要的屬性。利用這一點,能夠把一個類的全部屬性和方法調用所有動態化處理。

利用到實際中的例子,若是咱們要實現幾個API的話,會須要對應的URL就寫一個對應的方法去處理。API一旦改動,SDK也跟着要改。

利用徹底動態的__getattr__,咱們能夠寫出一個鏈式調用:

classChain(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
  • __call__

一個對象實例能夠有本身的屬性和方法,當咱們調用實例方法時,咱們用instance.method()來調用。能不能直接在實例自己上調用呢?在Python中,答案是確定的。

任何類,只須要定義一個__call__()方法,就能夠直接對實例進行調用。請看示例:

classStudent(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

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

 

使用枚舉類

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

JAN = 1
FEB = 2
MAR = 3
...
NOV = 11
DEC = 12

好處是簡單,缺點是類型是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)
...
Jan => Month.Jan , 1
Feb => Month.Feb , 2
Mar => Month.Mar , 3
Apr => Month.Apr , 4
May => Month.May , 5
Jun => Month.Jun , 6
Jul => Month.Jul , 7
Aug => Month.Aug , 8
Sep => Month.Sep , 9
Oct => Month.Oct , 10
Nov => Month.Nov , 11
Dec => Month.Dec , 12
>>>

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

若是須要更精確地控制枚舉類型,能夠從Enum派生出自定義類:

from enum import Enum, unique

# @unique裝飾器能夠幫助咱們檢查保證沒有重複值。
@unique
classWeekday(Enum):
    Sun = 0 # Sun的value被設定爲0
    Mon = 1
    Tue = 2
    Wed = 3
    Thu = 4
    Fri = 5
    Sat = 6

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

from enum import Enum, unique

# @unique裝飾器能夠幫助咱們檢查保證沒有重複值。
@unique
classWeekday(Enum):
    Sun = 0 # Sun的value被設定爲0
    Mon = 1
    Tue = 2
    Wed = 3
    Thu = 4
    Fri = 5
    Sat = 6

 

使用元類

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

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

classHello(object):
    defhello(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)...的定義:

>>> deffn(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個參數:

type(‘Hello’, (object,), dict(hello=fn))

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

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

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

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

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

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

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

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

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

來個例子感覺一下,按照默認習慣,metaclass的類名老是以Metaclass結尾,以便清楚地表示這是一個metaclass:

# metaclass是類的模板,因此必須從`type`類型派生:
classListMetaclass(type):
    def__new__(cls,name,bases,attrs):
        attrs['add'] = lambda self, value: self.append(value)
        return type.__new__(cls, name, bases, attrs)

有了ListMetaclass,咱們在定義類的時候還要指示使用ListMetaclass來定製類,傳入關鍵字參數metaclass

classMyList(list,metaclass=ListMetaclass):
    pass

當咱們傳入關鍵字參數metaclass時,魔術就生效了,它指示Python解釋器在建立MyList時,要經過ListMetaclass.__new__()來建立,在此,咱們能夠修改類的定義,好比,加上新的方法,而後,返回修改後的定義。

__new__()方法接收到的參數依次是:

  1. 當前準備建立的類的對象;
  2. 類的名字;
  3. 類繼承的父類集合;
  4. 類的方法集合。

 

參考