在Python中,属性查找(attribute lookup)是比较复杂的,特别是涉及到描述符descriptor的时候。

  在上一文章末尾,给出了一段代码,就涉及到descriptor与attribute lookup的问题。而get系列函数(__get__, __getattr__, __getattribute__) 也很容易搞晕,本文就这些问题简单总结一下。

  首先,我们知道:
  • python中一切都是对象,“everything is object”,包括类,类的实例,数字,模块
  • 任何object都是类(class or type)的实例(instance)
  • 如果一个descriptor只实现了__get__方法,我们称之为non-data descriptor, 如果同时实现了__get__ __set__我们称之为data descriptor。
 

实例属性查找

  按照python doc,如果obj是某个类的实例,那么obj.name(以及等价的getattr(obj,'name'))首先调用__getattribute__。如果类定义了__getattr__方法,那么在__getattribute__抛出 AttributeError 的时候就会调用到__getattr__,而对于描述符(__get__)的调用,则是发生在__getattribute__内部的。官网文档是这么描述的
    The implementation works through a precedence chain that gives data descriptors priority over instance variables, instance variables priority over non-data descriptors, and assigns lowest priority to __getattr__() if provided.
    obj = Clz(), 那么obj.attr 顺序如下:

(1)如果“attr”是出现在Clz或其基类的__dict__中, 且attr是data descriptor, 那么调用其__get__方法, 否则

(2)如果“attr”出现在obj的__dict__中, 那么直接返回 obj.__dict__['attr'], 否则

(3)如果“attr”出现在Clz或其基类的__dict__中

(3.1)如果attr是non-data descriptor,那么调用其__get__方法, 否则

(3.2)返回 __dict__['attr']

(4)如果Clz有__getattr__方法,调用__getattr__方法,否则

(5)抛出AttributeError

  下面是测试代码:
  

 #coding=utf-8
class DataDescriptor(object):
def __init__(self, init_value):
self.value = init_value def __get__(self, instance, typ):
return 'DataDescriptor __get__' def __set__(self, instance, value):
print ('DataDescriptor __set__')
self.value = value class NonDataDescriptor(object):
def __init__(self, init_value):
self.value = init_value def __get__(self, instance, typ):
return('NonDataDescriptor __get__') class Base(object):
dd_base = DataDescriptor(0)
ndd_base = NonDataDescriptor(0) class Derive(Base):
dd_derive = DataDescriptor(0)
ndd_derive = NonDataDescriptor(0)
same_name_attr = 'attr in class' def __init__(self):
self.not_des_attr = 'I am not descriptor attr'
self.same_name_attr = 'attr in object' def __getattr__(self, key):
return '__getattr__ with key %s' % key def change_attr(self):
self.__dict__['dd_base'] = 'dd_base now in object dict '
self.__dict__['ndd_derive'] = 'ndd_derive now in object dict ' def main():
b = Base()
d = Derive()
print 'Derive object dict', d.__dict__
assert d.dd_base == "DataDescriptor __get__"
assert d.ndd_derive == 'NonDataDescriptor __get__'
assert d.not_des_attr == 'I am not descriptor attr'
assert d.no_exists_key == '__getattr__ with key no_exists_key'
assert d.same_name_attr == 'attr in object'
d.change_attr()
print 'Derive object dict', d.__dict__
assert d.dd_base != 'dd_base now in object dict '
assert d.ndd_derive == 'ndd_derive now in object dict ' try:
b.no_exists_key
except Exception, e:
assert isinstance(e, AttributeError) if __name__ == '__main__':
main()
  注意第50行,change_attr给实例的__dict__里面增加了两个属性。通过上下两条print的输出如下:
  Derive object dict {'same_name_attr': 'attr in object', 'not_des_attr': 'I am not descriptor attr'}
  Derive object dict {'same_name_attr': 'attr in object', 'ndd_derive': 'ndd_derive now in object dict ', 'not_des_attr': 'I am not descriptor attr', 'dd_base': 'dd_base now in object dict '}

  调用change_attr方法之后,dd_base既出现在类的__dict__(作为data descriptor), 也出现在实例的__dict__, 因为attribute lookup的循序,所以优先返回的还是Clz.__dict__['dd_base']。而ndd_base虽然出现在类的__dict__, 但是因为是nondata descriptor,所以优先返回obj.__dict__['dd_base']。其他:line48,line56表明了__getattr__的作用。line49表明obj.__dict__优先于Clz.__dict__

cached_property例子

  我们再来看看上一文章的这段代码。

 1 import functools, time
2 class cached_property(object):
3 """ A property that is only computed once per instance and then replaces
4 itself with an ordinary attribute. Deleting the attribute resets the
5 property. """
6
7 def __init__(self, func):
8 functools.update_wrapper(self, func)
9 self.func = func
10
11 def __get__(self, obj, cls):
12 if obj is None: return self
13 value = obj.__dict__[self.func.__name__] = self.func(obj)
14 return value
15
16 class TestClz(object):
17 @cached_property
18 def complex_calc(self):
19 print 'very complex_calc'
20 return sum(range(100))
21
22 if __name__=='__main__':
23 t = TestClz()
24 print '>>> first call'
25 print t.complex_calc
26 print '>>> second call'
27 print t.complex_calc

cached_property是一个non-data descriptor。在TestClz中,用cached_property装饰方法complex_calc,返回值是一个descriptor实例,所以在调用的时候没有使用小括号。

    第一次调用t.complex_calc之前,obj(t)的__dict__中没有”complex_calc“, 根据查找顺序第三条,执行cached_property.__get__, 这个函数代用缓存的complex_calc函数计算出结果,并且把结果放入obj.__dict__。那么第二次访问t.complex_calc的时候,根据查找顺序,第二条有限于第三条,所以就直接返回obj.__dict__['complex_calc']。bottle的源码中还有两个descriptor,非常厉害!
 

类属性查找

  前面提到过,类的也是对象,类是元类(metaclass)的实例,所以类属性的查找顺序基本同上。区别在于第二步,由于Clz可能有基类,所以是在Clz及其基类的__dict__”查找“attr,注意这里的查找并不是直接返回clz.__dict__['attr']。具体来说,这第二步分为以下两种情况:

  (2.1)如果clz.__dict__['attr']是一个descriptor(不管是data descriptor还是non-data descriptor),都调用其__get__方法

  (2.2)否则返回clz.__dict__['attr']

  这就解释了一个很有意思的问题:method与function的问题

>>> class Widget(object):
... def func(self):
... pass
...
>>> w = Widget()
>>> Widget.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Widget' objects>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Widget' objects>, '__doc__': None, 'func': <function func at 0x7fdc7d0d1668>})
>>> w.__dict__
{}

>>> Widget.__dict__['func']
<function func at 0x7fdc7d0d1668>
>>> Widget.func
<unbound method Widget.func>
>>>

  Widget是一个之定义了一个func函数的类,func是类的属性,这个也可以通过Widget.__dict__、w.__dict__看到。Widget.__dict__['func']返回的是一个function,但Widget.func是一个unbound method,即Widget.func并不等同于Widget.__dict__['func'],按照前面的类属性的访问顺序,我们可以怀疑,func是一个descriptor,这样才不会走到第2.2这种情况。验证如下:

>>> dir(Widget.__dict__['func'])
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']

属性赋值

  Python的属性赋值(attribute assignment)也会受到descriptor(data descriptor)的影响,同时也会受到__setattr__函数的影响。当然Python中还有一个setattr,setattr(x, 'foobar', 123)等价于x.foobar = 123,二者都叫attribute assignment。

  首先看看__setattr__:

object.__setattr__(self, name, value)
Called when an attribute assignment is attempted. This is called instead of the normal mechanism

  那什么是normal mechanism,简单来说就是x.__dict__['foobar'] = 123,不管'foobar'之前是否是x的属性(当然赋值之后就一定是了)。但是如果‘’foobar‘’是类属性,且是data descriptor,那么回优先调用__set__。我们来看一个例子:

 class MaxValDes(object):
def __init__(self, attr, max_val):
self.attr = attr
self.max_val = max_val def __get__(self, instance, typ):
return instance.__dict__[self.attr] def __set__(self, instance, value):
instance.__dict__[self.attr] = min(self.max_val, value)
print 'MaxValDes __set__', self.attr, instance.__dict__[self.attr] class Widget(object):
a = MaxValDes('a', 10)
def __init__(self):
self.a = 0 # def __setattr__(self, name, value):
# self.__dict__[name] = value
# print 'Widget __setattr__', name, self.__dict__[name] if __name__ == '__main__':
w0 = Widget()
w0.a = 123

  输出如下:

MaxValDes __set__ a 0
MaxValDes __set__ a 10

  可以看到,即使Widget的实例也有一个‘a’属性,但是调用w.a的时候会调用类属性‘a’(一个descriptor)的__set__方法。如果不注释掉第18到第20行,输出如下

Widget __setattr__ a 0
Widget __setattr__ a 123

  可以看到,优先调用Widget 的__setattr__方法。因此:对于属性赋值,obj = Clz(), 那么obj.attr = var,按照这样的顺序:

  (1)如果Clz定义了__setattr__方法,那么调用该方法,否则

  (2)如果“attr”是出现在Clz或其基类的__dict__中, 且attr是data descriptor, 那么调用其__set__方法, 否则

  (3)等价调用obj.__dict__['attr'] = var

references

(2)Object attribute lookup in Python,  http://www.betterprogramming.com/object-attribute-lookup-in-python.html
(3)python __set__ __get__ 等解释, http://blog.csdn.net/huithe/article/details/7484606

python属性查找(attribute lookup)的更多相关文章

  1. python属性查找 深入理解(attribute lookup)

    在Python中,属性查找(attribute lookup)是比较复杂的,特别是涉及到描述符descriptor的时候. 在上一文章末尾,给出了一段代码,就涉及到descriptor与attribu ...

  2. python属性查找

    python中执行obj.attr时,将调用特殊方法obj.__getattribute__('attr'),该方法执行搜索来查找该属性,通常涉及检查特性.查找实例字典.查找类字典以及搜索基类.如果搜 ...

  3. python描述符和属性查找

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

  4. python中类的属性(class attribute)的解释

    python中的类叫 class object,类的实例叫instance object. 类 Class Objects 类拥有两种操作,1.类属性 attribute references 2.实 ...

  5. python 3 属性查找与绑定方法

    1.属性查找 类有两种属性:数据属性和函数属性 (1)类的数据属性是所有对象共享的 #类的数据属性是所有对象共享的,id都一样 class OldboyStudent: school='oldboy' ...

  6. python基础语法20 面向对象5 exec内置函数的补充,元类,属性查找顺序

    exec内置函数的补充 exec: 是一个python内置函数,可以将字符串的代码添加到名称空间中; - 全局名称空间 - 局部名称空间 exec(字符串形式的代码, 全局名称空间, 局部名称空间) ...

  7. python之属性描述符与属性查找规则

    描述符 import numbers class IntgerField: def __get__(self, isinstance, owner): print('获取age') return se ...

  8. Python属性的查找顺序

    属性查找顺序 关于属性描述符请看上文>属性描述符   在梳理属性查找相关知识时,查看了很多的书籍和他人的博客,发现很多讲的过于抽象,并没有一个清晰的流程呈现.特此写下我对于此方面的理解和总结. ...

  9. python面向编程:类继承、继承案例、单继承下属性查找、super方法

    一.类的继承 二.基于继承解决类与类的代码冗余问题 三.在单继承背景下属性的查找 四.super的方法 一.类的继承 1.什么是继承? 在程序中继承是一种新建子类的方法的方式,新创建的类成为子类\派生 ...

随机推荐

  1. switch条件语句规则

  2. Eclipse 快捷键使用

    ctrl+shift+T //查找当前工程下的某个类   实时提示 ctrl+shift+R//查找当前工程下的某个文件   实时提示 ctrl+/添加注释 Ctrl+1 快速修复(最经典的快捷键,就 ...

  3. 如何把程序钉到Windows7任务栏(修正版)

    源:如何把程序钉到Windows7任务栏(修正版) 在CSDN论坛看到有网友提问如何把程序钉到Windows7的任务栏,ccrun(妖哥)对这个问题很感兴趣,于是google了一下,没有找到相关的AP ...

  4. dependency injection(2)

    https://segmentfault.com/a/1190000002424023

  5. 漂亮的HTML表格 - ebirdfighter的日志 - 网易博客

    一个像素边框的表格: Info Header 1 Info Header 2 Info Header 3 Text 1A Text 1B Text 1C Text 2A Text 2B Text 2C ...

  6. Ubuntu“无法解析或打开软件包的列表或是状态文件”的解决办法。_StarSasumi_新浪博客

    Ubuntu"无法解析或打开软件包的列表或是状态文件"的解决办法. (2011-04-30 14:56:14) 转载▼ 标签: ubuntu apt 分类: Ubuntu/Linu ...

  7. Quick Cocos2dx 与 DragonBones

    照着官方的例子试验了一下DragonBone的使用,代码如下: local AnotherScene = class("AnotherScene", function() retu ...

  8. HDU 2859 Phalanx

    简单二维dp.o(n^3)效率过的.不知道有没有o(n^2)的解法. 为了方便点,先左右交换一下. dp[i][j]表示以[i,j]为左上角的最大对称矩阵长度 那么dp[i][j]=min(Max,d ...

  9. 比较全的JavaScript倒计时脚本

    JavaScript倒计时在Web中用得非常广泛,比如常见的团购啊.还有什么值得期待的事情,都可以用到倒计时.现在举了四个例子,比如时间长的倒计时,小时倒计时,最简的倒计时,还有秒表等等,应该可以满足 ...

  10. DM二维码识别库DMDECODER的使用--MFC例程

    DM码和QR码是当今比较主流的二维码,其中QR码容量大,容量密度为16kb,DM码容量要小一点,可在仅仅25mm²的面积上编码30个数字,但是DM码的容错率更高,所以实际的工业生产中经常使用DM码作为 ...