淺談Python裝飾器

凝霜發表於2014-03-30

淺談Python裝飾器

By 馬冬亮(凝霜  Loki)

一個人的戰爭(http://blog.csdn.net/MDL13412)

前置知識

一級物件

Python將一切視為 objec t的子類,即一切都是物件,因此函式可以像變數一樣被指向和傳遞,我們來看下面的例子:

def foo():
    pass
    
print issubclass(foo.__class__, object)
其執行結果如下:
True
上述程式碼說明了Python 中的函式是 object 的子類,下面讓我們看函式被當作引數傳遞時的效果:
def foo(func):
    func()

def bar():
    print "bar"

foo(bar)
其執行結果如下:
bar

Python中的namespace

Python中通過提供 namespace 來實現重名函式/方法、變數等資訊的識別,其一共有三種 namespace,分別為:

  • local namespace: 作用範圍為當前函式或者類方法
  • global namespace: 作用範圍為當前模組
  • build-in namespace: 作用範圍為所有模組

當函式/方法、變數等資訊發生重名時,Python會按照 local namespace -> global namespace -> build-in namespace的順序搜尋使用者所需元素,並且以第一個找到此元素的 namespace 為準。
下面以系統的 build-in 函式 str 為例進行說明:

def str(s):
    print "global str()"

def foo():
    def str(s):
        print "closure str()"
    str("dummy")

def bar():
    str("dummy")

foo()
bar()
首先定義三個 global namespace 的函式 strfoobar,然後在 foo 函式中定義一個內嵌的 local namespace 的函式str,然後在函式 foo 和 bar 中分別呼叫 str("dummy"),其執行結果如下所示:
closure str()
global str()
通過編碼實驗,我們可以看到:
  • foo 中呼叫 str 函式時,首先搜尋 local namespace,並且成功找到了所需的函式,停止搜尋,使用此namespace 中的定義
  • bar 中呼叫 str 函式時,首先搜尋 local namespace,但是沒有找到str 方法的定義,因此繼續搜尋 global namespace,併成功找到了 str 的定義,停止搜尋,並使用此定義
下面我們使用Python內建的 `ocals() 和 globals() 函式檢視不同 namespace 中的元素定義:
var = "var in global"

def fun():
    var = "var in fun"
    print "fun: " + str(locals())

print "globals: " + str(globals())
fun()
執行結果如下:
globals: {'__builtins__': <module '__builtin__' (built-in)>, '__file__': 'a.py', '__package__': None, 'fun': <function fun at 0x7f2ca74f66e0>, 'var': 'var in global', '__name__': '__main__', '__doc__': None}
fun: {'var': 'var in fun'}
通過執行結果,我們看到了 fun 定義了 local namespace 的變數var,在 global namespace 有一個全域性的 var 變數,那麼當在global namespace 中直接訪問 var 變數的時候,將會得到 var = "var in global" 的定義,而在fun 函式的 local namespace 中訪問 var 變數,則會得到fun 私有的 var = "var in fun" 定義。

*args and **kwargs

  • *args: 把所有的引數按出現順序打包成一個 list
  • **kwargs:把所有 key-value 形式的引數打包成一個 dict

下面給出一個 *args 的例子:

params_list = (1, 2)
params_tupple = (1, 2)

def add(x, y):
    print x + y

add(*params_list)
add(*params_tupple)
其執行結果如下:
3
3
**kwargs 的例子:
params = {
    'x': 1,
    'y': 2
}

def add(x, y):
    print x + y

add(**params)
其執行結果如下:
3

閉包

閉包在維基百科上的定義如下: 在電腦科學中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函式和與其相關的引用環境組合而成的實體。
下面給出一個使用閉包實現的logger factory的例子:
def logger_facroty(prefix="", with_prefix=True):
    if with_prefix:
        def logger(msg):
            print prefix + msg
        return logger
    else:
        def logger(msg):
            print msg
        return logger

logger_with_prefix = logger_facroty("Prefix: ")
logger_without_prefix = logger_facroty(with_prefix=False)
logger_with_prefix("msg")
logger_without_prefix("msg")
其執行結果如下:
Prefix: msg
msg
在上面這個閉包的例子中,prefix 變數時所謂的自由變數,其在 return logger 執行完畢後,便脫離了建立它的環境logger_factory,但因為其被 logger_factory 中定義的 logger 函式所引用,其生命週期將至少和 logger 函式相同。這樣,在 logger 中就可以引用到logger_factory 作用域內的變數 prefix
將閉包與 namespace 結合起來:

var = "var in global"

def fun_outer():
    var = "var in fun_outer"
    unused_var = "this var is not used in fun_inner"
    print "fun_outer: " + var
    print "fun_outer: " + str(locals())
    print "fun_outer: " + str(id(var))

    def fun_inner():
        print "fun_inner: " + var
        print "fun_inner: " + str(locals())
        print "fun_inner: " + str(id(var))

    return fun_inner

fun_outer()()
其執行結果如下:
fun_outer: var in fun_outer
fun_outer: {'var': 'var in fun_outer', 'unused_var': 'this var is not used in fun_inner'}
fun_outer: 140228141915584
fun_inner: var in fun_outer
fun_inner: {'var': 'var in fun_outer'}
fun_inner: 140228141915584
在這個例子中,當 fun_outer 被定義時,其內部的定義的 fun_inner 函式對 print "fun_inner: " + var中所引用的 var 變數進行搜尋,發現第一個被搜尋到的 var 定義在 fun_outerlocal namespace 中,因此使用此定義,通過 print "fun_outer: " + str(id(var)) print "fun_inner: " + str(id(var)),當var 超出 fun_outer 的作用域後,依然存活,而 fun_outer 中的unused_var 變數由於沒有被 fun_inner 所引用,因此會被 GC

探索裝飾器

定義

點選開啟裝飾器在維基百科上的定義連結如下: A decorator is any callable Python object that is used to modify a function, method or class definition. 

基本語法

語法糖

@bar
def foo():
    print "foo"
其等價於:
def foo():
    print "foo"
foo = bar(foo)

無引數裝飾器

def foo(func):
    print 'decorator foo'
    return func

@foo
def bar():
    print 'bar'

bar()
foo 函式被用作裝飾器,其本身接收一個函式物件作為引數,然後做一些工作後,返回接收的引數,供外界呼叫。

注意: 時刻牢記 @foo 只是一個語法糖,其本質是 foo = bar(foo)

帶引數裝飾器

import time

def function_performance_statistics(trace_this=True):
    if trace_this:
       def performace_statistics_delegate(func):
            def counter(*args, **kwargs):
                start = time.clock()
                func(*args, **kwargs)
                end =time.clock()
                print 'used time: %d' % (end - start, )
            return counter
    else:
       def performace_statistics_delegate(func):
            return func
    return performace_statistics_delegate

@function_performance_statistics(True)
def add(x, y):
    time.sleep(3)
    print 'add result: %d' % (x + y,)

@function_performance_statistics(False)
def mul(x, y=1):
    print 'mul result: %d' % (x * y,)

add(1, 1)
mul(10)
上述程式碼想要實現一個效能分析器,並接收一個引數,來控制效能分析器是否生效,其執行效果如下所示:
add result: 2
used time: 0
mul result: 10
上述程式碼中裝飾器的呼叫等價於:
add = function_performance_statistics(True)(add(1, 1))
mul = function_performance_statistics(False)(mul(10))

類的裝飾器

類的裝飾器不常用,因此只簡單介紹。

def bar(dummy):
    print 'bar'

def inject(cls):
    cls.bar = bar
    return cls

@inject
class Foo(object):
    pass

foo = Foo()
foo.bar()
上述程式碼的 inject 裝飾器為類動態的新增一個 bar 方法,因為類在呼叫非靜態方法的時候會傳進一個self 指標,因此 bar 的第一個引數我們簡單的忽略即可,其執行結果如下:
bar

類裝飾器

類裝飾器相比函式裝飾器,具有靈活度大,高內聚、封裝性等優點。其實現起來主要是靠類內部的 __call__ 方法,當使用 @ 形式將裝飾器附加到函式上時,就會呼叫此方法,下面時一個例項:

class Foo(object):
    def __init__(self, func):
        super(Foo, self).__init__()
        self._func = func

    def __call__(self):
        print 'class decorator'
        self._func()

@Foo
def bar():
    print 'bar'

bar()
其執行結果如下:
class decorator
bar

內建裝飾器

Python中內建的裝飾器有三個: staticmethodclassmethodproperty

staticmethod 是類靜態方法,其跟成員方法的區別是沒有 self 指標,並且可以在類不進行例項化的情況下呼叫,下面是一個例項,對比靜態方法和成員方法

class Foo(object):
    @staticmethod
    def statc_method(msg):
        print msg

    def member_method(self, msg):
        print msg

foo = Foo()
foo.member_method('some msg')
foo.statc_method('some msg')
Foo.statc_method('some msg')
其執行結果如下:
some msg
some msg
some msg
classmethod 與成員方法的區別在於所接收的第一個引數不是 self 類例項的指標,而是當前類的具體型別,下面是一個例項:
class Foo(object):
    @classmethod
    def class_method(cls):
        print repr(cls)

    def member_method(self):
        print repr(self)

foo = Foo()
foo.class_method()
foo.member_method()
其執行結果如下:
<class '__main__.Foo'>
<__main__.Foo object at 0x10a611c50>
property 是屬性的意思,即可以通過通過類例項直接訪問的資訊,下面是具體的例子:
class Foo(object):
    def __init__(self, var):
        super(Foo, self).__init__()
        self._var = var

    @property
    def var(self):
        return self._var

    @var.setter
    def var(self, var):
        self._var = var

foo = Foo('var 1')
print foo.var
foo.var = 'var 2'
print foo.var
注意: 如果將上面的 @var.setter 裝飾器所裝飾的成員函式去掉,則Foo.var 屬性為只讀屬性,使用 foo.var = 'var 2' 進行賦值時會丟擲異常,其執行結果如下:
var 1
var 2
注意: 如果使用老式的Python類定義,所宣告的屬性不是 read only的,下面程式碼說明了這種情況:
class Foo:
    def __init__(self, var):
        self._var = var

    @property
    def var(self):
        return self._var

foo = Foo('var 1')
print foo.var
foo.var = 'var 2'
print foo.var
其執行結果如下:
var 1
var 2

呼叫順序

裝飾器的呼叫順序與使用 @ 語法糖宣告的順序相反,如下所示:

def decorator_a(func):
    print "decorator_a"
    return func

def decorator_b(func):
    print "decorator_b"
    return func

@decorator_a
@decorator_b
def foo():
    print "foo"
    
foo()
其等價於:
def decorator_a(func):
    print "decorator_a"
    return func

def decorator_b(func):
    print "decorator_b"
    return func

def foo():
    print "foo"

foo = decorator_a(decorator_b(foo))
foo()
通過等價的呼叫形式我們可以看到,按照python的函式求值序列,decorator_b(fun) 會首先被求值,然後將其結果作為輸入,傳遞給decorator_a,因此其呼叫順序與宣告順序相反。其執行結果如下所示:
decorator_b
decorator_a
foo

呼叫時機

裝飾器很好用,那麼它什麼時候被呼叫?效能開銷怎麼樣?會不會有副作用?接下來我們就以幾個例項來驗證我們的猜想。
首先我們驗證一下裝飾器的效能開銷,程式碼如下所示:

def decorator_a(func):
    print "decorator_a"
    print 'func id: ' + str(id(func))
    return func

def decorator_b(func):
    print "decorator_b"
    print 'func id: ' + str(id(func))
    return func

print 'Begin declare foo with decorators'
@decorator_a
@decorator_b
def foo():
    print "foo"
print 'End declare foo with decorators'

print 'First call foo'
foo()
print 'Second call foo'
foo()
print 'Function infos'
print 'decorator_a id: ' + str(id(decorator_a))
print 'decorator_b id: ' + str(id(decorator_b))
print 'fooid : ' + str(id(foo))
其執行結果如下:
Begin declare foo with decorators
decorator_b
func id: 140124961990488
decorator_a
func id: 140124961990488
End declare foo with decorators
First call foo
foo
Second call foo
foo
Function infos
decorator_a id: 140124961954464
decorator_b id: 140124961988808
fooid : 140124961990488
在執行結果中的:
Begin declare foo with decorators
decorator_b
func id: 140124961990488
decorator_a
func id: 140124961990488
End declare foo with decorators
證實了裝飾器的呼叫時機為: 被裝飾物件定義時
而執行結果中的:
First call foo
foo
Second call foo
foo
證實了在相同 .py 檔案中,裝飾器對所裝飾的函式只進行一次裝飾,不會每次呼叫相應函式時都重新裝飾,這個很容易理解,因為其本質等價於下面的函式簽名重新繫結:
foo = decorator_a(decorator_b(foo))
對於跨模組的呼叫,我們編寫如下結構的測試程式碼:
.
├── common
│   ├── decorator.py
│   ├── __init__.py
│   ├── mod_a
│   │   ├── fun_a.py
│   │   └── __init__.py
│   └── mod_b
│       ├── fun_b.py
│       └── __init__.py
└── test.py
上述所有模組中的 __init__.py 檔案均為: # -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# common/mod_a/fun_a.py


from common.decorator import foo


def fun_a():
    print 'in common.mod_a.fun_a.fun_a call foo'
    foo()

# -*- coding: utf-8 -*-
# common/mod_b/fun_b.py

from common.decorator import foo

def fun_b():
    print 'in common.mod_b.fun_b.fun_b call foo'
    foo()
# -*- coding: utf-8 -*-
# common/decorator.py

def decorator_a(func):
    print 'init decorator_a'
    return func

@decorator_a
def foo():
    print 'function foo'
# -*- coding: utf-8 -*-
# test.py

from common.mod_a.fun_a import fun_a
from common.mod_b.fun_b import fun_b

fun_a()
fun_b()
上述程式碼通過建立 common.mod_a common.mod_b 兩個子模組,並呼叫common.decorator 中的 foo 函式,來測試跨模組時裝飾器的工作情況,執行 test.py 的結果如下所示:
init decorator_a
in common.mod_a.fun_a.fun_a call foo
function foo
in common.mod_b.fun_b.fun_b call foo
function foo
經過上面的驗證,可以看出,對於跨模組的呼叫,裝飾器也只會初始化一次,不過這要歸功於 *.pyc,這與本文主題無關,故不詳述。
關於裝飾器副作用的話題比較大,這不僅僅是裝飾器本身的問題,更多的時候是我們設計上的問題,下面給出一個初學裝飾器時大家都會遇到的一個問題——丟失函式元資訊:
def decorator_a(func):
    def inner(*args, **kwargs):
        res = func(*args, **kwargs)
        return res
    return inner

@decorator_a
def foo():
    '''foo doc'''
    return 'foo result'

print 'foo.__module__: ' + str(foo.__module__)
print 'foo.__name__: ' + str(foo.__name__)
print 'foo.__doc__: ' + str(foo.__doc__)
print foo()
其執行結果如下所示:
foo.__module__: __main__
foo.__name__: inner
foo.__doc__: None
foo result
我們可以看到,在使用 decorator_a 對 foo 函式進行裝飾後,foo 的元資訊會丟失,解決方案參見: functools.wraps

多個裝飾器執行期行為

前面已經講解過裝飾器的呼叫順序和呼叫時機,但是被多個裝飾器裝飾的函式,其執行期行為還是有一些細節需要說明的,而且很可能其行為會讓你感到驚訝,下面時一個例項:

def tracer(msg):
    print "[TRACE] %s" % msg

def logger(func):
    tracer("logger")
    def inner(username, password):
        tracer("inner")
        print "call %s" % func.__name__
        return func(username, password)
    return inner

def login_debug_helper(show_debug_info=False):
    tracer("login_debug_helper")
    def proxy_fun(func):
        tracer("proxy_fun")
        def delegate_fun(username, password):
            tracer("delegate_fun")
            if show_debug_info:
                print "username: %s\npassword: %s" % (username, password)
            return func(username, password)
        return delegate_fun
    return proxy_fun

print 'Declaring login_a'

@logger
@login_debug_helper(show_debug_info=True)
def login_a(username, password):
    tracer("login_a")
    print "do some login authentication"
    return True

print 'Call login_a'
login_a("mdl", "pwd")
大家先來看一下執行結果,看看是不是跟自己想象中的一致:
Declaring login_a
[TRACE] login_debug_helper
[TRACE] proxy_fun
[TRACE] logger
Call login_a
[TRACE] inner
call delegate_fun
[TRACE] delegate_fun
username: mdl
password: pwd
[TRACE] login_a
do some login authentication
首先,裝飾器初始化時的呼叫順序與我們前面講解的一致,如下:
Declaring login_a
[TRACE] login_debug_helper
[TRACE] proxy_fun
[TRACE] logger
然而,接下來,來自 logger 裝飾器中的 inner 函式首先被執行,然後才是login_debug_helper 返回的 proxy_fun 中的 delegate_fun 函式。各位讀者發現了嗎,執行期執行login_a 函式的時候,裝飾器中返回的函式的執行順序是相反的,難道是我們前面講解的例子有錯誤嗎?其實,如果大家的認為執行期呼叫順序應該與裝飾器初始化階段的順序一致的話,那說明大家沒有看透這段程式碼的呼叫流程,下面我來為大家分析一下。
def login_debug_helper(show_debug_info=False):
    tracer("login_debug_helper")
    def proxy_fun(func):
        tracer("proxy_fun")
        def delegate_fun(username, password):
            tracer("delegate_fun")
            if show_debug_info:
                print "username: %s\npassword: %s" % (username, password)
            return func(username, password)
        return delegate_fun
    return proxy_fun
當裝飾器 login_debug_helper 被呼叫時,其等價於:
login_debug_helper(show_debug_info=True)(login_a)('mdl', 'pwd')
對於只有 login_debug_helper 的情況,現在就應該是執行玩login_a輸出結果的時刻了,但是如果現在在加上logger 裝飾器的話,那麼這個 login_debug_helper(show_debug_info=True)(login_a)('mdl', 'pwd')就被延遲執行,而將 login_debug_helper(show_debug_info=True)(login_a) 作為引數傳遞給 logger,我們令 login_tmp = login_debug_helper(show_debug_info=True)(login_a),則呼叫過程等價於:
login_tmp = login_debug_helper(show_debug_info=True)(login_a)
login_a = logger(login_tmp)
login_a('mdl', 'pwd')
相信大家看過上面的等價變換後,已經明白問題出在哪裡了,如果你還沒有明白,我強烈建議你把這個例子自己敲一遍,並嘗試用自己的方式進行化簡,逐步得出結論。

一些例項參考

本文主要講解原理性的東西,具體的例項可以參考下面的連結:
Python裝飾器例項:呼叫引數合法性驗證

Python裝飾器與面向切面程式設計

Python裝飾器小結

Python tips: 超時裝飾器, @timeout decorator

python中判斷一個執行時間過長的函式

python利用裝飾器和threading實現非同步呼叫

python輸出指定函式執行時間的裝飾器

python通過裝飾器和執行緒限制函式的執行時間

python裝飾器的一個妙用

通過 Python 裝飾器實現DRY(不重複程式碼)原則

參考資料

Understanding Python Decorators in 12 Easy Steps

Decorators and Functional Python

Python Wiki: PythonDecorators

Meta-matters: Using decorators for better Python programming

Python裝飾器入門(譯)

Python裝飾器與面向切面程式設計

Python 的閉包和裝飾器

Python裝飾器學習(九步入門)

python 裝飾器和 functools 模組

相關文章