《最值得收藏的python3語法彙總》之面向對象編程 - 完整版

目錄

關於這個系列

1、POP和OOP

2、類的定義語法

3、類對象

屬性引用

類的實例化

實例對象

4、類變量和實例變量

5、繼承

概念

多重繼承

方法重寫

理解super

Isinstance和issubcass

6、多態

7、成員可見範圍

8、迭代器

9、生成器


關於這個系列

《最值得收藏的python3語法彙總》,是我爲了準備公衆號“跟哥一起學python”上面視頻教程而寫的課件。整個課件將近200頁,10w字,幾乎囊括了python3所有的語法知識點。

你可以關注這個公衆號“跟哥一起學python”,獲取對應的視頻和實例源碼。

這是我和幾位老程序員一起維護的個人公衆號,全是原創性的乾貨編程類技術文章,歡迎關注。


1、POP和OOP

有兩種主流的編程思想:面向過程編程(Procedure-Oriented Programming,簡稱POP)和麪向對象編程(Object-Oriented Programming,簡稱OOP)。

面向過程編程:就是分析出解決問題所需要的步驟,然後用函數把這些步驟一步一步實現,使用的時候一個一個依次調用就可以了。

面向對象編程:是把構成問題的事務分解成各個對象,建立對象的目的不是爲了完成一個步驟,而是爲了描敘某個事物在整個解決問題的步驟中的行爲。

面向過程編程,關注的是“做一件事”;而面向對象編程,關注的是“造一堆東西”。前者強調的是一系列行爲,而後者強調的是一系列事物(對象)。

這比較抽象,我們舉個實際的例子,比如我們要實現“把大象放進冰箱”這樣一段程序。

面向過程編程: 打開冰箱門 – 把大象放進去 – 關上冰箱門。

面向對象編程: 創建一個對象(冰箱),給它定義三個動作,開門(open)、放置物品(put)、關門(close)。然後分別讓冰箱執行這三個動作。

大家看出區別了嗎?面向過程編程,強調的是動作,程序員關注的是如何執行一系列動作的問題。而面向對象編程,強調的是對象,程序員關注的是如何合理地定義一個對象的屬性和行爲的問題。雖然它們都能實現相同的功能,但是編程的思想是截然不同的。

 

不同的高級編程語言,對OOP的支持是不一樣的。比如C語言,我們通常認爲它是一種面向過程的語言,因爲它沒有提供對面向對象編程的直接支持,但是C語言也可以封裝對象。C++和JAVA就是很明確的支持面向對象編程的語言。Python支持面向對象編程,同樣我們也可以不面向對象,而面向過程編程。POP和OOP僅僅是一種編程思想。

本節我們要講的類,就是Python支持OOP的語法機制。

 

2、類的定義語法

什麼是類(Class)?我們還是以上面“冰箱” 的例子來說明。大家可以想想我們該如何去描述“冰箱”呢?

首先,“冰箱”有很多品牌,海爾、美菱、三星、西門子等等,“冰箱”也有很多類型,單門、雙開門、三開門等等,“冰箱”也有不同的節能級別,一級能效、二級能效、三級能效等等,“冰箱”還有很多其他的特徵。這些我們可以統一稱之爲“冰箱”的屬性。

然後,我們對“冰箱”有很多操作方式。比如:打開冰箱門、關上冰箱門、調節保鮮區域溫度、調節冷凍區域溫度等等。這些不是“冰箱”的特徵,而是“冰箱”的操作,我們統一稱之爲“冰箱”的方法、行爲。

我們可以將這些“屬性”和“行爲”組合在一起,就可以描述“冰箱”了,我們將這個組合稱之爲“冰箱類”。如下圖所示:

 

在Python中,我們使用類(Class)的語法來描述這樣一個對象。它的表述如下圖所示:

 

面向對象編程中,我們通常使用UML類圖來設計類。以上就是冰箱類的UML類圖設計。我們定義了一個類叫Refrigerator,它裏面包含了一系列的屬性和成員函數。注意,前面的+號反應了這個屬性和成員函數的可見範圍是public,關於可見範圍我們後面會講。

我們可以將這個類圖轉換爲python的代碼(某些UML工具可以自動根據上面的UML類圖生成代碼,這裏我們手動敲代碼)。

#  author: Tiger,   關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./12/12_1.py
# 定義一個冰箱類

class Refrigerator:
    brand = '海爾'
    color = 'white'
    price = 0
    power_level = 1
    door_type = '雙開門'

    def open_door(self):
        pass

    def close_door(self):
        pass

    def temp_mod(self):
        pass

    def temp_show(self):
        pass

    def put(self):
        pass

這個代碼裏面,我們所有的成員函數都沒有具體實現,用了pass空語句佔位。

類以關鍵詞class開頭,後面跟類名(自定義名字,規則參考變量名。按照編程規範,我們建議採用駝峯命名法,就是每個單詞首字母大寫),再後面跟一個冒號:。

類裏面的屬性,其實就是在類裏面定義的一系列變量。而類的成員函數,其實就是在類裏面定義的一系列函數。

這樣,我們就定義了一個基礎的冰箱類。

下面我們將裏面的一些方法補充一些實現代碼,看看類該如何使用。我們在open_door()和close_door()方法中分別打印一些輸出。

def open_door(self):

    print("refrigerator's door is opened!")

def close_door(self):
    print("refrigerator's door is closed!")

然後使用這個類來模擬冰箱開門和關門的動作:

if __name__ == '__main__':

    inst1 = Refrigerator()
    inst1.open_door()
    inst1.close_door()

輸出爲:

refrigerator's door is opened!

refrigerator's door is closed!

上面的第二行代碼 inst1 = Refrigerator() ,它定義了一個變量inst1,並且指向了一個新創建的Refrigerator實例對象。我們把這個過程叫做類的實例化過程,實例化的結果是新生成一個實例對象。

如何理解實例化呢?Refrigerator類表示是冰箱這個類型,而inst1則表示的是具體的一款冰箱甚至是一臺冰箱。所謂實例化,就是通過冰箱這種類型,創建一個具體的冰箱對象的過程。實例化最終會生成一個具體的實例對象。

這裏我們提到了兩個比較容易混淆的概念:類對象和實例對象。

Python是萬物皆對象的,所以類本身也是一個對象,我們叫做類對象。類對象經過實例化之後,也會生成一個對象,這個叫實例對象。

 

3、類對象

類對象支持兩種操作,一個是屬性和成員函數的引用,一個是實例化。

屬性引用

類對象的屬於引用,包括對類裏面變量和成員函數的引用。我們採用obj.name的方式來引用。如果name是一個變量,那麼返回一個變量對象;而如果name是一個成員函數,那麼返回的就是一個函數對象。

通過屬性引用,我們可以查看、調用甚至修改變量或者成員函數。

if __name__ == '__main__':

    # 類對象屬性引用
    Refrigerator.brand = '西門子'
    func_open = Refrigerator.open_door
    func_open(Refrigerator('三星'))

上面代碼中,我們修改了類對象的變量brand。同時將函數對象open_door賦值給了一個臨時變量func_open,通過func_open去調用這個函數。

對類對象屬性的改變,會影響其例化的所有實例對象。

實例一、在實例化之前改變類對象屬性,直接影響實例對象。

Refrigerator.color = 'red'

inst4 = Refrigerator('三星')
print(inst4.color)

輸出爲:

red

 

實例二、在實例化之後改變類對象屬性,同樣會影響所有實例。

Refrigerator.color = 'green'  # 不可變類型

print(inst4.color)
print(inst4.goods_list)
Refrigerator.goods_list.append("pear")  # 可變類型,受影響
print(inst4.goods_list)

輸出爲:

Green

[]

[‘pear’]

我們把在類裏面直接定義的這些變量,叫做類變量。類變量是在類對象和實例對象之間共享的,任何對類變量的修改,都會影響其他實例對象。同理,在類裏面定義的成員函數也是這樣的。

 

類的實例化

上一節的例子中,我們已經對冰箱類做了一個簡單的實例化,它是不帶任何參數的。而實際代碼中,我們通常需要攜帶一些參數。比如,我們希望實例化一個海爾牌的冰箱,那麼需要攜帶一個品牌名參數。

類的實例化通過在類裏面重寫__init__()成員函數來實現,這個成員函數是python類自帶的。如果我們不重寫,那麼會採用自帶函數默認處理。自帶的__init__()是不攜帶參數的,比如我們前面的例子。

如果我們要支持帶品牌名參數的實例化方法,那麼我們需要在Refrigerator類裏面增加一個__init__的定義:

class Refrigerator:

    brand = '海爾'

    def __init__(self, brand):
        self.brand = brand



if __name__ == '__main__':
    inst1 = Refrigerator("三星")

這樣在實例化之後,inst1對象的brand屬性就是“三星”。我們創建了一個三星冰箱的實例對象!

__init__()這個成員函數我們通常稱之爲“構造方法”。在類的實例化過程中,這個函數會被系統自動調用。我們可以通過重寫該函數,實現我們自己的實例化邏輯,通常是給類裏面的屬性賦一些初始值。

在我們重寫的構造方法裏面,我們用到了self參數,實際上我們類裏面定義的所有成員函數的第一個參數都是self。

當我們調用實例對象的方法(obj.method)時,系統會自動給這個方法插入第一個實參,這個實參就是該對象本身的引用。所以self就是對象本身的引用,成員函數裏面可以用self來操作該對象的屬性和方法。

Self參數是系統給自動填充的,我們在調用實例對象方法的時候看不到這個參數。

這個參數不一定要命名爲self,你可以命名爲任意合法的參數名都行。只是習慣上我們將其命名爲self,這樣別人一看就明白它是實例對象的引用。這是一個約定俗成的習慣,就像其它編程語言中可能習慣將其命名爲this一樣。

 

實例對象

類對象經過實例化,產生的這個對象就是實例對象。實例對象只有一種操作類型:屬性引用。注意這裏的屬性包含了對象裏面的變量和方法。

屬性引用的語法和類對象是一樣的:obj.name。

當name是一個變量時,返回的就是一個變量對象;當name是一個函數時,返回的就是一個方法對象。

大家要注意,我們前面提到類對象的成員函數時,說的是函數對象。而這裏說的是方法對象。通常我們在說類的時候,喜歡把它們裏面定義的函數稱之爲成員函數;而在說實例對象時,喜歡把它們叫做方法。比如類的成員函數、實例的方法。

類對象引用的函數對象,和實例對象引用的方法對象,兩者是有差別的。它們在解釋器中是完全不同的兩種對象類型。

inst4 = Refrigerator(5000)

print(f'refrence by class obj: {type(Refrigerator.door_open)}')
print(f'refrence by instance obj: {type(inst4.door_open)}')

輸出爲:

refrence by class obj: <class 'function'>

refrence by instance obj: <class 'method'>

 

在引用函數對象時,系統不會自動給你補齊self參數;而在引用方法對象時,系統會把當前對象自動填充爲self實參。

比如我們前面在引用函數對象時,需要自己實例化一個對象作爲self參數傳入:

func_open(Refrigerator('三星'))

而如果是方法對象,則不需要:

# 實例對象屬性引用

inst1 = Refrigerator("三星")
inst1.open_door()

 

4、類變量和實例變量

類變量,是我們在類裏面定義的變量;而實例變量,則是某個實例定義的變量。看下面的例子:

#  author: Tiger,   關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./12/12_2.py

# 類變量和實例變量
class Dog:
    age = 2  # 類變量

    def __init__(self, name):
        self.name = name  # 實例變量
        self.color = 'white'  # 實例變量

    def set_color(self, color):
        self.color = color

    def get_name(self):
        print(self.name)

if __name__ == '__main__':
    Dog.height = 30  # 類變量
    mydog = Dog('apple')
    mydog.weight = 10  # 實例變量
    pass

我們可以斷點查看Dog類對象和mydog實例對象的變量:

 

 

可以看到,Dog類對象只有兩個變量age和height,它們都是類變量。而mydog除了從Dog類繼承過來的這兩個類變量以外,還增加了color、name、weight,這三個變量就是實例變量。

在成員函數中,採用self.name定義的變量,或者在外面通過實例名.name定義的變量,都是實例變量。

在類裏面直接定義的變量,或者在外面通過類對象.name定義的變量,都是類變量。

類變量在類對象和各個實例對象之間是共享的,而實例變量則只在本實例有效,實例之間是獨立的。我們看下面的例子:

# 類變量是共享的

mydog2 = Dog('coco')
Dog.age = 4
print(f"apple'age is {mydog.age}, coco'age is {mydog2.age}")

輸出爲:

apple'age is 4, coco'age is 4

當我們通過類引用的方式改變類變量age的值爲4之後,我們發現兩個實例對象mydog和mydog2對應的age都變成了4。可以看出,類變量age是共享的。

下面的代碼,你可能會覺得和上面的原則是矛盾的:

mydog2.age = 6  # 注意,這裏改變的不是類變量age,

# 解釋器認爲這行代碼表示新定義了一個實例變量age
print(f"apple'age is {mydog.age}, coco'age is {mydog2.age}")

輸出爲:

apple'age is 4, coco'age is 6

表象上看來,類變量age並沒有被共享。實際上,mydog2.age=6,這行代碼會被python解釋器理解爲新定義了一個實例變量age,只是它和類變量重名了。前面我們學過命名空間,所以我們知道類對象和實例對象是兩個獨立的命名空間,是可以重名的。對於這種重名的情況,系統會認爲實例變量優先。

定義了實例變量age之後,無論類變量age怎麼改變,都不會對mydog2產生影響了。如下代碼:

# 之後修改類變量age,將不會再影響mydog2了,因爲實例變量優先

Dog.age = 8
print(f"apple'age is {mydog.age}, coco'age is {mydog2.age}")

輸出爲:

apple'age is 8, coco'age is 6

可以看到,修改類變量age=8,mydog的age變了,而mydog2則還是6,不受影響。

如果類變量是一個可變數據類型呢?它依然遵循這一規則。

# 增加一個可變數據類型的類變量

Dog.foods = []
print(f"apple'foods is {mydog.foods}, coco'foods is {mydog2.foods}")
Dog.foods.append('meat')
print(f"apple'foods is {mydog.foods}, coco'foods is {mydog2.foods}")
mydog.foods.append('rice')  # 這裏是對類變量的引用,不是定義
print(f"apple'foods is {mydog.foods}, coco'foods is {mydog2.foods}")
mydog2.foods = ['fish']  # 這裏是定義了一個新的實例變量
print(f"apple'foods is {mydog.foods}, coco'foods is {mydog2.foods}")

輸出爲:

apple'foods is [], coco'foods is []

apple'foods is ['meat'], coco'foods is ['meat']

apple'foods is ['meat', 'rice'], coco'foods is ['meat', 'rice']

apple'foods is ['meat', 'rice'], coco'foods is ['fish']

 

對於實例變量來說,它的作用域僅限於本實例對象,不同實例對象之間的實例變量是完全獨立的,互不影響。實例變量對類對象是不可見的,也就是說類對象不能引用實例變量。看下面的例子:

# 不同實例對象的實例變量之間互不影響

mydog.color = 'black'
mydog2.color = 'yellow'
print(f"apple'color is {mydog.color}, coco'color is {mydog2.color}")

輸出爲:

apple'color is black, coco'color is yellow

 

另外補充一點,我們前面學過作用域,知道函數嵌套時,內層函數是可以訪問外層函數定義的變量的。我們剛接觸類的時候,會以爲類的成員函數可以直接訪問類變量,這其實是錯誤的。實際上,很多面向對象編程語言中,確實也是這樣設計的。但是python卻不是這樣的。比如下面的代碼:

#  author: Tiger,   關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./12/12_3.py

# 成員函數不能訪問類變量
class Dog:
    foods = []  # 類變量

    def __init__(self, name):
        self.name = name  # 實例變量
        self.color = 'white'  # 實例變量

    def set_age(self):
        foods.append('rice')  # 變量未定義

Set_age中引用變量foods,會報變量未定義錯誤。事實上,類對象和它裏面的成員函數,都有自己獨立的命名空間,它們之間不存在嵌套關係。所以在名字搜索時,並不會搜索到類對象的命名空間。

成員函數裏面只能對self實例對象裏面的變量進行操作。它可以通過這種self.name的間接引用方式來訪問類變量。

 

5、繼承

概念

“繼承”是面向對象編程的重要機制,python當然也支持類的“繼承”。注意,‘繼承’特指的是類對象之間的繼承關係,實例對象沒有繼承的概念。

什麼是“繼承”?我們來看看下面的例子:

上圖中,我們定義了四個類,分別是Person(人)、Student(學生)、Speaker(演講者)、StudentSpeaker(演講的學生)。

這些類之間,存在這樣的語義邏輯:

Student和Speaker,一定是Person;StudentSpeaker則一定同時是Student和Speaker。

換一種更加專業一點的說法,就是:

Student和Speaker,一定是擁有Person的屬性和行爲;StudentSpeaker則一定同時擁有Student和Speaker的屬性和行爲。

既然是這樣,那麼我們爲什麼要重複定義這些屬性和行爲呢?我們是否可以將Person的屬性和行爲直接“繼承”下來爲我所用呢?這就是面向對象編程中“繼承”的概念。

我們把繼承者稱爲“子類(Child),或者叫派生類”,被繼承者稱爲“父類(Parent),或者叫基類”。這個概念是相對的,比如Student是Person的子類,同時它又是StudentSpeaker的父類。

我們把這種繼承關係轉換爲python代碼來表述,如下:

#  author: Tiger,   關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./12/12_4.py

# 類繼承
class Person:
    def __init__(self, name, age=20, sex='male'):
        self.name = name
        self.age = age
        self.sex = sex

    def introduce(self):
        print(f'I\'m {self.name}, {self.age} years old.')

class Student(Person):
    def school_set(self, school_name, grade=1):
        self.school = school_name
        self.grade = grade

子類在定義時,可以在類名後面括號裏面列出其繼承的父類名。這就完成了“繼承”過程。子類對象繼承了父類對象的全部屬性和成員函數。同時,子類對象可以增加自己的屬性和成員函數,這些對父類對象不可見。

if __name__ == '__main__':

    s1 = Student(name='Jack')
    s1.introduce()

輸出爲:

I'm Jack, 20 years old.

 

大家可以看到,我們在子類Student中並沒有定義構造方法__init__(),它繼承了父類Person的構造方法。

同理,Speaker類也類似地繼承了Person類,我們不贅述。

 

多重繼承

我們再來看看StudentSpeaker類,它同時繼承了兩個父類(Student、Speaker),這種擁有多個父類的繼承方式,稱之爲“多重繼承”。

多重繼承的實現方式很簡單,你只需要在定義子類的時候,括號裏面列出所有父類的名稱即可。

#  author: Tiger,   關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./12/12_4.py

# 類繼承
class Person:
    def __init__(self, name, age=20, sex='male'):
        self.name = name
        self.age = age
        self.sex = sex

    def introduce(self):
        print(f'I\'m {self.name}, {self.age} years old.')

class Student(Person):
    def school_set(self, school_name):
        self.school = school_name

    def grade_set(self, grade):
        self.grade = grade

class Speaker(Person):
    topic = ''
    def speak(self):
        print(f'I\'m a speaker')

class StudentSpeaker(Student, Speaker):
    def student_speak(self):
        print(f'I\'m a student and a speaker.')

if __name__ == '__main__':
    ss1 = StudentSpeaker(name='Jack')
    ss1.introduce()  # 繼承自父類的父類 Person
    ss1.speak()  # 繼承自父類Speaker
    ss1.school_set('Beijing 101 Middle School')  # 繼承自父類Student
    ss1.student_speak()  # 自己的方法

StudentSpeaker類對象同時繼承了兩個父類Student和Speaker,可以看到,它的實例對象可以直接調用繼承自父類的所有方法。

類的繼承可以讓我們很方便的實現代碼功能的複用,你不需要重複實現這些功能,只需要簡單的繼承即可。

對於多重繼承,會出現以下問題,就是如果不同父類之間出現了相同命名的屬性或者成員函數,該怎麼辦呢?比如Student類和Speaker類同時定義了一個成員函數用於打印年齡age_get(),通過下面例子我們看看StudentSpeaker調用的是哪個父類的這個函數。

class Student(Person):

def age_get(self):
        print(f'I\'m a student, {self.age} years old')

class Speaker(Person):
    def age_get(self):
        print(f'I\'m a speaker, {self.age} years old')



class StudentSpeaker(Student, Speaker):
    def student_speak(self):
        print(f'I\'m a student and a speaker.')



if __name__ == '__main__':
    ss1 = StudentSpeaker(name='Jack')
    ss1.age_get()

輸出爲:

I'm a student, 20 years old

很顯然,它最終調用的是Student的成員函數。

實際上,python解釋器在多重繼承的情況下,我們可以簡單認爲它採用了深度優先、從左至右的原則去搜索。也就是說,它會按照StudentSpeaker的父類列表順序,先查找Student類,找不到就查找Student類的父類,依次往上查找,這就是所謂的深度優先。如果最終也找不到,則查找父類列表中的下一個,也就是Speaker,這就是所謂的從左至右。簡單講,就是父類列表從左至右搜索,每個父類要深度優先搜索。

但是我們這個例子中,Student和Speaker繼承自同一個父類Person,如果按照上面的規則,則Person會被搜索兩遍。

事實上python解釋器真實的搜索過程要複雜很多。它採用了一種線性化算法(C3算法),可以將這種複雜的搜索關係線性化爲一個列表,我們稱之爲方法解析順序列表(Method Resolution Order, MRO)。這個列表存儲在類對象的__mro__變量中。

print(StudentSpeaker.__mro__)

輸出爲:

(<class '__main__.StudentSpeaker'>, <class '__main__.Student'>, <class '__main__.Speaker'>, <class '__main__.Person'>, <class 'object'>)

這就是真正的搜索順序。StudentSpeaker->Student->Speaker->Person->object。

它把共同父類Person放在了所以子類的後面。最後的object是python所有類的最終父類(祖先類),如果一個類定義時沒有指定父類,那麼它實質上是繼承至object類。

看下面這個複雜的例子,它的搜索路徑是什麼呢?

它的MRO是: ABECDFobject,算法如下;

首先找入度爲0的點,只有A,把A取出,把A相關的邊去掉,再找下一個入度爲0的點,BC滿足條件,從左側開始取,取出B,這時順序是AB,然後去掉B相關的邊,這時候入度爲0的點有EC,依然取左邊的E,這時候順序爲ABE,接着去掉E相關的邊,這時只有一個點入度爲0,那就是C,取C,順序爲ABEC。去掉C的邊得到兩個入度爲0的點DF,取出D,順序爲ABECD,然後去掉D相關的邊,那麼下一個入度爲0的就是F,然後是object

 

方法重寫

相同的行爲,子類和父類的處理方式很可能不一樣。

比如,對於“自我介紹”這一行爲,父類的處理是“我叫XXX,我xxx歲”。而子類學生的處理是“我叫XXX,我XXX歲,我來自xxxxx學校”。

這種相同行爲,處理卻不一致的情況,我們就需要在子類中對父類的這個函數進行重寫,這就是方法重寫。按照前面講的搜索原則,子類肯定是調用自己重寫後的這個方法。

class Person:

    def introduce(self):
        print(f'I\'m {self.name}, {self.age} years old.')

class Student(Person):
    def introduce(self):
        print(f'I\'m {self.name}, {self.age} years old. I study in {self.school}')



if __name__ == '__main__':
    s1 = Student(name='Tom')
    s1.school_set('Beijing 101 Middle School')
    s1.introduce()

輸出爲:

I'm Tom, 20 years old. I study in Beijing 101 Middle School

 

方法重寫是面向對象編程中非常實用的機制,因爲它讓子類很容易具備自己的“個性”,而不僅僅是盲目地從父類那裏繼承。

方法重寫,在類的實例化中用得更多。通常,子類和父類的實例化過程是不太一樣的,我們需要重寫__init__()方法。

class Person:

    def __init__(self, name, age=20, sex='male'):
        self.name = name
        self.age = age
        self.sex = sex



class Speaker(Person):
    def __init__(self, name, topic, age=20, sex='male'):
        super().__init__(name, age, sex)
        self.topic = topic

if __name__ == '__main__':

sp1 = Speaker('Jeffery', 'Python在大數據分析領域的應用實踐')
    sp1.speak()

輸出爲:

I'm a speaker, the topic of my talk is "Python在大數據分析領域的應用實踐"

我們在子類Speaker中重寫了構造方法__init__(),在實例化Speaker對象時,系統是調用了Speaker類的構造方法,而不是Person類。

有意思的是,我們在Speaker類構造方法中,我們使用了下面的語句:

super().__init__(name, age, sex)

相信大家應該能看明白,它的功能是調用了父類的構造方法。通過super()去調用父類方法,是非常常用的,尤其是重寫構造方法時。這樣做能保證父類的屬性被正確初始化,同時也能減少子類和父類之間代碼的重複量。

這個簡單例子中,我們可以認爲super是調用了父類,但是這個說法並不完全正確。下面我們講講super()的本質。

 

理解super

Super()在多重繼承的情況下並不一定是調用了父類,看下面的例子:

#  author: Tiger,   關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./12/12_5.py

# super
class Base:
    def __init__(self):
        print("enter Base")
        print("leave Base")


class A(Base):
    def __init__(self):
        print("enter A")
        super(A, self).__init__()
        print("leave A")


class B(Base):
    def __init__(self):
        print("enter B")
        super(B, self).__init__()
        print("leave B")


class C(A, B):
    def __init__(self):
        print("enter C")
        super(C, self).__init__()
        print("leave C")

if __name__ == '__main__':
    inst_c = C()

輸出爲:

enter C

enter A

enter B

enter Base

leave Base

leave B

leave A

leave C

可以看到,當A裏面調用super()時,它並沒有調用其父類Base,而是調用了B。

這個例子中,類的繼承關係如下:

C是多重繼承了A、B。

實際上,當我們調用super(cls, inst) 時,python會獲取inst的MRO。前面我們學過,這裏inst_C的MRO應該是: C->A->B->Base。每次調用super(cls, inst).method(),它實際執行的,是cls在MRO中的下一個類對應的方法。所以,當我們在A的__init__中調用super時,它實際執行的是B的構造方法。

這就是super的本質,它是按照入參inst對應的MRO順序執行的,不一定是其父類。

Super不只是在構造方法中使用,在所有成員函數中都可以使用。

 

Isinstance和issubcass

我們介紹兩個有用的內置函數,他們在繼承機制中比較常用。

isinstance()函數:用來判斷一個對象是否是一個已知的類型,類似type()。

語法
isinstance(object, classinfo)
參數
object——實例對象
classinfo——可以是直接或間接類名、基本類型或者由他們組成的元組。
返回值
如果對象類型與參數二的類型(classinfo)相同則返回True,否則返回False

比如:

sp1 = Speaker('Jeffery', 'Python在大數據分析領域的應用實踐')

print(isinstance(sp1, Speaker))

輸出爲:

True

它和type()有一些區別,type()不考慮繼承關係,而isinstance要考慮繼承關係。比如:

print(isinstance(sp1, Speaker))

print(isinstance(sp1, Person))
print(type(sp1) == Speaker)
print(type(sp1) == Person)

輸出爲:

True

True

True

False

 

issubclass() 來檢查類的繼承關係。

語法

以下是 issubclass() 方法的語法:

issubclass(class, classinfo)

參數

class -- 類。

classinfo -- 類。

返回值

如果 class classinfo 的子類返回 True,否則返回 False

比如:

print(issubclass(StudentSpeaker, Person))  # True

print(issubclass(Student, Person))  # True
print(issubclass(Student, Speaker))  # False

輸出爲:

True

True

False

 

 

6、多態

多態性是面向對象編程的一個重要特性,所謂多態,就是一個事物的多種形態。

我們看看JAVA中多態的體現:

  • 方法支持重載(overload)和重寫(overwrite)
  • 對象多態性(將子類的對象賦給父類類型引用)

Python支持方法的重寫,前面已經講過。但是不支持重載,所謂重載,就是允許函數名相同而參數列表不同的函數同時存在。顯然,Python中,如果一個類(或者父子類)存在重載的函數,則直接認爲是重寫覆蓋了。

再看對象多態性,Python的變量是不需要聲明類型的,它指向什麼類型的對象,它就是什麼類型,它是一種動態類型語言。所以,它不存在對象多態性的說法。

所以,Python的多態性,其實就是體現在方法重寫。多態性在Python中體現很弱,所以大家可以看到很多教程根本就不會提這個特性的。但我們需要明確,python也是支持多態的。

 

7、成員可見範圍

在前面我們所有的例子中,類裏面定義的變量或者函數,外面都是可以訪問的。在JAVA或者C++中,我們可以通過關鍵字public、private、protect等,來指明成員變量或者成員函數的可見範圍。比如,private就表示是私有的,只能自己內部的成員函數訪問;public則是外部可見的;protect則是派生類可見。

在python中,我們規定,如果類裏面的成員名字以兩個下劃線__開頭(至少帶有兩個前綴下劃線,至多一個後綴下劃線),這樣的成員是私有成員(變量或者函數),比如下面例子中的__name

#  author: Tiger,   關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./12/12_6.py

# 私有成員
class Fruits:
    __name = ''

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


if __name__ == '__main__':
    f1 = Fruits('apple')
    print(f1.__name)

會報錯:

AttributeError: 'Fruits' object has no attribute '__name'

因爲__name是私有變量,所以它只能在成員函數中被引用,外層是看不見它的,

但是,Python並沒有真正像JAVA的private那樣限制訪問私有變量。它實際是使用了名字替代的方式將其藏起來了而已。我們可以通過__dict__查看這個實例對象的變量:

print(f1.__dict__)

輸出爲:

{'_Fruits__name': 'apple'}

這裏面出現了一個奇怪的變量_Fruits__name。這其實就是Python隱藏私有變量的方法,它會把滿足命名規則的私有變量,替換爲_classname+name的形式,這樣外部再想用變量名去訪問肯定就找不到符號了。

這種私有變量,其實是可以強制訪問的,我們只需要使用替換後的名字即可。

print(f1._Fruits__name)

輸出爲:

apple

 

所以,嚴格來說,python並沒有提供限制訪問私有變量的機制,它僅僅提供了名字替代的方式將其隱藏了。你完全可以強制訪問它,全靠自覺。

 

8、迭代器

迭代器是python中用於遍歷容器對象的一種機制,這裏的容器包括字符串、列表、集合、字典、文件等等。迭代器提供了兩個基本方法iter()和next(),iter()用於返回一個迭代對象,next()用於返回容器中的下一個元素。這兩個方法配合使用,可以讓我們很方便的遍歷一個對象。

看下面的例子:

#  author: Tiger,   關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./12/12_7.py

# 迭代器
list1 = [1, 2, 3, 4]
it = iter(list1)
while True:
    try:
        print(next(it), end=’’)
    except StopIteration:
        break

輸出爲:

1 2 3 4

 

我們通過iter(list1)獲取到了list1的迭代對象it,然後通過next(it)遍歷list1的所有元素。每次調用next時,迭代器會記錄下當前遍歷位置,所以再次調用next時會依次返回元素。當遍歷完所有元素時,會拋出異常StopIteration。

同樣,字符串、集合、元組、字典等等,都是支持迭代器的。我們知道,python裏面萬物皆對象,這些數據類型其實也是對象。這些對象裏面實現了成員函數__iter__(),它返回一個迭代器對象,沒錯,iter()方法其實就是調用的這個成員函數。

真正實現遍歷的,是這個迭代器對象,list的迭代器對象是list_iterator。這個對象裏面需要實現__next__()成員函數,它記錄當前遍歷位置,並且返回一個值。

所以,迭代器是一個類對象,它需要包含兩個成員函數__iter__()和__next__()。類對象本身就可以是迭代器,當然也可以分開寫。

我們看下面的例子,它把讓類對象本身就成了一個迭代器:

class ExampleAndIter:

    def __init__(self, max):
        self.max = max
        self.position = 0

    def __iter__(self):
        self.position = 0
        return self

    def __next__(self):
        if self.position > self.max:
            raise StopIteration
        else:
            ret = self.position ** 3
            self.position += 1
            return ret

inst1 = ExampleAndIter(10)
it1 = iter(inst1)
for item in it1:
    print(item, end='')

輸出爲:

0 1 8 27 64 125 216 343 512 729 1000

__iter__()裏面返回的是self,也就是它自己本身就是一個迭代器。這裏我們用了for循環的方式來遍歷,迭代器也支持這種方式,它實質上也是隱含調用了__next__()方法。

下面我們把迭代器分開寫:

class MyIter:

    def __init__(self, obj):
        self.obj = obj

    def __iter__(self):
        return self

    def __next__(self):
        if self.obj.position > self.obj.max:
            raise StopIteration
        else:
            ret = self.obj.position ** 3
            self.obj.position += 1
            return ret

class Example:
    def __init__(self, max):
        self.max = max
        self.position = 0

    def __iter__(self):
        self.position = 0
        return MyIter(self)

inst2 = Example(10)
it2 = iter(inst2)
for item in it2:
    print(item, end=' ')

得到的效果和上面例子一致。

 

 

9、生成器

生成器的作用是爲了更加簡便的生成一個迭代器,而不用向上節那樣需要我們在類對象裏面實現__iter__和__next__成員函數。

 

比如上節迭代器的例子,我們要遍歷一個範圍內的整數的3次方,利用生成器我們可以這樣實現:

# 生成器

def pow3(max):
    for item in range(max + 1):
        yield item ** 3


it3 = pow3(10)
while True:
    try:
        t = next(it3)
    except StopIteration:
        break

輸出爲:

0 1 8 27 64 125 216 343 512 729 1000

它能實現完全相同的功能,但是我們不需要定義一個完整的迭代器類對象,只需要一個簡單的函數即可搞定。生成器會幫我們實現一個迭代器。

我們來仔細看看上面的代碼,它與衆不同的地方在於它使用了一個關鍵詞yield。

Yield用於返回一個值,類似於return,但是它不會像return那樣真正返回並結束這個函數。Yield返回值後,函數的上下文會被臨時保存,程序切換到外層調用者運行。當再次被調用時,會獲取保存的函數上下文繼續執行。

如果一個函數包含了yield,那麼它就是一個生成器。它被調用時返回的是一個迭代器。我們可以像使用迭代器一樣使用它。生成器可以大大簡化迭代器的編寫。

生成器還有另外一種形態,就是生成器表達式,它和我們前面講的列表推導式幾乎一樣,只是它不用方括號[],而是用小括號()。

上面的例子我們也可以用生成器表達式來實現:

# 生成器表達式

it4 = (x ** 3 for x in range(11))
while True:
    try:
        print(next(it4), end=' ')
    except StopIteration:
        break

對於一些簡單的迭代邏輯,我們完全可以使用生成器表達式來替代生成器。

也許你會說,這個例子中我們爲什麼不用列表推導式呢?它一樣可以實現這個功能啊。沒錯,它們功能是一樣的,但是使用生成器表達式會比列表推導式要更省內存。因爲列表推導式會一次性把所有元素全部生成出來,存儲在內存中。而生成器表達式則在每次調用__next__時才產生一個元素。當元素的數量巨大時,它們消耗的內存差異會很大。

同樣,前面例子中,我們也可以不使用yeild返回,而定義一個列表,將每次獲取的值作爲元素存在列表裏面再返回列表。如果列表太大,這也會面臨內存消耗過大的問題。使用yeild可以達到獲取一個元素就處理一個元素的效果,可以極大的節省內存。

生成器可以表示一個無限數據流,如下例:

# 無限數據流

def get_all_even_number():  # 獲取無限偶數
    n = 1
    while True:
        if n % 2 == 0:
            yield n
        n += 1

# 獲取20次偶數
it5 = get_all_even_number()
for item in range(20):
    print(next(it5), end=' ')

輸出爲:

2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章