在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. centos常用配置收集

    配置ntp服务器: # vi /etc/ntp.conf driftfile /var/lib/ntp/driftrestrict -4 default kod notrap nomodifyrest ...

  2. CodeForces 510E Fox And Dinner

    网络流. 原点到偶数连边,容量为2, 奇数到汇点连边,容量为2, 偶数到与之能凑成素数的奇数连边,容量为1 如果奇数个数不等于偶数个数,输出不可能 如果原点到偶数的边不满流,输出不可能 剩下的情况有解 ...

  3. 红黑树(C#)

    红黑树C#算法. 在线javascript演示地址:http://sandbox.runjs.cn/show/2nngvn8w using System; using System.Collectio ...

  4. 高效PHP编程

    1.尽量静态化: 如果一个方法能被静态,那就声明它为静态的,速度可提高1/4,甚至我测试的时候,这个提高了近三倍.   当然了,这个测试方法需要在十万级以上次执行,效果才明显.   其实静态方法和非静 ...

  5. expect ------提供用户名和密码的SSH自动登录脚本

    使用VPN,每次都要在Terminal上重复输入命令: ssh -D port user@host 出来密码提示符后,把复杂的密码拷贝下来,然后粘贴到Terminal,敲回车... 终于忍受不了这样的 ...

  6. windows下egret环境搭建

    作者:zccst 2,下载安装WebStorm(经了解是西欧捷克开发的,欧洲人对审美不够,所以界面不太好看,但是功能很强大) (1)遇到的问题是注册码问题,不过也很容易在网上找到. (2)如何打开已创 ...

  7. NSString之Format

    三点用法总结: 类型转换:基本类型到对象类型 需要调用NSString的fotmat方法 NSString  *location = [NSString  stringWithFormat:@&quo ...

  8. Quick Cocos2dx 场景转换问题

    项目结构是这样子的: 主场景代码是这样子的: local MainScene = class("MainScene", function() return display.newS ...

  9. JavaScriptConvert.SerializeObject转换出错

    The length of the string exceeds the value set on the maxJsonLength property(字符串的长度超过maxjsonlength上设 ...

  10. Powerbuilder编程技巧 如何获取网页的HTML源码

    直接使用的三种方式 1.  PB内部对象 Inet object 2.  API 函数 3.  Ole中的Microsfot Web 游览器对象 一.Inet object: 1.Inet objec ...