Python 高階(二)
多繼承以及MRO順序
1. 單獨呼叫父類的方法
# coding=utf-8 print("******多繼承使用類名.__init__ 發生的狀態******") class Parent(object): def __init__(self, name): print('parent的init開始被呼叫') self.name = name print('parent的init結束被呼叫') class Son1(Parent): def __init__(self, name, age): print('Son1的init開始被呼叫') self.age = age Parent.__init__(self, name) print('Son1的init結束被呼叫') class Son2(Parent): def __init__(self, name, gender): print('Son2的init開始被呼叫') self.gender = gender Parent.__init__(self, name) print('Son2的init結束被呼叫') classGrandson(Son1, Son2): def __init__(self, name, age, gender): print('Grandson的init開始被呼叫') Son1.__init__(self, name, age) # 單獨呼叫父類的初始化方法 Son2.__init__(self, name, gender) print('Grandson的init結束被呼叫') gs = Grandson('grandson', 12, '男') print('姓名:', gs.name) print('年齡:', gs.age) print('性別:', gs.gender) print("******多繼承使用類名.__init__ 發生的狀態******\n\n")
執行結果:
******多繼承使用類名.__init__ 發生的狀態****** Grandson的init開始被呼叫 Son1的init開始被呼叫 parent的init開始被呼叫 parent的init結束被呼叫 Son1的init結束被呼叫 Son2的init開始被呼叫 parent的init開始被呼叫 parent的init結束被呼叫 Son2的init結束被呼叫 Grandson的init結束被呼叫 姓名: grandson 年齡: 12 性別: 男 ******多繼承使用類名.__init__ 發生的狀態******
2. 多繼承中super呼叫有所父類的被重寫的方法
print("******多繼承使用super().__init__ 發生的狀態******") class Parent(object): def __init__(self, name, *args, **kwargs): # 為避免多繼承報錯,使用不定長引數,接受引數 print('parent的init開始被呼叫') self.name = name print('parent的init結束被呼叫') class Son1(Parent): def __init__(self, name, age, *args, **kwargs): # 為避免多繼承報錯,使用不定長引數,接受引數 print('Son1的init開始被呼叫') self.age = age super().__init__(name, *args, **kwargs) # 為避免多繼承報錯,使用不定長引數,接受引數 print('Son1的init結束被呼叫') class Son2(Parent): def __init__(self, name, gender, *args, **kwargs): # 為避免多繼承報錯,使用不定長引數,接受引數 print('Son2的init開始被呼叫') self.gender = gender super().__init__(name, *args, **kwargs) # 為避免多繼承報錯,使用不定長引數,接受引數 print('Son2的init結束被呼叫') class Grandson(Son1, Son2): def __init__(self, name, age, gender): print('Grandson的init開始被呼叫') # 多繼承時,相對於使用類名.__init__方法,要把每個父類全部寫一遍 # 而super只用一句話,執行了全部父類的方法,這也是為何多繼承需要全部傳參的一個原因 # super(Grandson, self).__init__(name, age, gender) super().__init__(name, age, gender) print('Grandson的init結束被呼叫') print(Grandson.__mro__) gs = Grandson('grandson', 12, '男') print('姓名:', gs.name) print('年齡:', gs.age) print('性別:', gs.gender) print("******多繼承使用super().__init__ 發生的狀態******\n\n")
執行結果:
******多繼承使用super().__init__ 發生的狀態****** (<class '__main__.Grandson'>, <class '__main__.Son1'>, <class '__main__.Son2'>, <class '__main__.Parent'>, <class 'object'>) Grandson的init開始被呼叫 Son1的init開始被呼叫 Son2的init開始被呼叫 parent的init開始被呼叫 parent的init結束被呼叫 Son2的init結束被呼叫 Son1的init結束被呼叫 Grandson的init結束被呼叫 姓名: grandson 年齡: 12 性別: 男 ******多繼承使用super().__init__ 發生的狀態******
注意:
- 以上2個程式碼執行的結果不同
- 如果2個子類中都繼承了父類,當在子類中通過父類名呼叫時,parent被執行了2次
- 如果2個子類中都繼承了父類,當在子類中通過super呼叫時,parent被執行了1次
呼叫父類的三種方法:1、類名.xxx(self, 形參);2、super().xxx(形參);3、super(類名, self).xxx(形參)
區別:
在多繼承中,如果使用 類名.xxx(self, 形參) 的方法是話,可能會造成父類的多次呼叫,例如在建立套接字的時候,這種情況會使本來只需要建立一個套接字的情況變成建立兩個,浪費了資源。
使用super().xxx(形參)的方法的話,有可能出現無法呼叫父類的情況,因為在super()中採用C3演算法,C3演算法是保證將來每個類只調用一次的演算法。使用 類名.__mro__ 可以看到C3演算法的最終結論,是一個元組,裡面都是類的名字,類的名字的先後順序決定以後去呼叫的時候的先後順序。但super()裡面沒有寫類名時,就拿當前繼承的類名去元組裡面找,找到之後的下一個類裡面的方法就是將要呼叫的。
使用super(類名, self).xxx(形參),可以指定將來繼承的父類中要呼叫的類的名字。
3. 單繼承中super
print("******單繼承使用super().__init__ 發生的狀態******") class Parent(object): def __init__(self, name): print('parent的init開始被呼叫') self.name = name print('parent的init結束被呼叫') class Son1(Parent): def __init__(self, name, age): print('Son1的init開始被呼叫') self.age = age super().__init__(name) # 單繼承不能提供全部引數 print('Son1的init結束被呼叫') class Grandson(Son1): def __init__(self, name, age, gender): print('Grandson的init開始被呼叫') super().__init__(name, age) # 單繼承不能提供全部引數 print('Grandson的init結束被呼叫') gs = Grandson('grandson', 12, '男') print('姓名:', gs.name) print('年齡:', gs.age) #print('性別:', gs.gender) print("******單繼承使用super().__init__ 發生的狀態******\n\n")
總結
- super().__init__相對於類名.__init__,在單繼承上用法基本無差
- 但在多繼承上有區別,super方法能保證每個父類的方法只會執行一次,而使用類名的方法會導致方法被執行多次,具體看前面的輸出結果
- 多繼承時,使用super方法,對父類的傳引數,應該是由於python中super的演算法導致的原因,必須把引數全部傳遞,否則會報錯
- 單繼承時,使用super方法,則不能全部傳遞,只能傳父類方法所需的引數,否則會報錯
- 多繼承時,相對於使用類名.__init__方法,要把每個父類全部寫一遍, 而使用super方法,只需寫一句話便執行了全部父類的方法,這也是為何多繼承需要全部傳參的一個原因
*args、**kargs做形參用時即可以表示接收各種多餘引數,並轉化為元組和字典;也可以表示拆包。
(類物件是大家共用的,例項化的時候不會建立,只會指向。
例項物件中只有屬性是私有的,其他liru方法等都是公有的,指向類模板那裡。)
由上圖看出:
- 類屬性在記憶體中只儲存一份
- 例項屬性在每個物件中都要儲存一份
應用場景:
- 通過類建立例項物件時,如果每個物件需要具有相同名字的屬性,那麼就使用類屬性,用一份既可
2. 例項方法、靜態方法和類方法
方法包括:例項方法、靜態方法和類方法,三種方法在記憶體中都歸屬於類,區別在於呼叫方式不同。
- 例項方法:由物件呼叫;至少一個self引數;執行例項方法時,自動將呼叫該方法的物件賦值給self;
- 類方法:由類呼叫; 至少一個cls引數;執行類方法時,自動將呼叫該方法的類賦值給cls;
- 靜態方法:由類呼叫;無預設引數;
property屬性
1. 什麼是property屬性
一種用起來像是使用的例項屬性一樣的特殊屬性,可以對應於某個方法
# ############### 定義 ############### class Foo: def func(self): pass # 定義property屬性 @property def prop(self): pass # ############### 呼叫 ############### foo_obj = Foo() foo_obj.func() # 呼叫例項方法 foo_obj.prop # 呼叫property屬性
property屬性的定義和呼叫要注意一下幾點:
- 定義時,在例項方法的基礎上新增 @property 裝飾器;並且僅有一個self引數
- 呼叫時,無需括號
property屬性的有兩種方式
- 裝飾器 即:在方法上應用裝飾器
- 類屬性 即:在類中定義值為property物件的類屬性
3.1 裝飾器方式
在類的例項方法上應用@property裝飾器
Python中的類有經典類
和新式類
,新式類
的屬性比經典類
的屬性豐富。( 如果類繼object,那麼該類是新式類 )
新式類,具有三種@property裝飾器
#coding=utf-8 # ############### 定義 ############### class Goods: """python3中預設繼承object類 以python2、3執行此程式的結果不同,因為只有在python3中才有@xxx.setter @xxx.deleter """ @property def price(self): print('@property') @price.setter def price(self, value): print('@price.setter') @price.deleter def price(self): print('@price.deleter') # ############### 呼叫 ############### obj = Goods() obj.price # 自動執行 @property 修飾的 price 方法,並獲取方法的返回值 obj.price = 123 # 自動執行 @price.setter 修飾的 price 方法,並將 123 賦值給方法的引數 del obj.price # 自動執行 @price.deleter 修飾的 price 方法
注意
- 經典類中的屬性只有一種訪問方式,其對應被 @property 修飾的方法
- 新式類中的屬性有三種訪問方式,並分別對應了三個被@property、@方法名.setter、@方法名.deleter修飾的方法
由於新式類中具有三種訪問方式,我們可以根據它們幾個屬性的訪問特點,分別將三個方法定義為對同一個屬性:獲取、修改、刪除
class Goods(object): def __init__(self): # 原價 self.original_price = 100 # 折扣 self.discount = 0.8 @property def price(self): # 實際價格 = 原價 * 折扣 new_price = self.original_price * self.discount return new_price @price.setter def price(self, value): self.original_price = value @price.deleter def price(self): del self.original_price obj = Goods() obj.price # 獲取商品價格 obj.price = 200 # 修改商品原價 del obj.price # 刪除商品原價
3.2 類屬性方式,建立值為property物件的類屬性
- 當使用類屬性的方式建立property屬性時,
經典類
和新式類
無區別
class Foo: def get_bar(self): return 'laowang' BAR = property(get_bar) obj = Foo() reuslt = obj.BAR # 自動呼叫get_bar方法,並獲取方法的返回值 print(reuslt)
property方法中有個四個引數
- 第一個引數是方法名,呼叫 物件.屬性 時自動觸發執行方法
- 第二個引數是方法名,呼叫 物件.屬性 = XXX 時自動觸發執行方法
- 第三個引數是方法名,呼叫 del 物件.屬性 時自動觸發執行方法
- 第四個引數是字串,呼叫 物件.屬性.__doc__ ,此引數是該屬性的描述資訊
#coding=utf-8 class Foo(object): def get_bar(self): print("getter...") return 'laowang' def set_bar(self, value): """必須兩個引數""" print("setter...") return 'set value' + value def del_bar(self): print("deleter...") return 'laowang' BAR = property(get_bar, set_bar, del_bar, "description...") obj = Foo() obj.BAR # 自動呼叫第一個引數中定義的方法:get_bar obj.BAR = "alex" # 自動呼叫第二個引數中定義的方法:set_bar方法,並將“alex”當作引數傳入 desc = Foo.BAR.__doc__ # 自動獲取第四個引數中設定的值:description... print(desc) del obj.BAR # 自動呼叫第三個引數中定義的方法:del_bar方法
由於類屬性方式
建立property屬性具有3種訪問方式,我們可以根據它們幾個屬性的訪問特點,分別將三個方法定義為對同一個屬性:獲取、修改、刪除
綜上所述:
- 定義property屬性共有兩種方式,分別是【裝飾器】和【類屬性】,而【裝飾器】方式針對經典類和新式類又有所不同。
- 通過使用property屬性,能夠簡化呼叫者在獲取資料的流程
property屬性-應用
1. 私有屬性新增getter和setter方法
class Money(object): def __init__(self): self.__money = 0 def getMoney(self): return self.__money def setMoney(self, value): if isinstance(value, int): self.__money = value else: print("error:不是整型數字")
2. 使用property升級getter和setter方法
class Money(object): def __init__(self): self.__money = 0 def getMoney(self): return self.__money def setMoney(self, value): if isinstance(value, int): self.__money = value else: print("error:不是整型數字") # 定義一個屬性,當對這個money設定值時呼叫setMoney,當獲取值時呼叫getMoney money = property(getMoney, setMoney) a = Money() a.money = 100 # 呼叫setMoney方法 print(a.money) # 呼叫getMoney方法 #100
3. 使用property取代getter和setter方法
- 重新實現一個屬性的設定和讀取方法,可做邊界判定
class Money(object): def __init__(self): self.__money = 0 # 使用裝飾器對money進行裝飾,那麼會自動新增一個叫money的屬性,當呼叫獲取money的值時,呼叫裝飾的方法 @property def money(self): return self.__money # 使用裝飾器對money進行裝飾,當對money設定值時,呼叫裝飾的方法 @money.setter def money(self, value): if isinstance(value, int): self.__money = value else: print("error:不是整型數字") a = Money() a.money = 100 print(a.money)
列表的切片可以直接設定值
魔法屬性
什麼是魔法屬性:
特殊情況下Python直譯器會自動呼叫特殊的東西,這是魔法屬性的體現
無論人或事物往往都有不按套路出牌的情況,Python的類屬性也是如此,存在著一些具有特殊含義的屬性,詳情如下:
1. __doc__
- 表示類的描述資訊
class Foo: """ 描述類資訊,這是用於看片的神奇 """ def func(self): pass print(Foo.__doc__) #輸出:類的描述資訊
2. __module__ 和 __class__
- __module__ 表示當前操作的物件在那個模組
- __class__ 表示當前操作的物件的類是什麼
# test.py
# -*- coding:utf-8 -*- class Person(object): def __init__(self): self.name = 'laowang'
# main.py from test import Person obj = Person() print(obj.__module__) # 輸出 test 即:輸出模組 print(obj.__class__) # 輸出 test.Person 即:輸出類
3. __init__
- 初始化方法,通過類建立物件時,自動觸發執行
class Person: def __init__(self, name): self.name = name self.age = 18 obj = Person('laowang') # 自動執行類中的 __init__ 方法
4. __del__
- 當物件在記憶體中被釋放時,自動觸發執行。
注:此方法一般無須定義,因為Python是一門高階語言,程式設計師在使用時無需關心記憶體的分配和釋放,因為此工作都是交給Python直譯器來執行,所以,__del__的呼叫是由直譯器在進行垃圾回收時自動觸發執行的。
class Foo: def __del__(self): pass
5. __call__
- 物件後面加括號,觸發執行。
注:__init__方法的執行是由建立物件觸發的,即:物件 = 類名()
;而對於 __call__ 方法的執行是由物件後加括號觸發的,即:物件()
或者 類()()
class Foo: def __init__(self): pass def __call__(self, *args, **kwargs): print('__call__') obj = Foo() # 執行 __init__ obj() # 執行 __call__
6. __dict__
- 類或物件中的所有屬性
類的例項屬性屬於物件;類中的類屬性和方法等屬於類,即:
class Province(object): country = 'China' def __init__(self, name, count): self.name = name self.count = count def func(self, *args, **kwargs): print('func') # 獲取類的屬性,即:類屬性、方法、 print(Province.__dict__) # 輸出:{'__dict__': <attribute '__dict__' of 'Province' objects>, '__module__': '__main__', 'country': 'China', '__doc__': None, '__weakref__': <attribute '__weakref__' of 'Province' objects>, 'func': <function Province.func at 0x101897950>, '__init__': <function Province.__init__ at 0x1018978c8>} obj1 = Province('山東', 10000) print(obj1.__dict__) # 獲取 物件obj1 的屬性 # 輸出:{'count': 10000, 'name': '山東'} obj2 = Province('山西', 20000) print(obj2.__dict__) # 獲取 物件obj1 的屬性 # 輸出:{'count': 20000, 'name': '山西'}
7. __str__
- 如果一個類中定義了__str__方法,那麼在列印 物件 時,預設輸出該方法的返回值。
class Foo: def __str__(self): return 'laowang' obj = Foo() print(obj) # 輸出:laowang
8、__getitem__、__setitem__、__delitem__
- 用於索引操作,如字典。以上分別表示獲取、設定、刪除資料
# -*- coding:utf-8 -*- class Foo(object): def __getitem__(self, key): print('__getitem__', key) def __setitem__(self, key, value): print('__setitem__', key, value) def __delitem__(self, key): print('__delitem__', key) obj = Foo() result = obj['k1'] # 自動觸發執行 __getitem__ obj['k2'] = 'laowang' # 自動觸發執行 __setitem__ del obj['k1'] # 自動觸發執行 __delitem__
9、__getslice__、__setslice__、__delslice__
- 該三個方法用於分片操作,如:列表
# -*- coding:utf-8 -*- class Foo(object): def __getslice__(self, i, j): print('__getslice__', i, j) def __setslice__(self, i, j, sequence): print('__setslice__', i, j) def __delslice__(self, i, j): print('__delslice__', i, j) obj = Foo() obj[-1:1] # 自動觸發執行 __getslice__ obj[0:1] = [11,22,33,44] # 自動觸發執行 __setslice__ del obj[0:2] # 自動觸發執行 __delslice__
with與“上下文管理器”
如果你有閱讀原始碼的習慣,可能會看到一些優秀的程式碼經常出現帶有 “with” 關鍵字的語句,它通常用在什麼場景呢?今
對於系統資源如檔案、資料庫連線、socket 而言,應用程式開啟這些資源並執行完業務邏輯之後,必須做的一件事就是要關閉(斷開)該資源。
比如 Python 程式開啟一個檔案,往檔案中寫內容,寫完之後,就要關閉該檔案,否則會出現什麼情況呢?極端情況下會出現 "Too many open files" 的錯誤,因為系統允許你開啟的最大檔案數量是有限的。
同樣,對於資料庫,如果連線數過多而沒有及時關閉的話,就可能會出現 "Can not connect to MySQL server Too many connections",因為資料庫連線是一種非常昂貴的資源,不可能無限制的被建立。
來看看如何正確關閉一個檔案。
普通版:
def m1(): f = open("output.txt", "w") f.write("python之禪") f.close()
這樣寫有一個潛在的問題,如果在呼叫 write 的過程中,出現了異常進而導致後續程式碼無法繼續執行,close 方法無法被正常呼叫,因此資源就會一直被該程式佔用者釋放。那麼該如何改進程式碼呢?
進階版:
def m2(): f = open("output.txt", "w") try: f.write("python之禪") except IOError: print("oops error") finally: f.close()
改良版本的程式是對可能發生異常的程式碼處進行 try 捕獲,使用 try/finally 語句,該語句表示如果在 try 程式碼塊中程式出現了異常,後續程式碼就不再執行,而直接跳轉到 except 程式碼塊。而無論如何,finally 塊的程式碼最終都會被執行。因此,只要把 close 放在 finally 程式碼中,檔案就一定會關閉。
高階版:
def m3(): with open("output.txt", "r") as f: f.write("Python之禪")
一種更加簡潔、優雅的方式就是用 with 關鍵字。open 方法的返回值賦值給變數 f,當離開 with 程式碼塊的時候,系統會自動呼叫 f.close() 方法, with 的作用和使用 try/finally 語句是一樣的。那麼它的實現原理是什麼?在講 with 的原理前要涉及到另外一個概念,就是上下文管理器(Context Manager)。
上下文管理器
任何實現了 __enter__() 和 __exit__() 方法的物件都可稱之為上下文管理器,上下文管理器物件可以使用 with 關鍵字。顯然,檔案(file)物件也實現了上下文管理器。
那麼檔案物件是如何實現這兩個方法的呢?我們可以模擬實現一個自己的檔案類,讓該類實現 __enter__() 和 __exit__() 方法。
class File(): def __init__(self, filename, mode): self.filename = filename self.mode = mode def __enter__(self): print("entering") self.f = open(self.filename, self.mode) return self.f def __exit__(self, *args): print("will exit") self.f.close()
__enter__() 方法返回資源物件,這裡就是你將要開啟的那個檔案物件,__exit__() 方法處理一些清除工作。
因為 File 類實現了上下文管理器,現在就可以使用 with 語句了。
with File('out.txt', 'w') as f: print("writing") f.write('hello, python')
這樣,你就無需顯示地呼叫 close 方法了,由系統自動去呼叫,哪怕中間遇到異常 close 方法也會被呼叫。
實現上下文管理器的另外方式
Python 還提供了一個 contextmanager 的裝飾器,更進一步簡化了上下文管理器的實現方式。通過 yield 將函式分割成兩部分,yield 之前的語句在 __enter__ 方法中執行,yield 之後的語句在 __exit__ 方法中執行。緊跟在 yield 後面的值是函式的返回值。
from contextlib import contextmanager @contextmanager def my_open(path, mode): f = open(path, mode) yield f f.close()
呼叫
with my_open('out.txt', 'w') as f: f.write("hello , the simplest context manager")
總結
Python 提供了 with 語法用於簡化資源操作的後續清除操作,是 try/finally 的替代方法,實現原理建立在上下文管理器之上。此外,Python 還提供了一個 contextmanager 裝飾器,更進一步簡化上下管理器的實現方式。