1. 程式人生 > >Python描述符(descriptor)詳解及應用場景

Python描述符(descriptor)詳解及應用場景


Python中包含了許多內建的語言特性,它們使得程式碼簡潔且易於理解。這些特性包括列表/集合/字典推導式,屬性(property)、以及裝飾器(decorator)。對於大部分特性來說,這些“中級”的語言特性有著完善的文件,並且易於學習。

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

  1. 有關描述符的官方文件相當難懂,而且沒有包含優秀的示例告訴你為什麼需要編寫描述符(我得為Raymond Hettinger辯護一下,他寫的其他主題的Python文章和視訊對我的幫助還是非常大的)
  2. 編寫描述符的語法顯得有些怪異
  3. 自定義描述符可能是Python中用的最少的特性,因此你很難在開源專案中找到優秀的示例

但是一旦你理解了之後,描述符的確還是有它的應用價值的。這篇文章告訴你描述符可以用來做什麼,以及為什麼應該引起你的注意。

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

在這裡我要告訴你:從根本上講,描述符就是可以重複使用的屬性。也就是說,描述符可以讓你編寫這樣的程式碼:

Python
1 2 3 4 f=Foo() b=f.bar f.bar=c delf.bar

而在直譯器執行上述程式碼時,當發現你試圖訪問屬性(b = f.bar)、對屬性賦值(f.bar = c)或者刪除一個例項變數的屬性(del f.bar)時,就會去呼叫自定義的方法。

讓我們先來解釋一下為什麼把對函式的呼叫偽裝成對屬性的訪問是大有好處的。

property——把函式呼叫偽裝成對屬性的訪問

想象一下你正在編寫管理電影資訊的程式碼。你最後寫好的Movie類可能看上去是這樣的:

Python
1 2 3 4 5 6 7 8 9 10 classMovie(object): def__init__(self,title,rating,runtime,budget,gross): self.title=title self.rating=rating self.runtime=runtime self.budget=budget self.gross=gross defprofit(self): returnself.gross-self.budget

你開始在專案的其他地方使用這個類,但是之後你意識到:如果不小心給電影打了負分怎麼辦?你覺得這是錯誤的行為,希望Movie類可以阻止這個錯誤。 你首先想到的辦法是將Movie類修改為這樣:

Python
1 2 3 4 5 6 7 8 9 10 11 12 classMovie(object): def__init__(self,title,rating,runtime,budget,gross): self.title=title self.rating=rating self.runtime=runtime self.gross=gross ifbudget<0: raiseValueError("Negative value not allowed: %s"%budget) self.budget=budget defprofit(self): returnself.gross-self.budget

但這行不通。因為其他部分的程式碼都是直接通過Movie.budget來賦值的——這個新修改的類只會在__init__方法中捕獲錯誤的資料,但對於已經存在的類例項就無能為力了。如果有人試著執行m.budget = -100,那麼誰也沒法阻止。作為一個Python程式設計師同時也是電影迷,你該怎麼辦?

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

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 classMovie(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 defbudget(self): returnself._budget @budget.setter defbudget(self,value): ifvalue<0: raiseValueError("Negative value not allowed: %s"%value) self._budget=value defprofit(self): returnself.gross-self.budget m=Movie('Casablanca',97,102,964000,1300000) printm.budget# calls m.budget(), returns result try: m.budget=-100# calls budget.setter(-100), and raises ValueError exceptValueError: print"Woops. Not allowed" 964000 Woops.Notallowed

我們用@property裝飾器指定了一個getter方法,用@budget.setter裝飾器指定了一個setter方法。當我們這麼做時,每當有人試著訪問budget屬性,Python就會自動呼叫相應的getter/setter方法。比方說,當遇到m.budget = value這樣的程式碼時就會自動呼叫budget.setter。

花點時間來欣賞一下Python這麼做是多麼的優雅:如果沒有property,我們將不得不把所有的例項屬性隱藏起來,提供大量顯式的類似get_budget和set_budget方法。像這樣編寫類的話,使用起來就會不斷的去呼叫這些getter/setter方法,這看起來就像臃腫的Java程式碼一樣。更糟的是,如果我們不採用這種編碼風格,直接對例項屬性進行訪問。那麼稍後就沒法以清晰的方式增加對非負數的條件檢查——我們不得不重新建立set_budget方法,然後搜尋整個工程中的原始碼,將m.budget = value這樣的程式碼替換為m.set_budget(value)。太蛋疼了!!

因此,property讓我們將自定義的程式碼同變數的訪問/設定聯絡在了一起,同時為你的類保持一個簡單的訪問屬性的介面。幹得漂亮!

property的不足

對property來說,最大的缺點就是它們不能重複使用。舉個例子,假設你想為rating,runtime和gross這些欄位也新增非負檢查。下面是修改過的新類:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 classMovie(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 defbudget(self): returnself._budget @budget.setter defbudget(self,value): ifvalue<0: raiseValueError("Negative value not allowed: %s"%value) self._budget=value #ok     @property defrating(self): returnself._rating @rating.setter defrating(self,value): ifvalue<0: raiseValueError("Negative value not allowed: %s"%value) self._rating=value #uhh... @property defruntime(self): returnself._runtime @runtime.setter defruntime(self,value): ifvalue<0: raiseValueError("Negative value not allowed: %s"%value) self._runtime=value #is this forever? @property defgross(self): returnself._gross @gross.setter defgross(self,value): ifvalue<0: raiseValueError("Negative value not allowed: %s"%value) self._gross=value         defprofit(self): returnself.gross-self.budget

可以看到程式碼增加了不少,但重複的邏輯也出現了不少。雖然property可以讓類從外部看起來介面整潔漂亮,但是卻做不到內部同樣整潔漂亮。

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

這就是描述符所解決的問題。描述符是property的升級版,允許你為重複的property邏輯編寫單獨的類來處理。下面的示例展示了描述符是如何工作的(現在還不必擔心NonNegative類的實現):

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 fromweakrefimportWeakKeyDictionary classNonNegative(object):