1. 程式人生 > >深入理解Python 裝飾器(decorator)

深入理解Python 裝飾器(decorator)

返璞歸真, 看山還是山

剛看到Python裝飾器時, 覺得很神奇。簡單實驗下,發現也就那麼回事。但是慢慢的看到越來越多的裝飾器。很多時候又不瞭解到底是怎麼回事了。

最後還是決定好好研究下。

先看看一些例項, 然後再來分析下原理
假設我們有如下的基本函式

def do_something():
    for i in range(1000000):
        pass
    print "play game"

do_something()

結果如下:

play game

需求1: 統計函式的執行時間

1. 不是裝飾器的裝飾器

import time
def
decorator(fun):
start = time.time() fun() runtime = time.time()-start print runtime def do_something(): for i in range(1000000): pass print "play game" decorator(do_something)

結果如下:

play game
0.0299999713898

這種實現看上去還可以,但是每次呼叫的是decorator,還要把函式作為一個引數傳入。這樣需要修改呼叫的地方,使用起來就不方便了。

2. 最簡單的裝飾器

import time
def decorator(fun):
    def wrapper():
        start = time.time()
        fun()
        runtime = time.time()-start
        print runtime
    return wrapper
@decorator
def do_something():
    for i in range(1000000):
        pass
    print "play game"

do_something()

結果如下:

play game
0.0329999923706

裝飾器是在函式定義時前面加@,然後跟裝飾器的實現函式。可以看出,現在只要直接呼叫do_something就可以了。呼叫的地方不要作任何修改。

3. 目標函式帶固定引數的裝飾器

import time
def decorator(fun):
    def wrapper(name):
        start = time.time()
        fun(name)
        runtime = time.time()-start
        print runtime
    return wrapper
@decorator
def do_something(name):
    for i in range(1000000):
        pass
    print "play game " + name

do_something("san guo sha")

結果如下:

play game san guo sha
0.039999961853

實現很簡單, 就是給wrapper函式參加相同的引數

4. 目標函式帶不固定引數的裝飾器

import time
def decorator(fun):
    def wrapper(*args, **kwargs):
        start = time.time()
        fun(*args, **kwargs)
        runtime = time.time()-start
        print runtime
    return wrapper
@decorator
def do_something(name):
    for i in range(1000000):
        pass
    print "play game " + name

@decorator
def do_something2(user, name):
    for i in range(1000000):
        pass
    print user+" play game " + name

do_something("san guo sha")
do_something2("wang xiao er","san guo sha")

結果如下:

play game san guo sha
0.029000043869
wang xiao er play game san guo sha
0.0310001373291

需求2: 目標函式每次呼叫重複執行指定的次數

5. 讓裝飾器帶引數

import time
def decorator(max):
    def _decorator(fun):
        def wrapper(*args, **kwargs):
            start = time.time()
            for i in xrange(max):
                fun(*args, **kwargs)
            runtime = time.time()-start
            print runtime
        return wrapper
    return _decorator
@decorator(2)
def do_something(name):
    for i in range(1000000):
        pass
    print "play game " + name

do_something("san guo sha")

結果如下:

play game san guo sha
play game san guo sha
0.0600001811981

6. 原理

看了這麼多例項, 裝飾器的基本型別也基本上都有了。是不是清楚了呢?
如果還是不清楚,那就繼續看下面的內容。

1 不帶引數的裝飾器

@a_decorator
def f(...):
    ...

#經過a_decorator後, 函式f就相當於以f為引數呼叫a_decorator返回結果。
f = a_decorator(f)

來分析這個式子, 可以看出至少要滿足以下幾個條件
1. 裝飾器函式執行在函式定義的時候
2. 裝飾器需要返回一個可執行的物件
3. 裝飾器返回的可執行物件要相容函式f的引數

2 驗證分析

1 裝飾器執行時間

import time
def decorator(fun):
    print "decorator"
    def wrapper():
        print "wrapper"
        start = time.time()
        fun()
        runtime = time.time()-start
        print runtime
    return wrapper
@decorator
def do_something():
    for i in range(1000000):
        pass
    print "play game"

結果如下:

decorator

可以看出, 這裡的do_something並沒有呼叫, 但是卻列印了decorator, 可wrapper沒有打印出來。也就是說decorator是在do_something呼叫的時候執行的。

2 返回可執行的物件

import time
def decorator(fun):
    print "decorator"
    def wrapper():
        print "wrapper"
        start = time.time()
        fun()
        runtime = time.time()-start
        print runtime
    return None
@decorator
def do_something():
    for i in range(1000000):
        pass
    print "play game"

do_something()

結果如下:

decoratorTraceback (most recent call last):
  File "deco.py", line 17, in <module>
    do_something()
TypeError: 'NoneType' object is not callable

3 相容函式f的引數

import time
def decorator(fun):
    print "decorator"
    def wrapper():
        print "wrapper"
        start = time.time()
        fun()
        runtime = time.time()-start
        print runtime
    return wrapper
@decorator
def do_something(name):
    for i in range(1000000):
        pass
    print "play game"

do_something("san guo sha")

結果如下:

decoratorTraceback (most recent call last):
  File "deco.py", line 17, in <module>
    do_something("san guo sha")
TypeError: wrapper() takes no arguments (1 given)

看到這裡, 至少對不帶引數的裝飾器應該全弄清楚了, 也就是說能到看山還是山了。

3 帶引數的裝飾器

這裡就給一個式子, 剩下的問題可以自己去想

@decomaker(argA, argB, ...)
def func(arg1, arg2, ...):
    pass
#這個式子相當於
func = decomaker(argA, argB, ...)(func)

4 被裝飾過的函式的函式名

import time
def decorator(fun):
    def wrapper():
        start = time.time()
        fun()
        runtime = time.time()-start
        print runtime
    return wrapper
@decorator
def do_something():
    print "play game"

print do_something.__name__

結果如下:

wrapper

可以看出, do_something的函式名變成了wrapper,這不是我們想要的。原因估計各位也都清楚了。那要怎麼去解決呢?

import time
def decorator(fun):
    def wrapper():
        start = time.time()
        fun()
        runtime = time.time()-start
        print runtime
    wrapper.__name__ = fun.__name__
    return wrapper
@decorator
def do_something():
    print "play game"

print do_something.__name__

結果如下:

do_something

但是這個看起來是不是很不專業, python的unctools.wraps提供瞭解決方法

import time
import functools 
def decorator(fun):
    @functools.wraps(fun)
    def wrapper():
        start = time.time()
        fun()
        runtime = time.time()-start
        print runtime
    return wrapper
@decorator
def do_something():
    print "play game"

print do_something.__name__

結果如下:

do_something

到此為止, 你是不是覺得已經完全明白了呢?
但事實是, 這其實還不夠

7. 裝飾器類

需求3: 讓函式只能執行指定的次數
前面我們講的都是函式式的裝飾器, 那麼類能不能成為裝飾器呢?

import time
import functools 

class decorator(object):
    def __init__(self, max):
        self.max = max
        self.count = 0
    def __call__(self, fun):
        self.fun = fun
        return self.call_fun

    def call_fun(self, *args, **kwargs):
        self.count += 1
        if ( self.count == self.max):
            print "%s run more than %d times"%(self.fun.__name__, self.max)
        elif (self.count<self.max):
            self.fun(*args, **kwargs)
        else:
            pass

@decorator(10)
def do_something():
    print "play game"
@decorator(15)
def do_something1():
    print "play game 1"
for i in xrange(20):
    do_something()
    do_something1()

結果如下:

play game
play game 1
play game
play game 1
play game
play game 1
play game
play game 1
play game
play game 1
play game
play game 1
play game
play game 1
play game
play game 1
play game
play game 1
do_something run more than 10 times
play game 1
play game 1
play game 1
play game 1
play game 1
do_something1 run more than 15 times

是不是感覺有點怪, 但它確實是可行的。
在Python中, 其實函式也是物件。 反過來, 物件其實也可以像函式一樣呼叫, 只要在類的方法中實現__call__方法。回想一下建立物件的過程

class A:
    def __init__(self):
        pass
a = A()

這其實和函式呼叫沒什麼區別, 那麼把這個式子代入到之前兩個裝飾器的式子中,結果如下:
帶引數的裝飾器
fun = A.__init__(args)(fun)
不帶引數的裝飾器
fun = A.__init__(fun)()

現在裝飾器的內容基本差不多了。 還有一些問題, 可以自己去嘗試研究。

自己的才是自己的

還有幾個問題如下:
1. 類裝飾器(裝飾器裝飾的物件是類)
2. 類函式裝飾器(裝飾器裝飾的物件是類的函式)
3. 多個裝飾器一起使用(函式巢狀)

8 參考資料