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。

 class Test(object):
name = None

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

 class test(object):
name = None
@classmethod
def get_name(cls):
return cls.name
@classmethod
def set_name(cls,val):
if isinstance(val,str):
cls.name = val
else:
raise TypeError("Must be an string")

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

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

 class name_des(object):
def __init__(self):
self.__name = None
def __get__(self, instance, owner):
print('call __get__')
return self.__name
def __set__(self, instance, value):
print('call __set__')
if isinstance(value,str):
self.__name = value
else:
raise TypeError("Must be an string")

测试类如下:

 class test(object):
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__是实例查找属性的入口,因此有必要探究其实现过程,其逻辑伪代码(带注释说明)如下:

 __getattribute__伪代码:
__getattribute__(property) logic:
#先在类(包括父类、祖先类)的__dict__属性中查找描述符
descripter = find first descripter in class and bases's dict(property)
if descripter:#如果找到属性并且是数据描述符,就直接调用该数据描述符的__get__方法并将结果返回
return descripter.__get__(instance, instance.__class__)
else:#如果没有找到或者不是数据描述符,就去实例的__dict__属性中查找属性,如果找到了就直接返回这个属性值
if value in instance.__dict__
return value
#程序执行到这里,说明没有数据描述符和实例属性,则在类(父类、祖先类)的__dict__属性中查找非数据描述符
value = find first value in class and bases's dict(property)
if value is a function:#如果找到了并且这个属性是一个函数,就返回绑定后的函数
return bounded function(value)
else:#否则就直接返回这个属性值
return value
#程序执行到这里说明没有找到该属性,引发异常,__getattr__函数会被调用
raise AttributeNotFundedException

同样的,当对属性进行赋值操作的时候,内建函数__setattr__也会被调用,其伪代码如下:

 __setattr__伪代码:
__setattr__(property, value)logic:
#先在类(包括父类、祖先类)的__dict__属性中查找描述符
descripter = find first descripter in class and bases's dict(property)
if descripter:#如果找到了且是数据描述符,就调用描述符的__set__方法
descripter.__set__(instance, value)
else:#否则就是给实例属性赋值
instance.__dict__[property] = value

记住__getattribute__查找属性的优先级顺序,并且理解__getattribute__、__setattr__的实现逻辑(还包括__getattr__的调用时机)后,就可以很容易搞懂为什么有些类属性无法被实例属性覆盖(隐藏)、通过实例访问一个属性的时候到底访问的是类属性还是实例属性,为此,我专门写了一个综合测试实例,代码见本文最后。

5、装饰器

如果想在不修改源代码的基础上扩充现有函数和类的功能,装饰器是一个不错的选择(类还可以通过派生的方式),下面分别介绍函数和类的装饰器。

函数装饰器:

假设有如下函数:

 class myfun():
print('myfun called.')

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

 def myecho(fun):
def return_fun():
print('before called.')
fun()
print('after called.')
return return_fun

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

 @myecho
def myfun():
print('myfun called.')

调用myfun(执行myfun()相当于myecho(fun)()),得到如下输出:

before called.
myfun called.
after called.

装饰器可以带参数,比如定义一个日志功能的装饰器,代码如下:

 def log(header,footer):#相当于在无参装饰器外套一层参数
def log_to_return(fun):#这里接受被装饰的函数
def return_fun(*args,**kargs):
print(header)
fun(*args,**kargs)
print(footer)
return return_fun
return log_to_return

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

 @log('日志输出开始','结束日志输出')
def say(message):
print(message)

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

日志输出开始
my name is chenyang.
结束日志输出

类装饰器:

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

 def drinkable(message):
def drinkable_to_return(cls):
def drink(self):
print('i can drink',message)
cls.drink = drink #类属性也可以动态修改
return cls
return drinkable_to_return

测试类:

 @drinkable('water')
class test(object):
pass

执行测试:

   t = test()
t.drink()

结果如下:

i can drink water

6、自定义staticmethod和classmethod

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

 #定义一个非数据描述符
class myStaticObject(object):
def __init__(self,fun):
self.fun = fun
def __get__(self,instance,owner):
print('call myStaticObject __get__')
return self.fun
#无参的函数装饰器,返回的是非数据描述符对象
def my_static_method(fun):
return myStaticObject(fun)
#定义一个非数据描述符
class myClassObject(object):
def __init__(self,fun):
self.fun = fun
def __get__(self,instance,owner):
print('call myClassObject __get__')
def class_method(*args,**kargs):
return self.fun(owner,*args,**kargs)
return class_method
#无参的函数装饰器,返回的是非数据描述符对象
def my_class_method(fun):
return myClassObject(fun)

测试类如下:

 class test(object):
@my_static_method
def my_static_fun():
print('my_static_fun')
@my_class_method
def my_class_fun(cls):
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有两种使用方式,一种是函数模式,一种是装饰器模式。

函数模式代码如下:

 class test(object):
def __init__(self):
self._x = None
def getx(self):
print("get x")
return self._x
def setx(self, value):
print("set x")
self._x = value
def delx(self):
print("del x")
del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")

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

此处省略测试代码。

装饰器模式代码如下:

 class test(object):
 def __init__(self):
  self.__x=None
 
 @property
 def x(self):
  return self.__x
 @x.setter
 def x(self,value):
  self.__x=value
 @x.deleter
 def x(self):
  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):

 class Property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c" def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj) def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value) def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj) def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)

7、综合测试实例

      以下测试代码,结合了前文的知识点和测试代码,集中测试了描述符、property、装饰器等。并且重写了内建函数__getattribute__、__setattr__、__getattr__,增加了打印语句用以测试这些内建函数的调用时机。每一条测试结构都在相应的测试语句下用多行注释括起来。

 #带参数函数装饰器
def log(header,footer):#相当于在无参装饰器外套一层参数
def log_to_return(fun):#这里接受被装饰的函数
def return_fun(*args,**kargs):
print(header)
fun(*args,**kargs)
print(footer)
return return_fun
return log_to_return #带参数类型装饰器
def flyable(message):
def flyable_to_return(cls):
def fly(self):
print(message)
cls.fly = fly #类属性也可以动态修改
return cls
return flyable_to_return #say(meaasge) ==> log(parms)(say)(message)
@log('日志输出开始','结束日志输出')
def say(message):
print(message) #定义一个非数据描述符
class myStaticObject(object):
def __init__(self,fun):
self.fun = fun
def __get__(self,instance,owner):
print('call myStaticObject __get__')
return self.fun
#无参的函数装饰器,返回的是非数据描述符对象
def my_static_method(fun):
return myStaticObject(fun)
#定义一个非数据描述符
class myClassObject(object):
def __init__(self,fun):
self.fun = fun
def __get__(self,instance,owner):
print('call myClassObject __get__')
def class_method(*args,**kargs):
return self.fun(owner,*args,**kargs)
return class_method
#无参的函数装饰器,返回的是非数据描述符对象
def my_class_method(fun):
return myClassObject(fun) #非数据描述符
class des1(object):
def __init__(self,name=None):
self.__name = name
def __get__(self,obj,typ=None):
print('call des1.__get__')
return self.__name
#数据描述符
class des2(object):
def __init__(self,name=None):
self.__name = name
def __get__(self,obj,typ=None):
print('call des2.__get__')
return self.__name
def __set__(self,obj,val):
print('call des2.__set__,val is %s' % (val))
self.__name = val
#测试类
@flyable("这是一个测试类")
class test(object):
def __init__(self,name='test',age=0,sex='man'):
self.__name = name
self.__age = age
self.__sex = sex
#---------------------覆盖默认的内建方法
def __getattribute__(self, name):
print("start call __getattribute__")
return super(test, self).__getattribute__(name)
def __setattr__(self, name, value):
print("before __setattr__")
super(test, self).__setattr__(name, value)
print("after __setattr__")
def __getattr__(self,attr):
print("start call __getattr__")
return attr
#此处可以使用getattr()内建函数对包装对象进行授权
def __str__(self):
return str('name is %s,age is %d,sex is %s' % (self.__name,self.__age,self.__sex))
__repr__ = __str__
#-----------------------
d1 = des1('chenyang') #非数据描述符,可以被实例属性覆盖
d2 = des2('pengmingyao') #数据描述符,不能被实例属性覆盖
def d3(self): #普通函数,为了验证函数(包括函数、静态/类方法)都是非数据描述符,可悲实例属性覆盖
print('i am a function')
#------------------------
def get_name(self):
print('call test.get_name')
return self.__name
def set_name(self,val):
print('call test.set_name')
self.__name = val
name_proxy = property(get_name,set_name)#数据描述符,不能被实例属性覆盖,property本身就是一个描述符类 def get_age(self):
print('call test.get_age')
return self.__age
age_proxy = property(get_age) #非数据描述符,但是也不能被实例属性覆盖
#----------------------
@property
def sex_proxy(self):
print("call get sex")
return self.__sex
@sex_proxy.setter #如果没有setter装饰,那么sex_proxy也是只读的,实例属性也无法覆盖,同property
def sex_proxy(self,val):
print("call set sex")
self.__sex = val
#---------------------
@my_static_method #相当于my_static_fun = my_static_method(my_static_fun) 就是非数据描述符
def my_static_fun():
print('my_static_fun')
@my_class_method
def my_class_fun(cls):
print('my_class_fun') #主函数
if __name__ == "__main__":
say("函数装饰器测试")
'''
日志输出开始
函数装饰器测试
结束日志输出
'''
t=test( ) #创建测试类的实例对象
'''
before __setattr__
after __setattr__
before __setattr__
after __setattr__
before __setattr__
after __setattr__
'''
print(str(t)) #验证__str__内建函数
'''
start call __getattribute__
start call __getattribute__
start call __getattribute__
name is test,age is 0,sex is man
'''
print(repr(t))#验证__repr__内建函数
'''
start call __getattribute__
start call __getattribute__
start call __getattribute__
name is test,age is 0,sex is man
'''
t.fly() #验证类装饰器
'''
start call __getattribute__
这是一个测试类
'''
t.my_static_fun()#验证自定义静态方法
'''
start call __getattribute__
call myStaticObject __get__
my_static_fun
'''
t.my_class_fun()#验证自定义类方法
'''
start call __getattribute__
call myClassObject __get__
my_class_fun
'''
#以下为属性获取
t.d1
'''
start call __getattribute__
call des1.__get__
'''
t.d2
'''
start call __getattribute__
call des2.__get__
'''
t.d3()
'''
start call __getattribute__
i am a function
'''
t.name_proxy
'''
start call __getattribute__
call test.get_name
start call __getattribute__
'''
t.age_proxy
'''
start call __getattribute__
call test.get_age
start call __getattribute__
'''
t.sex_proxy
'''
start call __getattribute__
call get sex
start call __getattribute__
'''
t.xyz #测试访问不存在的属性,会调用__getattr__
'''
start call __getattribute__
start call __getattr__
'''
#测试属性写
t.d1 = 3 #由于类属性d1是非数据描述符,因此这里将动态产生实例属性d1
'''
before __setattr__
after __setattr__
'''
t.d1 #由于实例属性的优先级比非数据描述符优先级高,因此此处访问的是实例属性
'''
start call __getattribute__
'''
t.d2 = 'modefied'
'''
before __setattr__
call des2.__set__,val is modefied
after __setattr__
'''
t.d2
'''
start call __getattribute__
call des2.__get__
'''
t.d3 = 'not a function'
'''
before __setattr__
after __setattr__
'''
t.d3 #因为函数是非数据描述符,因此被实例属性覆盖
'''
start call __getattribute__
'''
t.name_proxy = 'modified'
'''
before __setattr__
call test.set_name
before __setattr__
after __setattr__
after __setattr__
'''
t.sex_proxy = 'women'
'''
before __setattr__
call set sex
before __setattr__
after __setattr__
after __setattr__
'''
t.age_proxy = 3 #age_proxy是只读的
'''
before __setattr__
Traceback (most recent call last):
File "test.py", line 191, in <module>
t.age_proxy = 3
File "test.py", line 121, in __setattr__
super(test, self).__setattr__(name, value)
AttributeError: can't set attribute
'''

python描述符(descriptor)、属性(property)、函数(类)装饰器(decorator )原理实例详解的更多相关文章

  1. Python 描述符(descriptor) 杂记

    转自:https://blog.tonyseek.com/post/notes-about-python-descriptor/ Python 引入的“描述符”(descriptor)语法特性真的很黄 ...

  2. python描述符descriptor(一)

    Python 描述符是一种创建托管属性的方法.每当一个属性被查询时,一个动作就会发生.这个动作默认是get,set或者delete.不过,有时候某个应用可能会有 更多的需求,需要你设计一些更复杂的动作 ...

  3. python描述符 descriptor

    descriptor 在python中,如果一个新式类定义了__get__, __set__, __delete__方法中的一个或者多个,那么称之为descriptor.descriptor通常用来改 ...

  4. python描述符和属性查找

    python描述符 定义 一般说来,描述符是一种访问对象属性时候的绑定行为,如果这个对象属性定义了__get__(),__set__(), and __delete__()一种或者几种,那么就称之为描 ...

  5. Python 描述符 (descriptor)

    1.什么是描述符? 描述符是Python新式类的关键点之一,它为对象属性提供强大的API,你可以认为描述符是表示对象属性的一个代理.当需要属性时,可根据你遇到的情况,通过描述符进行访问他(摘自Pyth ...

  6. python描述符descriptor(二)

    python内置的描述符 python有些内置的描述符对象,property.staticmethod.classmethod,python实现如下: class Property(object): ...

  7. Python描述符 (descriptor) 详解

    1.什么是描述符? python描述符是一个“绑定行为”的对象属性,在描述符协议中,它可以通过方法重写属性的访问.这些方法有 __get__(), __set__(), 和__delete__().如 ...

  8. 学习python第十三天,函数5 装饰器decorator

    定义:装饰器本质是函数,(装饰其他函数)就是为其他函数添加附加功能原则:1.不能修改被装饰的函数的源代码 2.不能修改装饰的函数的调用方式 实现装饰器知识储备1函数即变量2.高阶函数,满足2个条件之一 ...

  9. Python学习第四十天函数的装饰器用法

    在软件开发的过程中,要遵循软件的一些原则封装的,不改变原有的代码的基础增加一些需求,python提供了装饰器来扩展函数功能,下面说说函数装饰器用法 def debug(func):      def ...

随机推荐

  1. 使用Docker快速部署Storm环境

    Storm的部署虽然不是特别麻烦,但是在生产环境中,为了提高部署效率,方便管理维护,使用Docker来统一管理部署是一个不错的选择.下面是我开源的一个新的项目,一个配置好了storm与mono环境的D ...

  2. Oracle创建表空间和用户

    用户名为:C##NEWO ============================================================================= /*分为四步 */ ...

  3. 【推荐】CentOS安装Tomcat-7.0.57+启动配置+安全配置+性能配置

    注:以下所有操作均在CentOS 6.5 x86_64位系统下完成. #准备工作# 在安装Tomcat之前,请确保已经安装了JDK-1.7环境,具体见<CentOS安装JDK-1.7>. ...

  4. android html.fromHtml 用例

    private String content = "<b>标题</b><br>" + "内容<br>": mCo ...

  5. spring中的定时调度实现TimerFactoryBean引起的隐患

    手中的一个老项目,其中使用的TimerFactoryBean实现的调度任务.一般都是spring quartz实现,这种的着实少见.正因为少见资料比较少,当初为了确认这个会不会2个调度任务同时并行执行 ...

  6. Comparison of SQL Server Compact, SQLite, SQL Server Express and LocalDB

    Information about LocalDB comes from here and SQL Server 2014 Books Online. LocalDB is the full SQL ...

  7. ZBrush中的动态网格该怎么进行运用

    DynaMesh是ZBrush最新的基础模型创建工具,该命令用于基本模型的起稿到中模的制作.使用DynaMesh完全不启用考虑模型的拓扑,可以从一个图形拉扯出整个模型的分支,本文将以一个实例简单介绍Z ...

  8. hdu-5992 Finding Hotels(kd-tree)

    题目链接: Finding Hotels Time Limit: 2000/1000 MS (Java/Others)     Memory Limit: 102400/102400 K (Java/ ...

  9. NOIP2012同余方程[exgcd]

    题目描述 求关于 x 的同余方程 ax ≡ 1 (mod b)的最小正整数解. 输入输出格式 输入格式: 输入只有一行,包含两个正整数 a, b,用一个空格隔开 输出格式: 输出只有一行,包含一个正整 ...

  10. ArrayList,Vector,LinkedList

    在java.util包中定义的类集框架其核心的组成接口有如下:·Collection接口:负责保存单值的最大父接口 |-List子接口:允许保存重复元素,数据的保存顺序就是数据的增加顺序: |-Set ...