來源:人世間 鏈接:www.jianshu.com/p/250f0d305c35
Python黑魔法,前面已經(jīng)介紹了兩個魔法,裝飾器和迭代器,通常還有個生成器。生成器固然也是一個很優(yōu)雅的魔法。生成器更像是函數(shù)的行為。而連接類行為和函數(shù)行為的時候,還有一個描述器魔法,也稱之為描述符。
我們不止一次說過,Python的優(yōu)雅,很大程度在于如何設(shè)計成優(yōu)雅的API。黑魔法則是一大利器?;蛘哒fPython的優(yōu)雅很大程度上是建立在這些魔法巧技基礎(chǔ)上。
何謂描述器
當(dāng)定義迭代器的時候,描述是實現(xiàn)迭代協(xié)議的對象,即實現(xiàn)__iter__方法的對象。同理,所謂描述器,即實現(xiàn)了描述符協(xié)議,即__get__, __set__, 和 __delete__方法的對象。
單看定義,還是比較抽象的。talk is cheap??创a吧:
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,它實現(xiàn)了描述符協(xié)議__get__和__set__,該對象(類也是對象,一切都是對象)即成為了一個描述器。同時實現(xiàn)__get__和__set__的稱之為資料描述器(data descriptor)。僅僅實現(xiàn)__get__的則為非描述器。兩者的差別是相對于實例的字典的優(yōu)先級。
如果實例字典中有與描述器同名的屬性,如果描述器是資料描述器,優(yōu)先使用資料描述器,如果是非資料描述器,優(yōu)先使用字典中的屬性。
描述器的調(diào)用
對于這類魔法,其調(diào)用方法往往不是直接使用的。例如裝飾器需要用 @ 符號調(diào)用。迭代器通常在迭代過程,或者使用 next 方法調(diào)用。描述器則比較簡單,對象屬性的時候會調(diào)用。
In [15]: webframework = WebFramework() In [16]: webframework.__get__(webframework, WebFramework) Out[16]: 'Flask'
描述器與對象屬性
OOP的理論中,類的成員變量包括屬性和方法。那么在Python里什么是屬性?修改上面的PythonSite類如下:
class PythonSite(object): webframework = WebFramework() version = 0.01 def __init__(self, site): self.site = site
這里增加了一個version的類屬性,以及一個實例屬性site。分別查看一下類和實例對象的屬性:
In [1]: pysite = PythonSite('ghost') In [2]: vars(PythonSite).items() Out[2]: [('__module__', '__main__'), ('version', 0.01), ('__dict__', '__dict__' of 'PythonSite' objects>), ('webframework', <__main__.WebFramework at 0x10d55be90>), ('__weakref__', '__weakref__' of 'PythonSite' objects>), ('__doc__', None), ('__init__', __main__.__init__>)] In [3]: vars(pysite) Out[3]: {'site': 'ghost'} In [4]: PythonSite.__dict__ Out[4]: {'__dict__': '__dict__' of 'PythonSite' objects>, '__doc__': None, '__init__': __main__.__init__>, '__module__': '__main__', '__weakref__': '__weakref__' of 'PythonSite' objects>, 'version': 0.01, 'webframework': <__main__.WebFramework at 0x10d55be90>}>
vars方法用于查看對象的屬性,等價于對象的__dict__內(nèi)容。從上面的顯示結(jié)果,可以看到類PythonSite和實例pysite的屬性差別在于前者有 webframework,version兩個屬性,以及 __init__方法,后者僅有一個site屬性。
類與實例的屬性
類屬性可以使用對象和類訪問,多個實例對象共享一個類變量。但是只有類才能修改。
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類屬性,并且是同一個類屬性。當(dāng)pysite1修改了version,實際上是給自己添加了一個version屬性。類屬性并沒有被改變。當(dāng)PythonSite改變了version屬性的時候,pysite2的該屬性也對應(yīng)被改變。
屬性訪問的原理與描述器
知道了屬性訪問的結(jié)果。這個結(jié)果都是基于Python的描述器實現(xiàn)的。通常,類或者實例通過.操作符訪問屬性。例如pysite1.site和pysite1.version的訪問。先訪問對象的__dict__,如果沒有再訪問類(或父類,元類除外)的__dict__。如果最后這個__dict__的對象是一個描述器,則會調(diào)用描述器的__get__方法。
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) in () ----> 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'
實例方法,類方法,靜態(tài)方法與描述器
調(diào)用描述器的時候,實際上會調(diào)用object.__getattribute__()。這取決于調(diào)用描述其器的是對象還是類,如果是對象obj.x,則會調(diào)用type(obj).__dict__['x'].__get__(obj, type(obj))。如果是類,class.x, 則會調(diào)用type(class).__dict__['x'].__get__(None, type(class)。
這樣說還是比較抽象,下面來分析Python的方法,靜態(tài)方法和類方法。把PythonSite重構(gòu)一下:
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裝飾器定義。經(jīng)過該裝飾器的方法是一個描述器。類和實例都可以調(diào)用類方法:
In [1]: ps = PythonSite('ghost') In [2]: ps.get_version Out[2]: method type.get_version of '__main__.PythonSite'>> In [3]: ps.get_version() Out[3]: 0.01 In [4]: PythonSite.get_version Out[4]: method type.get_version of '__main__.PythonSite'>> In [5]: PythonSite.get_version() Out[5]: 0.01
get_version 是一個bound方法。下面再看下ps.get_version這個調(diào)用,會先查找它·的__dict__是否有g(shù)et_version這個屬性,如果沒有,則查找其類。
In [6]: vars(ps) Out[6]: {'site': 'ghost'} In [7]: type(ps).__dict__['get_version'] Out[7]: at 0x108952e18> In [8]: type(ps).__dict__['get_version'].__get__(ps, type(ps)) Out[8]: method type.get_version of '__main__.PythonSite'>> In [9]: type(ps).__dict__['get_version'].__get__(ps, type(ps)) == ps.get_version Out[9]: True
并且vars(ps)中,__dict__并沒有g(shù)et_version這個屬性,依據(jù)描述器協(xié)議,將會調(diào)用type(ps).__dict__['get_version']描述器的__get__方法,因為ps是實例,因此object.__getattribute__()會這樣調(diào)用__get__(obj, type(obj))。
現(xiàn)在再看類方法的調(diào)用:
In [10]: PythonSite.__dict__['get_version'] Out[10]: at 0x108952e18> In [11]: PythonSite.__dict__['get_version'].__get__(None, PythonSite) Out[11]: method type.get_version of '__main__.PythonSite'>> In [12]: PythonSite.__dict__['get_version'].__get__(None, PythonSite) == PythonSite.get_version Out[12]: True
因為這次調(diào)用get_version的是一個類對象,而不是實例對象,因此object.__getattribute__()會這樣調(diào)用__get__(None, Class)。
靜態(tài)方法,@staticmethod
實例和類也可以調(diào)用靜態(tài)方法:
In [13]: ps.find_version Out[13]: __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]: at 0x108952d70> In [17]: type(ps).__dict__['find_version'].__get__(ps, type(ps)) Out[17]: __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]: __main__.find_version> In [21]: type(ps).__dict__['find_version'].__get__(None, type(ps)) Out[21]: __main__.find_version> In [22]: type(ps).__dict__['find_version'].__get__(None, type(ps)) == PythonSite.find_version Out[22]: True
和類方法差別不大,他們的主要差別是在類方法內(nèi)部的時候,類方法可以有cls的類引用,靜態(tài)訪問則沒有,如果靜態(tài)方法想使用類變量,只能硬編碼類名。
實例方法
實例方法最為復(fù)雜,是專門屬于實例的,使用類調(diào)用的時候,會是一個unbound方法。
In [2]: ps.get_site Out[2]: 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]: __main__.get_site> In [5]: type(ps).__dict__['get_site'].__get__(ps, type(ps)) Out[5]: 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方法:
In [7]: PythonSite.get_site Out[7]: method PythonSite.get_site> In [8]: PythonSite.get_site() --------------------------------------------------------------------------- TypeError Traceback (most recent call last) in () ----> 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]: __main__.get_site> In [11]: PythonSite.__dict__['get_site'].__get__(None, PythonSite) Out[11]: 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]: method PythonSite.get_site of <__main__.PythonSite object at 0x1054ae2d0>> In [15]: PythonSite.__dict__['get_site'].__get__(ps, PythonSite)() Out[15]: 'ghost'
由此可見,類不能直接調(diào)用實例方法,除非在描述器手動綁定一個類實例。因為使用類對象調(diào)用描述器的時候,__get__的第一個參數(shù)是None,想要成功調(diào)用,需要把這個參數(shù)替換為實例ps,這個過程就是對方法的bound過程。
描述器的應(yīng)用
描述器的作用主要在方法和屬性的定義上。既然我們可以重新描述類的屬性,那么這個魔法就可以改變類的一些行為。最簡單的應(yīng)用則是可以配合裝飾器,寫一個類屬性的緩存。Flask的作者寫了一個werkzeug網(wǎng)絡(luò)工具庫,里面就使用描述器的特性,實現(xiàn)了一個緩存器。
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
運行結(jié)果可見,first calculate只在第一次調(diào)用時候被計算之后就把結(jié)果緩存起來了。這樣的好處是在網(wǎng)絡(luò)編程中,對HTTP協(xié)議的解析,通常會把HTTP的header解析成python的一個字典,而在視圖函數(shù)的時候,可能不知一次的訪問這個header,因此把這個header使用描述器緩存起來,可以減少多余的解析。
描述器在python的應(yīng)用十分廣泛,通常是配合裝飾器一起使用。強大的魔法來自強大的責(zé)任。描述器還可以用來實現(xiàn)ORM中對sql語句的”預(yù)編譯”。恰當(dāng)?shù)氖褂妹枋銎鳎梢宰屪约旱腜ython代碼更優(yōu)雅。
|