python 描述符總結(jié)

先以這篇文章來看
http://python.jobbole.com/81899/
以下是對原文的cp欢揖,并對其分析,環(huán)境是在Python2下面

Python中包含了許多內(nèi)建的語言特性挫掏,它們使得代碼簡潔且易于理解撵彻。這些特性包括列表/集合/字典推導(dǎo)式当悔,屬性(property)、以及裝飾器(decorator)。對于大部分特性來說镶摘,這些“中級”的語言特性有著完善的文檔,并且易于學(xué)習(xí)岳守。

但是這里有個例外凄敢,那就是描述符。至少對于我來說湿痢,描述符是Python語言核心中困擾我時間最長的一個特性涝缝。這里有幾點原因如下:

1.  有關(guān)描述符的官方文檔相當(dāng)難懂扑庞,而且沒有包含優(yōu)秀的示例告訴你為什么需要編寫描述符(我得為Raymond Hettinger辯護(hù)一下,他寫的其他主題的Python文章和視頻對我的幫助還是非常大的)
2.  編寫描述符的語法顯得有些怪異
3.  自定義描述符可能是Python中用的最少的特性拒逮,因此你很難在開源項目中找到優(yōu)秀的示例
4.為什么需要描述符罐氨?(因為Python是一個動態(tài)類型解釋性語言,不像C/C++等靜態(tài)編譯型語言滩援,數(shù)據(jù)類型在編譯時便可以進(jìn)行驗證栅隐,而Python中必須添加額外的類型檢查邏輯代碼才能做到這一點,這就是描述符的初衷玩徊。比如租悄,有一個測試類Test,其具有一個類屬性name恩袱。)

但是一旦你理解了之后泣棋,描述符的確還是有它的應(yīng)用價值的。這篇文章告訴你描述符可以用來做什么畔塔,以及為什么應(yīng)該引起你的注意潭辈。

一句話概括:描述符就是可重用的屬性

在這里我要告訴你:從根本上講,描述符就是可以重復(fù)使用的屬性俩檬。也就是說萎胰,描述符可以讓你編寫這樣的代碼:

f = Foo()
b = f.bar
f.bar = 'c'
del f.bar

而在解釋器執(zhí)行上述代碼時,當(dāng)發(fā)現(xiàn)你試圖訪問屬性(b = f.bar)棚辽、對屬性賦值(f.bar = 'c')或者刪除一個實例變量的屬性(del f.bar)時技竟,就會去調(diào)用自定義的方法。

讓我們先來解釋一下為什么把對函數(shù)的調(diào)用偽裝成對屬性的訪問是大有好處的屈藐。

property——把函數(shù)調(diào)用偽裝成對屬性的訪問

想象一下你正在編寫管理電影信息的代碼榔组。你最后寫好的Movie類可能看上去是這樣的:

class Movie(object):
    def __init__(self, title, rating, runtime, budget, gross):
        self.title = title
        self.rating = rating
        self.runtime = runtime
        self.budget = budget
        self.gross = gross

    def profit(self):
        return self.gross - self.budget

你開始在項目的其他地方使用這個類,但是之后你意識到:如果不小心給電影打了負(fù)分怎么辦联逻?你覺得這是錯誤的行為搓扯,希望Movie類可以阻止這個錯誤。 你首先想到的辦法是將Movie類修改為這樣:

class Movie(object):
    def __init__(self, title, rating, runtime, budget, gross):
        self.title = title
        self.rating = rating
        self.runtime = runtime
        self.gross = gross
        if budget < 0:
            raise ValueError("Negative value not allowed: %s" % budget)
        self.budget = budget

    def profit(self):
        return self.gross - self.budget

但這行不通包归。因為其他部分的代碼都是直接通過Movie.budget來賦值的——這個新修改的類只會在__init__方法中捕獲錯誤的數(shù)據(jù)锨推,但對于已經(jīng)存在的類實例就無能為力了。如果有人試著運行m.budget = -100公壤,那么誰也沒法阻止换可。作為一個Python程序員同時也是電影迷,你該怎么辦厦幅?

幸運的是沾鳄,Python的property解決了這個問題。如果你從未見過property的用法确憨,下面是一個示例:

class Movie(object):
    def __init__(self, title, rating, runtime, budget, gross):
        self._budget = None

        self.title = title
        self.rating = rating
        self.runtime = runtime
        self.gross = gross
        self.budget = budget

    @property
    def budget(self):
        return self._budget

    @budget.setter
    def budget(self, value):
        if value < 0:
            raise ValueError("Negative value not allowed: %s" % value)
        self._budget = value

    def profit(self):
        return self.gross - self.budget

m = Movie('Casablanca', 97, 102, 964000, 1300000)
print m.budget       # calls m.budget(), returns result
try:
    m.budget = -100  # calls budget.setter(-100), and raises ValueError
except ValueError:
    print "Woops. Not allowed"

964000
Woops. Not allowed

我們用@property裝飾器指定了一個getter方法译荞,用@budget.setter裝飾器指定了一個setter方法瓤的。當(dāng)我們這么做時,每當(dāng)有人試著訪問budget屬性吞歼,Python就會自動調(diào)用相應(yīng)的getter/setter方法圈膏。比方說,當(dāng)遇到m.budget = value這樣的代碼時就會自動調(diào)用budget.setter篙骡。

花點時間來欣賞一下Python這么做是多么的優(yōu)雅:如果沒有property本辐,我們將不得不把所有的實例屬性隱藏起來,提供大量顯式的類似get_budgetset_budget方法医增。像這樣編寫類的話慎皱,使用起來就會不斷的去調(diào)用這些getter/setter方法,這看起來就像臃腫的Java代碼一樣叶骨。更糟的是茫多,如果我們不采用這種編碼風(fēng)格,直接對實例屬性進(jìn)行訪問忽刽。那么稍后就沒法以清晰的方式增加對非負(fù)數(shù)的條件檢查——我們不得不重新創(chuàng)建set_budget方法天揖,然后搜索整個工程中的源代碼,將m.budget = value這樣的代碼替換為m.set_budget(value)跪帝。太蛋疼了=癫病!

因此伞剑,property讓我們將自定義的代碼同變量的訪問/設(shè)定聯(lián)系在了一起斑唬,同時為你的類保持一個簡單的訪問屬性的接口。干得漂亮黎泣!

property的不足

對property來說恕刘,最大的缺點就是它們不能重復(fù)使用。舉個例子抒倚,假設(shè)你想為rating褐着,runtime和gross這些字段也添加非負(fù)檢查。下面是修改過的新類:

class Movie(object):
    def __init__(self, title, rating, runtime, budget, gross):
        self._rating = None
        self._runtime = None
        self._budget = None
        self._gross = None

        self.title = title
        self.rating = rating
        self.runtime = runtime
        self.gross = gross
        self.budget = budget

    #nice
    @property
    def budget(self):
        return self._budget

    @budget.setter
    def budget(self, value):
        if value < 0:
            raise ValueError("Negative value not allowed: %s" % value)
        self._budget = value

    #ok    
    @property
    def rating(self):
        return self._rating

    @rating.setter
    def rating(self, value):
        if value < 0:
            raise ValueError("Negative value not allowed: %s" % value)
        self._rating = value

    #uhh...
    @property
    def runtime(self):
        return self._runtime

    @runtime.setter
    def runtime(self, value):
        if value < 0:
            raise ValueError("Negative value not allowed: %s" % value)
        self._runtime = value        

    #is this forever?
    @property
    def gross(self):
        return self._gross

    @gross.setter
    def gross(self, value):
        if value < 0:
            raise ValueError("Negative value not allowed: %s" % value)
        self._gross = value        

    def profit(self):
        return self.gross - self.budget

可以看到代碼增加了不少托呕,但重復(fù)的邏輯也出現(xiàn)了不少含蓉。雖然property可以讓類從外部看起來接口整潔漂亮,但是卻做不到內(nèi)部同樣整潔漂亮项郊。

描述符登場(最終的大殺器)

這就是描述符所解決的問題馅扣。描述符是property的升級版,允許你為重復(fù)的property邏輯編寫單獨的類來處理呆抑。下面的示例展示了描述符是如何工作的(現(xiàn)在還不必?fù)?dān)心NonNegative類的實現(xiàn)):

from weakref import WeakKeyDictionary

class NonNegative(object):
    """A descriptor that forbids negative values"""
    def __init__(self, default):
        self.default = default
        self.data = WeakKeyDictionary()

    def __get__(self, instance, owner):
        # we get here when someone calls x.d, and d is a NonNegative instance
        # instance = x
        # owner = type(x)
        return self.data.get(instance, self.default)

    def __set__(self, instance, value):
        # we get here when someone calls x.d = val, and d is a NonNegative instance
        # instance = x
        # value = val
        if value < 0:
            raise ValueError("Negative value not allowed: %s" % value)
        self.data[instance] = value

class Movie(object):

    #always put descriptors at the class-level
    rating = NonNegative(0)
    runtime = NonNegative(0)
    budget = NonNegative(0)
    gross = NonNegative(0)

    def __init__(self, title, rating, runtime, budget, gross):
        self.title = title
        self.rating = rating
        self.runtime = runtime
        self.budget = budget
        self.gross = gross

    def profit(self):
        return self.gross - self.budget

m = Movie('Casablanca', 97, 102, 964000, 1300000)
print m.budget  # calls Movie.budget.__get__(m, Movie)
m.rating = 100  # calls Movie.budget.__set__(m, 100)
try:
    m.rating = -1   # calls Movie.budget.__set__(m, -100)
except ValueError:
    print "Woops, negative value"

964000
Woops, negative value

這里引入了一些新的語法岂嗓,我們一條條的來看:

NonNegative是一個描述符對象汁展,因為它定義了__get__鹊碍,__set____delete__方法
python3.6還新增了__set_name__方法厌殉。

Movie類現(xiàn)在看起來非常清晰。我們在類的層面上創(chuàng)建了4個描述符侈咕,把它們當(dāng)做普通的實例屬性公罕。顯然,描述符在這里為我們做非負(fù)檢查耀销。

訪問描述符

當(dāng)解釋器遇到print(m.buget)時楼眷,它就會把budget當(dāng)作一個帶有__get__方法的描述符,調(diào)用Movie.budget.__get__方法并將方法的返回值打印出來熊尉,而不是直接傳遞m.budget來打印罐柳。這和你訪問一個property相似,Python自動調(diào)用一個方法狰住,同時返回結(jié)果张吉。
為什么會這樣呢,它為什么會去調(diào)用Movie.budget.__get__方法呢? 這里涉及到描述符的定義和解釋器對屬性調(diào)用的優(yōu)先級催植,具體如下:

描述符指的是實現(xiàn)了描述符協(xié)議的特殊的類肮蛹,三個描述符協(xié)議指的是` __get__ `, `__set__` ,` __delete__ `以及 `Python 3.6` 中
新增的` __set_name__ `方法,其中實現(xiàn)了` __get__ 以及 __set__ / __delete__ / __set_name__ `的是
`Data descriptors`(數(shù)據(jù)描述符创南,可以理解為`可讀寫`) 伦忠,而`只`實現(xiàn)了 `__get__ `的是 `Non-Data descriptor`(非數(shù)據(jù)描述符,可以理解為`只讀`) 稿辙。
那么有什么區(qū)別呢昆码,前面說了, 我們?nèi)绻{(diào)用一個屬性邻储,那么其順序是`優(yōu)先從實例`的 `__dict__ `里查找未桥,然后如果沒有查找到的話,那么一次查詢類字典芥备,
父類字典冬耿,直到徹底查不到為止。 但是萌壳,這里沒有考慮描述符的因素進(jìn)去亦镶,如果將描述符因素考慮進(jìn)去,那么正確的表述應(yīng)該是我們?nèi)绻{(diào)用一個屬性袱瓮,
那么其順序是優(yōu)先從實例的 `__dict__ `里查找缤骨,然后如果沒有查找到的話,那么一次查詢類字典尺借,父類字典绊起,直到徹底查不到為止。
其中如果在類實例字典中的該屬性是一個 Data descriptors 燎斩,那么無論實例字典中存在該屬性與否虱歪,
無條件走描述符協(xié)議進(jìn)行調(diào)用蜂绎,在類實例字典中的該屬性是一個 Non-Data descriptors ,
那么優(yōu)先調(diào)用實例字典中的屬性值而不觸發(fā)描述符協(xié)議笋鄙,如果實例字典中不存在該屬性值师枣,
那么觸發(fā) Non-Data descriptor 的描述符協(xié)議∠袈洌回到之前的問題践美,我們即使在` __set__` 將具體的屬性
寫入實例字典中,但是由于類字典中存在著 Data descriptors 找岖,因此陨倡,我們在調(diào)用 math 屬性時,依舊會觸發(fā)描述符協(xié)議许布。

__get__接收2個參數(shù):一個是點號左邊的實例對象(在這里玫膀,就是m.budget中的m),另一個是這個實例的類型(Movie)爹脾。在一些Python文檔中帖旨,Movie被稱作描述符的所有者(owner)。如果我們需要訪問Movie.budget灵妨,Python將會調(diào)用Movie.budget.__get__(None, Movie)解阅。可以看到泌霍,第一個參數(shù)要么是所有者的實例货抄,要么是None。這些輸入?yún)?shù)可能看起來很怪朱转,但是這里它們告訴了你描述符屬于哪個對象的一部分蟹地。當(dāng)我們看到NonNegative類的實現(xiàn)時這一切就合情合理了。

對描述符賦值

當(dāng)解釋器看到m.rating = 100時藤为,Python識別出rating是一個帶有__set__方法的描述符怪与,于是就調(diào)用Movie.rating.__set__(m, 100)。和__get__一樣缅疟,__set__的第一個參數(shù)是點號左邊的類實例(m.rating = 100中的m)分别。第二個參數(shù)是所賦的值(100)。

刪除描述符

為了說明的完整存淫,這里提一下刪除耘斩。如果你調(diào)用del m.budget,Python就會調(diào)用Movie.budget.__delete__(m)桅咆。

NonNegative類是如何工作的括授?

帶著前面的困惑,我們終于要揭示NonNegative類是如何工作的了。每個NonNegative的實例都維護(hù)著一個字典荚虚,其中保存著所有者實例和對應(yīng)數(shù)據(jù)的映射關(guān)系薛夜。當(dāng)我們調(diào)用m.budget時,__get__方法會查找與m相關(guān)聯(lián)的數(shù)據(jù)曲管,并返回這個結(jié)果(如果這個值不存在,則會返回一個默認(rèn)值)硕糊。__set__采用的方式相同院水,但是這里會包含額外的非負(fù)檢查。我們使用WeakKeyDictionary來取代普通的字典以防止內(nèi)存泄露——我們可不想僅僅因為它在描述符的字典中就讓一個無用的實例一直存活著简十。

使用描述符會有一點別扭檬某。因為它們作用于類的層次上,每一個類實例都共享同一個描述符螟蝙。這就意味著對不同的實例對象而言恢恼,描述符不得不手動地管理不同的狀態(tài),同時需要顯式的將類實例作為第一個參數(shù)準(zhǔn)確傳遞給__get__胰默、__set__以及__delete__方法场斑。

我希望這個例子解釋清楚了描述符可以用來做什么——它們提供了一種方法將property的邏輯隔離到單獨的類中來處理。如果你發(fā)現(xiàn)自己正在不同的property之間重復(fù)著相同的邏輯牵署,那么本文也許會成為一個線索供你思考為何用描述符重構(gòu)代碼是值得一試的漏隐。

秘訣和陷阱

把描述符放在類的層次上(class level)

為了讓描述符能夠正常工作,它們必須定義在類的層次上奴迅。如果你不這么做青责,那么Python無法自動為你調(diào)用getset方法。

class Broken(object):
    y = NonNegative(5)
    def __init__(self):
        self.x = NonNegative(0)  # NOT a good descriptor

b = Broken()
print "X is %s, Y is %s" % (b.x, b.y)

X is <__main__.NonNegative object at 0x10432c250>, Y is 5

可以看到取具,訪問類層次上的描述符y可以自動調(diào)用get脖隶。但是訪問實例層次上的描述符x只會返回描述符本身,真是魔法一般的存在啊暇检。

確保實例的數(shù)據(jù)只屬于實例本身

你可能會像這樣編寫NonNegative描述符:

class BrokenNonNegative(object):
    def __init__(self, default):
        self.value = default

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

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError("Negative value not allowed: %s" % value)
        self.value = value

class Foo(object):
    bar = BrokenNonNegative(5) 

f = Foo()
try:
    f.bar = -1
except ValueError:
    print "Caught the invalid assignment"

Caught the invalid assignment

這么做看起來似乎能正常工作产阱。但這里的問題就在于所有Foo的實例都共享相同的bar,這會產(chǎn)生一些令人痛苦的結(jié)果:

class Foo(object):
    bar = BrokenNonNegative(5) 

f = Foo()
g = Foo()

print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)
print "Setting f.bar to 10"
f.bar = 10
print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)  #ouch
f.bar is 5
g.bar is 5
Setting f.bar to 10
f.bar is 10
g.bar is 10

這就是為什么我們要在NonNegative中使用數(shù)據(jù)字典的原因块仆。getset的第一個參數(shù)告訴我們需要關(guān)心哪一個實例心墅。NonNegative使用這個參數(shù)作為字典的key,為每一個Foo實例單獨保存一份數(shù)據(jù)榨乎。

class Foo(object):
    bar = NonNegative(5)

f = Foo()
g = Foo()
print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)
print "Setting f.bar to 10"
f.bar = 10
print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)  #better
f.bar is 5
g.bar is 5
Setting f.bar to 10
f.bar is 10
g.bar is 5

這就是描述符最令人感到別扭的地方(坦白的說怎燥,我不理解為什么Python不讓你在實例的層次上定義描述符,并且總是需要將實際的處理分發(fā)給getset蜜暑。這么做行不通一定是有原因的)

注意不可哈希的描述符所有者

NonNegative類使用了一個字典來單獨保存專屬于實例的數(shù)據(jù)铐姚。這個一般來說是沒問題的,除非你用到了不可哈希(unhashable)的對象:

class MoProblems(list):  #you can't use lists as dictionary keys
    x = NonNegative(5)

m = MoProblems()
print m.x  # womp womp

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-8-dd73b177bd8d> in <module>()
      3 
      4 m = MoProblems()
----> 5 print m.x  # womp womp

<ipython-input-3-6671804ce5d5> in __get__(self, instance, owner)
      9         # instance = x
     10         # owner = type(x)
---> 11         return self.data.get(instance, self.default)
     12 
     13     def __set__(self, instance, value):

TypeError: unhashable type: 'MoProblems'

因為MoProblems的實例(list的子類)是不可哈希的,因此它們不能為MoProblems.x用做數(shù)據(jù)字典的key隐绵。有一些方法可以規(guī)避這個問題之众,但是都不完美。最好的方法可能就是給你的描述符加標(biāo)簽了依许。

class Descriptor(object):

    def __init__(self, label):
        self.label = label

    def __get__(self, instance, owner):
        print '__get__', instance, owner
        return instance.__dict__.get(self.label)

    def __set__(self, instance, value):
        print '__set__'
        instance.__dict__[self.label] = value

class Foo(list):
    x = Descriptor('x')
    y = Descriptor('y')

f = Foo()
f.x = 5
print f.x

__set__
__get__ [] <class '__main__.Foo'>
5

這種方法依賴于Python的方法解析順序(即棺禾,MRO)。我們給Foo中的每個描述符加上一個標(biāo)簽名峭跳,名稱和我們賦值給描述符的變量名相同膘婶,比如x = Descriptor(‘x’)。之后蛀醉,描述符將特定于實例的數(shù)據(jù)保存在f.dict[‘x’]中悬襟。這個字典條目通常是當(dāng)我們請求f.x時Python給出的返回值。然而拯刁,由于Foo.x 是一個描述符脊岳,Python不能正常的使用f.dict[‘x’],但是描述符可以安全的在這里存儲數(shù)據(jù)垛玻。只是要記住割捅,不要在別的地方也給這個描述符添加標(biāo)簽。
這個方式的做法就是python3.6中對描述符新增的__set_name__特殊方法所實現(xiàn)的
如下有個小例子:

import weakref

class WeakAttribute:

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        instance.__dict__[self.name] = value

    # this is the new initializer:
    def __set_name__(self, owner, name):
        print('name', name)
        self.name = name

class TreeNode:
    parent = WeakAttribute()
    def __init__(self,parent):
        self.parent = parent

t = TreeNode(12)
print(t.parent)

結(jié)果如下:
name parent
12
可以看到給添加了一個名字和屬性名一樣
class Foo(object):
    x = Descriptor('y')

f = Foo()
f.x = 5
print f.x

f.y = 4    #oh no!
print f.x
__set__
__get__ <__main__.Foo object at 0x10432c810> <class '__main__.Foo'>
5
__get__ <__main__.Foo object at 0x10432c810> <class '__main__.Foo'>
4

我不喜歡這種方式帚桩,因為這樣的代碼很脆弱也有很多微妙之處棺牧。但這個方法的確很普遍,可以用在不可哈希的所有者類上朗儒。David Beazley在他的中用到了這個方法颊乘。

在元類中使用帶標(biāo)簽的描述符

由于描述符的標(biāo)簽名和賦給它的變量名相同,所以有人使用元類來自動處理這個簿記(bookkeeping)任務(wù)醉锄。

class Descriptor(object):
    def __init__(self):
        #notice we aren't setting the label here
        self.label = None

    def __get__(self, instance, owner):
        print '__get__. Label = %s' % self.label
        return instance.__dict__.get(self.label, None)

    def __set__(self, instance, value):
        print '__set__'
        instance.__dict__[self.label] = value

class DescriptorOwner(type):
    def __new__(cls, name, bases, attrs):
        # find all descriptors, auto-set their labels
        for n, v in attrs.items():
            if isinstance(v, Descriptor):
                v.label = n
        return super(DescriptorOwner, cls).__new__(cls, name, bases, attrs)

class Foo(object):
    __metaclass__ = DescriptorOwner
    x = Descriptor()

f = Foo()
f.x = 10
print f.x

__set__
__get__. Label = x
10

我不會去解釋有關(guān)元類的細(xì)節(jié)——參考文獻(xiàn)中David Beazley已經(jīng)在他的文章中解釋的很清楚了乏悄。 需要指出的是元類自動的為描述符添加標(biāo)簽,并且和賦給描述符的變量名字相匹配恳不。

盡管這樣解決了描述符的標(biāo)簽和變量名不一致的問題檩小,但是卻引入了復(fù)雜的元類。雖然我很懷疑烟勋,但是你可以自行判斷這么做是否值得规求。

訪問描述符的方法

描述符僅僅是類,也許你想要為它們增加一些方法卵惦。舉個例子阻肿,描述符是一個用來回調(diào)property的很好的手段。比如我們想要一個類的某個部分的狀態(tài)發(fā)生變化時就立刻通知我們沮尿。下面的大部分代碼是用來做這個的:

class CallbackProperty(object):
    """A property that will alert observers when upon updates"""
    def __init__(self, default=None):
        self.data = WeakKeyDictionary()
        self.default = default
        self.callbacks = WeakKeyDictionary()

    def __get__(self, instance, owner):
        return self.data.get(instance, self.default)

    def __set__(self, instance, value):        
        for callback in self.callbacks.get(instance, []):
            # alert callback function of new value
            callback(value)
        self.data[instance] = value

    def add_callback(self, instance, callback):
        """Add a new function to call everytime the descriptor updates"""
        #but how do we get here?!?!
        if instance not in self.callbacks:
            self.callbacks[instance] = []
        self.callbacks[instance].append(callback)

class BankAccount(object):
    balance = CallbackProperty(0)

def low_balance_warning(value):
    if value < 100:
        print "You are poor"

ba = BankAccount()

# will not work -- try it
#ba.balance.add_callback(ba, low_balance_warning)

這是一個很有吸引力的模式——我們可以自定義回調(diào)函數(shù)用來響應(yīng)一個類中的狀態(tài)變化丛塌,而且完全無需修改這個類的代碼。這樣做可真是替人分憂解難呀。現(xiàn)在赴邻,我們所要做的就是調(diào)用ba.balance.add_callback(ba, low_balance_warning)印衔,以使得每次balance變化時low_balance_warning都會被調(diào)用。

但是我們是如何做到的呢姥敛?當(dāng)我們試圖訪問它們時奸焙,描述符總是會調(diào)用get。就好像add_callback方法是無法觸及的一樣彤敛!其實關(guān)鍵在于利用了一種特殊的情況与帆,即,當(dāng)從類的層次訪問時臊泌,get方法的第一個參數(shù)是None鲤桥。

class CallbackProperty(object):
    """A property that will alert observers when upon updates"""
    def __init__(self, default=None):
        self.data = WeakKeyDictionary()
        self.default = default
        self.callbacks = WeakKeyDictionary()

    def __get__(self, instance, owner):
        if instance is None:
            return self        
        return self.data.get(instance, self.default)

    def __set__(self, instance, value):
        for callback in self.callbacks.get(instance, []):
            # alert callback function of new value
            callback(value)
        self.data[instance] = value

    def add_callback(self, instance, callback):
        """Add a new function to call everytime the descriptor within instance updates"""
        if instance not in self.callbacks:
            self.callbacks[instance] = []
        self.callbacks[instance].append(callback)

class BankAccount(object):
    balance = CallbackProperty(0)

def low_balance_warning(value):
    if value < 100:
        print "You are now poor"

ba = BankAccount()
BankAccount.balance.add_callback(ba, low_balance_warning)

ba.balance = 5000
print "Balance is %s" % ba.balance
ba.balance = 99
print "Balance is %s" % ba.balance
Balance is 5000
You are now poor
Balance is 99

好了揍拆, 以上說了這么多渠概,我們來總結(jié)一下

  • 1、成為(數(shù)據(jù)/非數(shù)據(jù))描述符要實現(xiàn)哪些特殊方法嫂拴?(__get__,__set__,__delete__,__set_name__)
  • 2播揪、描述符只有定義在類變量中
  • 3、為了減少內(nèi)存泄露筒狠,盡量使用弱引用的字典來存儲實例屬性
  • 4猪狈、描述符就是像其名字一樣對屬性進(jìn)行相應(yīng)的描述,好比顧客想要一件紅色的外套辩恼,那么紅色就是對這個外套的描述雇庙,如果是其他的顏色就說明不是客戶想要的,從而對屬性進(jìn)行條件限制灶伊。
  • 5疆前、涉及到python解釋器對屬性的查找優(yōu)先級:
屬性查找的優(yōu)先級  

當(dāng)使用實例對象訪問屬性時,都會調(diào)用__getattribute__內(nèi)建函數(shù)聘萨,__getattribute__查找屬性的優(yōu)先級如下:

1竹椒、類屬性
2、數(shù)據(jù)描述符
3米辐、實例屬性
4胸完、非數(shù)據(jù)描述符
5、__getattr__()

1.如果attr是一個Python自動產(chǎn)生的屬性(內(nèi)置的特殊方法等)翘贮,找到赊窥!(優(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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末琢融,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子簿寂,更是在濱河造成了極大的恐慌漾抬,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陶耍,死亡現(xiàn)場離奇詭異奋蔚,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)烈钞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門泊碑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人毯欣,你說我怎么就攤上這事馒过。” “怎么了酗钞?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵腹忽,是天一觀的道長来累。 經(jīng)常有香客問我,道長窘奏,這世上最難降的妖魔是什么嘹锁? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮着裹,結(jié)果婚禮上领猾,老公的妹妹穿的比我還像新娘锉试。我一直安慰自己杂瘸,他們只是感情好叉讥,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布嚣州。 她就那樣靜靜地躺著,像睡著了一般您朽。 火紅的嫁衣襯著肌膚如雪黑界。 梳的紋絲不亂的頭發(fā)上劳澄,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天稍走,我揣著相機(jī)與錄音袁翁,去河邊找鬼。 笑死钱磅,一個胖子當(dāng)著我的面吹牛梦裂,可吹牛的內(nèi)容都是我干的似枕。 我是一名探鬼主播盖淡,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼凿歼!你這毒婦竟也來了褪迟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤答憔,失蹤者是張志新(化名)和其女友劉穎味赃,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體虐拓,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡心俗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蓉驹。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片城榛。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖态兴,靈堂內(nèi)的尸體忽然破棺而出狠持,到底是詐尸還是另有隱情,我是刑警寧澤瞻润,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布喘垂,位于F島的核電站甜刻,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏正勒。R本人自食惡果不足惜得院,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望章贞。 院中可真熱鬧尿招,春花似錦、人聲如沸阱驾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽里覆。三九已至丧荐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間喧枷,已是汗流浹背虹统。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留隧甚,地道東北人车荔。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像戚扳,于是被迫代替她去往敵國和親忧便。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344