1、前言
Python的描述符是接觸到Python核心編程中一個比較難以理解的內容,自己在學習的過程中也遇到過很多的疑惑,通過google和閱讀源碼,現將自己的理解和心得記錄下來,也為正在為了該問題苦惱的朋友提供一個思考問題的參考,由於個人能力有限,文中如有筆誤、邏輯錯誤甚至概念性錯誤,還請提出並指正。本文所有測試代碼使用Python 3.4版本
注:本文為自己整理和原創,如有轉載,請注明出處。
2、什么是描述符
Python 2.2 引進了 Python 描述符,同時還引進了一些新的樣式類,但是它們並沒有得到廣泛使用。Python 描述符是一種創建托管屬性的方法。描述符具有諸多優點,諸如:保護屬性不受修改、屬性類型檢查和自動更新某個依賴屬性的值等。
說的通俗一點,從表現形式來看,一個類如果實現了__get__,__set__,__del__方法(三個方法不一定要全部都實現),並且該類的實例對象通常是另一個類的類屬性,那么這個類就是一個描述符。__get__,__set__,__del__的具體聲明如下:
__get__(self, instance, owner)
__set__(self, instance, value)
__delete__(self, instance)
其中:
__get__ 用於訪問屬性。它返回屬性的值,或者在所請求的屬性不存在的情況下出現 AttributeError 異常。類似於javabean中的get。
__set__ 將在屬性分配操作中調用。不會返回任何內容。類似於javabean中的set。
__delete__ 控制刪除操作。不會返回內容。
注意:
只實現__get__方法的對象是非數據描述符,意味着在初始化之后它們只能被讀取。而同時實現__get__和__set__的對象是數據描述符,意味着這種屬性是可讀寫的。
3、為什么需要描述符
因為Python是一個動態類型解釋性語言,不像C/C++等靜態編譯型語言,數據類型在編譯時便可以進行驗證,而Python中必須添加額外的類型檢查邏輯代碼才能做到這一點,這就是描述符的初衷。比如,有一個測試類Test,其具有一個類屬性name。
1 class Test(object): 2 name = None
正常情況下,name的值(其實應該是對象,name是引用)都應該是字符串,但是因為Python是動態類型語言,即使執行Test.name = 3,解釋器也不會有任何異常。當然可以想到解決辦法,就是提供一個get,set方法來統一讀寫name,讀寫前添加安全驗證邏輯。代碼如下:
1 class test(object): 2 name = None 3 @classmethod 4 def get_name(cls): 5 return cls.name 6 @classmethod 7 def set_name(cls,val): 8 if isinstance(val,str): 9 cls.name = val 10 else: 11 raise TypeError("Must be an string")
雖然以上代碼勉強可以實現對屬性賦值的類型檢查,但是會導致類型定義的臃腫和邏輯的混亂。從OOP思想來看,只有屬性自己最清楚自己的類型,而不是它所在的類,因此如果能將類型檢查的邏輯根植於屬性內部,那么就可以完美的解決這個問題,而描述符就是這樣的利器。
為name屬性定義一個(數據)描述符類,其實現了__get__和__set__方法,代碼如下:
1 class name_des(object): 2 def __init__(self): 3 self.__name = None 4 def __get__(self, instance, owner): 5 print('call __get__') 6 return self.__name 7 def __set__(self, instance, value): 8 print('call __set__') 9 if isinstance(value,str): 10 self.__name = value 11 else: 12 raise TypeError("Must be an string")
測試類如下:
1 class test(object): 2 name = name_des()
測試代碼及輸出結果如下:
>>> t = test() >>> t.name call __get__ >>> t.name = 3 call __set__ Traceback (most recent call last): File "<pyshell#99>", line 1, in <module> t.name = 3 File "<pyshell#94>", line 12, in __set__ raise TypeError("Must be an string") TypeError: Must be an string >>> t.name = 'my name is chenyang' call __set__ >>> t.name call __get__ 'my name is chenyang' >>>
從打印的輸出信息可以看到,當使用實例訪問name屬性(即執行t.name)時,便會調用描述符的__get__方法(注意__get__中添加的打印語句)。當使用實例對name屬性進行賦值操作時(即t.name = 'my name is chenyang.'),從打印出的'call set'可以看到描述符的__set__方法被調用。熟悉Python的都知道,如果name是一個普通類屬性(即不是數據描述符),那么執行t.name = 'my name is chenyang.'時,將動態產生一個實例屬性,再次執行t.name讀取屬性時,此時讀取的屬性為實例屬性,而不是之前的類屬性(這涉及到一個屬性查找優先級的問題,下文會提到)。
至此,可以發現描述符的作用和優勢,以彌補Python動態類型的缺點。
4、屬性查找的優先級
當使用實例對象訪問屬性時,都會調用__getattribute__內建函數,__getattribute__查找屬性的優先級如下:
1、類屬性
2、數據描述符
3、實例屬性
4、非數據描述符
5、__getattr__()
由於__getattribute__是實例查找屬性的入口,因此有必要探究其實現過程,其邏輯偽代碼(帶注釋說明)如下:
1 __getattribute__偽代碼: 2 __getattribute__(property) logic: 3 #先在類(包括父類、祖先類)的__dict__屬性中查找描述符 4 descripter = find first descripter in class and bases's dict(property) 5 if descripter:#如果找到屬性並且是數據描述符,就直接調用該數據描述符的__get__方法並將結果返回 6 return descripter.__get__(instance, instance.__class__) 7 else:#如果沒有找到或者不是數據描述符,就去實例的__dict__屬性中查找屬性,如果找到了就直接返回這個屬性值 8 if value in instance.__dict__ 9 return value 10 #程序執行到這里,說明沒有數據描述符和實例屬性,則在類(父類、祖先類)的__dict__屬性中查找非數據描述符 11 value = find first value in class and bases's dict(property) 12 if value is a function:#如果找到了並且這個屬性是一個函數,就返回綁定后的函數 13 return bounded function(value) 14 else:#否則就直接返回這個屬性值 15 return value 16 #程序執行到這里說明沒有找到該屬性,引發異常,__getattr__函數會被調用 17 raise AttributeNotFundedException
同樣的,當對屬性進行賦值操作的時候,內建函數__setattr__也會被調用,其偽代碼如下:
1 __setattr__偽代碼: 2 __setattr__(property, value)logic: 3 #先在類(包括父類、祖先類)的__dict__屬性中查找描述符 4 descripter = find first descripter in class and bases's dict(property) 5 if descripter:#如果找到了且是數據描述符,就調用描述符的__set__方法 6 descripter.__set__(instance, value) 7 else:#否則就是給實例屬性賦值 8 instance.__dict__[property] = value
記住__getattribute__查找屬性的優先級順序,並且理解__getattribute__、__setattr__的實現邏輯(還包括__getattr__的調用時機)后,就可以很容易搞懂為什么有些類屬性無法被實例屬性覆蓋(隱藏)、通過實例訪問一個屬性的時候到底訪問的是類屬性還是實例屬性,為此,我專門寫了一個綜合測試實例,代碼見本文最后。
5、裝飾器
如果想在不修改源代碼的基礎上擴充現有函數和類的功能,裝飾器是一個不錯的選擇(類還可以通過派生的方式),下面分別介紹函數和類的裝飾器。
函數裝飾器:
假設有如下函數:
1 class myfun(): 2 print('myfun called.')
如果想在不修改myfun函數源碼的前提下,使之調用前后打印'before called'和'after called',則可以定義一個簡單的函數裝飾器,如下:
1 def myecho(fun): 2 def return_fun(): 3 print('before called.') 4 fun() 5 print('after called.') 6 return return_fun
使用裝飾器對myfun函數就行功能增強:
1 @myecho 2 def myfun(): 3 print('myfun called.')
調用myfun(執行myfun()相當於myecho(fun)()),得到如下輸出:
before called.
myfun called.
after called.
裝飾器可以帶參數,比如定義一個日志功能的裝飾器,代碼如下:
1 def log(header,footer):#相當於在無參裝飾器外套一層參數 2 def log_to_return(fun):#這里接受被裝飾的函數 3 def return_fun(*args,**kargs): 4 print(header) 5 fun(*args,**kargs) 6 print(footer) 7 return return_fun 8 return log_to_return
使用有參函數裝飾器對say函數進行功能增強:
1 @log('日志輸出開始','結束日志輸出') 2 def say(message): 3 print(message)
執行say('my name is chenyang.'),輸出結果如下:
日志輸出開始
my name is chenyang.
結束日志輸出
類裝飾器:
類裝飾器和函數裝飾器原理相似,帶參數的類裝飾器示例代碼如下:
1 def drinkable(message): 2 def drinkable_to_return(cls): 3 def drink(self): 4 print('i can drink',message) 5 cls.drink = drink #類屬性也可以動態修改 6 return cls 7 return drinkable_to_return
測試類:
1 @drinkable('water') 2 class test(object): 3 pass
執行測試:
1 t = test() 2 t.drink()
結果如下:
i can drink water
6、自定義staticmethod和classmethod
一旦了解了描述符和裝飾器的基本知識,自定義staticmethod和classmethod就變得非常容易,以下提供參考代碼:
1 #定義一個非數據描述符 2 class myStaticObject(object): 3 def __init__(self,fun): 4 self.fun = fun 5 def __get__(self,instance,owner): 6 print('call myStaticObject __get__') 7 return self.fun 8 #無參的函數裝飾器,返回的是非數據描述符對象 9 def my_static_method(fun): 10 return myStaticObject(fun) 11 #定義一個非數據描述符 12 class myClassObject(object): 13 def __init__(self,fun): 14 self.fun = fun 15 def __get__(self,instance,owner): 16 print('call myClassObject __get__') 17 def class_method(*args,**kargs): 18 return self.fun(owner,*args,**kargs) 19 return class_method 20 #無參的函數裝飾器,返回的是非數據描述符對象 21 def my_class_method(fun): 22 return myClassObject(fun)
測試類如下:
1 class test(object): 2 @my_static_method 3 def my_static_fun(): 4 print('my_static_fun') 5 @my_class_method 6 def my_class_fun(cls): 7 print('my_class_fun')
測試代碼:
>>> test.my_static_fun() call myStaticObject __get__ my_static_fun >>> test.my_class_fun() call myClassObject __get__ my_class_fun >>>
7、property
本文前面提到過使用定義類的方式使用描述符,但是如果每次為了一個屬性都單獨定義一個類,有時將變得得不償失。為此,python提供了一個輕量級的數據描述符協議函數Property(),其使用裝飾器的模式,可以將類方法當成屬性來訪問。它的標准定義是:
property(fget=None,fset=None,fdel=None,doc=None)
前面3個參數都是未綁定的方法,所以它們事實上可以是任意的類成員函數,分別對應於數據描述符的中的__get__,__set__,__del__方法,所以它們之間會有一個內部的與數據描述符的映射。
property有兩種使用方式,一種是函數模式,一種是裝飾器模式。
函數模式代碼如下:
1 class test(object): 2 def __init__(self): 3 self._x = None 4 def getx(self): 5 print("get x") 6 return self._x 7 def setx(self, value): 8 print("set x") 9 self._x = value 10 def delx(self): 11 print("del x") 12 del self._x 13 x = property(getx, setx, delx, "I'm the 'x' property.")
如果要使用property函數,首先定義class的時候必須是object的子類(新式類)。通過property的定義,當獲取成員x的值時,就會調用getx函數,當給成員x賦值時,就會調用setx函數,當刪除x時,就會調用delx函數。使用屬性的好處就是因為在調用函數,可以做一些檢查。如果沒有嚴格的要求,直接使用實例屬性可能更方便。
此處省略測試代碼。
裝飾器模式代碼如下:
1 class test(object): 2 def __init__(self): 3 self.__x=None 4 5 @property 6 def x(self): 7 return self.__x 8 @x.setter 9 def x(self,value): 10 self.__x=value 11 @x.deleter 12 def x(self): 13 del self.__x
注意:三個函數的名字(也就是將來要訪問的屬性名)必須一致。
使用property可以非常容易的實現屬性的讀寫控制,如果想要屬性只讀,則只需要提供getter方法,如下:
1 class test(object): 2 def __init__(self): 3 self.__x=None 4 5 @property 6 def x(self): 7 return self.__x
前文說過,只實現get函數的描述符是非數據描述符,根據屬性查找的優先級,非屬性優先級是可以被實例屬性覆蓋(隱藏)的,但是執行如下代碼:
>>> t=test() >>> t.x >>> t.x = 3 Traceback (most recent call last): File "<pyshell#39>", line 1, in <module> t.x = 3 AttributeError: can't set attribute
從錯誤信息中可以看出,執行t.x=3的時候並不是動態產生一個實例屬性,也就是說x並不是非數據描述符,那么原因是什么呢?其實原因就在property,雖然表面上看屬性x只設置了get方法,但是其實property是一個同時實現了__get__,__set__,__del__方法的類(是一個數據描述符),因此,使用property生成的屬性其實是一個數據描述符!
使用python模擬的property代碼如下,可以看到,上面的“AttributeError: can't set attribute”異常其實是在property中的__set__函數中引發的,因為用戶沒有設置fset(為None):
1 class Property(object): 2 "Emulate PyProperty_Type() in Objects/descrobject.c" 3 4 def __init__(self, fget=None, fset=None, fdel=None, doc=None): 5 self.fget = fget 6 self.fset = fset 7 self.fdel = fdel 8 if doc is None and fget is not None: 9 doc = fget.__doc__ 10 self.__doc__ = doc 11 12 def __get__(self, obj, objtype=None): 13 if obj is None: 14 return self 15 if self.fget is None: 16 raise AttributeError("unreadable attribute") 17 return self.fget(obj) 18 19 def __set__(self, obj, value): 20 if self.fset is None: 21 raise AttributeError("can't set attribute") 22 self.fset(obj, value) 23 24 def __delete__(self, obj): 25 if self.fdel is None: 26 raise AttributeError("can't delete attribute") 27 self.fdel(obj) 28 29 def getter(self, fget): 30 return type(self)(fget, self.fset, self.fdel, self.__doc__) 31 def setter(self, fset): 32 return type(self)(self.fget, fset, self.fdel, self.__doc__) 33 def deleter(self, fdel): 34 return type(self)(self.fget, self.fset, fdel, self.__doc__)
7、綜合測試實例
以下測試代碼,結合了前文的知識點和測試代碼,集中測試了描述符、property、裝飾器等。並且重寫了內建函數__getattribute__、__setattr__、__getattr__,增加了打印語句用以測試這些內建函數的調用時機。每一條測試結構都在相應的測試語句下用多行注釋括起來。
1 #帶參數函數裝飾器 2 def log(header,footer):#相當於在無參裝飾器外套一層參數 3 def log_to_return(fun):#這里接受被裝飾的函數 4 def return_fun(*args,**kargs): 5 print(header) 6 fun(*args,**kargs) 7 print(footer) 8 return return_fun 9 return log_to_return 10 11 #帶參數類型裝飾器 12 def flyable(message): 13 def flyable_to_return(cls): 14 def fly(self): 15 print(message) 16 cls.fly = fly #類屬性也可以動態修改 17 return cls 18 return flyable_to_return 19 20 #say(meaasge) ==> log(parms)(say)(message) 21 @log('日志輸出開始','結束日志輸出') 22 def say(message): 23 print(message) 24 25 #定義一個非數據描述符 26 class myStaticObject(object): 27 def __init__(self,fun): 28 self.fun = fun 29 def __get__(self,instance,owner): 30 print('call myStaticObject __get__') 31 return self.fun 32 #無參的函數裝飾器,返回的是非數據描述符對象 33 def my_static_method(fun): 34 return myStaticObject(fun) 35 #定義一個非數據描述符 36 class myClassObject(object): 37 def __init__(self,fun): 38 self.fun = fun 39 def __get__(self,instance,owner): 40 print('call myClassObject __get__') 41 def class_method(*args,**kargs): 42 return self.fun(owner,*args,**kargs) 43 return class_method 44 #無參的函數裝飾器,返回的是非數據描述符對象 45 def my_class_method(fun): 46 return myClassObject(fun) 47 48 #非數據描述符 49 class des1(object): 50 def __init__(self,name=None): 51 self.__name = name 52 def __get__(self,obj,typ=None): 53 print('call des1.__get__') 54 return self.__name 55 #數據描述符 56 class des2(object): 57 def __init__(self,name=None): 58 self.__name = name 59 def __get__(self,obj,typ=None): 60 print('call des2.__get__') 61 return self.__name 62 def __set__(self,obj,val): 63 print('call des2.__set__,val is %s' % (val)) 64 self.__name = val 65 #測試類 66 @flyable("這是一個測試類") 67 class test(object): 68 def __init__(self,name='test',age=0,sex='man'): 69 self.__name = name 70 self.__age = age 71 self.__sex = sex 72 #---------------------覆蓋默認的內建方法 73 def __getattribute__(self, name): 74 print("start call __getattribute__") 75 return super(test, self).__getattribute__(name) 76 def __setattr__(self, name, value): 77 print("before __setattr__") 78 super(test, self).__setattr__(name, value) 79 print("after __setattr__") 80 def __getattr__(self,attr): 81 print("start call __getattr__") 82 return attr 83 #此處可以使用getattr()內建函數對包裝對象進行授權 84 def __str__(self): 85 return str('name is %s,age is %d,sex is %s' % (self.__name,self.__age,self.__sex)) 86 __repr__ = __str__ 87 #----------------------- 88 d1 = des1('chenyang') #非數據描述符,可以被實例屬性覆蓋 89 d2 = des2('pengmingyao') #數據描述符,不能被實例屬性覆蓋 90 def d3(self): #普通函數,為了驗證函數(包括函數、靜態/類方法)都是非數據描述符,可悲實例屬性覆蓋 91 print('i am a function') 92 #------------------------ 93 def get_name(self): 94 print('call test.get_name') 95 return self.__name 96 def set_name(self,val): 97 print('call test.set_name') 98 self.__name = val 99 name_proxy = property(get_name,set_name)#數據描述符,不能被實例屬性覆蓋,property本身就是一個描述符類 100 101 def get_age(self): 102 print('call test.get_age') 103 return self.__age 104 age_proxy = property(get_age) #非數據描述符,但是也不能被實例屬性覆蓋 105 #---------------------- 106 @property 107 def sex_proxy(self): 108 print("call get sex") 109 return self.__sex 110 @sex_proxy.setter #如果沒有setter裝飾,那么sex_proxy也是只讀的,實例屬性也無法覆蓋,同property 111 def sex_proxy(self,val): 112 print("call set sex") 113 self.__sex = val 114 #--------------------- 115 @my_static_method #相當於my_static_fun = my_static_method(my_static_fun) 就是非數據描述符 116 def my_static_fun(): 117 print('my_static_fun') 118 @my_class_method 119 def my_class_fun(cls): 120 print('my_class_fun') 121 122 #主函數 123 if __name__ == "__main__": 124 say("函數裝飾器測試") 125 ''' 126 日志輸出開始 127 函數裝飾器測試 128 結束日志輸出 129 ''' 130 t=test( ) #創建測試類的實例對象 131 ''' 132 before __setattr__ 133 after __setattr__ 134 before __setattr__ 135 after __setattr__ 136 before __setattr__ 137 after __setattr__ 138 ''' 139 print(str(t)) #驗證__str__內建函數 140 ''' 141 start call __getattribute__ 142 start call __getattribute__ 143 start call __getattribute__ 144 name is test,age is 0,sex is man 145 ''' 146 print(repr(t))#驗證__repr__內建函數 147 ''' 148 start call __getattribute__ 149 start call __getattribute__ 150 start call __getattribute__ 151 name is test,age is 0,sex is man 152 ''' 153 t.fly() #驗證類裝飾器 154 ''' 155 start call __getattribute__ 156 這是一個測試類 157 ''' 158 t.my_static_fun()#驗證自定義靜態方法 159 ''' 160 start call __getattribute__ 161 call myStaticObject __get__ 162 my_static_fun 163 ''' 164 t.my_class_fun()#驗證自定義類方法 165 ''' 166 start call __getattribute__ 167 call myClassObject __get__ 168 my_class_fun 169 ''' 170 #以下為屬性獲取 171 t.d1 172 ''' 173 start call __getattribute__ 174 call des1.__get__ 175 ''' 176 t.d2 177 ''' 178 start call __getattribute__ 179 call des2.__get__ 180 ''' 181 t.d3() 182 ''' 183 start call __getattribute__ 184 i am a function 185 ''' 186 t.name_proxy 187 ''' 188 start call __getattribute__ 189 call test.get_name 190 start call __getattribute__ 191 ''' 192 t.age_proxy 193 ''' 194 start call __getattribute__ 195 call test.get_age 196 start call __getattribute__ 197 ''' 198 t.sex_proxy 199 ''' 200 start call __getattribute__ 201 call get sex 202 start call __getattribute__ 203 ''' 204 t.xyz #測試訪問不存在的屬性,會調用__getattr__ 205 ''' 206 start call __getattribute__ 207 start call __getattr__ 208 ''' 209 #測試屬性寫 210 t.d1 = 3 #由於類屬性d1是非數據描述符,因此這里將動態產生實例屬性d1 211 ''' 212 before __setattr__ 213 after __setattr__ 214 ''' 215 t.d1 #由於實例屬性的優先級比非數據描述符優先級高,因此此處訪問的是實例屬性 216 ''' 217 start call __getattribute__ 218 ''' 219 t.d2 = 'modefied' 220 ''' 221 before __setattr__ 222 call des2.__set__,val is modefied 223 after __setattr__ 224 ''' 225 t.d2 226 ''' 227 start call __getattribute__ 228 call des2.__get__ 229 ''' 230 t.d3 = 'not a function' 231 ''' 232 before __setattr__ 233 after __setattr__ 234 ''' 235 t.d3 #因為函數是非數據描述符,因此被實例屬性覆蓋 236 ''' 237 start call __getattribute__ 238 ''' 239 t.name_proxy = 'modified' 240 ''' 241 before __setattr__ 242 call test.set_name 243 before __setattr__ 244 after __setattr__ 245 after __setattr__ 246 ''' 247 t.sex_proxy = 'women' 248 ''' 249 before __setattr__ 250 call set sex 251 before __setattr__ 252 after __setattr__ 253 after __setattr__ 254 ''' 255 t.age_proxy = 3 #age_proxy是只讀的 256 ''' 257 before __setattr__ 258 Traceback (most recent call last): 259 File "test.py", line 191, in <module> 260 t.age_proxy = 3 261 File "test.py", line 121, in __setattr__ 262 super(test, self).__setattr__(name, value) 263 AttributeError: can't set attribute 264 '''