今日學習內容總結2.8
今日學習內容總結
通過上週的學習,我們對面向程式設計的學習也已經走向正軌,對面向物件的三大特性的學習,只要做好複習。再配合上一些小練習對所學的知識進行一個靈活運用。就能從中體驗程式設計思想。而今日就是對面向程式設計學習內容的收尾了。
反射的實際應用
反射的實際應用:
# 利用面向物件編寫系統終端功能 class WinCmd(object): def ls(self): print('windows系統正在執行ls命令') def dir(self): print('windows系統正在執行dir命令') def cd(self): print('windows系統正在執行cd命令') class LinuxCmd(object): def ls(self): print('Linux系統正在執行ls命令') def dir(self): print('Linux系統正在執行dir命令') def cd(self): print('Linux系統正在執行cd命令') # 建立物件 obj1 = WinCmd() obj2 = LinuxCmd() # 反射的使用 def run(obj): while True: cmd = input('請輸入您的指令>>>:') if hasattr(obj, cmd): func_name = getattr(obj, cmd) func_name() else: print('cmd command not found') run(obj1) run(obj2) # 反射提供了一種不需要考慮程式碼的前提下操作資料和功能。
實際執行後會發現,在run(obj1)中,我們確實能夠根據輸入的指令來執行WinCmd類中的效果。並且判斷當WinCmd類中不存在您輸入的指令的時候返回一個 cmd command not found 。在run(obj2)中同樣如此,執行了 LinuxCmd 類中的方法。這也是反射的強大之處
面向物件中的各種雙下方法
面向物件中有很多雙下方法,並且這些方法被一些人稱之為魔法方法。而某些雙下方法不需要可以呼叫,而是達到某個條件會自動觸發。比如:init 在物件例項化的時候會自動觸發。而現在我們就對這些雙下方法進行學習與總結。
1.str
物件被執行列印(print、前端展示)操作的時候自動觸發,該方法必須返回字串型別的資料,很多時候用來更加精準的描述物件。案例:
class A:
def __init__(self,name,age):
self.name = name
self.age = age
def __str__(self):
return f'姓名:{self.name} 年齡:{self.age}'
a = A('海狗',18)
print(a) # 姓名:海狗 年齡:18 列印物件觸發__str__方法
2.del
物件被執行(被動、主動)刪除操作之後自動執行。案例:
class Foo: def __del__(self): print('執行我啦') f1=Foo() del f1 # 執行我啦
3.getattr
物件查詢不存在名字的時候自動觸發。
4.setattr
物件在執行新增屬性操作的時候自動觸發。比如:obj.變數名=變數值
5.call
物件被加括號呼叫的時候自動觸發。案例:
class A:
def __init__(self):
pass
def __call__(self, *args, **kwargs):
print('雞哥好帥')
obj = A()
obj() # 雞哥好帥 物件() 自動觸發物件從屬於類(父類)的__call__方法
6.enter 和 exit
enter 是在物件被執行with上下文管理語法開始自動觸發,該方法返回什麼as後面的變數名就會得到什麼。exit 在物件被執行with上下文管理語法結束之後自動觸發。並且這兩個雙下方法是一組的,作用為上下文管理。案例:
class A:
def __init__(self,name):
self.name = name
def __enter__(self):
print(111)
return self # 必須返回self
def __exit__(self, exc_type, exc_val, exc_tb):
print(333)
with A('海狗') as obj:
print(obj.name)
# 列印結果為: 111 海狗 333
7.getattribute
只要物件查詢名字無論名字是否存在都會執行該方法,如果類中有__getattribute__方法那麼就不會去執行__getattr__方法。
8.new
物件是object類的__new__方法 產生了一個物件。例項化物件時,先觸發,object 的__new__方法,此方法在記憶體中開闢一個物件空間。再執行__init__方法,給物件封裝屬性。案例:
class A:
__instance = None
def __init__(self,name):
self.name = name
def __new__(cls, *args, **kwargs):
if not cls.__instance:
cls.__instance = object.__new__(cls)
return cls.__instance
obj = A('liky')
讓字典具備句點符查詢值的功能
# 定義一個類繼承字典
class MyDict(dict):
def __getattr__(self, item):
return self.get(item)
def __setattr__(self, key, value):
self[key] = value
# 建立一個obj物件
obj = MyDict({'name': 'jason', 'age': 18})
# 具備句點符取v
print(obj.name) # jason
print(obj.age) # 18
# 具備句點符新增k:v
obj['gender'] = 'male'
obj.pwd = 123 # 給字典名稱空間新增名字 不是資料k:v
print(obj) # {'name': 'jason', 'age': 18, 'gender': 'male', 'pwd': 123}
元類
元類簡介
所有的物件都是例項化或者說是通過呼叫類而得到的,python中一切皆物件,通過class關鍵字定義的類本質也是物件,物件又是通過呼叫類得到的,因此通過class關鍵字定義的類肯定也是呼叫了一個類得到的,這個類就是元類。
元類就是用來例項化產生類的類,因此我們可以得到如下的關係:
檢視元類的方式
class MyClass(object):
pass
obj = MyClass()
print(type(obj)) # <class '__main__.MyClass'>
print(type(MyClass)) # <class 'type'> 檢視MyClass類的型別
產生類的兩種表現形式
產生類的兩種表現形式是class關鍵字和type元類實現。但其本質其實是一種。因為class關鍵建立類時,一定呼叫了元類。而這兩種產生類的表現形式的寫法:
# class關鍵字
class C1(object):
pass
print(C1) # <class '__main__.C1'>
# type元類
type(類名,父類,類的名稱空間)
res = type('C1', (), {})
print(res) # <class '__main__.C1'>
由上述程式碼我們發現,在呼叫type時會依次傳入三個引數。這三個引數分別是:
引數一:包含一系列符合python語法程式碼的字串;
引數二:字典形式的全域性名稱空間中的名字及所對應的值;
引數三:字典形式的區域性名稱空間中的名字及所對應的值;
學習元類的目的:
元類能夠控制類的建立,也就意味著我們可以高度定製類的行為。比如:掌握了物品的生產過程,就可以在過程中做任何的額外操作。而元類可以高度制定類的行為,比如:要求類的名字必須首字母大寫。思考在哪裡編寫定製化程式碼等等。
元類的基本使用
元類是不能通過繼承的方式直接指定的,需要通過關鍵字引數的形式修改。案例:
class MyTypeClass(type):
def __init__(cls, cls_name, cls_bases, cls_dict):
# print(cls, cls_name, cls_bases, cls_dict)
if not cls_name.istitle():
raise Exception("類名的首字母必須大寫")
super().__init__(cls_name, cls_bases, cls_dict)
class C1(metaclass=MyTypeClass):
school = '清華大學'
# 建立成功,符合條件
class a(metaclass=MyTypeClass):
school = '清華大學'
# 報錯,Exception: 類名的首字母必須大寫
元類的進階操作
在之前的 call 方法中,物件加括號會自動執行產生該物件的類裡面的__call__,並且該方法返回什麼物件加括號就會得到什麼。由此可以推匯出類加括號會執行元類的裡面的__call__該方法返回什麼其實類加括號就會得到什麼。
那麼為什麼類加括號就可以被呼叫呢?類呼叫之後又是如何保證先執行類中的__new__方法再執行類中的__init__方法的?其實答案就在__call__方法中。如果想讓一個物件變成一個可呼叫物件(加括號可以呼叫),需要在該物件的類中定義__call__方法,呼叫可呼叫物件的返回值就是__call__方法的返回值。案例:
class Test():
def __init__(self):
self.name = 'python'
def __call__(self, *args, **kwargs): # self是Test類的物件
print(self) # <__main__.Test object at 0x000001C78CE78FD0>
print(self.name)
t = Test()
t() # <__main__.Test object at 0x0000027912B19278> python
因此我們可以得到以下結論:
物件加括號呼叫會呼叫該物件的類中定義的__call__方法;
類加括號呼叫會呼叫內建元類或自定義元類中的__call__方法,取決於類的元類是什麼;
自定義元類加括號呼叫會內建元類中的__call__方法。
我們可以通過一個案例驗證一下上述結論:
class MyMeta(type):
def __call__(self, *args, **kwargs):
print(self)
print(args)
print(kwargs)
return 'test'
class Test(metaclass=MyMeta):
def __init__(self, name, age):
self.name = name
self.age = age
# 呼叫Test就呼叫了
t = Test()
print(t)
'''
產生結果:
<class '__main__.Test'>
('haha', '123')
{}
test
'''
通過上述案例我們可以推斷出呼叫Test時,會呼叫自定義元類中的__call__方法,並將返回值賦值給呼叫類產生的物件。
我們可以通過__call__方法自定義元類來控制類的呼叫,也就是產生物件。案例:
class Mymeta(type):
def __init__(self, class_name, class_bases, class_dict):
# 實現類名首字母必須大寫,否則丟擲異常
if not class_name.istitle():
raise NameError('類名的首字母必須大寫')
# 實現建立的類必須有文件註釋,否則丟擲異常
if '__doc__' not in class_dict or len(class_dict['__doc__'].strip())==0:
raise TypeError('必須有文件註釋')
def __new__(cls, *args, **kwargs):
return type.__new__(cls, *args, **kwargs)
def __call__(self, *args, **kwargs):
people_obj = self.__new__(self)
self.__init__(people_obj,*args, **kwargs)
return people_obj
Test = MyMeta(class_name, class_bases, class_dic) # 呼叫的是內建元類type的__call__方法
class Test(metaclass=Mymeta):
def __new__(cls, *args, **kwargs):
# 產生空物件--真正的物件,真正造物件的是object
return object.__new__(cls) # 這裡使用type也沒有問題
def __init__(self,name):
self.name = name
t = Test() # 呼叫的是自定義元類中的__call__方法
通過自定義元類來建立類的時候,會呼叫type的__call__方法,該方法內部會做三件事情:
1、先呼叫自定義元類中的__new__方法,產生一個空物件
2、在呼叫自定義元類中的__init__方法,為空物件新增獨有屬性
3、返回一個初始化好的自定義元類的物件,就是上述的Test類
呼叫Test類時則會呼叫自定義元類MyMeta.__call__方法,同樣也會做三件事:
1、先呼叫 Test類中的__new__方法產生一個空物件
2、再呼叫Test 類中的__init__方法為空物件新增獨有屬性
3、返回一個初始化好的Test類的物件賦值給t
所以,我們可以得到這個道理:如果你想高度定製類的產生過程。如果你想高度定製物件的產生過程,那麼編寫元類裡面的__call__方法。
__new__方法
建立類時先執行type的__init__方法,當一個類例項化時(建立一個物件)執行type的__call__方法,__call__方法的返回值就是例項化的物件。例項化物件是誰取決於__new__方法,__new__返回什麼就是什麼。而 new 方法的特性:
__new__方法是在類準備將自身例項化時呼叫。
__new__方法始終都是類的靜態方法,即使沒有被加上靜態方法裝飾器。
__new__方法的作用主要有兩個:
1.為物件分配記憶體空間
2.返回物件的引用
返回物件的引用的作用是:
我們學習過python中__init__方法,__init__方法中的第一個引數self就是例項物件的引用,也就是說,哪個物件呼叫該方法,self就指向哪個物件。self中物件的引用就是從__new__方法中返回的,換句話說,__new__方法返回物件的引用,__init__方法用self引數接收了。因此我們可以知道,pyhon在建立物件時,首先呼叫了內建__new__方法,其次再呼叫__init__方法。
其實我們在程式碼中寫了__new__方法相當於是對該__new__方法進行了一次重寫,重寫 __new__方法一定要 return super().new(cls),否則python直譯器得不到分配的記憶體空間的物件引用,就不會呼叫物件的初始化方法,不會吧物件的引用傳遞到__init__方法中的self中。
同時,不是所有的地方都可以直接呼叫__new__ ,如果是在元類的__new__裡面,可以直接呼叫。案例:
class Meta(type):
def __new__(cls, *args, **kwargs):
obj = type.__new__(cls,*args,**kwargs)
return obj
如果是在元類的__call__裡面,需要間接呼叫:
class Mate(type):
def __call__(self, *args, **kwargs):
obj = object.__new__(self) # 建立一個空物件
self.__init__(obj,*args,**kwargs) # 讓物件去初始化
return obj