Python高級編程—描述符Descriptor超詳細講解上

Python高級編程——描述符Descriptor超詳細講解(上篇之屬性優先級)

Python高級編程—描述符Descriptor超詳細講解上

送你小心心記得關注我哦!!

進入正文

Python高級編程—描述符Descriptor超詳細講解上

全文摘要

描述符descriptor,是python語言的重要特性,上上一個系列文章專門講解python高級編程系列之裝飾器decorator,從英文上看起來它們還的確有那麼一點點像,不過它們確實也是有關係的,後面會講解到。本次系列文章將帶各位小夥伴深入詳解python描述符descriptor,鑑於我自己的文章風格,我會和前面的文章一樣,依然從它的誕生背景開始,到它的深入應用,由淺入深,逐層深入。

重新聲明,python高級編程——描述符descriptor系列文章一共分為上、中、下、補充篇,本文講解上篇,後續會陸續更新系列文章哦,請記得持續關注哦!文章偏長,閱讀全文約30min。

上篇請參考:

Python高級編程—描述符Descriptor超詳細講解上

全文目錄

01 從python對象的__dict__屬性開始說起

1.1 類屬性or實例屬性

1.2 子類屬性or父類屬性

1.3 python對象的__dict__屬性

02 python屬性優先級的實驗驗證

2.1 簡單的實現代碼

03 python的“屬性攔截器”

3.1 __getattibute__魔術方法

3.2 __getattibute__方法的升級實現

3.3 __getattibute__方法的使用陷阱

3.4 __getattibute__方法實用總結

04 python高級編程——描述符descriptor(中篇)(預告

python描述符詳解(上篇)

python的描述符descriptor,這是屬於python高級編程的一些概念和實現方法,可能有很多的小夥伴還並沒有用到過,甚至每聽說過,但是在Python的面試過程中有可能會出現,究竟什麼是python描述符,有什麼作用,使用有什麼意義,它的誕生背景是什麼,很少有文章專門介紹這一塊,有的文章介紹的太過粗淺,以至於看過之後依然不能夠理解描述符的本質。鑑於此,我尋思著出一期專門講解python描述符的系列文章,跟前面的python裝飾器系列文章一樣,因為涉及到的內容偏多,本文依然是分為上、中、下、補充篇四個系列部分進行講解,本文是

第一篇——上篇,介紹Python的對象的屬性訪問優先級與對象屬性的簡單控制(屬性攔截器)。

01

從對象的__dict__屬性開始說起

聲明:本文所採用的面向對象設計並不是十分嚴格,所以很多可能覺得面向對象的設計不合理,本文僅僅藉助簡單的示例講清楚一些python的語言特性。

01 對象的__dict__屬性

1.1 類屬性or實例屬性?

我們都知道,在Python的面向對象裡面,他的靈活程度是比其他語言更加靈活的,為什麼這麼說呢?因為以下幾點:

(1)python面向對象中,類屬性,我既可以通過類名訪問,也可以通過實例訪問(這與C#不同,C#的靜態屬性只能夠通過類訪問,不能夠通過實例訪問)

(2)python中的類方法(@classmethod裝飾的方法),我也可以通過類名訪問,也可以通過實例訪問(這與C#中的靜態方法也不一樣)

我們可以通過一個例子來說明,代碼如下:

class Animal:

name='老虎'

def __init__(self,name,age):

self.name=name

self.age=age

def eat(self):

return '我需要吃東西!'

@classmethod

def sleep(cls):

return '我需要睡覺'

a=Animal('老虎',5) #構造對象

print(a.name)

print(a.age)

print(a.eat()) #實例訪問實例方法

print(a.sleep()) #實例也可以訪問類方法哦!

#----------------------------------

print(Animal.name)

print(Animal.sleep())

上面的運行結果為:

老虎

5

我需要吃東西!

我需要睡覺

老虎

我需要睡覺

細心的小夥伴應該發現了一個問題,在Animal中我定義了兩個屬性name,一個是類屬性、一個是實例屬性,這裡因為這二者的值都是“老虎”,我沒有辦法辨別,a.name 到底是輸出的類屬性還是實例屬性啊,如果我們改一下,改成如下:

a=Animal('獅子',5) #構造對象

print(a.name)

發現運行結果為:

獅子

得到的結論是,a.name 訪問的是實例屬性。那到底怎麼判斷呢?通過我的__dict__屬性去判斷。

01 對象的__dict__屬性

1.2 子類屬性or父類屬性?

先看一個例子,代碼如下:

class Animal:

name='老虎'

def __init__(self,name='老虎',age=5):

self.name=name

self.age=age

def eat(self):

return '我需要吃東西!'

@classmethod

def sleep(cls):

return '我需要睡覺'

class Dog(Animal):

#name_='小狗'

def __init__(self,age):

self.age=age

d=Dog(8)

print(d.name)

print(d.age)

運行結果為:

老虎 #說明子類繼承了父類的類屬性

8

這裡通過兩個簡單的例子,說明了

兩種關係

關係一:類成員與實例成員的關係(包括屬性和方法這兩種成員)

關係二:父類和子類的關係

那這二者具體的關係到底是怎麼樣的呢?先告訴你答案吧!

對於關係一而言,實例是可以訪問類的成員的,但是類不能訪問實例成員;對於關係二而言,子類繼承父類的類成員和實例成員。

01 對象的__dict__屬性

1.3 python對象的__dict__屬性

上面的兩個例子所反映出的實際上是一個“屬性的優先級問題”,即什麼樣的屬性優先訪問。

Python一切皆對象,故而都有__dict__屬性,包括對象實例、函數、類本身。我們可以通過對象的__dict__查看,它返回的是一個字典對象,對於下面的代碼,

class Animal:

name='老虎'

def __init__(self,name='老虎',age=5):

self.name=name

self.age=age

self.weight=200

def eat(self):

self.height=100

return '我需要吃東西!'

@classmethod

def sleep(cls):

return '我需要睡覺'

class Dog(Animal):

def __init__(self,age):

self.age=age

a=Animal()

d=Dog(8)

print(Animal.__dict__.keys())

print(a.__dict__.keys())

print(Dog.__dict__.keys())

print(d.__dict__.keys())

運行結果為:

dict_keys(['__module__', 'name', '__init__', 'eat', 'sleep', '__dict__', '__weakref__', '__doc__'])

dict_keys(['name', 'age', 'weight'])

dict_keys(['__module__', '__init__', '__doc__'])

dict_keys(['age'])

由此可見,實例a最先直接訪問的屬性就只有name,age,weight,並不包含類屬性name,我們可以這麼,name、age、weight是屬於a的,但是類屬性name是不屬於a的。

那麼,對象對屬性的訪問到底遵循怎樣一個優先級呢?先告訴你答案!

①.實例屬性

②.類屬性

③.父類的類屬性

④.__getattr__()方法(訪問一個不存在的屬性的時候發生的行為)

注意:這裡的優先級只是最基本的,沒有涉及到任何的屬性設置和屬性控制,後面還會講到屬性的優先級。

02

屬性優先級的實驗驗證

02 屬性優先級的實驗驗證

2.1 優先級的代碼實現

根據上面的描述,我們來做一個實驗,

class Animal:

name='老虎'

def __init__(self):

pass

class Dog(Animal):

age=10

def __init__(self,height):

self.height=height

def __getattr__(self,propertyname):

if propertyname=='weight':

return 200

else:

raise AttributeError

d=Dog(150)

print(d.height)

print(d.age)

print(d.name)

print(d.weight)

運行結果為:

150 #第一優先級訪問,訪問實例屬性

10 #第二優先級訪問,訪問類屬性

老虎 #第三優先級訪問,訪問父類的類屬性

200 #第四優先級訪問,如果這裡改成d.sex,則會拋出異常總結:對象屬性的訪問優先級順序為:①.實例屬性

②.類屬性

③.父類的類屬性

④.__getattr__()方法

注意:這裡的優先級只是暫時的哦!!!後面還會講到屬性的控制與代理,情況還會更加複雜一點。

總結

對象屬性的訪問優先級順序為:(這只是暫時的哦)

①.實例屬性

②.類屬性

③.父類的類屬性

④.__getattr__()方法

03

python的“屬性攔截器”

概述

我們經常看見下面的一些魔術方法,比如__getattribute__、__getattr__、__setattr__、__delattr__,那麼他們每一個函數代表什麼意思呢,在什麼樣的使用場景下使用呢?他們到底有什麼樣的作用,使用這些方法有什麼意義?本系列文章都將一一討論,但是,限於篇幅,本文只講第一個

__getattribute__函數。

03 python的屬性攔截器

3.1 __getattribute__魔術方法

當一個屬性被訪問的時候發生的行為,稱之為“屬性攔截器”。其實這是對屬性的一種高級控制,我們也常稱之為“為屬性綁定行為”

Python中只要定義了繼承object的類,就默認存在屬性攔截器,只不過是攔截後沒有進行任何操作,而是直接返回。所以我們可以自己改寫__getattribute__方法來實現相關功能,比如查看權限、打印log日誌等

Python的屬性訪問方式很直觀,使用點屬性運算符。在新式類中,對對象屬性的訪問,都會調用特殊方法__getattribute__。__getattribute__允許我們在訪問對象屬性時自定義訪問行為,但是使用它特別要小心無限遞歸的問題。

通過一個簡單的例子來說明:

class Animal(object):

run = '我會跑'

def die(self):

return '我會死'

class Dog(Animal):

color='Blue'

def __init__(self, name,age):

self.name=name

self.age = age

# 重寫__getattribute__。需要注意的是重寫的方法中不能

# 使用對象的點運算符訪問屬性,否則使用點運算符訪問屬性時,

# 會再次調用__getattribute__。這樣就會陷入無限遞歸。

# 可以使用super()方法避免這個問題。

def __getattribute__(self, key):

print('調用了__getattribute__屬性')

return super(Dog, self).__getattribute__(key)

def sound(self):

return "汪汪汪"

dog=Dog('泰迪',4)

print(dog.name,end='\n------------------------------\n') #調用實例屬性

print(dog.age,end='\n------------------------------\n')

print(dog.color,end='\n------------------------------\n') #調用方法屬性

print(dog.run,end='\n------------------------------\n') #調用父類的類屬性

print(dog.sound(),end='\n------------------------------\n') #調用實例方法

print(dog.die(),end='\n------------------------------\n') #調用父類的方法

運行結果為:

調用了__getattribute__屬性

泰迪

------------------------------

調用了__getattribute__屬性

4

------------------------------

調用了__getattribute__屬性

Blue

------------------------------

調用了__getattribute__屬性

我會跑

------------------------------

調用了__getattribute__屬性

汪汪汪

------------------------------

調用了__getattribute__屬性

我會死

------------------------------

從上面的例子中,我們可以看出,不管是訪問對象的實例屬性、類屬性、父類的類屬性、對象方法、父類方法,總是需要

先過__getattribute__著一關的,即如果定義了__getattribute__魔法方法,他總是優先調用,要不怎麼稱之為“屬性攔截器”呢。

03 python的屬性攔截器

3.2 __getattribute__方法的升級實現

事實上,關於__getattribute__的重寫,並不是一定要按照上面的格式,他可以有更加靈活的方式去實現的,上面的攔截方式太簡單了,不管我做什麼都是打印同樣一句話,這很沒特點,我要使得調用不同的屬性,顯示不同的信息該怎麼做呢?比如我也可以這樣,將上面的__getattribute__函數體重新定義如下:

def __getattribute__(self, key):

if key=='name':

print('name屬性被調用了')

return super(Dog, self).__getattribute__(key)

elif key=='age':

print('age屬性被調用了')

return super(Dog, self).__getattribute__(key)

elif key=='color':

print('color屬性被調用了')

return super(Dog, self).__getattribute__(key)

elif key=='run':

print('run屬性被調用了')

return super(Dog, self).__getattribute__(key)

elif key=='sound':

print('sound方法被調用了')

return super(Dog, self).__getattribute__(key)

elif key=='die':

print('die方法屬性被調用了')

return super(Dog, self).__getattribute__(key)

else:

print('調用了__getattribute__屬性')

return super(Dog, self).__getattribute__(key)

def sound(self):

return "汪汪汪"

dog=Dog('泰迪',4)

print(dog.name,end='\n------------------------------\n') #調用實例屬性

print(dog.age,end='\n------------------------------\n')

print(dog.color,end='\n------------------------------\n') #調用方法屬性

print(dog.run,end='\n------------------------------\n') #調用父類的類屬性

print(dog.sound(),end='\n------------------------------\n') #調用實例方法

print(dog.die(),end='\n------------------------------\n') #調用父類的方法

print(dog.height,end='\n------------------------------\n') #調用一個不存在的屬性

運行結果為:

name屬性被調用了

泰迪

------------------------------

age屬性被調用了

4

------------------------------

color屬性被調用了

Blue

------------------------------

run屬性被調用了

我會跑

------------------------------

sound方法被調用了

汪汪汪

------------------------------

die方法屬性被調用了

我會死

------------------------------

調用了__getattribute__屬性

Traceback (most recent call last):顯示沒有定義height屬性。

注意:沒一個條件分支一定要返回父類的__getattribute__方法哦,如果沒有返回,會發現雖然打印了相關的提示信息,但是並沒有返回屬性或方法的值,而是返回的一個None。

03 python的屬性攔截器

3.3 __getattribute__魔術方法的使用陷阱

def __getattribute__(self, key):

if key=='sound':

print('sound方法被調用了')

return self.sound()

else:

print('調用了__getattribute__屬性')

return super(Dog, self).__getattribute__(key)

def sound(self):

return "汪汪汪"

比如改成如上代碼:上面的代碼運行並不會出現預期的結果,即

sound方法被調用了

汪汪汪

為什麼呢?因為調用self.sound()方法的時候一定會先調用__getattribute__方法,然後__getattribute__裡面又調用sound方法,然後又調用__getattribute__方法。。。。。。如此遞歸循環下去,而且沒有退出機制,直到程序崩潰。

03 python的屬性攔截器

3.4 __getattribute__方法的實用總結

(1)一定要在每一個需要訪問的屬性裡面設置返回值,否則會返回None,一般有兩種做法:

即:return super(Dog, self).__getattribute__(key)這種形式或:return object.__getattribute__(self,key)

即返回父類的__getattribute__方法。

(2)不要再在__getattribute__方法的定義內部顯示使用self.成員 這種方法,這樣可能會造成無限遞歸,導致系統崩潰。

04

“描述符”下篇預告

下一篇預告:

下面篇文章的重點是python類中屬性訪問、修改、刪除的控制方法:

限於篇幅,關於python高級編程之描述符descriptor系列文章的第一篇預熱就到這裡,在本文還沒有真正涉及到python的描述符,按照我一貫的風格,這裡只是介紹python面向對象中屬性的訪問優先級,屬性的控制等基礎知識,關於python描述符,歡迎繼續關注哦!

有興趣的小夥伴持續關注哦!

Python高級編程—描述符Descriptor超詳細講解上

關注轉發文章私信老師(學習)獲取!


分享到:


相關文章: