Python 黑魔法 --- 描述器(descriptor)

發表於2016-05-24

Python 黑魔法—描述器(descriptor)

Python黑魔法,前面已經介紹了兩個魔法,裝飾器和迭代器,通常還有個生成器。生成器固然也是一個很優雅的魔法。生成器更像是函式的行為。而連線類行為和函式行為的時候,還有一個描述器魔法,也稱之為描述符。

我們不止一次說過,Python的優雅,很大程度在於如何設計成優雅的API。黑魔法則是一大利器。或者說Python的優雅很大程度上是建立在這些魔法巧技基礎上。

何謂描述器

當定義迭代器的時候,描述是實現迭代協議的物件,即實現__iter__方法的物件。同理,所謂描述器,即實現了描述符協議,即__get__, __set__, 和 __delete__方法的物件。

單看定義,還是比較抽象的。talk is cheap。看程式碼吧:

定義了一個類WebFramework,它實現了描述符協議__get____set__,該物件(類也是物件,一切都是物件)即成為了一個描述器。同時實現__get____set__的稱之為資料描述器(data descriptor)。僅僅實現__get__的則為非描述器。兩者的差別是相對於例項的字典的優先順序。

如果例項字典中有與描述器同名的屬性,如果描述器是資料描述器,優先使用資料描述器,如果是非資料描述器,優先使用字典中的屬性。

描述器的呼叫

對於這類魔法,其呼叫方法往往不是直接使用的。例如裝飾器需要用 @ 符號呼叫。迭代器通常在迭代過程,或者使用 next 方法呼叫。描述器則比較簡單,物件屬性的時候會呼叫。

描述器與物件屬性

OOP的理論中,類的成員變數包括屬性和方法。那麼在Python裡什麼是屬性?修改上面的PythonSite類如下:

這裡增加了一個version的類屬性,以及一個例項屬性site。分別檢視一下類和例項物件的屬性:

vars方法用於檢視物件的屬性,等價於物件的__dict__內容。從上面的顯示結果,可以看到類PythonSite和例項pysite的屬性差別在於前者有 webframework,version兩個屬性,以及 __init__方法,後者僅有一個site屬性。

類與例項的屬性

類屬性可以使用物件和類訪問,多個例項物件共享一個類變數。但是隻有類才能修改。

正如上面的程式碼顯示,兩個例項物件都可以訪問version類屬性,並且是同一個類屬性。當pysite1修改了version,實際上是給自己新增了一個version屬性。類屬性並沒有被改變。當PythonSite改變了version屬性的時候,pysite2的該屬性也對應被改變。

屬性訪問的原理與描述器

知道了屬性訪問的結果。這個結果都是基於Python的描述器實現的。通常,類或者例項通過.操作符訪問屬性。例如pysite1.sitepysite1.version的訪問。先訪問物件的__dict__,如果沒有再訪問類(或父類,元類除外)的__dict__。如果最後這個__dict__的物件是一個描述器,則會呼叫描述器的__get__方法。

例項方法,類方法,靜態方法與描述器

呼叫描述器的時候,實際上會呼叫object.__getattribute__()。這取決於呼叫描述其器的是物件還是類,如果是物件obj.x,則會呼叫type(obj).__dict__['x'].__get__(obj, type(obj))。如果是類,class.x, 則會呼叫type(class).__dict__['x'].__get__(None, type(class)

這樣說還是比較抽象,下面來分析Python的方法,靜態方法和類方法。把PythonSite重構一下:

類方法,@classmethod裝飾器

先看類方法,類方法使用@classmethod裝飾器定義。經過該裝飾器的方法是一個描述器。類和例項都可以呼叫類方法:

get_version 是一個bound方法。下面再看下ps.get_version這個呼叫,會先查詢它·的__dict__是否有get_version這個屬性,如果沒有,則查詢其類。

並且vars(ps)中,__dict__並沒有get_version這個屬性,依據描述器協議,將會呼叫type(ps).__dict__['get_version']描述器的__get__方法,因為ps是例項,因此object.__getattribute__()會這樣呼叫__get__(obj, type(obj))

現在再看類方法的呼叫:

因為這次呼叫get_version的是一個類物件,而不是例項物件,因此object.__getattribute__()會這樣呼叫__get__(None, Class)

靜態方法,@staticmethod

例項和類也可以呼叫靜態方法:

和類方法差別不大,他們的主要差別是在類方法內部的時候,類方法可以有cls的類引用,靜態訪問則沒有,如果靜態方法想使用類變數,只能硬編碼類名。

例項方法

例項方法最為複雜,是專門屬於例項的,使用類呼叫的時候,會是一個unbound方法。

一切工作正常,例項方法也是類的一個屬性,但是對於類,描述器使其變成了unbound方法:

由此可見,類不能直接呼叫例項方法,除非在描述器手動繫結一個類例項。因為使用類物件呼叫描述器的時候,__get__的第一個引數是None,想要成功呼叫,需要把這個引數替換為例項ps,這個過程就是對方法的bound過程。

描述器的應用

描述器的作用主要在方法和屬性的定義上。既然我們可以重新描述類的屬性,那麼這個魔法就可以改變類的一些行為。最簡單的應用則是可以配合裝飾器,寫一個類屬性的快取。Flask的作者寫了一個werkzeug網路工具庫,裡面就使用描述器的特性,實現了一個快取器。

執行結果可見,first calculate只在第一次呼叫時候被計算之後就把結果快取起來了。這樣的好處是在網路程式設計中,對HTTP協議的解析,通常會把HTTP的header解析成python的一個字典,而在檢視函式的時候,可能不知一次的訪問這個header,因此把這個header使用描述器快取起來,可以減少多餘的解析。

描述器在python的應用十分廣泛,通常是配合裝飾器一起使用。強大的魔法來自強大的責任。描述器還可以用來實現ORM中對sql語句的”預編譯”。恰當的使用描述器,可以讓自己的Python程式碼更優雅。

相關文章