2019年1月13日

decorator in python

python 的 decorator 功能,使用的語法看起來很像 Java 的 annotation,是在 function 前面加上 @decorator,雖然名稱是 decorator,但實際上比 Decorator Design Pattern 的功能還要多一點。

decorator class

function decorator 使用起來,就是直接在 function 定義前面,加上 @my_decorator。

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

實際上,當 compiler 編譯 aFunction 時,會將 aFunction 整個 function 傳入 myDecorator,有取代原本 aFunction 的感覺,也有點像是 my_decorator(aFunction) 這樣的意思。

要實作 my_decorator 必須要實作 __init__ 以及 __call__ 這兩個 function,__init__會在一開始就被呼叫,而 __call__ 是在結束時被呼叫。

class my_decorator(object):

    def __init__(self, f):
        print("inside my_decorator.__init__()")
        f()

    def __call__(self):
        print("inside my_decorator.__call__()")

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

print("Finished decorating aFunction()")

aFunction()

執行結果如下,在程式中,因為 decorator 初始化的關係,會先呼叫 my_decorator.__init__(),在這個函數裡面,會取得 aFunction 的函數定義,所以可以在裡面呼叫 aFunction,在 aFunction 結束後,才列印了 Finished 的部分。

而最後是因為呼叫 aFunction(),所以轉為呼叫 my_decorator.__call__()

inside my_decorator.__init__()
inside aFunction()
Finished decorating aFunction()
inside my_decorator.__call__()

也就是說,decorator 是在 init 的時候,就已經取得了 aFunction 的定義,而不是在呼叫 aFunction 時,才傳入 my_decorator。

decorator 其實就只是將 function 傳入另一個 function 的 syntax sugar。


在剛剛的例子中,我們在 __init__ 直接呼叫了 f(),但以下這個例子,才是比較重要的功能,他先將 f 儲存到變數中,然後等到 __call 才使用 f()

class entry_exit(object):

    def __init__(self, f):
        self.f = f

    def __call__(self):
        print("Entering", self.f.__name__)
        self.f()
        print("Exited", self.f.__name__)

@entry_exit
def func1():
    print("inside func1()")

@entry_exit
def func2():
    print("inside func2()")

func1()
func2()

所以可以在呼叫 func1() 的前面以及執行完成的後面,在 __call__裡面呼叫 self.f() 的前後加上自己需要增加的 code

Entering func1
inside func1()
Exited func1
Entering func2
inside func2()
Exited func2

function as decorator

剛剛是使用 class 作為 decorator,但可以直接定義一個 decorator function,以下的例子中,定義了 decorator function 以 f 為參數,在裡面定義了新的 new_f,取代了原本的 f

def entry_exit(f):
    def new_f():
        print("Entering", f.__name__)
        f()
        print("Exited", f.__name__)
    return new_f

@entry_exit
def func1():
    print("inside func1()")

@entry_exit
def func2():
    print("inside func2()")

func1()
func2()
print(func1.__name__)

執行結果為,因為 new_f 已經取代了 f,所以 func1.__name__ 的名稱結果是 new_f

Entering func1
inside func1()
Exited func1
Entering func2
inside func2()
Exited func2
new_f

如果覺得奇怪,還是可以刻意將 new_f 的名稱修改為跟 f 一樣

def entry_exit(f):
    def new_f():
        print("Entering", f.__name__)
        f()
        print("Exited", f.__name__)
    new_f.__name__ = f.__name__
    return new_f

Decorator with Arguments

因為 decorator 其實也是呼叫一個 function,所以可以直接在 decorator 上加上參數,以下是在 decorator class 裡面取得 decorator 參數的例子

class decorator_with_arguments(object):

    def __init__(self, arg1, arg2, arg3):
        print("Inside __init__()")
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3

    def __call__(self, f):
        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

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

print("After decoration")

print("Preparing to call sayHello()")
sayHello("say", "hello", "argument", "list")
print("after first sayHello() call")
sayHello("a", "different", "set of", "arguments")
print("after second sayHello() call")

執行結果如下,在 __init__,可以取得 decorator 的參數,並在 __call__ 裡面使用這些參數

Inside __init__()
Inside __call__()
After decoration

Preparing to call sayHello()
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: say hello argument list
After f(*args)
after first sayHello() call

Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: a different set of arguments
After f(*args)
after second sayHello() call

function decorator with arguments

同樣的 function decorator 也可以使用參數

def decorator_function_with_arguments(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

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

print("After decoration")

print("Preparing to call sayHello()")
sayHello("say", "hello", "argument", "list")
print("after first sayHello() call")
sayHello("a", "different", "set of", "arguments")
print("after second sayHello() call")
Inside wrap()
After decoration

Preparing to call sayHello()
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: say hello argument list
After f(*args)
after first sayHello() call

Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: a different set of arguments
After f(*args)
after second sayHello() call

因為被 decorator 修飾的原始 function,無法在實作 decorator 時就預先知道參數的個數,因此在定義 wrapper function 時,是定義為 def wrapped_f(*args):

其中 *args 代表不固定個數的參數,如果要支援有參數名稱的參數,則可以使用 **kwargs

多個 decorator

@a
@b
@c
def f ():
    pass

如果有好幾個 decorator,那麼實際上呼叫的順序,是以下這樣

f = a(b(c(f)))

處理時間的 decorator

以下這是計算函數處理時間的 decorator

from time import time

def timer(func):
    def wraper(*args, **kwargs):
        before = time()
        result = func(*args, **kwargs)
        after = time()
        print("elapsed: ", after - before)
        return result
    return wraper

@timer
def add(x, y = 10):
    return x + y

@timer
def sub(x, y = 10):
    return x - y

print("add(10)",       add(10))
print("add(20, 30)",   add(20, 30))

執行結果為

elapsed:  9.5367431640625e-07
add(10) 20
elapsed:  1.1920928955078125e-06
add(20, 30) 50

函數參數的型別檢查 pylimit

pylimit2

利用裝飾器給python的函式加上型別限制

pylimit

python 因為是動態資料型別語言,變數不需要定義資料型別,隨時會改變,但是在實作時,常常會遇到一個問題,就是實作的 function 只能處理某些資料型別的參數,程式如果寫錯,很容易就 crash。

上面的兩個 pylimit 套件,就是利用 decorator 的做法,在裡面加上 function 參數的資料型別定義,然後就可以在呼叫該函數時,進行資料型別檢查,這樣就不需要在實作時,寫上很多 type() 檢查的程式碼。

References

Decorators

Python Decorator(裝飾器)

[Python] Decorator 介紹

理解 Python 裝飾器看這一篇就夠了

萬惡的 Python Decorator 究竟是什麼?

Python Decorator 入門教學

沒有留言:

張貼留言