Python 裝飾器使用指南

goodspeed發表於2017-08-15

裝飾器是可呼叫的物件,其引數是另一個函式(被裝飾的函式)。

裝飾器基礎知識

首先看一下這段程式碼

def deco(fn):
    print "I am %s!" % fn.__name__

@deco
def func():
    pass

# output
I am func!

# 沒有執行func 函式 但是 deco 被執行了複製程式碼

HUGOMORE42

在用某個@decorator來修飾某個函式func時

@decorator
def func():
    pass複製程式碼

其直譯器會解釋成下面這樣的語句:

func = decorator(func)

其實就是把一個函式當引數傳到另一個函式中,然後再回撥,但是值得注意的是裝飾器必須返回一個函式給func

裝飾器的一大特性是,能把被裝飾的函式替換成其他函式。第二大特性是,裝飾器在載入模組時立即執行。

裝飾器何時執行

裝飾器的一個關鍵特性是,它們在被裝飾的函式定義後立即執行。這通常在匯入是(python 載入模組時)。

看下下面的示例:

registry = []  # registry 儲存被@register 裝飾的函式的引用

def register(func):  # register 的引數是一個函式
    print('running register(%s)' % func)  # 列印被裝飾的函式
    registry.append(func)  # 把 func 存入 `registery`
    return func  # 返回 func:必須返回函式,這裡返回的函式與通過引數傳入的一樣

@register  # `f1` 和 `f2`被 `@register` 裝飾
def f1():
    print('running f1()')

@register
def f2():
    print('running f2()')

def f3():  # <7>
    print('running f3()')

def main():  # main 列印 `registry`,然後呼叫 f1()、f2()和 f3()
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()

if __name__=='__main__':
    main()  # <9>複製程式碼

執行程式碼結果如下:

running register(<function f1 at 0x1023fb378>)
running register(<function f2 at 0x1023fb400>)
running main()
registry -> [<function f1 at 0x1023fb378>, <function f2 at 0x1023fb400>]
running f1()
running f2()
running f3()複製程式碼

從結果可以發現register 在模組中其他函式之前執行了兩次。呼叫 register 時,傳給它的引數是被裝飾的函式(例如)。

看完上邊的示例我們知道,函式被裝飾器裝飾後會變成裝飾器函式的一個引數,那這時就不得不說變數的作用域了。

變數作用域

先看下下邊這段程式碼:

def f1(a):
    print(locals())
    print(a)
    print(b)


f1(3)
# output
{'a': 3}
3
Traceback(most recent call last):
    File "<stdin>", line 1, in <module>
    File "<stdin>", line 3, in f1
NameError: global name 'b' is not defined複製程式碼

這裡的錯誤是因為全域性變數 b 沒有定義,如果我們先在函式外部給 b 賦值,再呼叫這個方法就不會報錯了。

函式執行時會建立一個新的作用域(名稱空間)。函式的名稱空間隨著函式呼叫開始而開始,結束而銷燬。
這個例子中 f1 的名稱空間中只有 {'a': 3},所以 b 會被認為是全域性變數。

再看一個例子:

b = 6
def f2(a):
    print(a)
    print(globals())
    print(locals())
    print(b)
    b = 9

f2(3)
# output
3
{
    '__name__': '__main__',
    '__doc__': None, 
    '__package__': None, 
    '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x10c7f2dd8>, 
    '__spec__': None, 
    '__annotations__': {}, 
    '__builtins__': <module 'builtins' (built-in)>, 
    '__file__': '~/var_local.py', 
    '__cached__': None, 
    'b': 6, 
    'f2': <function f2 at 0x10c7e7598>
}
{'a': 3}
3
Traceback(most recent call last):
    File "<stdin>", line 1, in <module>
    File "<stdin>", line 3, in f1
UnboundLocalError: local variable 'b' referenced before assignment複製程式碼

這個例子和上一個例子不同是,我現在函式外部定義了全域性變數b,但是執行f2 這個方法並沒有列印6,這是為什麼呢?
這是因為執行函式時 Python 會嘗試從區域性變數中獲取 b,函式對於已經引用但未賦值的變數並不會自動宣告為區域性變數,所以直譯器發現後邊的賦值之前有引用就會丟擲 UnboundLocalError 錯誤。

Python 不要求宣告變數,但是假定在函式定義體中賦值的變數是區域性變數。

如果要讓直譯器把b當做全域性變數,要使用global宣告:

b = 6
def f3(a):
    global b
    print(a)
    print(b)
    b = 9

f2(3)
# output
3
6複製程式碼

閉包

閉包是一種函式,它會保留定義函式時存在的自由變數的繫結,這樣呼叫函式時,雖然定義作用域不可用,但仍能使用那些繫結。

介紹閉包前先要說明一下 Python 的函式引數

函式的兩種引數

函式有兩種引數

  1. 位置引數
  2. 命名引數
def foo(x, y=0):
    return x - y複製程式碼

python 中一切都是物件

函式和python中其他一樣都是物件

In [7]: class A(object):
   ...:     pass


In [8]: A
Out[8]: __main__.A
In [9]: type(A)
Out[9]: type
In [10]: def foo():
   ....:     pass


In [11]: type(foo)
Out[11]: function

In [12]: A.__class__
Out[12]: type

In [13]: foo.__class__
Out[13]: function

In [14]: a = 1
In [15]: a.__class__
Out[15]: int

# 類 是物件
In [16]: issubclass(A.__class__, object)
Out[16]: True

# 變數 是物件
In [17]: issubclass(a.__class__, object)
Out[17]: True

# 函式 是物件
In [18]: issubclass(foo.__class__, object)
Out[18]: True複製程式碼

所以函式也可以作為引數傳遞給其它函式,也可以被當做返回值返回

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

def apply(func):
    return func

>> a = apply(add)
>> type(a)
<type 'function'>

>> a(1, 2)
>> 3複製程式碼

閉包的使用

先來看一個示例:假設有個名為 avg 的函式,它的作用是計算不斷增加的系列值的均值;
它是這麼使用的:

>>> avg(10)
10
>>> avg(11)
10.5
>>> avg(12)
11複製程式碼

那麼我們考慮下,avg 從何而來,它又在哪裡儲存歷史值呢,這個用閉包如何實現呢?
下邊的程式碼是閉包的實現:

def make_averager():
    series = []

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager複製程式碼

呼叫 make_averager 時,返回一個 averager 函式物件。每次呼叫 averager 時,它都會把引數新增到系列值中,然後計算當前平均值。

avg = make_averager()

>>> avg(10)
10
>>> avg(11)
10.5
>>> avg(12)
11複製程式碼

seriesmake_averager 函式的區域性變數,因為那個函式的定義體中初始化了series: series=[]。但在averager 函式中,series 是自由變數(指未在本地作用域中繫結的變數)。

變數示意圖
變數示意圖

averager 的閉包延伸到那個函式的作用域之外,包含自由變數series的繫結。

  • avg 就是一個閉包
  • 也可以說 make_averager 指向一個閉包
  • 或者說 make_averager 是閉包的工廠函式

閉包可以認為是一個內層函式(averager),由一個變數指代,而這個變數相對於外層包含它的函式而言,是本地變數
巢狀定義在非全域性作用域裡面的函式能夠記住它在被定義的時候它所處的封閉名稱空間

閉包 只是在形式和表現上像函式,但實際上不是函式。函式是一些可執行的程式碼,這些程式碼在函式被定義後就確定了,不會在執行時發生變化,所以一個函式只有一個例項。閉包在執行時可以有多個例項,不同的引用環境和相同的函式組合可以產生不同的例項。

裝飾器

實現一個簡單的裝飾器

對一個已有的模組做一些“修飾工作”,所謂修飾工作就是想給現有的模組加上一些小裝飾(一些小功能,這些小功能可能好多模組都會用到),但又不讓這個小裝飾(小功能)侵入到原有的模組中的程式碼裡去

def my_decorator(func):
    def wrapper():
        print "Before the function runs"
        func() # 這行程式碼可用,是因為 wrapper 的閉包中包含自由變數 func
        print "After the function runs"
    return wrapper

def my_func():
    print "I am a stand alone function"


>> my_func()

# output
I am a stand alone function

# 然後,我們在這裡裝飾這個函式
# 將函式傳遞給裝飾器,裝飾器將動態地將其包裝在任何想執行的程式碼中,然後返回一個新的函式

>> my_func = my_decorator(my_func)
>> my_func()
#output
Before the function runs
I am a stand alone function
After the function runs

# 也可以這麼寫

@ my_decorator
def my_func():
    print "I am a stand alone function"

>> my_func()
#output
Before the function runs
I am a stand alone function
After the function runs複製程式碼

裝飾器是設計模式中裝飾器模式英文版)的python實現。

多個裝飾器

裝飾器可以巢狀使用

def bread(func):
    def wrapper():
        print "</''''''\>"
        func()
        print "<\______/>"
    return wrapper

def ingredients(func):
    def wrapper():
        print "#tomatoes#"
        func()
        print "~salad~"
    return wrapper

def sandwich(food="--ham--"):
    print food

#### outputs:複製程式碼

巢狀兩個裝飾器

>> sandwich = bread(ingredients(sandwich))
>> sandwich()

#### outputs
</''''''\>
 #tomatoes#
 --ham--
 ~salad~
<\______/>複製程式碼

更簡單的寫法

@bread
@ingredients
def sandwich(food="--ham--"):
    print food複製程式碼

裝飾器的順序是很重要的

如果我們換下順序就會發現,三明治變成了披薩。。

@ingredients
@bread
def sandwich(food="--ham--"):
    print food

# outputs:

 #tomatoes#
 </' ' ' ' ' '\>
   --ham--
 <\______/>
 ~salad~複製程式碼

Decorator 的工作原理

首先看一下這段程式碼

def deco(fn):
    print "I am %s!" % fn.__name__

@deco
def func():
    pass

# output
I am func!

# 沒有執行func 函式 但是 deco 被執行了複製程式碼

在用某個@decorator來修飾某個函式func時

@decorator
def func():
    pass複製程式碼

其直譯器會解釋成下面這樣的語句:

func = decorator(func)

其實就是把一個函式當引數傳到另一個函式中,然後再回撥
但是值得注意的是裝飾器必須返回一個函式給func

回到剛才的例子


def my_decorator(func):
    def wrapper():
        print "Before the function runs"
        func()
        print "After the function runs"
    return wrapper

def my_func():
    print "I am a stand alone function"

>> my_func = my_decorator(my_func)
>> my_func()
#output
Before the function runs
I am a stand alone function
After the function runs複製程式碼

my_decorator(my_func)返回了wrapper()函式,所以,my_func其實變成了wrapper的一個變數,而後面的my_func()執行其實變成了wrapper()

比如:多個decorator

@decorator_one
@decorator_two
def func():
    pass複製程式碼

相當於:

func = decorator_one(decorator_two(func))複製程式碼

比如:帶引數的decorator:

@decorator(arg1, arg2)
def func():
    pass
# 相當於:

func = decorator(arg1,arg2)(func)複製程式碼

帶引數的裝飾器

首先看一下, 如果被裝飾的方法有引數

def a_decorator(method_to_decorate):
    def wrapper(self, x):
        x -= 3
        print 'x is %s' % x
        method_to_decorate(self, x)
    return wrapper


class A(object):

    def __init__(self):
        self.b = 42

    @a_decorator
    def number(self, x):
        print "b is %s" % (self.b + x)


a = A()
a.number(-3)

# output
x is -6
b is 36複製程式碼

通常我們都使用更加通用的裝飾器,可以作用在任何函式或物件方法上,而不必關心其引數使用

def a_decorator(method_to_decorate):
    def wrapper(*args, **kwargs):
        print '****** args ******'
        print args
        print kwargs
        method_to_decorate(*args, **kwargs)
    return wrapper

@a_decorator
def func():
    pass

func()
#output
****** args ******
()
{}

@a_decorator
def func_with_args(a, b=0):
    pass
    return a + b

func_with_args(1, b=2)

#output
****** args ******
(1,)
{'b': 2}複製程式碼

上邊的示例是帶引數的被裝飾函式

現在我們看一下向裝飾器本身傳遞引數

向裝飾器本身傳遞引數

裝飾器必須使用函式作為引數,你不能直接傳遞引數給裝飾器本身
如果想傳遞引數給裝飾器,可以 宣告一個用於建立裝飾器的函式

# 我是一個建立裝飾器的函式
def decorator_maker():
    print "I make decorators!"

    def my_decorator(func):
        print "I am a decorator!"

        def wrapped():
            print "I am the wrapper around the decorated function. "
            return func()

        print "As the decorator, I return the wrapped function."
        return wrapped

    print "As a decorator maker, I return a decorator"
    return my_decorator

# decorator_maker()返回的是一個裝飾器
new_deco = decorator_maker()

#outputs
I make decorators!
As a decorator maker, I return a decorator

# 使用裝飾器
def decorated_function():
    print "I am the decorated function"

decorated_function = new_deco(decorated_function)   
decorated_function()

# outputs
I make decorators!
As a decorator maker, I return a decorator
I am a decorator!
As the decorator, I return the wrapped function.
I am the wrapper around the decorated function.
I am the decorated  function複製程式碼

使用@修飾

decorated_function = new_deco(decorated_function)

# 等價於下面的方法

@new_deco
def func():
    print "I am the decorated function"


@decorator_maker()
def func():
    print "I am the decorated function"複製程式碼

my_decorator(裝飾器函式)是decorator_maker(裝飾器生成函式)的內部函式
所以可以使用把引數加在decorator_maker(裝飾器生成函式)的方法像裝飾器傳遞引數

# 我是一個建立帶引數裝飾器的函式
def decorator_maker_with_arguments(darg1, darg2):
    print "I make decorators! And I accept arguments:", darg1, darg2

    def my_decorator(func):
        print "I am a decorator! Somehow you passed me arguments:", darg1, darg2

        def wrapped(farg1, farg2):
            print "I am the wrapper around the decorated function."
            print "I can access all the variables", darg1, darg2, farg1, farg2
            return func(farg1, farg2)

        print "As the decorator, I return the wrapped function."
        return wrapped

    print "As a decorator maker, I return a decorator"
    return my_decorator

@decorator_maker_with_arguments("deco_arg1", "deco_arg2")
def decorated_function_with_arguments(function_arg1, function_arg2):
    print ("I am the decorated function and only knows about my arguments: {0}"
           " {1}".format(function_arg1, function_arg2))


decorated_function_with_arguments('farg1', 'farg2')

# outputs

I make decorators! And I accept arguments: deco_arg1 deco_arg2
As a decorator maker, I return a decorator
I am a decorator! Somehow you passed me arguments: deco_arg1 deco_arg2
As the decorator, I return the wrapped function.
I am the wrapper around the decorated function.
I can access all the variables deco_arg1 deco_arg2 farg1 farg2
I am the decorated function and only knows about my arguments: farg1 farg2複製程式碼

這裡裝飾器生成函式內部傳遞引數是閉包的特性

使用裝飾器需要注意

  • 裝飾器是Python2.4的新特性
  • 裝飾器會降低程式碼的效能
  • 裝飾器僅在Python程式碼匯入時被呼叫一次,之後你不能動態地改變引數.當你使用"import x",函式已經被裝飾

使用 functools.wraps

最後Python2.5解決了最後一個問題,它提供functools模組,包含functools.wraps,這個函式會將被裝飾函式的名稱、模組、文件字串拷貝給封裝函式

def foo():
    print "foo"

print foo.__name__
#outputs: foo

# 但當你使用裝飾器
def bar(func):
    def wrapper():
        print "bar"
        return func()
    return wrapper

@bar
def foo():
    print "foo"

print foo.__name__
#outputs: wrapper複製程式碼

"functools" 可以修正這個錯誤


import functools

def bar(func):
    # 我們所說的 "wrapper", 封裝 "func"
    @functools.wraps(func)
    def wrapper():
        print "bar"
        return func()
    return wrapper

@bar
def foo():
    print "foo"

# 得到的是原始的名稱, 而不是封裝器的名稱
print foo.__name__
#outputs: foo複製程式碼

類裝飾器

class myDecorator(object):

    def __init__(self, func):
        print "inside myDecorator.__init__()"
        self.func = func

    def __call__(self):
        self.func()
        print "inside myDecorator.__call__()"

@myDecorator
def aFunction():
    print "inside aFunction()"

print "Finished decorating aFunction()"

aFunction()

# output:
# inside myDecorator.__init__()
# Finished decorating aFunction()
# inside aFunction()
# inside myDecorator.__call__()複製程式碼

我們可以看到這個類中有兩個成員:

  1. 一個是init(),這個方法是在我們給某個函式decorator時被呼叫,所以,需要有一個func的引數,也就是被decorator的函式。
  2. 一個是call(),這個方法是在我們呼叫被decorator函式時被呼叫的

如果decorator有引數的話,init() 就不能傳入func了,而fn是在call的時候傳入

class myDecorator(object):

    def __init__(self, arg1, arg2):
        self.arg1 = arg2

    def __call__(self, func):
        def wrapped(*args, **kwargs):
            return self.func(*args, **kwargs)
        return wrapped複製程式碼

裝飾器示例

Python 內建了三個用於裝飾方法的函式:property、classmethod和 staticmethod。
另一個常見的裝飾器是 functools.wraps,它的作用是協助構建行為良好的裝飾器。

functools.lru_cache

functools.lru_cache 實現了記憶體快取功能,它可以把耗時長的函式結果儲存起來,避免傳入相同引數時重複計算。

我們自己的實現程式碼如下:


from functools import wraps
def memo(fn):
    cache = {}
    miss = object()

    @wraps(fn)
    def wrapper(*args):
        result = cache.get(args, miss)
        if result is miss:
            result = fn(*args)
            print "{0} has been used: {1}x".format(fn.__name__, wrapper.count)
            cache[args] = result
        return result

    return wrapper

@memo
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)複製程式碼

統計函式執行次數的裝飾器

def counter(func):
    """
    記錄並列印一個函式的執行次數
    """
    def wrapper(*args, **kwargs):
        wrapper.count = wrapper.count + 1
        res = func(*args, **kwargs)
        print "{0} has been used: {1}x".format(func.__name__, wrapper.count)
        return res
    wrapper.count = 0
    return wrapper複製程式碼
  • 裝飾器做快取

帶有過期時間的記憶體快取

def cache_for(duration):
    def deco(func):
        @wraps(func)
        def fn(*args, **kwargs):
            key = pickle.dumps((args, kwargs))
            value, expire = func.func_dict.get(key, (None, None))
            now = int(time.time())
            if value is not None and expire > now:
                return value
            value = func(*args, **kwargs)
            func.func_dict[key] = (value, int(time.time()) + duration)
            return value
        return fn
    return deco複製程式碼

統計程式碼執行時間

def timeit(fn):

    @wraps(fn)
    def real_fn(*args, **kwargs):
        if config.common['ENVIRON'] == 'PRODUCTION':
            return fn(*args, **kwargs)

        _start = time.time()
        #app.logger.debug('Start timeit for %s' % fn.__name__)
        result = fn(*args, **kwargs)
        _end = time.time()
        _last = _end - _start
        app.logger.debug('End timeit for %s in %s seconds.' %
                         (fn.__name__, _last))
        return result

    return real_fn複製程式碼

參考連結

  1. How can I make a chain of function decorators in Python?
  2. 理解PYTHON中的裝飾器
  3. Python修飾器的函數語言程式設計
  4. Understanding Python Decorators in 12 Easy Steps!
  5. PEP 0318 -- Decorators for Functions and Methods
  6. PEP 3129 -- Class Decorators
  7. args and *kwargs? [duplicate]
  8. why-cant-i-set-a-global-variable-in-python
  9. 【flask route】
  10. PythonDecoratorLibrary
  11. 關於Python Decroator的各種提案

最後,感謝女朋友支援。

歡迎關注(April_Louisa) 請我喝芬達
歡迎關注
歡迎關注
請我喝芬達
請我喝芬達

相關文章