Python面試題之Python面向對象編程匯總


 

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

面向對象的抽象程度又比函數要高,因為一個Class既包含數據,又包含操作數據的方法。

數據封裝、繼承和多態是面向對象的三大特點,我們后面會詳細講解。

 

類和實例

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

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

在python中定義一個類:

classStudent(object):
    pass

關鍵字class后面跟着類名,類名通常是大寫字母開頭的單詞,緊接着是(object),表示該類是從哪個類繼承下來的。通常,如果沒有合適的繼承類,就使用object類,這是所有類最終都會繼承下來的類。

定義好了 ,就可以根據Student類創建實例:

>>> 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. 類的方法集合。

 

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM