Python面向對象高級

Python面向對象高級
一、 特性
特性是指的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__)
輸出結果:

(, , , )
在搜索方法時,是按照__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 組成的元組

原文地址https://www.cnblogs.com/yanadoude/p/12625816.html

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