當前位置:
首頁 > 知識 > Python 中的屬性訪問與描述符

Python 中的屬性訪問與描述符

(點擊

上方藍字

,快速關注我們)




來源:Learn Python


fanchunke.me/Python/Python中的屬性訪問與描述符


如有好文章投稿,請點擊 → 這裡了解詳情




在Python中,對於一個對象的屬性訪問,我們一般採用的是點(.)屬性運算符進行操作。例如,有一個類實例對象foo,它有一個name屬性,那便可以使用foo.name對此屬性進行訪問。一般而言,點(.)屬性運算符比較直觀,也是我們經常碰到的一種屬性訪問方式。然而,在點(.)屬性運算符的背後卻是別有洞天,值得我們對對象的屬性訪問進行探討。



在進行對象屬性訪問的分析之前,我們需要先了解一下對象怎麼表示其屬性。為了便於說明,本文以新式類為例。有關新式類和舊式類的區別,大家可以查看Python官方文檔。




對象的屬性




Python中,「一切皆對象」。我們可以給對象設置各種屬性。先來看一個簡單的例子:





class

Animal

(

object

)

:


    

run

=

True


class

Dog

(

Animal

)

:


    

fly

=

False


    

def

__init__

(

self

,

age

)

:

        

self

.

age

=

age


    

def

sound

(

self

)

:


        

return

"wang wang~"




上面的例子中,我們定義了兩個類。類Animal定義了一個屬性run;類Dog繼承自Animal,定義了一個屬性fly和兩個函數。接下來,我們實例化一個對象。對象的屬性可以從特殊屬性__dict__中查看。





# 實例化一個對象dog


>>>

dog

=

Dog

(

1

)


# 查看dog對象的屬性


>>>

dog

.

__dict_

_


{

"age"

:

1

}


# 查看類Dog的屬性


>>>

Dog

.

__dict__


dict_proxy

({

"__doc__"

:

None

,


            

"__init__"

: <

function __main__

.

__init__

>

,


            

"__module__"

:

"__main__"

,


            

"fly"

:

False

,


            

"sound"

: <

function __main__

.

sound

>

})


# 查看類Animal的屬性


>>>

Animal

.

__dict__


dict_proxy

({

"__dict__"

: <

attribute

"__dict__"

of

"Animal"

objects

>

,


            

"__doc__"

:

None

,


            

"__module__"

:

"__main__"

,


            

"__weakref__"

: <

attribute

"__weakref__"

of

"Animal"

objects

>

,


            

"run"

:

True

})




由上面的例子可以看出:屬性在哪個對象上定義,便會出現在哪個對象的__dict__中。例如:






  • 類Animal定義了一個屬性run,那這個run屬性便只會出現在類Animal的__dict__中,而不會出現在其子類中。



  • 類Dog定義了一個屬性fly和兩個函數,那這些屬性和方法便會出現在類Dog的__dict__中,同時它們也不會出現在實例的__dict__中。



  • 實例對象dog的__dict__中只出現了一個屬性age,這是在初始化實例對象的時候添加的,它沒有父類的屬性和方法。



  • 由此可知:Python中對象的屬性具有 「層次性」,屬性在哪個對象上定義,便會出現在哪個對象的__dict__中。




在這裡我們首先了解的是屬性值會存儲在對象的__dict__中,查找也會在對象的__dict__中進行查找的。至於Python對象進行屬性訪問時,會按照怎樣的規則來查找屬性值呢?這個問題在後文中進行討論。




對象屬性訪問與特殊方法__getattribute__




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




還是以上面的情景為例:





class

Animal

(

object

)

:


    

run

=

True


class

Dog

(

Animal

)

:


    

fly

=

False


    

def

__init__

(

self

,

age

)

:


        

self

.

age

=

age


    

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


    

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


    

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


    

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


    

def

__getattribute__

(

self

,

key

)

:


        

print

  

"calling __getattribute__
"


        

return

super

(

Dog

,

self

).

__getattribute__

(

key

)


    

def

sound

(

self

)

:


        

return

"wang wang~"




上面的例子中我們重寫了__getattribute__方法。注意我們使用了super()方法來避免無限循環問題。下面我們實例化一個對象來說明訪問對象屬性時__getattribute__的特性。





# 實例化對象dog


>>>

dog

=

Dog

(

1

)


# 訪問dog對象的age屬性


>>>

dog

.

age


calling

__getattribute_

_


1


# 訪問dog對象的fly屬性


>>>

dog

.

fly


calling __getattribute__


False


# 訪問dog對象的run屬性


>>>

dog

.

run


calling __getattribute__


True


# 訪問dog對象的sound方法


>>>

dog

.

sound


calling

__getattribute__


<

bound method

Dog

.

sound

of

<

__main__

.

Dog object

at

0x0000000005A90668

>>




由上面的驗證可知,__getattribute__是實例對象查找屬性或方法的入口。實例對象訪問屬性或方法時都需要調用到__getattribute__,之後才會根據一定的規則在各個__dict__中查找相應的屬性值或方法對象,若沒有找到則會調用__getattr__(後面會介紹到)。__getattribute__是Python中的一個內置方法,關於其底層實現可以查看相關官方文檔,後面將要介紹的屬性訪問規則就是依賴於__getattribute__的。




對象屬性控制




在繼續介紹後面相關內容之前,讓我們先來了解一下Python中和對象屬性控制相關的相關方法。






  • __getattr__(self, name)__getattr__可以用來在當用戶試圖訪問一個根本不存在(或者暫時不存在)的屬性時,來定義類的行為。前面講到過,當__getattribute__方法找不到屬性時,最終會調用__getattr__方法。它可以用於捕捉錯誤的以及靈活地處理AttributeError。只有當試圖訪問不存在的屬性時它才會被調用。



  • __setattr__(self, name, value)__setattr__方法允許你自定義某個屬性的賦值行為,不管這個屬性存在與否,都可以對任意屬性的任何變化都定義自己的規則。關於__setattr__有兩點需要說明:第一,使用它時必須小心,不能寫成類似self.name = 「Tom」這樣的形式,因為這樣的賦值語句會調用__setattr__方法,這樣會讓其陷入無限遞歸;第二,你必須區分 對象屬性 和 類屬性 這兩個概念。後面的例子中會對此進行解釋。



  • __delattr__(self, name)__delattr__用於處理刪除屬性時的行為。和__setattr__方法要注意無限遞歸的問題,重寫該方法時不要有類似del self.name的寫法。




還是以上面的例子進行說明,不過在這裡我們要重寫三個屬性控制方法。





class

Animal

(

object

)

:


    

run

=

True


class

Dog

(

Animal

)

:


    

fly

=

False


    

def

__init__

(

self

,

age

)

:


        

self

.

age

=

age


    

def

__getattr__

(

self

,

name

)

:


        

print

"calling __getattr__
"


        

if

name

==

"adult"

:


            

return

True

if

self

.

age

>=

2

else

False


        

else

:


            

raise

AttributeError


    

def

__setattr__

(

self

,

name

,

value

)

:


        

print

"calling __setattr__"


        

super

(

Dog

,

self

).

__setattr__

(

name

,

value

)


    

def

__delattr__

(

self

,

name

)

:


        

print

"calling __delattr__"


        

super

(

Dog

,

self

).

__delattr__

(

name

)




以下進行驗證。首先是__getattr__:





# 創建實例對象dog


>>>

dog

=

Dog

(

1

)


calling

__setattr__


# 檢查一下dog和Dog的__dict__


>>>

dog

.

__dict_

_


{

"age"

:

1

}


>>>

Dog

.

__dict__


dict_proxy

({

"__delattr__"

: <

function __main__

.

__delattr__

>

,


            

"__doc__"

:

None

,


            

"__getattr__"

: <

function __main__

.

__getattr__

>

,


            

"__init__"

: <

function __main__

.

__init__

>

,


            

"__module__"

:

"__main__"

,


            

"__setattr__"

: <

function __main__

.

__setattr__

>

,


            

"fly"

:

False

})


# 獲取dog的age屬性


>>>

dog

.

age


1


# 獲取dog的adult屬性。


# 由於__getattribute__沒有找到相應的屬性,所以調用__getattr__。


>>>

dog

.

adult


calling __getattr__


False


# 調用一個不存在的屬性name,__getattr__捕獲AttributeError錯誤


>>>

dog

.

name


calling __getattr__


Traceback

(

most recent call

last

)

:


  

File

"<stdin>"

,

line

1

,

in

<

module

>


  

File

"<stdin>"

,

line

10

,

in

__getattr__


AttributeError




可以看到,屬性訪問時,當訪問一個不存在的屬性時觸發__getattr__,它會對訪問行為進行控制。接下來是__setattr__:





# 給dog.age賦值,會調用__setattr__方法


>>>

dog

.

age

=

2


calling

__setattr__


>>>

dog

.

age


2


# 先調用dog.fly時會返回False,這時因為Dog類屬性中有fly屬性;


# 之後再給dog.fly賦值,觸發__setattr__方法。


>>>

dog

.

fly


False


>>>

dog

.

fly

=

True


calling

__setattr__


# 再次查看dog.fly的值以及dog和Dog的__dict__;


# 可以看出對dog對象進行賦值,會在dog對象的__dict__中添加了一條對象屬性;


# 然而,Dog類屬性沒有發生變化


# 注意:dog對象和Dog類中都有fly屬性,訪問時會選擇哪個呢?


>>>

dog

.

fly


True


>>>

dog

.

__dict_

_


{

"age"

:

2

,

"fly"

:

True

}


>>>

Dog

.

__dict__


dict_proxy

({

"__delattr__"

: <

function __main__

.

__delattr__

>

,


            

"__doc__"

:

None

,


            

"__getattr__"

: <

function __main__

.

__getattr__

>

,


            

"__init__"

: <

function __main__

.

__init__

>

,


            

"__module__"

:

"__main__"

,


            

"__setattr__"

: <

function __main__

.

__setattr__

>

,


            

"fly"

:

False

})




實例對象的__setattr__方法可以定義屬性的賦值行為,不管屬性是否存在。當屬性存在時,它會改變其值;當屬性不存在時,它會添加一個對象屬性信息到對象的__dict__中,然而這並不改變類的屬性。從上面的例子可以看出來。




最後,看一下__delattr__:





# 由於上面的例子中我們為dog設置了fly屬性,現在刪除它觸發__delattr__方法


>>>

del

dog

.

fly


calling

__delattr__


# 再次查看dog對象的__dict__,發現和fly屬性相關的信息被刪除


>>>

dog

.

__dict_

_


{

"age"

:

2

}




描述符




描述符是Python 2.2 版本中引進來的新概念。描述符一般用於實現對象系統的底層功能, 包括綁定和非綁定方法、類方法、靜態方法特特性等。關於描述符的概念,官方並沒有明確的定義,可以在網上查閱相關資料。這裡我從自己的認識談一些想法,如有不當之處還請包涵。




在前面我們了解了對象屬性訪問和行為控制的一些特殊方法,例如__getattribute__、__getattr__、__setattr__、__delattr__。以我的理解來看,這些方法應當具有屬性的」普適性」,可以用於屬性查找、設置、刪除的一般方法,也就是說所有的屬性都可以使用這些方法實現屬性的查找、設置、刪除等操作。但是,這並不能很好地實現對某個具體屬性的訪問控制行為。例如,上例中假如要實現dog.age屬性的類型設置(只能是整數),如果單單去修改__setattr__方法滿足它,那這個方法便有可能不能支持其他的屬性設置。




在類中設置屬性的控制行為不能很好地解決問題,Python給出的方案是:__getattribute__、__getattr__、__setattr__、__delattr__等方法用來實現屬性查找、設置、刪除的一般邏輯,而對屬性的控制行為就由屬性對象來控制。這裡單獨抽離出來一個屬性對象,在屬性對象中定義這個屬性的查找、設置、刪除行為。這個屬性對象就是描述符。




描述符對象一般是作為其他類對象的屬性而存在。在其內部定義了三個方法用來實現屬性對象的查找、設置、刪除行為。這三個方法分別是:






  • get(self, instance, owner):定義當試圖取出描述符的值時的行為。



  • set(self, instance, value):定義當描述符的值改變時的行為。



  • delete(self, instance):定義當描述符的值被刪除時的行為。




其中:instance為把描述符對象作為屬性的對象實例;


owner為instance的類對象。




以下以官方的一個例子進行說明:





class

RevealAccess

(

object

)

:


    

def

__init__

(

self

,

initval

=

None

,

name

=

"var"

)

:


        

self

.

val

=

initval


        

self

.

name

=

name


    

def

__get__

(

self

,

obj

,

objtype

)

:


        

print

"Retrieving"

,

self

.

name


        

return

self

.

val


    

def

__set__

(

self

,

obj

,

val

)

:


        

print

"Updating"

,

self

.

name


        

self

.

val

=

val


class

MyClass

(

object

)

:


    

x

=

RevealAccess

(

10

,

"var "x""

)


    

y

=

5




以上定義了兩個類。其中RevealAccess類的實例是作為MyClass類屬性x的值存在的。而且RevealAccess類定義了__get__、__set__方法,它是一個描述符對象。注意,描述符對象的__get__、__set__方法中使用了諸如self.val和self.val = val等語句,這些語句會調用__getattribute__、__setattr__等方法,這也說明了__getattribute__、__setattr__等方法在控制訪問對象屬性上的一般性(一般性是指對於所有屬性它們的控制行為一致),以及__get__、__set__等方法在控制訪問對象屬性上的特殊性(特殊性是指它針對某個特定屬性可以定義不同的行為)。




以下進行驗證:





# 創建Myclass類的實例m


>>>

m

=

MyClass

()


# 查看m和MyClass的__dict__


>>>

m

.

__dict_

_


{}


>>>

MyClass

.

__dict__


dict_proxy

({

"__dict__"

: <

attribute

"__dict__"

of

"MyClass"

objects

>

,


            

"__doc__"

:

None

,


            

"__module__"

:

"__main__"

,


            

"__weakref__"

: <

attribute

"__weakref__"

of

"MyClass"

objects

>

,


            

"x"

: <

__main__

.

RevealAccess

at

0x5130080

>

,


            

"y"

:

5

})


# 訪問m.x。會先觸發__getattribute__方法


# 由於x屬性的值是一個描述符,會觸發它的__get__方法


>>>

m

.

x


Retrieving

var

"x"


10


# 設置m.x的值。對描述符進行賦值,會觸發它的__set__方法


# 在__set__方法中還會觸發__setattr__方法(self.val = val)


>>>

m

.

x

=

20


Updating

var

"x"


# 再次訪問m.x


>>>

m

.

x


Retrieving

var

"x"


20


# 查看m和MyClass的__dict__,發現這與對描述符賦值之前一樣。


# 這一點與一般屬性的賦值不同,可參考上述的__setattr__方法。


# 之所以前後沒有發生變化,是因為變化體現在描述符對象上,


# 而不是實例對象m和類MyClass上。


>>>

m

.

__dict_

_


{}


>>>

MyClass

.

__dict__


dict_proxy

({

"__dict__"

: <

attribute

"__dict__"

of

"MyClass"

objects

>

,


            

"__doc__"

:

None

,


            

"__module__"

:

"__main__"

,


            

"__weakref__"

: <

attribute

"__weakref__"

of

"MyClass"

objects

>

,


            

"x"

: <

__main__

.

RevealAccess

at

0x5130080

>

,


            

"y"

:

5

})




上面的例子對描述符進行了一定的解釋,不過對描述符還需要更進一步的探討和分析,這個工作先留待以後繼續進行。




最後,還需要注意一點:描述符有數據描述符和非數據描述符之分。






  • 只要至少實現__get__、__set__、__delete__方法中的一個就可以認為是描述符;



  • 只實現__get__方法的對象是非數據描述符,意味著在初始化之後它們只能被讀取;



  • 同時實現__get__和__set__的對象是數據描述符,意味著這種屬性是可讀寫的。




屬性訪問的優先規則




在以上的討論中,我們一直迴避著一個問題,那就是屬性訪問時的優先規則。我們了解到,屬性一般都在__dict__中存儲,但是在訪問屬性時,在對象屬性、類屬型、基類屬性中以怎樣的規則來查詢屬性呢?以下對Python中屬性訪問的規則進行分析。




由上述的分析可知,屬性訪問的入口點是__getattribute__方法。它的實現中定義了Python中屬性訪問的優先規則。Python官方文檔中對__getattribute__的底層實現有相關的介紹,本文暫時只是討論屬性查找的規則,相關規則可見下圖:







上圖是查找b.x這樣一個屬性的過程。在這裡要對此圖進行簡單的介紹:






  1. 查找屬性的第一步是搜索基類列表,即type(b).__mro__,直到找到該屬性的第一個定義,並將該屬性的值賦值給descr;



  2. 判斷descr的類型。它的類型可分為數據描述符、非數據描述符、普通屬性、未找到等類型。若descr為數據描述符,則調用desc.__get__(b, type(b)),並將結果返回,結束執行。否則進行下一步;



  3. 如果descr為非數據描述符、普通屬性、未找到等類型,則查找實例b的實例屬性,即b.__dict__。如果找到,則將結果返回,結束執行。否則進行下一步;



  4. 如果在b.__dict__

    未找到相關屬性,則重新回到

    descr

    值的判斷上。




    1. 若descr為非數據描述符,則調用desc.__get__(b, type(b)),並將結果返回,結束執行;



    2. 若descr為普通屬性,直接返回結果並結束執行;



    3. 若descr為空(未找到),則最終拋出 AttributeError 異常,結束查找。




看完本文有收穫?請轉

發分享給更多人


關注「P

ython開發者」,提升Python技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發者 的精彩文章:

Python 源碼閱讀:對象
構建多層感知器神經網路對數字圖片進行文本識別
Gevent 調度流程解析
為什麼你應該學 Python ?
入門 Python 要多久?

TAG:Python開發者 |