Python 黑魔法—描述器(descriptor)
Python黑魔法,前面已經介紹了兩個魔法,裝飾器和迭代器,通常還有個生成器。生成器固然也是一個很優雅的魔法。生成器更像是函式的行為。而連線類行為和函式行為的時候,還有一個描述器魔法,也稱之為描述符。
我們不止一次說過,Python的優雅,很大程度在於如何設計成優雅的API。黑魔法則是一大利器。或者說Python的優雅很大程度上是建立在這些魔法巧技基礎上。
何謂描述器
當定義迭代器的時候,描述是實現迭代協議的物件,即實現__iter__
方法的物件。同理,所謂描述器,即實現了描述符協議,即__get__
, __set__
, 和 __delete__
方法的物件。
單看定義,還是比較抽象的。talk is cheap。看程式碼吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class WebFramework(object): def __init__(self, name='Flask'): self.name = name def __get__(self, instance, owner): return self.name def __set__(self, instance, value): self.name = value class PythonSite(object): webframework = WebFramework() In [1]: PythonSite.webframework Out[1]: 'Flask' In [2]: PythonSite.webframework = 'Tornado' In [3]: PythonSite.webframework Out[3]: 'Tornado' |
定義了一個類WebFramework,它實現了描述符協議__get__
和__set__
,該物件(類也是物件,一切都是物件)即成為了一個描述器。同時實現__get__
和__set__
的稱之為資料描述器(data descriptor)。僅僅實現__get__
的則為非描述器。兩者的差別是相對於例項的字典的優先順序。
如果例項字典中有與描述器同名的屬性,如果描述器是資料描述器,優先使用資料描述器,如果是非資料描述器,優先使用字典中的屬性。
描述器的呼叫
對於這類魔法,其呼叫方法往往不是直接使用的。例如裝飾器需要用 @ 符號呼叫。迭代器通常在迭代過程,或者使用 next 方法呼叫。描述器則比較簡單,物件屬性的時候會呼叫。
1 2 3 4 |
In [15]: webframework = WebFramework() In [16]: webframework.__get__(webframework, WebFramework) Out[16]: 'Flask' |
描述器與物件屬性
OOP的理論中,類的成員變數包括屬性和方法。那麼在Python裡什麼是屬性?修改上面的PythonSite類如下:
1 2 3 4 5 6 7 8 |
class PythonSite(object): webframework = WebFramework() version = 0.01 def __init__(self, site): self.site = site |
這裡增加了一個version的類屬性,以及一個例項屬性site。分別檢視一下類和例項物件的屬性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
In [1]: pysite = PythonSite('ghost') In [2]: vars(PythonSite).items() Out[2]: [('__module__', '__main__'), ('version', 0.01), ('__dict__', <attribute '__dict__' of 'PythonSite' objects>), ('webframework', <__main__.WebFramework at 0x10d55be90>), ('__weakref__', <attribute '__weakref__' of 'PythonSite' objects>), ('__doc__', None), ('__init__', <function __main__.__init__>)] In [3]: vars(pysite) Out[3]: {'site': 'ghost'} In [4]: PythonSite.__dict__ Out[4]: <dictproxy {'__dict__': <attribute '__dict__' of 'PythonSite' objects>, '__doc__': None, '__init__': <function __main__.__init__>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'PythonSite' objects>, 'version': 0.01, 'webframework': <__main__.WebFramework at 0x10d55be90>}> |
vars方法用於檢視物件的屬性,等價於物件的__dict__
內容。從上面的顯示結果,可以看到類PythonSite和例項pysite的屬性差別在於前者有 webframework,version兩個屬性,以及 __init__
方法,後者僅有一個site屬性。
類與例項的屬性
類屬性可以使用物件和類訪問,多個例項物件共享一個類變數。但是隻有類才能修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
In [6]: pysite1 = PythonSite('ghost') In [7]: pysite2 = PythonSite('admin') In [8]: PythonSite.version Out[8]: 0.01 In [9]: pysite1.version Out[9]: 0.01 In [10]: pysite2.version Out[10]: 0.01 In [11]: pysite1.version is pysite2.version Out[11]: True In [12]: pysite1.version = 'pysite1' In [13]: vars(pysite1) Out[13]: {'site': 'ghost', 'version': 'pysite1'} In [14]: vars(pysite2) Out[14]: {'site': 'admin'} In [15]: PythonSite.version = 0.02 In [16]: pysite1.version Out[16]: 'pysite1' In [17]: pysite2.version Out[17]: 0.02 |
正如上面的程式碼顯示,兩個例項物件都可以訪問version類屬性,並且是同一個類屬性。當pysite1修改了version,實際上是給自己新增了一個version屬性。類屬性並沒有被改變。當PythonSite改變了version屬性的時候,pysite2的該屬性也對應被改變。
屬性訪問的原理與描述器
知道了屬性訪問的結果。這個結果都是基於Python
的描述器實現的。通常,類或者例項通過.
操作符訪問屬性。例如pysite1.site
和pysite1.version
的訪問。先訪問物件的__dict__
,如果沒有再訪問類(或父類,元類除外)的__dict__
。如果最後這個__dict__
的物件是一個描述器,則會呼叫描述器的__get__
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
In [21]: pysite1.site Out[21]: 'ghost' In [22]: pysite1.__dict__['site'] Out[22]: 'ghost' In [23]: pysite2.version Out[23]: 0.02 In [24]: pysite2.__dict__['version'] --------------------------------------------------------------------------- KeyError Traceback (most recent call last) <ipython-input-24-73ef6aeba259> in <module>() ----> 1 pysite2.__dict__['version'] KeyError: 'version' In [25]: type(pysite2).__dict__['version'] Out[25]: 0.02 In [32]: type(pysite1).__dict__['webframework'] Out[32]: <__main__.WebFramework at 0x103426e90> In [38]: type(pysite1).__dict__['webframework'].__get__(None, PythonSite) Out[38]: 'Flask' |
例項方法,類方法,靜態方法與描述器
呼叫描述器的時候,實際上會呼叫object.__getattribute__()
。這取決於呼叫描述其器的是物件還是類,如果是物件obj.x
,則會呼叫type(obj).__dict__['x'].__get__(obj, type(obj))
。如果是類,class.x, 則會呼叫type(class).__dict__['x'].__get__(None, type(class)
。
這樣說還是比較抽象,下面來分析Python的方法,靜態方法和類方法。把PythonSite重構一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class PythonSite(object): webframework = WebFramework() version = 0.01 def __init__(self, site): self.site = site def get_site(self): return self.site @classmethod def get_version(cls): return cls.version @staticmethod def find_version(): return PythonSite.version |
類方法,@classmethod裝飾器
先看類方法,類方法使用@classmethod裝飾器定義。經過該裝飾器的方法是一個描述器。類和例項都可以呼叫類方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
In [1]: ps = PythonSite('ghost') In [2]: ps.get_version Out[2]: <bound method type.get_version of <class '__main__.PythonSite'>> In [3]: ps.get_version() Out[3]: 0.01 In [4]: PythonSite.get_version Out[4]: <bound method type.get_version of <class '__main__.PythonSite'>> In [5]: PythonSite.get_version() Out[5]: 0.01 |
get_version 是一個bound方法。下面再看下ps.get_version
這個呼叫,會先查詢它·的__dict__
是否有get_version
這個屬性,如果沒有,則查詢其類。
1 2 3 4 5 6 7 8 9 10 11 |
In [6]: vars(ps) Out[6]: {'site': 'ghost'} In [7]: type(ps).__dict__['get_version'] Out[7]: <classmethod at 0x108952e18> In [8]: type(ps).__dict__['get_version'].__get__(ps, type(ps)) Out[8]: <bound method type.get_version of <class '__main__.PythonSite'>> In [9]: type(ps).__dict__['get_version'].__get__(ps, type(ps)) == ps.get_version Out[9]: True |
並且vars(ps)中,__dict__
並沒有get_version
這個屬性,依據描述器協議,將會呼叫type(ps).__dict__['get_version']
描述器的__get__
方法,因為ps是例項,因此object.__getattribute__()
會這樣呼叫__get__(obj, type(obj))
。
現在再看類方法的呼叫:
1 2 3 4 5 6 7 8 |
In [10]: PythonSite.__dict__['get_version'] Out[10]: <classmethod at 0x108952e18> In [11]: PythonSite.__dict__['get_version'].__get__(None, PythonSite) Out[11]: <bound method type.get_version of <class '__main__.PythonSite'>> In [12]: PythonSite.__dict__['get_version'].__get__(None, PythonSite) == PythonSite.get_version Out[12]: True |
因為這次呼叫get_version的是一個類物件,而不是例項物件,因此object.__getattribute__()
會這樣呼叫__get__(None, Class)
。
靜態方法,@staticmethod
例項和類也可以呼叫靜態方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
In [13]: ps.find_version Out[13]: <function __main__.find_version> In [14]: ps.find_version() Out[14]: 0.01 In [15]: vars(ps) Out[15]: {'site': 'ghost'} In [16]: type(ps).__dict__['find_version'] Out[16]: <staticmethod at 0x108952d70> In [17]: type(ps).__dict__['find_version'].__get__(ps, type(ps)) Out[17]: <function __main__.find_version> In [18]: type(ps).__dict__['find_version'].__get__(ps, type(ps)) == ps.find_version Out[18]: True In [19]: PythonSite.find_version() Out[19]: 0.01 In [20]: PythonSite.find_version Out[20]: <function __main__.find_version> In [21]: type(ps).__dict__['find_version'].__get__(None, type(ps)) Out[21]: <function __main__.find_version> In [22]: type(ps).__dict__['find_version'].__get__(None, type(ps)) == PythonSite.find_version Out[22]: True |
和類方法差別不大,他們的主要差別是在類方法內部的時候,類方法可以有cls
的類引用,靜態訪問則沒有,如果靜態方法想使用類變數,只能硬編碼類名。
例項方法
例項方法最為複雜,是專門屬於例項的,使用類呼叫的時候,會是一個unbound
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
In [2]: ps.get_site Out[2]: <bound method PythonSite.get_site of <__main__.PythonSite object at 0x1054ae2d0>> In [3]: ps.get_site() Out[3]: 'ghost' In [4]: type(ps).__dict__['get_site'] Out[4]: <function __main__.get_site> In [5]: type(ps).__dict__['get_site'].__get__(ps, type(ps)) Out[5]: <bound method PythonSite.get_site of <__main__.PythonSite object at 0x1054ae2d0>> In [6]: type(ps).__dict__['get_site'].__get__(ps, type(ps)) == ps.get_site Out[6]: True |
一切工作正常,例項方法也是類的一個屬性,但是對於類,描述器使其變成了unbound
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
In [7]: PythonSite.get_site Out[7]: <unbound method PythonSite.get_site> In [8]: PythonSite.get_site() --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-8-99c7d7607137> in <module>() ----> 1 PythonSite.get_site() TypeError: unbound method get_site() must be called with PythonSite instance as first argument (got nothing instead) In [9]: PythonSite.get_site(ps) Out[9]: 'ghost' In [10]: PythonSite.__dict__['get_site'] Out[10]: <function __main__.get_site> In [11]: PythonSite.__dict__['get_site'].__get__(None, PythonSite) Out[11]: <unbound method PythonSite.get_site> In [12]: PythonSite.__dict__['get_site'].__get__(None, PythonSite) == PythonSite.get_site Out[12]: True In [14]: PythonSite.__dict__['get_site'].__get__(ps, PythonSite) Out[14]: <bound method PythonSite.get_site of <__main__.PythonSite object at 0x1054ae2d0>> In [15]: PythonSite.__dict__['get_site'].__get__(ps, PythonSite)() Out[15]: 'ghost' |
由此可見,類不能直接呼叫例項方法,除非在描述器手動繫結一個類例項。因為使用類物件呼叫描述器的時候,__get__
的第一個引數是None,想要成功呼叫,需要把這個引數替換為例項ps
,這個過程就是對方法的bound過程。
描述器的應用
描述器的作用主要在方法和屬性的定義上。既然我們可以重新描述類的屬性,那麼這個魔法就可以改變類的一些行為。最簡單的應用則是可以配合裝飾器,寫一個類屬性的快取。Flask的作者寫了一個werkzeug網路工具庫,裡面就使用描述器的特性,實現了一個快取器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
class _Missing(object): def __repr__(self): return 'no value' def __reduce__(self): return '_missing' _missing = _Missing() class cached_property(object): def __init__(self, func, name=None, doc=None): self.__name__ = name or func.__name__ self.__module__ = func.__module__ self.__doc__ = doc or func.__doc__ self.func = func def __get__(self, obj, type=None): if obj is None: return self value = obj.__dict__.get(self.__name__, _missing) if value is _missing: value = self.func(obj) obj.__dict__[self.__name__] = value return value class Foo(object): @cached_property def foo(self): print 'first calculate' result = 'this is result' return result f = Foo() print f.foo # first calculate this is result print f.foo # this is result |
執行結果可見,first calculate
只在第一次呼叫時候被計算之後就把結果快取起來了。這樣的好處是在網路程式設計中,對HTTP協議的解析,通常會把HTTP的header解析成python的一個字典,而在檢視函式的時候,可能不知一次的訪問這個header,因此把這個header使用描述器快取起來,可以減少多餘的解析。
描述器在python的應用十分廣泛,通常是配合裝飾器一起使用。強大的魔法來自強大的責任。描述器還可以用來實現ORM中對sql語句的”預編譯”。恰當的使用描述器,可以讓自己的Python程式碼更優雅。