基本認知: 在 Python 的世界裡, everything is object, 包含 type (class) 也是 object, 一些基本型別如整數、浮點數、字串也是 object (我個人對於這點頗感冒的, 效率變差卻不會方便多少)。Python 2.2 後採用 new style class, 也就是所有 class 都要繼承 object, 像是寫成 "class NewStyle(object):", 而寫成 "class OldStyle:" 就是舊式寫法 。 Python 3 裡則是只有 new style class, 寫成 "class A:" 等同於 "class A(object):"。new style class 才能使用 descriptor, 以下討論的行為都是 new style class。
物件實際取用 attribute 的順序
對於物件 x 來說, x.a 實際取到的物件, 會依下列順序執行, 直到成功取得物件為止:- 取得 x 的 data descriptor a
- 取得 x 自身物件的屬性 (即 x.__dict__['a'], __dict__ 是 dict)
- 取得 x 的 non-data descriptor
- raise AttributeError
- x.a
- x.a = ...
- del x.a
Python 找 descriptor 的順序是
- type(x).__dict__['a']
- 往 type(x) 的父類別找
- ...
property、method、classmethod、staticmethod
property 是產生 descriptor 的 helper function。method 是 non-data descriptor 的一種例子, 換句話說, 物件的 method 之後可以被取代:class X(object): def a(self): print 'a value' x = X() print x.a # <bound method X.a of <__main__.X object at 0x800fcd6d0>> x.a() # a value x.a = 3 print x.a # 3可以看出 x.a 被替換掉了。
classmethod、staticmethod 都是 helper function, 幫忙產生不同的 method。method / classmethod / staticmethod 都是 non-data descriptor, 和前面 CachedAttribute 的差別, 在於它們傳回一個 callable object, 而不是直接傳回最後要用的值。callable object 有實作 __call__, 之後再被呼叫 (使用 () )。Python 用 descriptor 實作 method / classmethod / staticmethod, 藉此傳回一個 callable object。method 會先置入 self, classmethod 則是置入「type(self)」。以上面的例子來說, 下面四者得到一樣的結果:
- x.a()
- type(x).a(x)
- type(x).__dict__['a'](x)
- type(x).__dict__['a'].__get__(x, type(x))()
這裡的程式碼 顯示上述這些屬性的特徵, 這裡是執行結果。type(x).__dict__ 包含 method / classmethod / staticmethod, 但是它們的值和 x.method / x.staticmethod / x.classmethod 不同, 反而是 type(x).__dict__[...].__get___(...) 的結果和 x.method / ... 相同, 由此可驗證它們都是 descriptor。剩下的細節就請參照參考資料, 只需要一點耐心, 並不困難。
descriptor 的用途
property 算是最常見的用法, 將原本是普通屬性的 x.a 替換成函式操作, 藉此在不改變介面的情況下, 置入較複雜的操作, 省下改 caller 程式的成本。不過一般不建議濫用 property, 畢竟 function call 的成本比直接取值高。在大量取值的情況下, 用 property 會讓人誤以為可以很便宜的使用而造成效率問題。descriptor 本身有自己的狀態可用, 方便管理 attribute 的控制邏輯。舉例來說, 若想寫個會自動維護 cache 的 attribute, 可以這麼寫:
class CachedAttribute(object): def __init__(self, getter, setter): self._getter = getter self._setter = setter self._cache = None def __get__(self, obj, objtype=None): if self._cache is None: self._cache = self._getter() return self._cache def __set__(self, obj, val): self._setter(val) self._cache = val使用 CachedAttribute 的 class 這麼寫:
class User(object): # get_name 和 set_name 是寫好的函式, # 比方說從 DB 取值和設值 name = CachedAttribute(get_name, set_name) user = User() user.name # 呼叫 type(user).__dict__['name'].__get__(user, type(user))
這樣不同 class 就能共用管理 cache 的程式, 像是改寫成每取十次就再取新值, 或是加上 expire 機制之類的, 而這些使用 CachedAttribute 的 class, 彼此之間沒有關聯, 也不用增加 state 管理 cache。愈少 class 之間的相依性, 愈少內部 state, 程式就愈容易維護。
參考資料
- Python Types and Objects: 解釋 object、class 和 meta class (即 type)。附有超清楚的關係圖。讀懂會很爽, 跳過不讀對理解 descriptor 似乎沒太大影響
- Python Attributes and Methods: 詳細的說明 __dict__ 取值流程, 還有 descriptor 和 method 等的關係
- How-To Guide for Descriptors: 和上篇互補, 我覺得這篇寫得最清楚, 還有提到一點 CPython 相關的程式碼, 不過前兩篇介紹背景知識介紹得較詳細
沒有留言:
張貼留言