[python] decorator 之很難理解的快速理解法

[python] decorator 之很難理解的快速理解法

這次遇到的 decorator 不是 design pattern 的那一個 decorator。python 有一種語法是在 function 前面一行加上 @辨識子 的語法,叫做 decorator。

python 的 decorator 常見的範例很多,可是我一直很難了解為什麼。

有人舉了個非常簡單的例子[1],以下是準備好的 function,可以幫忙在每次呼叫前寫出被呼叫函式名字的 log。

def logged(func):
    def with_logging(*args, **kwargs):
        print func.__name__ + " was called"
        return func(*args, **kwargs)
    return with_logging

當在自己寫的程式裡,想要在呼叫前寫下名字,就只要在函式前加上 @logged。

@logged
def f(x):
   """does some math"""
   return x + x * x


print f(3)

>>>
f was called
12
>>>

上面那一組程式,等於下面這一句。

def f(x):
   """does some math"""
   return x + x * x

f = logged(f)

print f(3)

看似簡單,但其中有幾點巧妙。

logged 這個準備要裝飾別的函式的函式,必須要傳回一個函式。在例子中,logged 是一個函式,傳入的 func 也是個函式,在 logged 裡面,也有一個 with_logging 的函式。logged 要回傳的是 with_logging 這個函式,而我們希望要執行的 func,要放在 with_logging 裡面執行,要回傳 func 的值,就在 with_logging 這裡回傳。我想了很久,簡單來說,就是打包一個函式,之後就用打包過的函式。在這個簡單的例子裡,只是 print 一行,logged 可以改成更簡單的型式(最簡單的是用 lambda,但我說的不是這個):

def logged2(func):
    print func.__name__ + " was called"
    return func

print func 的名字,就又把原來的函式傳回去,這是非常容易理解的。但是,若像文章[3],有人想要包裹 html 標籤在某一文字,那麼單是回傳原始 func 是辦不到的。(當然,直接回傳包裏完的文字就無法使用 decorator 的特色了。)

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped

def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

@makeitalic
@makebold
def hello():
    return "hello world"

print hello()

>>>
<i><b>hello world</b></i>
>>>

你會發現,為了要使用 decorator 這種語法,所有的操作都是在函式上面。尤其是裝飾函式,傳入值跟傳出值都是函式。同時從這個例子裡可以看到,包裏的順序是從下往上,先外包好 <b>,再外包好 <i>。幫助記憶順序的記法是先進 makebold 包好一層,再進 makeitalic 再包一層。另一種方法就是當 recursive 來理解,看到 @makeitalic ,makeitalic 傳入的 fn,就是含有 @makebold 的函式,而在 makebold 的 fn 就是 hello。這只是想像,實作如何不知道。不過,我是這樣幫助記憶順序。

很久以前我在網路上找到下面的範例,我已經忘記在哪兒找到,我自己有改寫了一下幫助我體會它的用法。這範例特別的是識別字指到一個類別而不是一個函式。在使用的時候,就好像是把類別實例化,然後在 __call__ 裡把要處理的事、要呼叫的函數一一執行。似乎很有條理,我就留下來了。

class entryExit(object):

    def __init__(self, f):
        print 'entry init enter'
        self.f = f
        print 'entry init exit'

    def __call__(self, *args):
        print "Entering", self.f.__name__
        r = self.f(*args)
        print "Exited", self.f.__name__
        return r

print 'decorator using'

@entryExit
def hello(a):
    print 'inside hello'
    return "hello world " + a

print 'test start'
print hello('friends')

>>> ================================ RESTART ================================
>>>
decorator using
entry init enter
entry init exit
test start
Entering hello
inside hello
Exited hello
hello world friends

比較一開始的函式例子跟後來的類別例子,雖然識別字指的一個是類別,一個是函數,在程式碼中,在函式名後面加上(),變成函式呼叫。而類別本來是不能被呼叫的(not callable),但加上類別方法__call__之後,就變得可以被呼叫,我們似乎可以把這類別視為一個函式(?)。從程式的結果來看 test start 出現在 entry init exit 的後面,代表在 print 'test start' 之前,entryExit 就已實例化,應該就是 @entryExit 這句執行的。當程式執行到 hello('friend') 的時候,先進入的是 entryExit 的 __call__,後來才是 hello 自己的內容,這樣的流程觀察比較清楚,但是 cpython 倒底如何實作這段,可能真的要去看 source code 吧?不過這個例子很漂亮的介紹兩件事,第一件事是 decorator,第二件事就是 python 函式與物件之間的巧妙關聯。

 

參考:

1. http://stackoverflow.com/questions/308999/what-does-functools-wraps-do

2. http://www.artima.com/weblogs/viewpost.jsp?thread=240808

3. http://stackoverflow.com/questions/739654/how-can-i-make-a-chain-of-function-decorators-in-python

 

 

 

分享