跳到主要內容

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 上使用自行編譯的第三方函式庫

以使用 LevelDB 為例。 抓好並編好相關檔案,編譯方式見第三方函式庫附的說明:$ ls include/ # header files leveldb/ $ ls out-shared/libleveldb.so* # shared library out-shared/libleveldb.so@ out-shared/libleveldb.so.1@ out-shared/libleveldb.so.1.20* 下面的例子用 clang++ 編譯,這裡用到的參數和 g++ 一樣。 問題一:找不到 header$ clang++ sample.cpp sample.cpp:5:10: fatal error: 'leveldb/db.h' file not found #include "leveldb/db.h" ^ 1 error generated. 解法:用 -I 指定 header 位置 問題二:找不到 shared library$ clang++ sample.cpp -I include/ /tmp/sample-2e7dd8.o: In function `main': sample.cpp:(.text+0x1e): undefined reference to `leveldb::Options::Options()' sample.cpp:(.text+0x6f): undefined reference to `leveldb::DB::Open(leveldb::Options const&, std::string const&, leveldb::DB**)' sample.cpp:(.text+0x10c): undefined reference to `leveldb::Status::ToString() const' sample.cpp:(.text+0x7d0): undefined reference to `leveldb::Status::ToString() const' clang: error: linker command failed with exit code 1 (u…

熟悉系統工具好處多多

記一下以前很困擾, 現在秒殺的小事。 更新這篇的時候, 忘了函式庫用的 man page 裝在那個 package。以前就會想辦法 google, 運氣好一下會找到, 運氣不好會多找一會兒。 這回我想到新作法:$ strace -e open man 3 printf > /dev/null # 發現是讀 /usr/share/man/man3/printf.3.gz $ dpkg --search /usr/share/man/man3/printf.3.gz # 找到套件名稱 manpages-dev $ aptitude show manpages-dev # 確認描述符合, 收工

virtualbox 使用 USB 裝置

2012-12-16 更新 現在 (4.x 版) 似乎無需做任何設定, 只要有裝 Oracle VM VirtualBox Extension Pack, 在 VirtualBox 視窗右下角按 USB 的圖示, 再點目標裝置, 即可加入或移除該裝置 同一時間只有 host 或 guest 可擁有該裝置, 所以從 guest OS 移除, 相當於接回 host OS 目前 VirtualBox 只支援 USB 2.0 的插槽, 若偵測不到時, 注意一下是否為這個問題 有時拔拔插插, VirtualBox 會進入奇怪的狀態, 接上去 guest OS 無法連接且跳出 device is busy 的錯誤訊息。試看看拔除該裝置, 重開 guest OS (續上則) 若重開 guest OS 無效, 並且 host OS 已移除該裝置, VirtualBox 的 USB 清單卻仍顯示 "captured", 試看看拔除該裝置, 重開 host OS原文網路上搜一下, 比較多是 Ubuntu 當 host 的解法, 我的情況是 Win7 當 host, Ubuntu 當 guest。 這兩篇說明很詳細《Learn How to Set Up USB and Networking Options in VirtualBox》《幻影千瞳的部落格: VirtualBox 使用筆記(二):使用 USB 裝置》 現在的版本圖形介面很好用了, 不用像第二篇說的那樣用指令操作。這裡記下我的操作步驟: 關掉 guest OS 在 VirtualBox 選單, 選擇 guest OS -> Settings -> USB -> Enable USB 2.0 會出現訊息框, 說明要安裝 Oracle VM VirtualBox Extension Pack。下載後安裝它 host OS 插入 USB 隨身碟 在 VirtualBox 選單, 選擇 guest OS -> Settings -> USB, 點右邊有綠色 "+" 的 USB 頭的圖示, 選擇該 USB 隨身碟, 加入它的 filter 從 host OS 移除 USB 隨身碟 開啟 guest OS 插入 USB 隨身碟, 於是 guest OS 會自動偵測…