Python中的Descriptor描述符學(xué)習(xí)教程
Descriptor是什么?簡而言之,Descriptor是用來定制訪問類或?qū)嵗某蓡T的一種協(xié)議。額。。好吧,一句話是說不清楚的。下面先介紹一下Python中成員變量的定義和使用。
我們知道,在Python中定義類成員和C/C++相比得到的結(jié)果具有很大的差別。如下面的定義:
class Cclass { int I; void func(); }; Cclass c;
在上面的定義中,C++定義了一個類型,所有該類型的對象都包含有一個成員整數(shù)i和函數(shù)func;而Python則創(chuàng)建了一個名為Pclass、類型(__class__)為type(詳情請參見MetaClass,Python中一切皆為對象,類型也不例外)的對象,然后再創(chuàng)建一個名為p、類型為Pclass的對象。如下所示:
In [71]: type(pclass) Out[71]: <type 'type'> In [72]: p = pclass() In [73]: type(p) Out[73]: <class '__main__.pclass'>
p和Pclass各自包含了一些成員,如下所示:
1 p.__class__ p.__init__ p.__sizeof__
2 p.__delattr__ p.__module__ p.__str__
3 p.__dict__ p.__new__ p.__subclasshook__
4 p.__doc__ p.__reduce__ p.__weakref__
5 p.__format__ p.__reduce_ex__ p.f
6 p.__getattribute__ p.__repr__ p.i
7 p.__hash__ p.__setattr__
其中,帶有雙下劃線的成員為特殊成員,或者可以稱之為固定成員(和__slots__定義的成員類似),這些成員變量的值可以被改變,但不能被刪除(del)。其中,__class__變量為對象所屬的類型,__doc__為對象的文檔字符串。有一個特殊成員值得注意:__dict__,該字典中保存了對象的自定義變量。相信大家在初學(xué)Python對于其中對象可以任意增加刪除成員變量的能力感到驚訝,其實這個功能的玄機(jī)就在于__dict__成員中(注意type的__dict__為dictproxy類型):
In [10]: p.x = 2 In [11]: p.__dict__ Out[11]: {'x': 2}
通過上面的演示可以很清楚地看出:Python將對象的自定義成員以鍵值對的形式保存到__dict__字典中,而前面提到的類型定義只是這種情況的語法糖而已,即上面的類型定義等價于以下形式的定義:
Class Pclass(object): pass Pclass.i = 1 Pclass.f = lambda x: x
訪問成員變量時,Python也是從__dict__字典中取出變量名對應(yīng)的值,如下形式的兩種訪問形式是等價的——在Descriptor被引入之前:
p.i p.__dict__['i']
Descriptor的引入即將改變上面的規(guī)則,且看下文分解。
定義:Descriptor Protocol
Descriptor如何改變對象成員的訪問規(guī)則呢?根據(jù)計算機(jī)理論中“絕大多數(shù)軟件問題都可以用增加一個中間層的方式解決”這一名言,我們需要為對象訪問提供一個中間層,而非直接訪問所需的對象。實現(xiàn)這一中間層的方式是定義Descriptor協(xié)議。Descriptor的定義很簡單,如果一個類包含以下三個方法(之一),則可以稱之為一個Descriptor:
1.object.__get__(self, instance, owner)
成員被訪問時調(diào)用,instance為成員所屬的對象、owner為instance所屬的類型
2.object.__set__(self, instance, value)
成員被賦值時調(diào)用
3.0object.__delete__(self, instance)
成員被刪除時調(diào)用
如果我們需要改變一個對象在其它對象中的訪問規(guī)則,需要將其定義成Descriptor,之后在對該成員進(jìn)行訪問時將調(diào)用該Descriptor的相應(yīng)函數(shù)。下面是一個使用Descriptor改變訪問規(guī)則的例子:
class MyDescriptor(object): def __init__(self, x): self.x = x def __get__(self, instance, owner): print 'get from descriptor' return self.x def __set__(self, instance, value): print 'set from descriptor' self.x = value def __delete__(self, instance) print 'del from descriptor, the val is', self.x class C(object): d = MyDescriptor('hello') >> C.d get from descriptor >> c = C() >> c.d get from descriptor >> c.d = 1 set from descriptor >> del c.d del from descriptor, the val is 1
從例子中可以看出:當(dāng)我們對對象成員進(jìn)行引用(Reference)、賦值(Assign)和刪除(Dereference)操作時,如果對象成員為一個Descriptor,則這些操作將執(zhí)行該Descriptor對象的相應(yīng)成員函數(shù)。以上約定即為Descriptor協(xié)議。
obj.name背后的魔法
引入了Descriptor之后,Python對于對象成員訪問的規(guī)則是怎樣的呢?在回答這一問題之前,需要對Descriptor進(jìn)行簡單的劃分:
Overriding或Data:對象同時提供了__get__和__set__方法
Nonoverriding或Non-Data:對象僅提供了__get__方法
(__del__方法表示自己被忽略了,很傷心~)
下面是從一個類對象中訪問其成員(如C.name)的規(guī)則:
如果“name”在C.__dict__能找到,C.name將訪問C.__dict__['name'],假設(shè)為v。如果v是一個Descriptor,則返回type(v).__get__(v, None, C),否則直接返回v;
如果“name”不在C.__dict__中,則向上查找C的父類,根據(jù)MRO(Method Resolution Order)對C的父類重復(fù)第一步;
還是沒有找到“name”,拋出AttributeError異常。
從一個類實例對象中訪問其成員(如x.name,type(x)為C)要稍微復(fù)雜一些:
如果“name”能在C(或C的父類)中找到,且其值v為一個Overriding Descriptor,則返回type(v).__get__(v, x, C)的值;
否則,如果“name”能在x.__dict__中找到,則返回x.__dict__['name']的值;
如果“name”仍未找到,則執(zhí)行類對象成員的查找規(guī)則;
如果C定義了__getattr__函數(shù),則調(diào)用該函數(shù);否則拋出AttributeError異常。
成員賦值的查找規(guī)則與訪問規(guī)則類似,但還是有一點區(qū)別:對類成員執(zhí)行賦值操作時將直接設(shè)置C.__dict__中的值,而不會調(diào)用Descriptor的__set__函數(shù)。
以上面的代碼為例,當(dāng)訪問C.d時,Python將在C.__dict__中找到d,并且發(fā)現(xiàn)d是一個Descriptor,因此將調(diào)用d.__get__(None, C);當(dāng)訪問c.d時,Python首先查找C,并且在其中發(fā)現(xiàn)d的定義,且d為一個Overriding Descriptor,因此執(zhí)行d.__get__(c, C)。
前面介紹了Descriptor的一些細(xì)節(jié),那么Descriptor的作用是什么呢?在Python中,Descriptor主要用來實現(xiàn)一些Python本身的功能,如類方法調(diào)用、staticmethod和Property等。下面將對這些使用Descriptor進(jìn)行類方法調(diào)用的實現(xiàn)進(jìn)行介紹。
Bound & Unbound Method
在python中,函數(shù)是第一級的對象,即其本質(zhì)與其它對象相同,差別在于函數(shù)對象是callable對象,即對于函數(shù)對象f,可以用語法f()來調(diào)用函數(shù)。上面提到的對象成員訪問規(guī)則,對于函數(shù)來說是完全一樣的。Python在實現(xiàn)成員函數(shù)調(diào)用時obj.f()時,會執(zhí)行一下兩個步驟:
根據(jù)對象成員訪問規(guī)則獲取函數(shù)對象;
用函數(shù)對象執(zhí)行函數(shù)調(diào)用;
為了驗證上述過程,我們可以執(zhí)行以下代碼:
Class C(object): def f(self): pass >> fun = C.f Unbound Method >> fun() >> c = C() >> fun = c.f Bound Method >> fun()
我們可以看到C.f和c.f返回了instancemethod類型的對象,這兩個對象也是可調(diào)用的,但是卻不是我們本以為的func對象。那么instancemethod對象和func對象之間具有什么關(guān)聯(lián)呢?
func類型:func類型為Python中原始的函數(shù)對象類型,即def f(): pass將定義一個func類型的對象f;
instancemethod:func的一個wrapper,如果類方法沒有綁定到對象,則該instancemethod為一個Unbound Method,對Unbound Method的調(diào)用將導(dǎo)致TypeError錯誤;如果類方法綁定到了對象,則該instancemethod為一個Bound Method,對Bound Method的調(diào)用不許要指定self參數(shù)的值。
如果查看Unbound Method對象和Bound Method對象的成員,我們可以發(fā)現(xiàn)它們都包含了一下三個成員:im_func、im_self和im_class。其中im_func為所封裝的func對象,im_self則為所綁定對象的值,而im_class則為定義該函數(shù)的類對象。由此我們可以知道,Python會根據(jù)不同的情況返回函數(shù)的不同wrapper,當(dāng)通過類對象訪問函數(shù)時,返回的是名為Unbound Method對象的Wrapper,而通過類實例訪問函數(shù)是,返回的則是綁定了該實例的名為Bound Method對象的Wrapper。
現(xiàn)在是Descriptor大顯身手的時候了。
Python中將func定義為一個Overriding Descriptor,在其__get__方法中構(gòu)造一個instancemethod對象,并根據(jù)被訪問函數(shù)被訪問的情況設(shè)置im_func、im_self和im_class成員。在instancemethod實例被調(diào)用時,則根據(jù)im_func和im_self來完成真正的函數(shù)調(diào)用。演示這一過程的代碼如下:
Class instancemethod(object): def __call__(self, *args): if self.im_self == None: raise 'unbound error' return self.im_func(self.im_self, *args) def __init__(self, im_self, im_func, im_class): self.im_self = im_self self.im_func = im_func self.im_class = im_class class func(object): ... def __get__(self, instance, owner): return instancemethod(instance, self, owner) def __set__(self, instance, value): pass ...
一個小問題的解決
分享一下剛遇到的一個小問題,我有一段類似于這樣的python代碼:
# coding: utf-8 class A(object): @property def _value(self): # raise AttributeError("test") return {"v": "This is a test."} def __getattr__(self, key): print "__getattr__:", key return self._value[key] if __name__ == '__main__': a = A() print a.v
運行后可以得到正確的結(jié)果
__getattr__: v This is a test.
但是注意,如果把
# raise AttributeError("test")
這行的注釋去掉的話,即在_value方法里面拋出AttributeError異常,事情就會變得有些奇怪。程序運行的時候并不會拋出異常,而是會進(jìn)入一個無限遞歸:
File "attr_test.py", line 12, in __getattr__ return self._value[key] File "attr_test.py", line 12, in __getattr__ return self._value[key] RuntimeError: maximum recursion depth exceeded while calling a Python object
通過多方查找后發(fā)現(xiàn)是property裝飾器的問題,property實際上是一個descriptor。在python doc中可以發(fā)現(xiàn)這樣的文字:
object.__get__(self, instance, owner)
Called to get the attribute of the owner class (class attribute access) or of an instance of that class (instance attribute access). owner is always the owner class, while instance is the instance that the attribute was accessed through, or None when the attribute is accessed through the owner. This method should return the (computed) attribute value or raise an AttributeError exception.
這樣當(dāng)用戶訪問._value時,拋出了AttributeError從而調(diào)用了__getattr__方法去嘗試獲取。這樣程序就變成了無限遞歸。
這個問題看上去不復(fù)雜,但是當(dāng)你的_value方法是比較隱晦的拋出AttributeError的話,調(diào)試起來就會比較困難了。
小結(jié)
Descriptor是訪問對象成員時的一個中間層,為我們提供了自定義對象成員訪問的方式。通過對Descriptor的探索,對原來的一些看似神秘的概念頓時有種豁然開朗的感覺:
類方法調(diào)用:編譯器并沒有為其提供專門的語法規(guī)則,而是使用Descriptor返回instancemethod來封裝func,從而實現(xiàn)類似obj.func()的調(diào)用方式;
staticmethod:decorator將創(chuàng)建一個StaticMethod并在其中保存func對象,StaticMethod是一個Descriptor,其__get__函數(shù)中返回前面所保存的func對象;
Property:創(chuàng)建一個Property對象,在其__get__、__set__和__delete__方法中分別執(zhí)行構(gòu)造對象是傳入的fget、fset、和fdel函數(shù)。現(xiàn)在知道為什么Property只提供這三個函數(shù)作為參數(shù)么。。
最后一個問題是,Python引入Descriptor之后的性能會不會有影響?性能影響是必須的:每次訪問成員時的查找規(guī)則,之后再調(diào)用Descriptor的__get__函數(shù),如果是方法調(diào)用的話之后才是執(zhí)行真正的函數(shù)調(diào)用。每次訪問對象成員時都要經(jīng)歷以上過程,對Python的性能應(yīng)該會有較大的影響。但是,在Python的世界,貌似Pythonic才是被關(guān)注的重點,性能神馬的就別提了。。
- Python descriptor(描述符)的實現(xiàn)
- Python描述符descriptor使用原理解析
- 通過實例解析python描述符原理作用
- python實現(xiàn)裝飾器、描述符
- Python 描述符(Descriptor)入門
- Python中屬性和描述符的正確使用
- 詳解Python中的Descriptor描述符類
- Python黑魔法Descriptor描述符的實例解析
- Python 的描述符 descriptor詳解
- 解密Python中的描述符(descriptor)
- Python中的類與對象之描述符詳解
- python的描述符(descriptor)、裝飾器(property)造成的一個無限遞歸問題分享
- Python基礎(chǔ)詳解之描述符
相關(guān)文章
python tkinter圖形界面代碼統(tǒng)計工具
這篇文章主要為大家詳細(xì)介紹了python tkinter圖形界面代碼統(tǒng)計工具,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-09-09如何將Python字符串轉(zhuǎn)換為JSON的實現(xiàn)方法
在本教程中,你將學(xué)習(xí)JSON的基礎(chǔ)知識,它是什么,常用在哪里以及它的語法,還將看到如何在Python中將字符串轉(zhuǎn)換為JSON,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12openCV入門學(xué)習(xí)基礎(chǔ)教程第一篇
OpenCV是計算機(jī)視覺領(lǐng)域一個圖像和視頻處理庫,用于各種圖像和視頻分析,如面部識別和檢測,車牌閱讀,照片編輯,高級機(jī)器人視覺,光學(xué)字符識別等等,下面這篇文章主要給大家介紹了關(guān)于openCV入門學(xué)習(xí)基礎(chǔ)教程第一篇的相關(guān)資料,需要的朋友可以參考下2022-11-11Python數(shù)據(jù)類型轉(zhuǎn)換匯總
這篇文章主要給大家分享的是Python數(shù)據(jù)類型轉(zhuǎn)換匯總,int,float相互轉(zhuǎn)換、int,string相互轉(zhuǎn)換、float,string相互轉(zhuǎn)換、string,list相互轉(zhuǎn)換等常見內(nèi)容,需要的小伙伴可以參考一下2022-03-03Python使用pandasai實現(xiàn)數(shù)據(jù)分析
本文主要介紹了Python使用pandasai實現(xiàn)數(shù)據(jù)分析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06Python機(jī)器學(xué)習(xí)應(yīng)用之工業(yè)蒸汽數(shù)據(jù)分析篇詳解
本篇文章介紹了如何用Python進(jìn)行工業(yè)蒸汽數(shù)據(jù)分析的過程及思路,通讀本篇對大家的學(xué)習(xí)或工作具有一定的價值,需要的朋友可以參考下2022-01-01