属性查找

python set get 解释:

 class T(object):  
    name = 'name'  
    def hello(self):  
        print 'hello'  
t = T()  
使用dir(t)列出t的所有有效属性:
 >>> dir(t)  
    ['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__',  
     '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__',  
     '__repr__', '__setattr__', '__str__', '__weakref__', 'hello', 'name']  
属性可以分为两类,一类是Python自动产生的,如__class__,__hash__等,另一类是我们自定义的,如上面的hello,name。我们只关心自定义属性。
类和实例对象(实际上,Python中一切都是对象,类是type的实例)都有__dict__属性,里面存放它们的自定义属性(对与类,里面还存放了别的东西)。
>>> t.__dict__  
    {}  
>>> T.__dict__  
    <dictproxy object at 0x00CD0FF0>  
>>> dict(T.__dict__)            #由于T.__dict__并没有直接返回dict对象,这里进行转换,以方便观察其中的内容  
    {'__module__': '__main__', 'name': 'name',  
     'hello': <function hello at 0x00CC2470>,  
     '__dict__': <attribute '__dict__' of 'T' objects>,  
     '__weakref__': <attribute '__weakref__' of 'T' objects>, '__doc__': None}  
 >>>   
 有些内建类型,如list和string,它们没有__dict__属性,随意没办法在它们上面附加自定义属性。
到现在为止t.__dict__是一个空的字典,因为我们并没有在t上自定义任何属性,它的有效属性hello和name都是从T得到的。T的__dict__中包含hello和name。当遇到t.name语句时,Python怎么找到t的name属性呢?

首先,Python判断name属性是否是个自动产生的属性,如果是自动产生的属性,就按特别的方法找到这个属性,当然,这里的name不是自动产生的属性,而是我们自己定义的,Python于是到t的__dict__中寻找。还是没找到。
接着,Python找到了t所属的类T,搜索T.__dict__,期望找到name,很幸运,直接找到了,于是返回name的值:字符串‘name’。如果在T.__dict__中还没有找到,Python会接着到T的父类(如果T有父类的话)的__dict__中继续查找。

这不足以解决我们的困惑,因为事情远没有这么简单,上面说的其实是个简化的步骤。
继续上面的例子,对于name属性T.name和T.__dict__['name']是完全一样的。
     >>> T.name  
    'name'  
    >>> T.__dict__['name']  
    'name'  
    >>>   
但是对于hello,情形就有些不同了
     >>> T.hello  
    <unbound method T.hello>  
    >>> T.__dict__['hello']  
    <function hello at 0x00CC2470>  
    >>>   
 可以发现,T.hello是个unbound method。而T.__dict__['hello']是个函数(不是方法)。

推断:方法在类的__dict__中是以函数的形式存在的(方法的定义和函数的定义简直一样,除了要把第一个参数设为self)。那么T.hello得到的应该也是个函数啊,怎么成了unbound method了。

再看看从实例t中访问hello
 >>> t.hello  
<bound method T.hello of <__main__.T object at 0x00CD0E50>>  
>>>  
 是一个bound method。

有意思,按照上面的查找策略,既然在T的__dict__中hello是个函数,那么T.hello和t.hello应该都是同一个函数才对。到底是怎么变成方法的,而且还分为unbound method和bound method。

关于unbound和bound到还好理解,我们不妨先作如下设想:方法是要从实例调用的嘛(指实例方法,classmethod和staticmethod后面讲),如果从类中访问,如T.hello,hello没有和任何实例发生联系,也就是没绑定(unbound)到任何实例上,所以是个unbound,对t.hello的访问方式,hello和t发生了联系,因此是bound。

但从函数<function hello at 0x00CC2470>到方法<unbound method T.hello>的确让人费解。 

一切的魔法都源自今天的主角:descriptor

查找属性时,如obj.attr,如果Python发现这个属性attr有个__get__方法,Python会调用attr的__get__方法,返回__get__方法的返回值,而不是返回attr(这一句话并不准确,我只是希望你能对descriptor有个初步的概念)。

Python中iterator(怎么扯到Iterator了?)是实现了iterator协议的对象,也就是说它实现了下面两个方法__iter__和next()。类似的,descriptor也是实现了某些特定方法的对象。descriptor的特定方法是__get__,__set__和__delete__,其中__set__和__delete__方法是可选的。iterator必须依附某个对象而存在(由对象的__iter__方法返回),descriptor也必须依附对象,作为对象的一个属性,它而不能单独存在。还有一点,descriptor必须存在于类的__dict__中,这句话的意思是只有在类的__dict__中找到属性,Python才会去看看它有没有__get__等方法,对一个在实例的__dict__中找到的属性,Python根本不理会它有没有__get__等方法,直接返回属性本身。descriptor到底是什么呢:简单的说,descriptor是对象的一个属性,只不过它存在于类的__dict__中并且有特殊方法__get__(可能还有__set__和__delete)而具有一点特别的功能,为了方便指代这样的属性,我们给它起了个名字叫descriptor属性。

可能你还是不明白,下面开始用例子说明。

先定义这个类:
     class Descriptor(object):  
        def __get__(self, obj, type=None):  
                return 'get', self, obj, type  
        def __set__(self, obj, val):  
            print 'set', self, obj, val  
        def __delete__(self, obj):  
            print 'delete', self, obj  
 这里__set__和__delete__其实可以不出现,不过为了后面的说明,暂时把它们全写上。

下面解释一下三个方法的参数:

self当然不用说,指的是当前Descriptor的实例。obj值拥有属性的对象。这应该不难理解,前面已经说了,descriptor是对象的稍微有点特殊的属性,这里的obj就是拥有它的对象,要注意的是,如果是直接用类访问descriptor(别嫌啰嗦,descriptor是个属性,直接用类访问descriptor就是直接用类访问类的属性),obj的值是None。type是obj的类型,刚才说过,如果直接通过类访问descriptor,obj是None,此时type就是类本身。

三个方法的意义,假设T是一个类,t是它的一个实例,d是T的一个descriptor属性(牛什么啊,不就是有个__get__方法吗!),value是一个有效值:

读取属性时,如T.d,返回的是d.__get__(None, T)的结果,t.d返回的是d.__get__(t, T)的结果。

设置属性时,t.d = value,实际上调用d.__set__(t, value),T.d = value,这是真正的赋值,T.d的值从此变成value。删除属性和设置属性类似。

下面用例子说明,看看Python中执行是怎么样的:

重新定义我们的类T和实例t
 class T(object):  
    d = Descriptor()  
t = T() 
 d是T的类属性,作为Descriptor的实例,它有__get__等方法,显然,d满足了所有的条件,现在它就是一个descriptor!
 >>> t.d         #t.d,返回的实际是d.__get__(t, T)  
('get', <__main__.Descriptor object at 0x00CD9450>, <__main__.T object at 0x00CD0E50>, <class '__main__.T'>)  
>>> T.d        #T.d,返回的实际是d.__get__(None, T),所以obj的位置为None  
('get', <__main__.Descriptor object at 0x00CD9450>, None, <class '__main__.T'>)  
>>> t.d = 'hello'   #在实例上对descriptor设置值。要注意的是,现在显示不是返回值,而是__set__方法中  
                               print语句输出的。  
set <__main__.Descriptor object at 0x00CD9450> <__main__.T object at 0x00CD0E50> hello  
>>> t.d         #可见,调用了Python调用了__set__方法,并没有改变t.d的值  
('get', <__main__.Descriptor object at 0x00CD9450>, <__main__.T object at 0x00CD0E50>, <class '__main__.T'>)  
>>> T.d = 'hello'   #没有调用__set__方法  
>>> T.d                #确实改变了T.d的值  
'hello'  
>>> t.d               #t.d的值也变了,这可以理解,按我们上面说的属性查找策略,t.d是从T.__dict__中得到的  
                              T.__dict__['d']的值是'hello',t.d当然也是'hello'  
'hello' 
 data descriptor和non-data descriptor

象上面的d,同时具有__get__和__set__方法,这样的descriptor叫做data descriptor,如果只有__get__方法,则叫做non-data descriptor。容易想到,由于non-data descriptor没有__set__方法,所以在通过实例对属性赋值时,例如上面的t.d = 'hello',不会再调用__set__方法,会直接把t.d的值变成'hello'吗?口说无凭,实例为证:
     class Descriptor(object):  
        def __get__(self, obj, type=None):  
                return 'get', self, obj, type  
    class T(object):  
           d = Descriptor()  
    t = T()  
 >>> t.d  
('get', <__main__.Descriptor object at 0x00CD9550>, <__main__.T object at 0x00CD9510>, <class '__main__.T'>)  
>>> t.d = 'hello'  
>>> t.d  
'hello'  
>>>  
 在实例上对non-data descriptor赋值隐藏了实例上的non-data descriptor!

 

是时候坦白真正详细的属性查找策略 了,对于obj.attr(注意:obj可以是一个类):

1.如果attr是一个Python自动产生的属性,找到!(优先级非常高!)

2.查找obj.__class__.__dict__,如果attr存在并且是data descriptor,返回data descriptor的__get__方法的结果,如果没有继续在obj.__class__的父类以及祖先类中寻找data descriptor

3.在obj.__dict__中查找,这一步分两种情况,第一种情况是obj是一个普通实例,找到就直接返回,找不到进行下一步。第二种情况是obj是一个类,依次在obj和它的父类、祖先类的__dict__中查找,如果找到一个descriptor就返回descriptor的__get__方法的结果,否则直接返回attr。如果没有找到,进行下一步。

4.在obj.__class__.__dict__中查找,如果找到了一个descriptor(插一句:这里的descriptor一定是non-data descriptor,如果它是data descriptor,第二步就找到它了)descriptor的__get__方法的结果。如果找到一个普通属性,直接返回属性值。如果没找到,进行下一步。

5.很不幸,Python终于受不了。在这一步,它raise AttributeError

 

利用这个,我们简单分析一下上面为什么要强调descriptor要在类中才行。我们感兴趣的查找步骤是2,3,4。第2步和第4步都是在类中查找。对于第3步,如果在普通实例中找到了,直接返回,没有判断它有没有__get__()方法。

 

对属性赋值时的查找策略 ,对于obj.attr = value

1.查找obj.__class__.__dict__,如果attr存在并且是一个data descriptor,调用attr的__set__方法,结束。如果不存在,会继续到obj.__class__的父类和祖先类中查找,找到 data descriptor则调用其__set__方法。没找到则进入下一步。

2.直接在obj.__dict__中加入obj.__dict__['attr'] = value

 

顺便分析下为什么在实例上对non-data descriptor赋值隐藏了实例上的non-data descriptor。

接上面的non-data descriptor例子
     >>> t.__dict__  
    {'d': 'hello'}  
在t的__dict__里出现了d这个属性。根据对属性赋值的查找策略,第1步,确实在t.__class__.__dict__也就是T.__dict__中找到了属性d,但它是一个non-data descriptor,不满足data descriptor的要求,进入第2步,直接在t的__dict__属性中加入了属性和属性值。当获取t.d时,执行查找策略,第2步在T.__dict__中找到了d,但它是non-data descriptor,步满足要求,进行第3步,在t的__dict__中找到了d,直接返回了它的值'hello'。

 

说了这么半天,还没到函数和方法!

算了,明天在说吧

简单提一下,所有的函数(方法)都有__get__方法,当它们在类的__dict__中是,它们就是non-data descriptor。

python descriptor 详解:
正文
descriptor简介

在python中,如果一个新式类定义了__get__, __set__, __delete__方法中的一个或者多个,那么称之为descriptor。descriptor有分为data descriptor与non-data descriptor, descriptor通常用来改变默认的属性访问(attribute lookup),这部分会在下一遍文章中介绍。注意 ,descriptor的实例是一定是的属性(class attribute)。

  这三个特殊的函数签名是这样的:

  object.__get__(self, instance, owner):return value

  object.__set__(self, instance, value):return None

  object.__delete__(self, instance): return None

  
  下面的代码展示了简单的用法:
# -*- coding: utf-8 -*-
class Des(object):
    def __init__(self, init_value):
        self.value = init_value
 
    def __get__(self, instance, typ):
        print('call __get__', instance, typ)
        return self.value
 
    def __set__(self, instance, value):
        print ('call __set__', instance, value)
        self.value = value
 
    def __delete__(self, instance):
        print ('call __delete__', instance)
 
class Widget(object):
    t = Des(1)
 
def main():
    w = Widget()
    print type(w.t)
    w.t = 1
    print w.t, Widget.t
    del w.t
 
if __name__=='__main__':
    main()

运行结果如下:

    ('call __get__', <__main__.Widget object at 0x02868570>, <class '__main__.Widget'>)
    <type 'int'>

    ('call __set__', <__main__.Widget object at 0x02868570>, 1)

    ('call __get__', <__main__.Widget object at 0x02868570>, <class '__main__.Widget'>)
    1 ('call __get__', None, <class '__main__.Widget'>)

    1

    ('call __delete__', <__main__.Widget object at 0x02868570>)

从输出结果可以看到,对于这个三个特殊函数,形参instance是descriptor实例所在的类的实例(w), 而形参owner就是这个类(widget)
  w.t 等价于 Pro.__get__(t, w, Widget).而Widget.t 等价于 Pro.__get__(t, None, Widget)

descriptor注意事项:
需要注意的是, descriptor的实例一定是类的属性,因此使用的时候需要自行区分实例。比如下面这个例子,我们需要保证以下属性不超过一定的阈值。

class MaxValDes(object):
    def __init__(self, inti_val, max_val):
        self.value = inti_val
        self.max_val = max_val

    def __get__(self, instance, typ):
        return self.value

    def __set__(self, instance, value):
        self.value= min(self.max_val, value)

class Widget(object):
    a = MaxValDes(0, 10)

if __name__ == '__main__':
    w0 = Widget()
    print 'inited w0', w0.a
    w0.a = 123
    print 'after set w0',w0.a
    w1 = Widget()
    print 'inited w1', w1.a
代码很简单,我们通过MaxValDes这个descriptor来保证属性的值不超过一定的范围。运行结果如下:

    inited w0 0
    after set w0 10
    inited w1 10
可以看到,对w0.a的赋值符合预期,但是w1.a的值却不是0,而是同w0.a一样。这就是因为,a是类Widget的类属性, Widget的实例并没有'a'这个属性,可以通过__dict__查看。

那么要怎么修改才符合预期呢,看下面的代码:
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)

class Widget(object):
    a = MaxValDes('a', 10)
    b = MaxValDes('b', 12)
    def __init__(self):
        self.a = 0
        self.b = 1

if __name__ == '__main__':
    w0 = Widget()
    print 'inited w0', w0.a, w0.b
    w0.a = 123
    w0.b = 123
    print 'after set w0',w0.a, w0.b

    w1 = Widget()
    print 'inited w1', w1.a, w1.b

运行结果如下:
    inited w0 0 1
    after set w0 10 12
    inited w0 0 1
可以看到,运行结果比较符合预期,w0、w1两个实例互不干扰。上面的代码中有两点需要注意:

  第一:第7、10行都是通过instance.__dict__来取值、赋值,而不是调用getattr、setattr,否则会递归调用,死循环。

  第二:现在类和类的实例都拥有‘a’属性,不过w0.a调用的是类属性‘a',具体原因参见下一篇文章

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

python getattr 巧妙应用

9_JZL@{C$DDD29IAI%TT$FG.png

Python概念-上下文管理协议中的enterexit

QQ截图20180613111132.png

对比Python中getattrgetattribute获取属性的用法

python魔法方法:getattr,setattr,getattribute

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • Python 面向对象Python从设计之初就已经是一门面向对象的语言,正因为如此,在Python中创建一个类和对...
    顺毛阅读 4,212评论 4 16
  • 1.该花的钱要花,该享受的要享受,该捐助的要捐助;趁我们还能吃能动,不要舍不得。 2.不必对死后的事考虑太多,因为...
    白头布衣阅读 256评论 0 0
  • 三月,日光温柔,微风正好。
    我如何能静阅读 248评论 0 2
  • 夏至未至,燥然心头。 何以解忧?唯有画画。 每一颗水果在光影里闪耀着诱人的色泽,就像,就像……诱惑了夏娃的那颗禁果...
    清清summer阅读 318评论 1 3
  • 第一次用Pad画的,大家喜欢吗?没有Applepencile 纯手指啊……求打赏pencile……
    九点半月光阅读 307评论 5 5