banner
Zoney

Zoney

一个励志成为Web安全工程师的女青年!

python 學習 4

七 面向對象編程#

面向對象編程 ——Object Oriented Programming,簡稱OOP,是一種程序設計思想。OOP 把對象作為程序的基本單元,一個對象包含了數據和操作數據的函數。

面向過程的程序設計把計算機程序視為一系列的命令集合,即一組函數的順序執行。為了簡化程序設計,面向過程把函數繼續切分為子函數,即把大塊函數通過切割成小塊函數來降低系統的複雜度。

而面向對象的程序設計把計算機程序視為一組對象的集合,而每個對象都可以接收其他對象發過來的消息,並處理這些消息,計算機程序的執行就是一系列消息在各個對象之間傳遞。

在 Python 中,所有數據類型都可以視為對象,當然也可以自定義對象。自定義的對象數據類型就是面向對象中的類(Class)的概念。

我們以一個例子來說明面向過程和面向對象在程序流程上的不同之處。

假設我們要處理學生的成績表,為了表示一個學生的成績,面向過程的程序可以用一個 dict 表示:

std1 = { 'name': 'Michael', 'score': 98 }
std2 = { 'name': 'Bob', 'score': 81 }

而處理學生成績可以通過函數實現,比如打印學生的成績:

def print_score(std):
    print('%s: %s' % (std['name'], std['score']))

如果採用面向對象的程序設計思想,我們首選思考的不是程序的執行流程,而是 Student 這種數據類型應該被視為一個對象,這個對象擁有 name 和 score 這兩個屬性(Property)。如果要打印一個學生的成績,首先必須創建出這個學生對應的對象,然後,給對象發一個 print_score 消息,讓對象自己把自己的數據打印出來。

class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

    def print_score(self):
        print('%s: %s' % (self.name, self.score))

給對象發消息實際上就是調用對象對應的關聯函數,我們稱之為對象的方法(Method)。面向對象的程序寫出來就像這樣:

bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()

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

所以,面向對象的設計思想是抽象出 Class,根據 Class 創建 Instance。

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

數據封裝繼承多態是面向對象的三大特點

類和實例
面向對象最重要的概念就是類(Class)和實例(Instance),必須牢記類是抽象的模板,比如 Student 類,而實例是根據類創建出來的一個個具體的 **“對象”**,每個對象都擁有相同的方法,但各自的數據可能不同。

仍以 Student 類為例,在 Python 中,定義類是通過 class 關鍵字:

class Student(object):
    pass

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

定義好了 Student 類,就可以根據 Student 類創建出 Student 的實例,創建實例是通過 ** 類名 +()** 實現的:

>>> bart = Student()
>>> bart
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>

可以看到,變量 bart 指向的就是一個 Student 的實例,後面的 0x10a67a590 是內存地址,每個 object 的地址都不一樣,而 Student 本身則是一個類。

可以自由地給一個實例變量綁定屬性,比如,給實例 bart 綁定一個 name 屬性:

>>> bart.name = 'Bart Simpson'
>>> bart.name
'Bart Simpson'

由於類可以起到模板的作用,因此,可以在創建實例的時候,把一些我們認為必須綁定的屬性強制填寫進去。通過定義一個特殊的__init__方法,在創建實例的時候,就把 name,score 等屬性綁上去:

class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

注意:特殊方法 “init” 前後分別有兩個下劃線!!!
注意到__init__方法的第一個參數永遠是self,表示創建的實例本身,因此,在__init__方法內部,就可以把各種屬性綁定到 self,因為self 就指向創建的實例本身

有了__init__方法,在創建實例的時候,就不能傳入空的參數了,必須傳入與__init__方法匹配的參數,但 self 不需要傳,Python 解釋器自己會把實例變量傳進去:

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

和普通的函數相比,在類中定義的函數只有一點不同,就是第一個參數永遠是實例變量 self,並且,調用時不用傳遞該參數。除此之外,類的方法和普通函數沒有什麼區別,所以,你仍然可以用默認參數、可變參數、關鍵字參數和命名關鍵字參數。

數據封裝
面向對象編程的一個重要特點就是數據封裝。在上面的 Student 類中,每個實例就擁有各自的 name 和 score 這些數據。我們可以通過函數來訪問這些數據,比如打印一個學生的成績:

>>> def print_score(std):
...     print('%s: %s' % (std.name, std.score))
...
>>> print_score(bart)
Bart Simpson: 59

但是,既然 Student 實例本身就擁有這些數據,要訪問這些數據,就沒有必要從外面的函數去訪問,可以直接在 Student 類的內部定義訪問數據的函數,這樣,就把 “數據” 給封裝起來了。這些封裝數據的函數是和 Student 類本身是關聯起來的,我們稱之為類的方法:

class Student(object):

    def __init__(self, name, score):
        self.name = name
        self.score = score

    def print_score(self):
        print('%s: %s' % (self.name, self.score))

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

這樣一來,我們從外部看 Student 類,就只需要知道,創建實例需要給出 name 和 score,而如何打印,都是在 Student 類的內部定義的,這些數據和邏輯被 “封裝” 起來了,調用很容易,但卻不用知道內部實現的細節

封裝的另一個好處是可以給 Student 類增加新的方法,比如 get_grade:

class Student(object):
    ...

    def get_grade(self):
        if self.score >= 90:
            return 'A'
        elif self.score >= 60:
            return 'B'
        else:
            return 'C'

同樣的,get_grade 方法可以直接在實例變量上調用,不需要知道內部實現細節

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

2 方法就是與實例綁定的函數,和普通函數不同,方法可以直接訪問實例的數據;

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

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

>>> bart = Student('Bart Simpson', 59)
>>> lisa = Student('Lisa Simpson', 87)
>>> bart.age = 8
>>> bart.age
8
>>> 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', 59)
>>> bart.score
59
>>> bart.score = 99
>>> bart.score
99

如果要讓內部屬性不被外部訪問,可以把屬性的名稱前加上兩個下劃線__,在 Python 中,實例的變量名如果以__開頭,就變成了一個私有變量(private),只有內部可以訪問,外部不能訪問,所以,我們把 Student 類改一改:

class Student(object):

    def __init__(self, name, score):
        self.__name = name
        self.__score = score

    def print_score(self):
        print('%s: %s' % (self.__name, self.__score))

改完後,對於外部代碼來說,沒什麼變動,但是已經無法從外部訪問實例變量.__name 和實例變量.__score 了:

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

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

但是如果外部代碼要獲取 name 和 score 怎麼辦?可以給 Student 類增加get_nameget_score這樣的方法:

class Student(object):
    ...

    def get_name(self):
        return self.__name

    def get_score(self):
        return self.__score

如果又要允許外部代碼修改 score 怎麼辦?可以再給 Student 類增加set_score方法:

class Student(object):
    ...

    def set_score(self, score):
        self.__score = score

你也許會問,原先那種直接通過 bart.score = 99 也可以修改啊,為什麼要定義一個方法大費周折?因為在方法中,可以對參數做檢查,避免傳入無效的參數

class Student(object):
    ...

    def set_score(self, score):
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')

需要注意的是,在 Python 中,變量名類似__xxx__的,也就是以雙下劃線開頭,並且以雙下劃線結尾的,是特殊變量,特殊變量是可以直接訪問的,不是 private 變量,所以,不能用__name__、__score__這樣的變量名。

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

雙下劃線開頭的實例變量是不是一定不能從外部訪問呢?其實也不是。不能直接訪問__name 是因為 Python 解釋器對外把__name 變量改成了_Student__name,所以,仍然可以通過_Student__name 來訪問__name 變量:

>>> bart._Student__name
'Bart Simpson'

但是強烈建議你不要這麼幹,因為不同版本的 Python 解釋器可能會把__name 改成不同的變量名。

總的來說就是,Python 本身沒有任何機制阻止你幹壞事,一切全靠自覺

最後注意下面的這種錯誤寫法:

>>> bart = Student('Bart Simpson', 59)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 設置__name變量!
>>> bart.__name
'New Name'

表面上看,外部代碼 “成功” 地設置了__name 變量,但實際上這個__name 變量和 class 內部的__name 變量不是一個變量!內部的__name 變量已經被 Python 解釋器自動改成了_Student__name,而外部代碼給 bart新增了一個__name 變量。

>>> bart.get_name() # get_name()內部返回self.__name
'Bart Simpson'

繼承和多態

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

比如,我們已經編寫了一個名為 Animal 的 class,有一個 run () 方法可以直接打印:

class Animal(object):
    def run(self):
        print('Animal is running...')

當我們需要編寫 Dog 和 Cat 類時,就可以直接從 Animal 類繼承:

class Dog(Animal):
    pass

class Cat(Animal):
    pass

對於 Dog 來說,Animal 就是它的父類,對於 Animal 來說,Dog 就是它的子類。Cat 和 Dog 類似。

繼承有什麼好處?最大的好處是子類獲得了父類的全部功能。由於 Animial 實現了 run () 方法,因此,Dog 和 Cat 作為它的子類,什麼事也沒幹,就自動擁有了 run () 方法:

dog = Dog()
dog.run()

cat = Cat()
cat.run()

運行結果如下:

Animal is running...
Animal is running...

當然,也可以對子類增加一些方法,比如 Dog 類:

class Dog(Animal):

    def run(self):
        print('Dog is running...')

    def eat(self):
        print('Eating meat...')

繼承的第二個好處需要我們對代碼做一點改進。你看到了,無論是 Dog 還是 Cat,它們 run () 的時候,顯示的都是 Animal is running...,符合邏輯的做法是分別顯示 Dog is running... 和 Cat is running...,因此,對 Dog 和 Cat 類改進如下:

class Dog(Animal):

    def run(self):
        print('Dog is running...')

class Cat(Animal):

    def run(self):
        print('Cat is running...')

再次運行,結果如下:

Dog is running...
Cat is running...

當子類和父類都存在相同的 run () 方法時,我們說,子類的 run () 覆蓋了父類的 run (),在代碼運行的時候,總是會調用子類的 run ()。這樣,我們就獲得了繼承的另一個好處:多態

要理解什麼是多態,我們首先要對數據類型再作一點說明。當我們定義一個 class 的時候,我們實際上就定義了一種數據類型。我們定義的數據類型和 Python 自帶的數據類型,比如 str、list、dict 沒什麼兩樣:

a = list() # a是list類型
b = Animal() # b是Animal類型
c = Dog() # c是Dog類型

判斷一個變量是否是某個類型可以用isinstance() 判斷:

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

看來 a、b、c 確實對應著 list、Animal、Dog 這 3 種類型。
但是等等,試試:

>>> isinstance(c, Animal)
True

看來 c 不僅僅是 Dog,c 還是 Animal!

不過仔細想想,這是有道理的,因為 Dog 是從 Animal 繼承下來的,當我們創建了一個 Dog 的實例 c 時,我們認為 c 的數據類型是 Dog 沒錯,但 c 同時也是 Animal 也沒錯,Dog 本來就是 Animal 的一種!

所以,在繼承關係中,如果一個實例的數據類型是某個子類,那它的數據類型也可以被看做是父類。但是,反過來就不行:

>>> b = Animal()
>>> isinstance(b, Dog)
False

Dog 可以看成 Animal,但 Animal 不可以看成 Dog。

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

def run_twice(animal):
    animal.run()
    animal.run()

當我們傳入 Animal 的實例時,run_twice () 就打印出:

>>> run_twice(Animal())
Animal is running...
Animal is running...

當我們傳入 Dog 的實例時,run_twice () 就打印出:

>>> run_twice(Dog())
Dog is running...
Dog is running...

當我們傳入 Cat 的實例時,run_twice () 就打印出:

>>> run_twice(Cat())
Cat is running...
Cat is running...

看上去沒啥意思,但是仔細想想,現在,如果我們再定義一個 Tortoise 類型,也從 Animal 派生:

class Tortoise(Animal):
    def run(self):
        print('Tortoise is running slowly...')

當我們調用 run_twice () 時,傳入 Tortoise 的實例:

>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...

你會發現,新增一個 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 () 等函數。

繼承還可以一级一级地繼承下來,就好比從爺爺到爸爸、再到兒子這樣的關係。而任何類,最終都可以追溯到根類object,這些繼承關係看上去就像一顆倒著的樹。

靜態語言 vs 動態語言
對於靜態語言(例如 Java)來說,如果需要傳入 Animal 類型,則傳入的對象必須是 Animal 類型或者它的子類,否則,將無法調用 run () 方法。

對於 Python 這樣的動態語言來說,則不一定需要傳入 Animal 類型。我們只需要保證傳入的對象有一個 run () 方法就可以了:

class Timer(object):
    def run(self):
        print('Start...')

這就是動態語言的 “鴨子類型”,它並不要求嚴格的繼承體系,一個對象只要 “看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。

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

小結
1 繼承可以把父類的所有功能都直接拿過來,這樣就不必重零做起,子類只需要新增自己特有的方法,也可以把父類不適合的方法覆蓋重寫。

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

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

使用type()
來判斷對象類型,使用 type () 函數:

基本類型都可以用 type () 判斷:

>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>

如果一個變量指向函數或者類,也可以用 type () 判斷:

>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class '__main__.Animal'>

但是 type () 函數返回的是什麼類型呢?它返回對應的 Class 類型。

如果我們要在 if 語句中判斷,就需要比較兩個變量的 type 類型是否相同:

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

判斷基本數據類型可以直接寫 int,str 等,但如果要判斷一個對象是否是函數怎麼辦?可以使用 types 模塊中定義的常量:

>>> import types
>>> def fn():
...     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 () 就可以告訴我們,一個對象是否是某種類型。先創建 3 種類型的對象:

>>> a = Animal()
>>> d = Dog()
>>> h = Husky()

然後,判斷:

>>> isinstance(h, Husky)
True

沒有問題,因為 h 變量指向的就是 Husky 對象。

再判斷:

>>> isinstance(h, Dog)
True

h 雖然自身是 Husky 類型,但由於 Husky 是從 Dog 繼承下來的,所以,h 也還是 Dog 類型。換句話說,isinstance () 判斷的是一個對象是否是該類型本身,或者位於該類型的父繼承鏈上。

因此,我們可以确信,h 還是 Animal 類型:

>>> isinstance(h, Animal)
True

同理,實際類型是 Dog 的 d 也是 Animal 類型:

>>> isinstance(d, Dog) and isinstance(d, Animal)
True

但是,d 不是 Husky 類型:

>>> isinstance(d, Husky)
False

能用 type () 判斷的基本類型也可以用 isinstance () 判斷:

>>> isinstance('a', str)
True
>>> isinstance(123, int)
True
>>> isinstance(b'a', bytes)
True

並且還可以判斷一個變量是否是某些類型中的一種,比如下面的代碼就可以判斷是否是 list 或者 tuple:

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

總是優先使用 isinstance () 判斷類型,可以將指定類型及其子類 “一網打盡”。

使用 dir ()
如果要獲得一個對象的所有屬性和方法,可以使用 dir () 函數,它返回一個包含字符串的 list,比如,獲得一個 str 對象的所有屬性和方法:

>>> dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']

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

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

我們自己寫的類,如果也想用 len (myObj) 的話,就自己寫一個__len__() 方法:

>>> class MyDog(object):
...     def __len__(self):
...         return 100
...
>>> dog = MyDog()
>>> len(dog)
100

剩下的都是普通屬性或方法,比如 lower () 返回小寫的字符串:

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

僅僅把屬性和方法列出來是不夠的,配合getattr()setattr()以及hasattr(),我們可以直接操作一個對象的狀態:

>>> class MyObject(object):
...     def __init__(self):
...         self.x = 9
...     def power(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

如果試圖獲取不存在的屬性,會拋出 AttributeError 的錯誤:

>>> getattr(obj, 'z') # 獲取屬性'z'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'

可以傳入一個 default 參數,如果屬性不存在,就返回默認值:

>>> getattr(obj, 'z', 404) # 獲取屬性'z',如果不存在,返回默認值404
404

也可以獲得對象的方法:

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

小結

1 通過內置的一系列函數,我們可以對任意一個 Python 對象進行剖析,拿到其內部的數據。要注意的是,只有在不知道對象信息的時候,我們才會去獲取對象信息。如果可以直接寫:

sum = obj.x + obj.y

就不要寫:

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

一個正確的用法的例子如下:

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

假設我們希望從文件流 fp 中讀取圖像,我們首先要判斷該 fp 對象是否存在 read 方法,如果存在,則該對象是一個流,如果不存在,則無法讀取。hasattr () 就派上了用場。

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

實例屬性和類屬性

由於 Python 是動態語言,根據類創建的實例可以任意綁定屬性

給實例綁定屬性的方法是通過實例變量,或者通過 self 變量:

class Student(object):
    def __init__(self, name):
        self.name = name

s = Student('Bob')
s.score = 90

但是,如果 Student 類本身需要綁定一個屬性呢?可以直接在 class 中定義屬性,這種屬性是類屬性,歸 Student 類所有:

class Student(object):
    name = 'Student'

當我們定義了個類屬性後,這個屬性雖然歸類所有,但類的所有實例都可以訪問到。來測試一下:

>>> class Student(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

在編寫程序的時候,千萬不要對實例屬性和類屬性使用相同的名字,因為相同名稱的實例屬性將屏蔽掉類屬性,但是當你刪除實例屬性後,再使用相同的名稱,訪問到的將是類屬性。

小結
1 實例屬性屬於各個實例所有,互不干擾;

2 類屬性屬於類所有,所有實例共享一個屬性;

3 不要對實例屬性和類屬性使用相同的名字,否則將產生難以發現的錯誤。

八 面向對象高級編程#

多重繼承、定制類、元類
使用__slots__
正常情況下,當我們定義了一個 class,創建了一個 class 的實例後,我們可以給該實例綁定任何屬性和方法,這就是動態語言的靈活性。先定義 class:

class Student(object):
    pass

然後,嘗試給實例綁定一個屬性:

>>> s = Student()
>>> s.name = 'Michael' # 動態給實例綁定一個屬性
>>> print(s.name)
Michael

還可以嘗試給實例綁定一個方法:

>>> def set_age(self, age): # 定義一個函數作為實例方法
...     self.age = age
...
>>> from types import MethodType
>>> s.set_age = MethodType(set_age, s) # 給實例綁定一個方法
>>> s.set_age(25) # 調用實例方法
>>> s.age # 測試結果
25

但是,給一個實例綁定的方法,對另一個實例是不起作用的:

>>> s2 = Student() # 創建新的實例
>>> s2.set_age(25) # 嘗試調用方法
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'set_age'

為了給所有實例都綁定方法,可以給 class 綁定方法:

>>> def set_score(self, score):
...     self.score = score
...
>>> Student.set_score = set_score

給 class 綁定方法後,所有實例均可調用:

>>> s.set_score(100)
>>> s.score
100
>>> s2.set_score(99)
>>> s2.score
99

通常情況下,上面的 set_score 方法可以直接定義在 class 中,但動態綁定允許我們在程序運行的過程中動態給 class 加上功能,這在靜態語言中很難實現。

使用__slots__
但是,如果我們想要限制實例的屬性怎麼辦?比如,只允許對 Student 實例添加 name 和 age 屬性。

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

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

在綁定屬性時,如果我們直接把屬性暴露出去,雖然寫起來很簡單,但是,沒辦法檢查參數,導致可以把成績隨便改:

s = Student()
s.score= 9999

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

class Student(object):

    def get_score(self):
         return self._score

    def set_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

現在,對任意的 Student 實例進行操作,就不能隨心所欲地設置 score 了:

>>> s = Student()
>>> s.set_score(60) # ok!
>>> s.get_score()
60
>>> s.set_score(9999)
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

但是,上面的調用方法又略顯複雜,沒有直接用屬性這麼直接簡單。

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

還記得裝飾器(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,我們在對實例屬性操作的時候,就知道該屬性很可能不是直接暴露的,而是通過 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 和當前時間計算出來。

要特別注意:屬性的方法名不要和實例變量重名。例如,以下的代碼是錯誤的:

class Student(object):

    # 方法名稱和實例變量均為birth:
    @property
    def birth(self):
        return self.birth

這是因為調用 s.birth 時,首先轉換為方法調用,在執行 return self.birth 時,又視為訪問 self 的屬性,因此又轉換為方法調用,造成無限遞歸,最終導致棧溢出報錯RecursionError。

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

多重繼承
類層次仍按照哺乳類和鳥類設計:

class Animal(object):
    pass

# 大類:
class Mammal(Animal):
    pass

class Bird(Animal):
    pass

# 各種動物:
class Dog(Mammal):
    pass

class Bat(Mammal):
    pass

class Parrot(Bird):
    pass

class Ostrich(Bird):
    pass

現在,我們要給動物再加上 Runnable 和 Flyable 的功能,只需要先定義好 Runnable 和 Flyable 的類:

class Runnable(object):
    def run(self):
        print('Running...')

class Flyable(object):
    def fly(self):
        print('Flying...')

對於需要 Runnable 功能的動物,就多繼承一個 Runnable,例如 Dog:

class Dog(Mammal, Runnable):
    pass

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

class Bat(Mammal, Flyable):
    pass

通過多重繼承,一個子類就可以同時獲得多個父類的所有功能

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

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

class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
    pass

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

Python 自帶的很多庫也使用了 MixIn。舉個例子,Python 自帶了 TCPServer 和 UDPServer 這兩類網絡服務,而要同時服務多個用戶就必須使用多進程或多線程模型,這兩種模型由 ForkingMixIn 和 ThreadingMixIn 提供。通過組合,我們就可以創造出合適的服務來。

比如,編寫一個多進程模式的 TCP 服務,定義如下:

class MyTCPServer(TCPServer, ForkingMixIn):
    pass

編寫一個多線程模式的 UDP 服務,定義如下:

class MyUDPServer(UDPServer, ThreadingMixIn):
    pass

如果你打算搞一個更先進的協程模型,可以編寫一個 ```
CoroutineMixIn:

class MyTCPServer(TCPServer, CoroutineMixIn):
    pass

這樣一來,我們不需要複雜而龐大的繼承鏈,只要選擇組合不同類的功能,就可以快速構造出所需的子類。

小結
1 由於 Python 允許使用多重繼承,因此,MixIn 就是一種常見的設計。

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

定制類
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'))
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
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

現在,就可以按下標訪問數列的任意一項了:

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

現在試試 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
正常情況下,當我們調用類的方法或屬性時,如果不存在,就會報錯。比如定義 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)

這實際上可以把一個類的所有屬性和方法調用全部動態化處理了,不需要任何特殊手段。

這種完全動態調用的特性有什麼實際作用呢?作用就是,可以針對完全動態的情況作調用。

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() 還可以定義參數。對實例進行直接調用就好比對一個函數進行調用一樣,所以你完全可以把對象看成函數,把函數看成對象,因為這兩者之間本來就沒啥根本的區別。

如果你把對象看成函數,那麼函數本身其實也可以在運行期動態創建出來,因為類的實例都是運行期創建出來的,這麼一來,我們就模糊了對象和函數的界限。

那麼,怎麼判斷一個變量是對象還是函數呢?其實,更多的時候,我們需要判斷一個對象是否能被調用,能被調用的對象就是一個可調用對象,比如函數和我們上面定義的帶有__call__() 的類實例:

>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False

通過callable () 函數,我們就可以判斷一個對象是否是 “可調用” 對象

小結
Python 的 class 允許定義許多定制方法,可以讓我們非常方便地生成特定的類。

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

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)

image

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 創建出來的 “實例”。

metaclass 是 Python 面向對象里最難理解,也是最難使用的魔術代碼。正常情況下,你不會碰到需要使用 metaclass 的情況,所以,以下內容看不懂也沒關係,因為基本上你不會用到。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。