Python第七章-面向對象高級
- 2020 年 4 月 3 日
- 筆記
面向對象高級
一、 特性
特性是指的property
.
property
這個詞的翻譯一直都有問題, 很多人把它翻譯為屬性, 其實是不恰當和不準確的. 在這裡翻譯成特性是為了和屬性區別開來.
屬性是指的attribute
, 我們以前學習的實例變數和類變數是attribute
, 所以也可以叫做實例屬性和類屬性.
property
(特性)到底是個什麼東西?
我們前面學習類屬性和實例屬性的時候知道, 訪問他們的時候就可以直接獲取到這些屬性的值.
而特性可以看成一種特殊的屬性, 為什麼呢?
但從訪問方式來看, 特性和屬性看不出來差別, 但是特性實際上會經過計算之後再返回值. 所以每一個特性都始終與一個方法相關聯.
1.1 定義特性
定義特性和定義實例方法類似, 只需要另外在方法上面添加一個內置裝飾器:@property
訪問特性和訪問實例變數完全一樣, 不需要使用添加括弧去調用.
import math class Circle: def __init__(self, r): self.r = r @property def area(self): """ 定義特性 這個特性是計算出來圓的面積 :return: """ return math.pi * (self.r ** 2) c = Circle(10) print(c.area)
很明顯, 特性背後的本質是一個方法的存在, 所以你不可能在外面去修改這個特性的值!
試圖修改特性的值只會拋出一個異常.
c.area = 100
1.2 使用特性的設計哲學
這種特性使用方式遵循所謂的 統一訪問原則.
實際上, 定義一個類總是保持介面的統一總是好的.
有了特性, 把訪問屬性和訪問方法統一了, 都像在訪問屬性一樣, 省得去考慮到底什麼時候需要添加括弧,什麼時候不用添加括弧.
1.3 特性的攔截操作
python 還提供了設置和刪除屬性.
通過給方法添加其他內置裝飾器來實現
設置:@特性名.setter
刪除:@特性名.deleter
class Student: def __init__(self, name): self._name = name # name 是特性了, 所以用實例變數存儲特性的值的是換個變數名!!! @property def name(self): return self._name @name.setter def name(self, name): if type(name) is str and len(name) > 2: self._name = name else: print("你提供的值" + str(name) + "不合法!") @name.deleter def name(self): print("對不起, name 不允許刪除") s = Student("李四") print(s.name) s.name = "彩霞" print(s.name) s.name = "張三" print(s.name) del s.name
二、三大特性之一-封裝性
面向對象的三大特徵:封裝, 繼承, 多態
2.1什麼是封裝性
1.封裝是面向對象編程的一大特點 2.面向對象編程的第一步,就是講屬性和方法封裝到一個抽象的類中 3.外界使用類創建對象,然後讓對象調用方法 4.對象方法的細節都被封裝在類的內部
在類中定義屬性, 定義方法就是在封裝數據和程式碼.
2.2 私有化屬性
首先先明確一點, python 不能真正的對屬性(和方法)進行私有, 因為 python 沒有想 java 那樣的private
可用.
python 提供的"私有", 是為了怕在編程的過程中對對象屬性不小心"誤傷"提供的一種保護機制! 這種級別的私有稍微只要知道了規則, 是很容易訪問到所謂的私有屬性或方法的.
2.2.1 為什麼需要私有
封裝和保護數據的需要.
默認情況下, 類的所有屬性和方法都是公共的, 也就意味著對他們的訪問沒有做任何的限制.
意味著, 在基類中定義的所有內容都可以都會被派生類繼承, 並可從派生類內部進行訪問.
在面向對象的應用程式設計中, 我們通常不希望這種行為, 因為他們暴露基類的內部實現, 可能導致派生類中的使用的私有名稱與基類中使用的相同的私有名稱發生衝突.
屬性或方法私有後就可以避免這種問題!
2.2.2 "私有"機制
為了解決前面說的問題, python 提供了一種叫做名稱改寫(name mangling)的機制
如果給屬性或者方法命名的時候, 使用兩個下劃線開頭(__
)的屬性和方法名會自動變形為_類名__方法名
, 這樣就避免了在基礎中命名衝突的問題.
class Student: def __init__(self): pass def __say(self): print("我是私有方法你信嗎?") s = Student() s.__say() # 雙下劃線開頭的方法已經被形變, 此處訪問不到
s._Student__say()
2.2.3 不是真正的私有
儘管這種方案隱藏了數據, 但是並沒有提供嚴格的機制來限制對私有屬性和方法的訪問.
雖然這種機制好像多了一層處理, 但是這種變形是發生在類的定義期間, 並不會在方法執行期間發生, 所以並沒有添加額外的開銷.
2.2.4 不同的聲音
有部分人認為這種使用雙__
的機制好辣雞, 寫兩個下劃線影響效率. 他們使用一個下劃線, 並把這個作為一個約定.
好吧, 你喜歡哪種呢?
三、面向對象三大特性-繼承性(Inheritance)
這一節我們來學習面向的對象的再一個特徵: 繼承
3.1繼承性的概念
繼承(extends
)是創建新類的一種機制, 目的是專門使用和修改先有類的行為.
原有類稱為超類(super class
), 基類(base class
)或父類.
新類稱為子類或派生類.
通過繼承創建類時, 所創建的類將繼承其基類所有的屬性和方法, 派生類也可以重新定義任何這些屬性和方法, 並添加自己的新屬性和方法
3.2 繼承性的意義
繼承實現程式碼的重用,相同的程式碼不需要重複的編寫
從子類的角度來看,避免了重複的程式碼。(子類繼承父類後,子類可以直接使用父類的屬性和方法)
從父類的角度來看,子類擴展了父類的功能。(因為子類也是一個特殊的父類)
- 子類可以直接訪問父類的屬性和方法。
- 子類可以新增自己的屬性和方法。
- 子類可以重寫父類的方法。
3.3 繼承的語法和具體實現
繼承的語法如下:
class 父類名: pass class 子類名(父類名): pass
3.3.1最簡單的繼承
python 的繼承是在類名的後面添加括弧, 然後在括弧中聲明要繼承的父類.
class Father: def speak(self): print("我是父類中的 speak 方法") # Son繼承 Father 類 class Son(Father): pass s = Son() s.speak()
說明:
- 從字面上我們看到
Son
沒有定義任何的方法, 但是由於Son
繼承自Father
, 則Son
會繼承Father
的所有屬性和方法 - 調用方法時, 方法的查找規則: 先在當前類中查找, 當前類找不到想要的方法, 則去父類中查找, 還找不到然後繼續向上查找. 一旦找到則立即執行. 如果找到最頂層還找不到, 則會拋出異常
示例程式碼
# 創建人類 class Person: # 定義吃東西方法 def eat(self): print("吃窩窩頭。。") # 定義睡覺方法 def sleep(self): print("睡著啦。。") # 創建學生類 class Student(Person): # 子類新增方法:學習 def study(self): print("學生學習啦。。。把你爸樂壞了。。。。。") # 創建父類對象,訪問父類的方法 zhangsan = Person(); zhangsan.eat() zhangsan.sleep() # 創建子類對象,訪問父類的方法和子類的方法 ergou = Student(); ergou.eat() # 訪問父類的方法 ergou.sleep() # 訪問父類的方法 ergou.study() # 訪問子類的新增方法
3.3.2 繼承中的__init__()
的調用規則
如果子類沒有手動__init__()
方法, 則 python 自動調用子類的__init__()
的時候, 也會自動的調用基類的__init()__
方法.
class Father: def __init__(self): print("基類的 init ") # Son繼承 Father 類 class Son(Father): def speak(self): pass s = Son()
如果子類手動添加了__init__()
, 則 python 不會再自動的去調用基類的__init__()
class Father: def __init__(self): print("基類的 init ") # Son繼承 Father 類 class Son(Father): def __init__(self): print("子類的 init ") def speak(self): pass s = Son()
如果想通過基類初始化一些數據, 則必須顯示的調用這個方法, 調用語法是:
基類名.__init__(self, 參數...)
class Father: def __init__(self, name): print("基類的 init ") self.name = name def speak(self): print("我是父類中的 speak 方法" + self.name) # Son繼承 Father 類 class Son(Father): def __init__(self, name, age): # name 屬性的初始化應該交給基類去完成, 手動調用基類的方法. 一般放在首行 Father.__init__(self, name) # 調動指定類的方法, 並手動綁定這個方法的 self print("子類的 init ") self.age = age s = Son("李四", 20) s.speak() print(s.name) print(s.age)
3.4方法的重寫(override)
3.4.1重寫的概念
我們已經了解了調用方法時候的查找規則, 先在子類中查找, 子類查找不到再去父類中查找.
如果父類的方法不滿足子類的需求, 利用這個查找規則, 我們就可以在子類中添加一個與父類的一樣的方法, 那麼以後就會直接執行子類的方法, 而不會再去父類中查找.
這就叫方法的覆寫.(override
)
>重寫,就是子類將父類已有的方法重新實現。
父類封裝的方法,不能滿足子類的需求,子類可以重寫父類的方法。在調用時,調用的是重寫的方法,而不會調用父類封裝的方法。
3.4.2重寫父類方法的兩種情況
-
覆蓋父類的方法
父類的方法實現和子類的方法實現,完全不同,子類可以重新編寫父類的方法實現。
具體的實現方式,就相當於在子類中定義了一個和父類同名的方法並且實現
-
對父類方法進行擴展
子類的方法實現中包含父類的方法實現。(也就是說,父類原本封裝的方法實現是子類方法的一部分)。
在子類中重寫父類的方法
在需要的位置使用
super().父類方法
來調用父類的方法程式碼其他的位置針對子類的需求,編寫子類特有的程式碼實現。
如果在覆寫的方法中, 子類還需要執行父類的方法, 則可以手動調用父類的方法:
父類名.方法(self, 參數...)
class Father: def __init__(self, name): self.name = name def speak(self): print("我是父類中的 speak 方法" + self.name) # Son繼承 Father 類 class Son(Father): def __init__(self, name, age): Father.__init__(self, name) self.age = age # 子類中覆寫了父類的方法 def speak(self): Father.speak(self) print("我是子類的 speak 方法" + self.name + " 年齡:" + str(self.age)) s = Son("李四", 20) s.speak()
3.4.3關於super
在Python中super是一個特殊的類(Python 3.x以後出現)
super()就是使用super類創建出來的對象
最常使用的場景就是在重寫父類方法時,調用在父類中封裝的方法實現
3.5、父類的私有屬性和方法
- 子類對象不能在自己的方法內部,直接訪問父類的私有屬性或私有方法
- 子類對象可以通過父類的共有方法間接訪問到私有屬性或私有方法
私有屬性和方法是對象的隱私,不對外公開,外界以及子類都不能直接訪問
私有屬性和方法通常用於做一些內部的事情
3.6、多繼承
3.6.1多繼承的概念
多繼承:子類可以擁有多個父類,並且具有所有父類的屬性和方法
比如:孩子會繼承自己的父親和母親的特性
3.6.2多繼承的語法
class 子類名(父類名1, 父類名2...): pass
示例程式碼:
# 父類A class A: def test1(self): print("A類中的test1方法。。") # 父類B class B: def test2(self): print("B類中的test2方法。。") # 子類C同時繼承A和B class C(A,B): pass # 創建C對象 c1 = C() c1.test1() c1.test2()
3.6.3多繼承的注意事項
提問:如果不同的父類中存在同名的方法,子類對象在調用方法時,會調用哪一個父類中的方法呢?
開發時,應該盡量避免這種容易產生混淆的情況。如果父類之間存在同名的屬性或者方法,應該盡量避免使用多繼承
3.6.4 Python中的 MRO (方法搜索順序)[擴展]
python中針對類提供了一個內置屬性,___mro__
可以查看方法搜索順序
MRO是method resolution order,主要用於在多繼承時判斷方法,屬性的調用路徑
print(C.__mro__)
輸出結果:
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
- 在搜索方法時,是按照__mro_-的輸出結果從左至右的順序查找
- 如果當前類中找到方法,就直接執行,不再搜索
- 如果沒有找到,就查找下一個類中是否有對應的方法,如果找到,就直接執行,不再搜索
- 如果找到最後一個雷,還沒有對應的方法,程式報錯
3.6.5 python 中的上帝類型
python 中有個類比較特殊, 所有的類都直接和間接的繼承自這個類.
這個類就是:object
. 他是所有類的基類.
如果一個類沒有顯示的去繼承一個類, 則這個類默認就繼承object
, 也可以去顯示的繼承這個類.
class Student(object): pass
3.6.6 新式類和舊式(經典)類[擴展]
object
是python為所有對象提供的基類,提供有一些內置的屬性和方法,可以使用
dir
函數查看
新式類:以object為基類的類,推薦使用
經典類:不以object為基類的類,不推薦使用
-
在python 3.x中定義類時,如果沒有指定父類,會默認使用object作為該類的父類。所以python 3.x中定義的類都是新式類
-
在python 2.x中定義類時,如果沒有指定父類,則不會以object作為父類
新式類和經典類在多繼承時,會影響到方法的搜索順序
提示:為了保證編寫的程式碼能夠同時在python 2.x 和python 3.x 運行,在定義類的時候,如果沒有父類,建議統一繼承自object
class 類名(object): pass
四、面向對象三大特性-多態性(Polymorphism)
4.1多態性的概念
-
封裝性,根據職責將屬性和方法封裝到一個抽象的類中
定義類的準則
-
繼承性,實現程式碼的重用,相同的程式碼不需要重複的編寫
設計類的技巧
子類針對自己特有的書需求,編寫特定的程式碼
-
多態性,不同的子類對象,調用相同的父類方法,產生不同的執行結果
多態可以增加程式碼的靈活性
以繼承和重寫父類方法為前提
是調用方法的技巧,不會影響到類的內部設計
示例程式碼:
""" 多態性: 繼承和重寫為前提,創建不同的對象執行的具體方法不同 """ class Father(object): def __init__(self, name): print('父類的init方法') self.name = name def say(self): print('父類的say方法' + self.name) # Son類繼承於Father類,python中是類繼承於類的 class Son(Father): def __init__(self, name, age): Father.__init__(self, name) self.age = age print('子類的init方法') def say(self): Father.say(self) print('子類的say方法:' + self.name + ',' + str(self.age)) # 以下程式會體現出多態性 def mytest(obj): obj.say() f1 = Father("張爸爸") mytest(f1) print("---------------") f2 = Son("小頭兒子",5) mytest(f2)
4.2屬性和方法查找順序
多態性(多態綁定)是在有繼承背景情況下使用的一種特性.
是指在不考慮實例背景的情況下使用實例
多態的理論根據是屬性和方法的查找過程. 只要使用obj.attr
的方式使用屬性和方法, 則查找順序一定是: 對象本身, 類定義, 基類定義…
關於先查找對象本身的說明: 因為 python 是一門動態語言, 允許我們在程式碼執行的過程中去動態的給對象添加屬性和方法, 所以先從對象本身查找.
class Father: def __init__(self, name): self.name = name def speak(self): print("我是父類中的 speak 方法" + self.name) # Son繼承 Father 類 class Son(Father): def __init__(self, name, age): Father.__init__(self, name) self.age = age def speak(self): Father.speak(self) print("我是子類的 speak 方法" + self.name + " 年齡:" + str(self.age)) def foo(): print("我是動態添加上去的...") s = Son("李四", 20) s.speak = foo s.speak()
4.3 鴨子類型
python 的多態有的時候很多人把它稱之為鴨子類型
鴨子類型是指: 看起來像鴨子, 叫起來像鴨子, 走起來像鴨子, 那麼它既是鴨子, 你就可以把它當鴨子來用.
換成程式語言的說法就是: 對象屬性和方法的時候完成時和類型分開的.
class A: def speak(self): print("a 類中的方法") class B: def speak(self): print("b 類中的方法") def foo(obj): obj.speak() a = A() b = B() foo(a) foo(b)
說明:
foo
接受一個對象, 只要這個對象中有speak()
方法, 就可以正常執行, 我們並不關注他的類型A, B
這兩個類沒有任何的關係, 但是他們都有speak
方法, 所以傳遞過去都沒有任何的問題.- 這就是鴨子模型, 只要你看起來有
speak
就可以了
五、其他
5.1 特殊屬性__slot__
5.1.1動態添加屬性的問題
通過前面的學習中我們知道, 由於 python 的動態語言的特性, 我們可以動態的給對象添加屬性和方法.
但是這種方式添加的屬性和方法, 只在當前對象上有用, 在其他對象上是沒用.
class A: pass a1 = A() a1.name = "李四" #給 a1 對象添加一個屬性 print(a1.name) a2 = A() print(a2.name) # a2中沒有 name 屬性, 所以拋異常
5.1.2 __slot__
的基本使用
添加屬性和方法最好直接在類中添加, 這樣所有的對象都可以擁有了.
如果我想避免把某些屬性直接添加到實例對象上, 可以使用一個特殊屬性:__slot__
類實現.
給__slot__
定義一個元組, 則元組內的屬性名允許在實例對象上直接添加, 其他的都不允許.
class A: __slots__ = ("name", ) a1 = A() a1.name = "李四" # 給 a1 對象添加一個屬性 name 屬性是允許的 print(a1.name) a1.age = 20 # age 不允許, 所以拋異常 print(a1.age)
注意:
- 我們的
__init__()
中添加屬性是在self
上添加的, 其實也是直接在對象上添加, 所以沒有在元組中的屬性名, 也是不允許的. - 對於我們直接在類中添加方法是沒有任何的影響的.
class A: __slots__ = ("name",) def __init__(self): self.age = 30 # 也是不允許的 a = A()
5.1.3 繼承中的__slot__
__slot__
只對當前類有用, 對他的子類不起作用. 所以子類也要有自己的__slot__
class A: __slots__ = ("name",) def __init__(self): self.age = 30 # 也是不允許的 class B: def __init__(self): self.age = 30 b = B() print(b.age)
5.1.4 __slot__
對性能上的提升
一些人把__slot__
作為一種安全的特性來實現, 然後實際上他對記憶體和執行速度上的性能優化才是最重要的.
不使用__slot__
, python 使用字典的方式去存儲實例數據的, 如果一個程式使用大量的實例, 測記憶體佔用和執行效率都會影響比較大.
使用__slot__
後, python 存儲實例數據的時候, 不再使用字典, 而是使用一種更加高效的基於數組的數據結構. 可以顯著減少記憶體佔用和執行時間.
5.2 實例的測試類型
任何一個類都可以做為類型!
創建類的實例時, 該實例的類型是這個類本身, 如果有繼承存在, 則父類型也是這個實例的類型.
有些情況下, 我們需要先測試實例的類型然後再寫相應的程式碼.
python 支援 2 種測試方法:
5.2.1 內置函數:type(實例)
class A: pass class B(A): pass class C: pass a = A() b = B() c = C() print(type(a)) print(type(b)) print(type(c))
說明:
type
返回的是這個實例的所屬類的類對象.
補充一下:
其實我們經常接觸到的有兩種對象:1. 實例對象 2. 類對象
類對象就是: 表示類本身的那個對象!
5.2.2 內置函數:isinstance(實例, 類型)
class A: pass class B(A): pass class C: pass a = A() b = B() c = C() print(isinstance(a, A)) # True print(isinstance(b, B)) # True print(isinstance(b, A)) # True 繼承關係 print(isinstance(c, C)) # True print(isinstance(c, A)) # False
說明:
- 這個函數返回的是布爾值, 使用起來方便, 所以以後測試類型建議用這個函數
- 這個函數繼承關係也可以測試出來.
b
是B
類創建出來的,B
繼承自A
, 所以b
也算是類A
的實例. - 對一個實例也可以同時測試多個類型, 有一個滿足就返回
True
,isinstance(實例, (類 a, 類 b, ...)))
. 需要把多個類封裝到一個tuple
中.
print(isinstance(c, (A, B, C))) # True
5.2.3 類與類的關係: issubclass(類1, 類2)
用來測試類1
是不是類2
的子類.
class A: pass class B(A): pass class C: pass print(issubclass(B, A)) # True print(issubclass(C, A)) # False print(issubclass(A, B)) # False print(issubclass(A, object)) # True print(issubclass(C, (object, A))) # True 第二個參數也是可以 class 組成的元組