• 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     '''
  • 相关阅读:
    Unity NGUI 2D场景添加按钮
    EaseType缓动函数
    在没有网络的情况下用安卓手机和数据线让台式电脑上网
    面向对象编程
    static与C#中的static
    C#基础
    iSensor APP 之 摄像头调试 OV5642
    iSensor APP 之 摄像头调试 OV9655
    USB3.0之高速视频传输测试 双目相机(mt9p031、mt9m001)带宽高达300M测试 配合isensor测试 500万像素15fps
    模拟摄像头解码模块最新测试 TVP5150模块 FPGA+SDRAM+TVP5150+VGA 实现PAL AV输入 VGA视频输出
  • 原文地址:https://www.cnblogs.com/chenyangyao/p/python_descriptor.html
Copyright © 2020-2023  润新知