python property描述符_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 classTest(object):2 name = None

正常情况下,name的值(其实应该是对象,name是引用)都应该是字符串,但是因为Python是动态类型语言,即使执行Test.name = 3,解释器也不会有任何异常。当然可以想到解决办法,就是提供一个get,set方法来统一读写name,读写前添加安全验证逻辑。代码如下:

1 classtest(object):2 name =None3 @classmethod4 defget_name(cls):5 returncls.name6 @classmethod7 defset_name(cls,val):8 ifisinstance(val,str):9 cls.name =val10 else:11 raise TypeError("Must be an string")

虽然以上代码勉强可以实现对属性赋值的类型检查,但是会导致类型定义的臃肿和逻辑的混乱。从OOP思想来看,只有属性自己最清楚自己的类型,而不是它所在的类,因此如果能将类型检查的逻辑根植于属性内部,那么就可以完美的解决这个问题,而描述符就是这样的利器。

为name属性定义一个(数据)描述符类,其实现了__get__和__set__方法,代码如下:

1 classname_des(object):2 def __init__(self):3 self.__name =None4 def __get__(self, instance, owner):5 print('call __get__')6 return self.__name

7 def __set__(self, instance, value):8 print('call __set__')9 ifisinstance(value,str):10 self.__name =value11 else:12 raise TypeError("Must be an string")

测试类如下:

1 classtest(object):2 name = name_des()

测试代码及输出结果如下:

>>> t =test()>>>t.name

call__get__

>>> t.name = 3call __set__Traceback (most recent call last):

File "", line 1, in

t.name = 3

File "", 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 returnvalue10 #程序执行到这里,说明没有数据描述符和实例属性,则在类(父类、祖先类)的__dict__属性中查找非数据描述符

11 value = find first value in class and bases's dict(property)

12 if value is a function:#如果找到了并且这个属性是一个函数,就返回绑定后的函数

13 returnbounded function(value)14 else:#否则就直接返回这个属性值

15 returnvalue16 #程序执行到这里说明没有找到该属性,引发异常,__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 classmyfun():2 print('myfun called.')

如果想在不修改myfun函数源码的前提下,使之调用前后打印'before called'和'after called',则可以定义一个简单的函数装饰器,如下:

1 defmyecho(fun):2 defreturn_fun():3 print('before called.')4 fun()5 print('after called.')6 return return_fun

使用装饰器对myfun函数就行功能增强:

1 @myecho2 defmyfun():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 returnreturn_fun8 return log_to_return

使用有参函数装饰器对say函数进行功能增强:

1 @log('日志输出开始','结束日志输出')2 defsay(message):3 print(message)

执行say('my name is chenyang.'),输出结果如下:

日志输出开始

my name is chenyang.

结束日志输出

类装饰器:

类装饰器和函数装饰器原理相似,带参数的类装饰器示例代码如下:

1 defdrinkable(message):2 defdrinkable_to_return(cls):3 defdrink(self):4 print('i can drink',message)5 cls.drink = drink #类属性也可以动态修改

6 returncls7 return drinkable_to_return

测试类:

1 @drinkable('water')2 classtest(object):3 pass

执行测试:

1 t =test()2 t.drink()

结果如下:

i can drink water

6、自定义staticmethod和classmethod

一旦了解了描述符和装饰器的基本知识,自定义staticmethod和classmethod就变得非常容易,以下提供参考代码:

1 #定义一个非数据描述符

2 classmyStaticObject(object):3 def __init__(self,fun):4 self.fun =fun5 def __get__(self,instance,owner):6 print('call myStaticObject __get__')7 returnself.fun8 #无参的函数装饰器,返回的是非数据描述符对象

9 defmy_static_method(fun):10 returnmyStaticObject(fun)11 #定义一个非数据描述符

12 classmyClassObject(object):13 def __init__(self,fun):14 self.fun =fun15 def __get__(self,instance,owner):16 print('call myClassObject __get__')17 def class_method(*args,**kargs):18 return self.fun(owner,*args,**kargs)19 returnclass_method20 #无参的函数装饰器,返回的是非数据描述符对象

21 defmy_class_method(fun):22 return myClassObject(fun)

测试类如下:

1 classtest(object):2 @my_static_method3 defmy_static_fun():4 print('my_static_fun')5 @my_class_method6 defmy_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 classtest(object):2 def __init__(self):3 self._x =None4 defgetx(self):5 print("get x")6 returnself._x7 defsetx(self, value):8 print("set x")9 self._x =value10 defdelx(self):11 print("del x")12 delself._x13 x = property(getx, setx, delx, "I'm the 'x' property.")

如果要使用property函数,首先定义class的时候必须是object的子类(新式类)。通过property的定义,当获取成员x的值时,就会调用getx函数,当给成员x赋值时,就会调用setx函数,当删除x时,就会调用delx函数。使用属性的好处就是因为在调用函数,可以做一些检查。如果没有严格的要求,直接使用实例属性可能更方便。

此处省略测试代码。

装饰器模式代码如下:

1 classtest(object):2  def __init__(self):3   self.__x=None4 5 @property6  defx(self):7   return self.__x

8 @x.setter9  defx(self,value):10   self.__x=value11 @x.deleter12  defx(self):13   del self.__x

注意:三个函数的名字(也就是将来要访问的属性名)必须一致。

使用property可以非常容易的实现属性的读写控制,如果想要属性只读,则只需要提供getter方法,如下:

1 classtest(object):2  def __init__(self):3   self.__x=None4

5@property6  defx(self):7   return self.__x

前文说过,只实现get函数的描述符是非数据描述符,根据属性查找的优先级,非属性优先级是可以被实例属性覆盖(隐藏)的,但是执行如下代码:

>>> t=test()>>>t.x>>> t.x = 3Traceback (most recent call last):

File "", line 1, in

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 classProperty(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 =fget6 self.fset =fset7 self.fdel =fdel8 if doc is None and fget is notNone:9 doc = fget.__doc__

10 self.__doc__ =doc11

12 def __get__(self, obj, objtype=None):13 if obj isNone:14 returnself15 if self.fget isNone:16 raise AttributeError("unreadable attribute")17 returnself.fget(obj)18

19 def __set__(self, obj, value):20 if self.fset isNone:21 raise AttributeError("can't set attribute")22 self.fset(obj, value)23

24 def __delete__(self, obj):25 if self.fdel isNone:26 raise AttributeError("can't delete attribute")27 self.fdel(obj)28

29 defgetter(self, fget):30 return type(self)(fget, self.fset, self.fdel, self.__doc__)31 defsetter(self, fset):32 return type(self)(self.fget, fset, self.fdel, self.__doc__)33 defdeleter(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 returnreturn_fun9 returnlog_to_return10

11 #带参数类型装饰器

12 defflyable(message):13 defflyable_to_return(cls):14 deffly(self):15 print(message)16 cls.fly = fly #类属性也可以动态修改

17 returncls18 returnflyable_to_return19

20 #say(meaasge) ==> log(parms)(say)(message)

21 @log('日志输出开始','结束日志输出')22 defsay(message):23 print(message)24

25 #定义一个非数据描述符

26 classmyStaticObject(object):27 def __init__(self,fun):28 self.fun =fun29 def __get__(self,instance,owner):30 print('call myStaticObject __get__')31 returnself.fun32 #无参的函数装饰器,返回的是非数据描述符对象

33 defmy_static_method(fun):34 returnmyStaticObject(fun)35 #定义一个非数据描述符

36 classmyClassObject(object):37 def __init__(self,fun):38 self.fun =fun39 def __get__(self,instance,owner):40 print('call myClassObject __get__')41 def class_method(*args,**kargs):42 return self.fun(owner,*args,**kargs)43 returnclass_method44 #无参的函数装饰器,返回的是非数据描述符对象

45 defmy_class_method(fun):46 returnmyClassObject(fun)47

48 #非数据描述符

49 classdes1(object):50 def __init__(self,name=None):51 self.__name =name52 def __get__(self,obj,typ=None):53 print('call des1.__get__')54 return self.__name

55 #数据描述符

56 classdes2(object):57 def __init__(self,name=None):58 self.__name =name59 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 =val65 #测试类

66 @flyable("这是一个测试类")67 classtest(object):68 def __init__(self,name='test',age=0,sex='man'):69 self.__name =name70 self.__age =age71 self.__sex =sex72 #---------------------覆盖默认的内建方法

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 returnattr83 #此处可以使用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 defget_name(self):94 print('call test.get_name')95 return self.__name

96 defset_name(self,val):97 print('call test.set_name')98 self.__name =val99 name_proxy = property(get_name,set_name)#数据描述符,不能被实例属性覆盖,property本身就是一个描述符类

100

101 defget_age(self):102 print('call test.get_age')103 return self.__age

104 age_proxy = property(get_age) #非数据描述符,但是也不能被实例属性覆盖

105 #----------------------

106 @property107 defsex_proxy(self):108 print("call get sex")109 return self.__sex

110 @sex_proxy.setter #如果没有setter装饰,那么sex_proxy也是只读的,实例属性也无法覆盖,同property

111 defsex_proxy(self,val):112 print("call set sex")113 self.__sex =val114 #---------------------

115 @my_static_method #相当于my_static_fun = my_static_method(my_static_fun) 就是非数据描述符

116 defmy_static_fun():117 print('my_static_fun')118 @my_class_method119 defmy_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 man145 '''

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

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

164 t.my_class_fun()#验证自定义类方法

165 '''

166 start call __getattribute__167 call myClassObject __get__168 my_class_fun169 '''

170 #以下为属性获取

171 t.d1172 '''

173 start call __getattribute__174 call des1.__get__175 '''

176 t.d2177 '''

178 start call __getattribute__179 call des2.__get__180 '''

181 t.d3()182 '''

183 start call __getattribute__184 i am a function185 '''

186 t.name_proxy187 '''

188 start call __getattribute__189 call test.get_name190 start call __getattribute__191 '''

192 t.age_proxy193 '''

194 start call __getattribute__195 call test.get_age196 start call __getattribute__197 '''

198 t.sex_proxy199 '''

200 start call __getattribute__201 call get sex202 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 modefied223 after __setattr__224 '''

225 t.d2226 '''

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_name243 before __setattr__244 after __setattr__245 after __setattr__246 '''

247 t.sex_proxy = 'women'

248 '''

249 before __setattr__250 call set sex251 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 260 t.age_proxy = 3261 File "test.py", line 121, in __setattr__262 super(test, self).__setattr__(name, value)263 AttributeError: can't set attribute264 '''

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python描述符是一种特殊的对象,它可以控制属性的访问和赋值操作。描述符是通过实现特定的魔术方法来实现的,这些魔术方法包括`__get__`、`__set__`和`__delete__`。 当一个定义了一个描述符,并且该描述符被赋值给另一个属性时,它会改变对该属性的访问行为。当我们通过实例访问该属性时,实际上是调用描述符的`__get__`方法;当我们对该属性进行赋值时,实际上是调用描述符的`__set__`方法;当我们删除该属性时,实际上是调用描述符的`__delete__`方法。 描述符的主要应用场景之一是实现属性访问控制。通过使用描述符,我们可以在访问属性前后执行自定义的逻辑,例如验证数据的有效性、记录属性访问日志等。 下面是一个简单的描述符示例: ```python class Descriptor: def __get__(self, instance, owner): print("Getting the value") return instance._value def __set__(self, instance, value): print("Setting the value") instance._value = value class MyClass: descriptor = Descriptor() my_obj = MyClass() my_obj.descriptor = 42 # 调用描述符的 __set__ 方法 print(my_obj.descriptor) # 调用描述符的 __get__ 方法 ``` 运行以上代码会输出以下结果: ``` Setting the value Getting the value 42 ``` 这只是描述符的一个简单示例,实际上描述符还有更多的用法和功能。通过使用描述符,我们可以实现更高级的属性访问控制和定制行为。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值