| 
 來源:人世間 鏈接:www.jianshu.com/p/250f0d305c35
 
 Python黑魔法,前面已經(jīng)介紹了兩個魔法,裝飾器和迭代器,通常還有個生成器。生成器固然也是一個很優(yōu)雅的魔法。生成器更像是函數(shù)的行為。而連接類行為和函數(shù)行為的時候,還有一個描述器魔法,也稱之為描述符。 
 我們不止一次說過,Python的優(yōu)雅,很大程度在于如何設計成優(yōu)雅的API。黑魔法則是一大利器?;蛘哒fPython的優(yōu)雅很大程度上是建立在這些魔法巧技基礎上。 
 何謂描述器 
 當定義迭代器的時候,描述是實現(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類屬性,并且是同一個類屬性。當pysite1修改了version,實際上是給自己添加了一個version屬性。類屬性并沒有被改變。當PythonSite改變了version屬性的時候,pysite2的該屬性也對應被改變。 
 屬性訪問的原理與描述器 
 知道了屬性訪問的結(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)方法想使用類變量,只能硬編碼類名。 
 實例方法 
 實例方法最為復雜,是專門屬于實例的,使用類調(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過程。 
 描述器的應用 
 描述器的作用主要在方法和屬性的定義上。既然我們可以重新描述類的屬性,那么這個魔法就可以改變類的一些行為。最簡單的應用則是可以配合裝飾器,寫一個類屬性的緩存。Flask的作者寫了一個werkzeug網(wǎng)絡工具庫,里面就使用描述器的特性,實現(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)絡編程中,對HTTP協(xié)議的解析,通常會把HTTP的header解析成python的一個字典,而在視圖函數(shù)的時候,可能不知一次的訪問這個header,因此把這個header使用描述器緩存起來,可以減少多余的解析。 
 描述器在python的應用十分廣泛,通常是配合裝飾器一起使用。強大的魔法來自強大的責任。描述器還可以用來實現(xiàn)ORM中對sql語句的”預編譯”。恰當?shù)氖褂妹枋銎?,可以讓自己的Python代碼更優(yōu)雅。 
 |