這其實頗複雜的, 還有一些細節摸清楚後應該很有意思, 先記下目前理解的程度。
基本認知: 在 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
data descriptor 是一個有實作 __get__, __set__ 的類別產生的物件。另有 __del__ 可以實作, 這三者分表在這三種情況被呼叫:
non-data descriptor 只有實作 __get__, 可以被 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))()
x.a() 實際運作的方式, 就是最後一項的取法。Python 用這種繞彎的方式執行 method, 以提供很大的彈性置換操作行為。取值的過程中從另外的物件 (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, 程式就愈容易維護。
參考資料