Python 取 attribute 的規則以及 descriptor

這其實頗複雜的, 還有一些細節摸清楚後應該很有意思, 先記下目前理解的程度。

基本認知: 在 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 實際取到的物件, 會依下列順序執行, 直到成功取得物件為止:
  1. 取得 x 的 data descriptor a
  2. 取得 x 自身物件的屬性 (即 x.__dict__['a'], __dict__ 是 dict)
  3. 取得 x 的 non-data descriptor
  4. raise AttributeError
data descriptor 是一個有實作 __get__, __set__ 的類別產生的物件。另有 __del__ 可以實作, 這三者分表在這三種情況被呼叫:
  • x.a
  • x.a = ...
  • del x.a
non-data descriptor 只有實作 __get__, 可以被 x.a = ... 取代掉。

Python 找 descriptor 的順序是
  1. type(x).__dict__['a']
  2. 往 type(x) 的父類別找
  3. ...

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, 程式就愈容易維護。

參考資料

  • Python Types and Objects: 解釋 object、class 和 meta class (即 type)。附有超清楚的關係圖。讀懂會很爽, 跳過不讀對理解 descriptor 似乎沒太大影響
  • Python Attributes and Methods: 詳細的說明 __dict__ 取值流程, 還有 descriptor 和 method 等的關係
  • How-To Guide for Descriptors: 和上篇互補, 我覺得這篇寫得最清楚, 還有提到一點 CPython 相關的程式碼, 不過前兩篇介紹背景知識介紹得較詳細

留言

這個網誌中的熱門文章

(C/C++ ) 如何在 Linux 上使用自行編譯的第三方函式庫

熟悉系統工具好處多多

virtualbox 使用 USB 裝置