鍍金池/ 教程/ Python/ 裝飾器
附錄
進(jìn)程通信
操作系統(tǒng)
迭代器
模塊
描述符
裝飾器
第三部分 擴(kuò)展庫
內(nèi)置類型
數(shù)據(jù)存儲(chǔ)
數(shù)據(jù)類型
基本環(huán)境
文件與目錄
異常
程序框架
數(shù)學(xué)運(yùn)算
函數(shù)
元類
字符串
表達(dá)式

裝飾器

裝飾器 (Decorator) 在 Python 編程中極為常見,可輕松實(shí)現(xiàn) Metadata、Proxy、 AOP 等模式。簡單點(diǎn)說,裝飾器通過返回包裝對象實(shí)現(xiàn)間接調(diào)用,以此來插入額外邏輯。

語法看上去和 Java Annotation、C# Attribute 類似,但不僅僅是添加元數(shù)據(jù)。

>>> @check_args
... def test(*args):
... print args

還原成容易理解的方式:

>>> test = check_args(test)

類似的做法,我們在使用 staticmethod、classmethod 時(shí)就已見過。

>>> def check_args(func):
...     def wrap(*args):
...         args = filter(bool, args)
...         func(*args)
...
...     return wrap   # 返回 wrap 函數(shù)對象

>>> @check_args    # 解釋器執(zhí)行 test = check_args(test)
... def test(*args):
...     print args

>>> test     # 現(xiàn)在 test 名字與 wrap 關(guān)聯(lián)。
<function wrap at 0x108affde8>

>>> test(1, 0, 2, "", [], 3)  # 通過 wrap(test(args)) 完成調(diào)用。
(1, 2, 3)

整個(gè)過程非常簡單:

  • 將目標(biāo)函數(shù)對象 test 作為參數(shù)傳遞給裝飾器 check_args。
  • 裝飾器返回包裝函數(shù) wrap 實(shí)現(xiàn)對 test 的間接調(diào)用。
  • 原函數(shù)名字 test 被重新關(guān)聯(lián)到 wrap,所有對該名字的調(diào)用實(shí)際都是調(diào)用 wrap。

你完全可以把 "@" 當(dāng)做語法糖,也可以直接使用函數(shù)式寫法。只不過那樣不便于代碼維護(hù),畢竟 AOP 極力避免代碼侵入。

裝飾器不一定非得是個(gè)函數(shù)返回包裝對象,也可以是個(gè)類,通過 call 完成目標(biāo)調(diào)用。

>>> class CheckArgs(object):
...     def __init__(self, func):
...         self._func = func
...
...     def __call__(self, *args):
...         args = filter(bool, args)
...         self._func(*args)

>>> @CheckArgs      # 生成 CheckArgs 實(shí)例。
... def test(*args):
...     print args

>>> test       # 名字指向該實(shí)例。
<__main__.CheckArgs object at 0x107a237d0>

>>> test(1, 0, 2, "", [], 3)    # 每次都是通過該實(shí)例的 __call__ 調(diào)用。
(1, 2, 3)

用類裝飾器對象實(shí)例替代原函數(shù),以后的每次調(diào)用的都是該實(shí)例的 call 方法。這種寫法有點(diǎn)啰嗦,還得注意避免在裝飾器對象上保留狀態(tài)。

Class

為 Class 提供裝飾器同樣簡單,無非是將類型對象做為參數(shù)而已。

>>> def singleton(cls):
...     def wrap(*args, **kwargs):
...         o = getattr(cls, "__instance__", None)
...         if not o:
...             o = cls(*args, **kwargs)
...             cls.__instance__ = o
...
...         return o
...
...     return wrap   # 返回 wrap 函數(shù),可以看做原 class 的工廠方法。

>>> @singleton
... class A(object):
...     def __init__(self, x):
...         self.x = x

>>> A
<function wrap at 0x108afff50>

>>> a, b = A(1), A(2)
>>> a is b
True

將 class A 替換成 func wrap 可能有些不好看,修改一下,返回 class wrap。

>>> def singleton(cls):
...     class wrap(cls):
...         def __new__(cls, *args, **kwargs):
...             o = getattr(cls, "__instance__", None)
...             if not o:
...                 o = object.__new__(cls)
...                 cls.__instance__ = o
...
...             return o
...
...     return wrap

>>> @singleton
... class A(object):
...     def test(self): print hex(id(self))

>>> a, b = A(), A()

>>> a is b
True

>>> a.test()
0x1091e9990

創(chuàng)建繼承自原類型的 class wrap,然后在 new 里面做手腳就行了。

大多數(shù)時(shí)候,我們僅用裝飾器為原類型增加一些額外成員,那么可直接返回原類型。

>>> def action(cls):
...     cls.mvc = staticmethod(lambda: "Action")
...     return cls

>>> @action
... class Login(object): pass

>>> Login.mvc()
'Action'

這就是典型的 metaprogramming 做法了。

參數(shù)

參數(shù)讓裝飾器擁有變化,也更加靈活。只是需要兩步才能完成:先傳參數(shù),后送類型。

>>> def table(name):
...     def _table(cls):
...         cls.__table__ = name
...         return cls
...
...     return _table

>>> @table("t_user")
... class User(object): pass

>>> @table("t_blog")
... class Blog(object): pass

>>> User.__table__
't_user'

>>> Blog.__table__
't_blog'

只比無參數(shù)版本多了傳遞參數(shù)的調(diào)用,其他完全相同。

User = (table("t_user"))(User)

嵌套

可以在同一目標(biāo)上使用多個(gè)裝飾器。

>>> def A(func):
...     print "A"
...     return func

>>> def B(func):
...     print "B"
...     return func

>>> @A
... @B
... def test():
...     print "test"

B
A

分解一下,無非是函數(shù)嵌套調(diào)用。

test = A(B(test))

functools.wraps

如果裝飾器返回的是包裝對象,那么有些東西必然是不同的。

>>> def check_args(func):
...     def wrap(*args):
...         return func(*filter(bool, args))
...
...     return wrap

>>> @check_args
def test(*args):
...     """test function"""
...     print args

>>> test.__name__   # 冒牌貨!
'wrap'

>>> test.__doc__   # 山寨貨連個(gè)說明書都木有!

一旦 test 的調(diào)用者要檢查某些特殊屬性,那么這個(gè) wrap 就會(huì)暴露了。幸好有 functools.wraps。

>>> def check_args(func):
...     @functools.wraps(func)
...     def wrap(*args):
...         return func(*filter(bool, args))
...
...     return wrap

>>> @check_args
def test(*args):
    """test function"""
    print args

>>> test
<function test at 0x108b026e0>

>>> test.__name__
'test'

>>> test.__doc__
'test function'

>>> test(1, 0, 2, "", 3)
(1, 2, 3)

functools.wraps 是裝飾器的裝飾器,它的作用是將原函數(shù)對象的指定屬性復(fù)制給包裝函數(shù)對象,默認(rèn)有 modulename、doc,或者通過參數(shù)選擇。

想想看裝飾器都能干嘛?

  • AOP: 身份驗(yàn)證、參數(shù)檢查、異常日志等等。
  • Proxy: 對目標(biāo)函數(shù)注入權(quán)限管理等。
  • Context: 提供函數(shù)級別的上下文環(huán)境,比如 Synchronized(func) 同步。
  • Caching: 先檢查緩存是否過期,然后再?zèng)Q定是否調(diào)用目標(biāo)函數(shù)。
  • Metaprogramming: 這個(gè)自不必多說了。
  • 等等……
上一篇:數(shù)據(jù)類型下一篇:異常