python 隨筆----__dict__

先上一段代碼,來源是github

class Borg(object):
    __shared_state = {}

    def __init__(self):
        self.__dict__ = self.__shared_state
        self.state = 'Init'

    def __str__(self):
        return self.state

class YourBorg(Borg):
    pass

if __name__ == '__main__':
    rm1 = Borg()
    rm2 = Borg()

    rm1.state = 'Idle'
    rm2.state = 'Running'

    print('rm1: {0}'.format(rm1))
    print('rm2: {0}'.format(rm2))

    rm2.state = 'Zombie'

    print('rm1: {0}'.format(rm1))
    print('rm2: {0}'.format(rm2))

    print('rm1 id: {0}'.format(id(rm1)))
    print('rm2 id: {0}'.format(id(rm2)))

    rm3 = YourBorg()

    print('rm1: {0}'.format(rm1))
    print('rm2: {0}'.format(rm2))
    print('rm3: {0}'.format(rm3))

### OUTPUT ###
# rm1: Running
# rm2: Running
# rm1: Zombie
# rm2: Zombie
# rm1 id: 140732837899224
# rm2 id: 140732837899296
# rm1: Init
# rm2: Init
# rm3: Init

上面這一段代碼,乍看挺神奇的,Borg 的各個實例共享了state。實現(xiàn)起來也很巧妙,利用了__dict__。 我們知道,python中__dict__存儲了該對象的一些屬性。類和實例分別擁有自己的__dict__,且實例會共享類的__dict__。

這里有一個我一直以來都搞混的知識點,在__init__ 中聲明的變量 ,以及在方法體之外聲明的變量分別是在哪里。很簡單的測試就能得到,在__init__中,self.xxx = xxx會把變量存在實例的__dict__中,僅會在該實例中能獲取到,而在方法體外聲明的,會在class的__dict__中。下面的簡單的測試。

class MyclassA():

    in_class = {}

    def __init__(self):
        self.in_func = {}

a = MyclassA()


print MyclassA.__dict__  # {'in_class': {}, '__module__': '__main__', '__doc__': None, '__init__': <function __init__ at 0x10eb040c8>}
print a.__dict__ # {'in_func': {}}


有個有意思的問題,調(diào)用obj.key的搜索順序是咋樣的?

attr的搜索順序

這個很有意思,比我想象的要復(fù)雜很多,因為涉及到了descriptor,不僅僅只是查詢實例或者是類的__dict__這么簡單,直接上個結(jié)論

1.如果attr是一個Python自動產(chǎn)生的屬性,找到!(優(yōu)先級非常高!)
2.查找obj.__class__.__dict__,如果attr存在并且是data descriptor,返回data descriptor的__get__方法的結(jié)果,如果沒有繼續(xù)在obj.__class__的父類以及祖先類中尋找data descriptor
3.在obj.__dict__中查找,這一步分兩種情況,第一種情況是obj是一個普通實例,找到就直接返回,找不到進(jìn)行下一步。第二種情況是obj是一個類,依次在obj和它的父類、祖先類的__dict__中查找,如果找到一個descriptor就返回descriptor的__get__方法的結(jié)果,否則直接返回attr。如果沒有找到,進(jìn)行下一步。
4.在obj.__class__.__dict__中查找,如果找到了一個descriptor(插一句:這里的descriptor一定是non-data descriptor,如果它是data descriptor,第二步就找到它了)descriptor的__get__方法的結(jié)果。如果找到一個普通屬性,直接返回屬性值。如果沒找到,進(jìn)行下一步。
5.很不幸,Python終于受不了。在這一步,它raise AttributeError

用代碼解釋上面的過程(這其中包含了descriptor相關(guān)概念)

作者:劉縉
鏈接:https://www.zhihu.com/question/25391709/answer/30634637
來源:知乎
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。

def object_getattr(obj, name):
    'Look for attribute /name/ in object /obj/.'
    # First look in class and base classes.
    v, cls = class_lookup(obj.__class__, name)
    if (v is not None) and hasattr(v, '__get__') and hasattr(v, '__set__'):
        # Data descriptor.  Overrides instance member.
        return v.__get__(obj, cls)
    w = obj.__dict__.get(name)
    if w is not None:
        # Found in object
        return w
    if v is not None:
        if hasattr(v, '__get__'):
            # Function-like descriptor.
            return v.__get__(obj, cls)
        else:
            # Normal data member in class
            return v
    raise AttributeError(obj.__class__, name)
    
 def class_lookup(cls, name): 
     'Look for attribute /name/ in class /cls/ and bases.' 
     v = cls.__dict__.get(name) 
     if v is not None: 
         # found in this class 
         return v, cls 
     # search in base classes 
     for i in cls.__bases__: 
         v, c = class_lookup(i, name) 
         if v is not None: 
             return v, c 
     # not found 
     return None

經(jīng)??吹接腥苏fobj.key = val 等價于obj.__dict__[key] = val,其實根據(jù)我們上面的那個搜索順序,在有descriptor的情況下,會不太一樣。

class DataDescriptor(object):
    def __init__(self, init_value):
        self.value = init_value

    def __get__(self, instance, owner):
        return "DataDescriptor __get__" + str(self.value)

    def __set__(self, instance, value):
        self.value = value
        print "DataDescriptor __set__"

class ClassA(object):
    val_descriptor = DataDescriptor(0)
    val_normal = 0

if __name__ == '__main__':
    a = ClassA()
    print a.val_descriptor # DataDescriptor __get__0
    print a.val_normal # 0
    a.val_descriptor = "has change"
    a.val_normal = "has change"
    print a.val_descriptor  # DataDescriptor __get__has change
    print a.val_normal # has change
    a.__dict__["val_descriptor"] = "change again"
    a.__dict__["val_normal"] = "change again"
    print a.val_descriptor # DataDescriptor __get__has change
    print a.val_normal #change again

重點是最后兩行行,要修改的是一個descriptor 的話,obj.key = val 的方式是而已修改成功的,會調(diào)用descriptor 的__set__ 方法,而直接修改__dict__的方式則會有問題,因為在搜索attr的時候會先查找class的descriptor(對,即便a.__dict__["val_descriptor"] = DataDescriptor("change again")這樣子也是沒用的)。


關(guān)于__dict__,還有一個有趣的點。cls.__dict__ 是一個dictproxy,obj.__dict__ 是一個dict,前者是一個只讀對象,后者是一般的dict。

這意味著,對于一個實例,可以使用obj.__dict__[key] = val的方式去賦值,也可以使用obj.key = value的方式賦值(這種方式會調(diào)用__setattr__方法,會按照attr的搜索順序去搜索),而cls只能使用后者,即必須經(jīng)過__setattr__ ,stackoverflow 上有人解釋說是為了編譯器優(yōu)化。

個人認(rèn)為這種設(shè)置還有另外一種原因,即保護(hù)了descriptor的使用。

看上面那長段的代碼,我們發(fā)現(xiàn),如果一個類中設(shè)置了一個descriptor,后續(xù)沒有任何辦法繞過descriptor的__set__方法。

  • 使用obj.key = val 的方式,毫無以為會調(diào)用descriptor的__set__方法。
  • obj.__dict__[key] = val的方式,會修改實例的__dict__,但是按照搜索的順序會先搜索類的descriptor,所以相當(dāng)于沒有修改
  • cls.key = val,會調(diào)用descriptor的__set__方法
  • 于是只剩下直接修改cls.__dict__,但很可惜,這是一個只讀對象無法修改。

后記

非常的亂,這些東西都是我在看python的設(shè)計模式的時候逐漸發(fā)現(xiàn)問題慢慢搜索的,主要是為了給自己做個記錄,當(dāng)做一個隨筆,不太適合除了我以外的人看... 有空的時候會整理下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,054評論 0 9
  • 憨憨五十載,彈指一揮間。 淡忘青年血,蹉跎秀士歡。 人前常笑臉,夜半自憂煩。 愧教兒孫輩,羞言心意寒。 癡呆難混世...
    花屋主人蕭寒閱讀 779評論 0 6
  • 這是我2016年6月18號的一篇日記 大學(xué)時間已經(jīng)?,歲月時間都很快 當(dāng)年的單純和青澀已然不在,取而代之的是成長和...
    JingErr閱讀 232評論 0 0
  • 醉臥佳人側(cè) 烽火連天雨 一怒為紅顏 掛陣赴國難 問君何為故 亂世覓自由 隨伊化作蝶 同赴楊柳岸
    基督山伯爵和他的城堡閱讀 327評論 0 0

友情鏈接更多精彩內(nèi)容