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