python描述符(descriptor)、屬性(property)、函數(類)裝飾器(decorator )原理實例詳解


 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     '''

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM