跳至內容

Python/裝飾器

維基教科書,自由的教學讀本

Python的裝飾器是高階函數的語法糖。

一個函數decorators用於函數定義,它位於在函數定義之前的一行。例如:

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

當編譯器處理這段代碼時,aFunction()被編譯然後將結果函數對象傳遞給myDecorator代碼,後者創建一個類函數對象並取代原來的aFunction()。

Decorator所返回的對象唯一的約束是它可以作為函數使用。意味著它必須是可調用的。因此,作為decorators使用的任何一個類必須實現__call__。

簡介

[編輯]

屬性裝飾器(property decorator)的最簡單例子:

>>> class Foo(object):
...     @property
...     def bar(self):
...         return 'baz'
...
>>> F = Foo()
>>> print F.bar
baz

上例是下述代碼的語法糖:

>>> class Foo(object):
...     def bar(self):
...         return 'baz'
...     bar = property(bar)
...
>>> F = Foo()
>>> print F.bar
baz

泛型裝飾器(generic decorator)的最簡單例子:

>>> def decorator(f):
...     def called(*args, **kargs):
...         print 'A function is called somewhere'
...         return f(*args, **kargs)
...     return called
...
>>> class Foo(object):
...     @decorator
...     def bar(self):
...         return 'baz'
...
>>> F = Foo()
>>> print F.bar()
A function is called somewhere
baz

下述例子列印出函數的每次調用及其實參:

#define the Trace class that will be 
#invoked using decorators
class Trace(object):
    def __init__(self, f): #接收被装饰函数 
        self.f =f

    def __call__(self, *args, **kwargs): #实现装饰逻辑
        print "entering function " + self.f.__name__
        i=0
        for arg in args:
            print "arg {0}: {1}".format(i, arg)
            i =i+1
            
        return self.f(*args, **kwargs)

@Trace
def sum(a, b):
    print "inside sum"
    return a + b

>>> sum(3,2)
entering function sum
arg 0: 3
arg 1: 2
inside sum

或者用一個函數代替類作為裝飾器:

def Trace(f):
    def my_f(*args, **kwargs):
        print "entering " +  f.__name__
        result= f(*args, **kwargs)
        print "exiting " +  f.__name__
        return result
    my_f.__name = f.__name__
    my_f.__doc__ = f.__doc__
    return my_f

#An example of the trace decorator
@Trace
def sum(a, b):
    print "inside sum"
    return a + b

#if you run this you should see
>>> sum(3,2)
entering sum
inside sum
exiting sum
5

裝飾器是一個著名的設計模式,經常被用於有切面(aspect)需求的場景,如插入日誌、性能測試、事務處理等。裝飾器可以抽離出大量函數中與函數功能本身無關的雷同代碼並繼續重用。概括的講,裝飾器的作用就是為已經存在的對象添加額外的功能。這種編程方式被稱為面向切面的編程(Aspect-Oriented Programming)。

Python於是提供了一些語法糖來降低為使用裝飾器而帶來的原始碼輸入量。

import time
 
def timeit(func):
    def wrapper():
        start = time.clock()
        func()
        end =time.clock()
        print 'used:', end - start
    return wrapper
 
@timeit
def foo():
    print 'in foo()'
 
foo()

內置的裝飾器有三個,分別是staticmethod、classmethod和property,分別把類中定義的實例方法變成靜態方法、類方法和類屬性。由於模塊里可以定義函數,所以靜態方法和類方法的用處並不是太多,除非你想要完全的物件導向編程。屬性也不是不可或缺的。

class Rabbit(object):     
    def __init__(self, name):
        self._name = name
     
    @staticmethod
    def newRabbit(name):
        return Rabbit(name)
     
    @classmethod
    def newRabbit2(cls):
        return Rabbit('')
     
    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, name):
        self._name = name

functools模塊

[編輯]
def wrapper(func):
    def inner_function():
        pass
    return inner_function

def wrapped():
    pass

print(wrapper(wrapped).__name__)
#inner_function

如何避免這種情況的產生?方法是使用 functools .wraps 裝飾器,它的作用就是將 被修飾的函數(wrapped) 的一些屬性值賦值給 修飾器函數(wrapper) ,最終讓屬性的顯示更符合我們的直覺。

wraps(wrapped[, assigned][, updated]): 將裝飾過的函數的特殊屬性保留。

from functools import wraps

def wrapper(func):
    @wraps(func) 
    def inner_function():
        pass
    return inner_function

@wrapper
def wrapped():
    pass

print(wrapped.__name__)
# wrapped

wraps 其實是一個偏函數對象(partial),源碼如下

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

可以看到wraps其實就是調用了一個函數update_wrapper,知道原理後,我們改寫上面的代碼,在不使用 wraps的情況下,也可以讓 wrapped.__name__ 列印出 wrapped,代碼如下:

from functools import update_wrapper

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')

def wrapper(func):
    def inner_function():
        pass

    update_wrapper(inner_function, func, assigned=WRAPPER_ASSIGNMENTS)
    return inner_function

@wrapper
def wrapped():
    pass

print(wrapped.__name__)

帶參數的裝飾器

[編輯]

不傳參的裝飾器,只能對被裝飾函數,執行固定邏輯。如果要用到兩個內容大體一致,只是某些地方不同的邏輯。不傳參的話,就要寫兩個裝飾器。裝飾器如何實現傳參呢,會比較複雜,需要多一層嵌套,最外層的裝飾器的函數接受這些參數。例如: 函數閉包實現的示例:

def decoratorFunctionWithArguments(arg1, arg2, arg3):
    def wrap(f):
        print("Inside wrap()")
        def wrapped_f(*args):
            print("Inside wrapped_f()")
            print("Decorator arguments:", arg1, arg2, arg3)
            f(*args)
            print("After f(*args)")
        return wrapped_f
    return wrap
 
@decoratorFunctionWithArguments("hello", "world", 42)
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

帶參數的情況與無參情況截然不同:__call__()對象不能再作為decorated函數使用了。帶參數情況的decoration方法調用構造函數,然後就馬上調用__call__(),後者只能包含一個參數(函數對象)且返回替代原有函數的decorated函數對象。注意decoration期間__call__()僅被調用一次,此後從__call__()返回的decorated函數就可以在實際調用中使用了。

帶參數的類裝飾器示例:

class decoratorWithArguments(object):
    def __init__(self, arg1, arg2, arg3):#不再接收被装饰函数,而是接收装饰器传入参数。
        """
        If there are decorator arguments, the function
        to be decorated is not passed to the constructor!
        """
        print "Inside __init__()"
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3

    def __call__(self, f):#接收被装饰函数,实现装饰逻辑。
        """
        If there are decorator arguments, __call__() is only called
        once, as part of the decoration process! You can only give
        it a single argument, which is the function object.
        """
        print "Inside __call__()"
        def wrapped_f(*args):
            print "Inside wrapped_f()"
            print "Decorator arguments:", self.arg1, self.arg2, self.arg3
            f(*args)
            print "After f(*args)"
        return wrapped_f

@decoratorWithArguments("hello", "world", 42)
def sayHello(a1, a2, a3, a4):
    print 'sayHello arguments:', a1, a2, a3, a4