python裝飾器
函數也是對象
在python中,function是一級對象,這也就意味着function可以作爲arguments一樣傳遞,就像其它的對象一樣(字符串,整型變量,浮點型變量,列表等等)。當函數名像其它類型的變量一樣作爲另一個函數的參數被傳遞時,實現了函數的嵌套調用。
def say_hello(name):
return f"Hello {name}"
def be_awesome(name):
return f"Yo {name}, you are awesome!"
def greet_bob(greeter_func):
return greeter_func("Bob")
print(greet_bob(say_hello))
print(greet_bob(be_awesome))
Hello Bob
Yo Bob, you are awesome!
函數內部定義的函數成爲inner function, 內部函數。內部函數只存在在局部空間,也就是說只能在定義的函數中被調用,下面的例子就說明了這一點。
def parent():
print('The parent() function')
def first_child():
print('The first_child() function')
def second_child():
print('The second_child() function')
second_child()
first_child()
parent()
The parent() function
The second_child() function
The first_child() function
函數名也可以用作返回參數。這時,內部定義的函數也可以被外部訪問到,可以當作一個普通函數調用,也就是要加上()。
def parent(num):
def first_child():
return "HI I am Emma"
def second_child():
return "HI I am Lin"
if num == 1:
return first_child
else:
return second_child
first_child = parent(1)
second_child = parent(2)
print(first_child()) # 注意不是print(first_child)
print(second_child())
HI I am Emma
HI I am Lin
print(first_child)
<function parent.<locals>.first_child at 0x1115e74d0>
簡單裝飾器
理解了函數是python對象,函數名可以作爲參數傳來傳去這個點後,我們下面來個稍微有點難度的例子,let’s go!
def my_decorator(func):
def wrapper():
print("Before the function is called")
func()
print("After the function is called")
return wrapper
def say_whee():
print("whee")
say_whee = my_decorator(say_whee)
say_whee()
Before the function is called
whee
After the function is called
say_whee = my_decorator(say_whee)這一句就是應用了所謂的函數裝飾器。事實上,這裏把say_whee函數名作爲my_decorator的輸入參數,之後將返回的函數名wrapper賦予了最左側的say_whee變量。這時候,最左側的say_whee實際上指向了my_decorator的內部函數wrapper()。
簡單滴說,就是“裝飾器“是封裝了一系列函數的行爲,將被裝飾的函數嵌入到這些行爲中。(decorators wrap a function, modifying its behavior)”
下面的例子中,函數被限制在了工作時間執行(7到22點),首先看看如果用普通的函數定義:
from datetime import datetime
def not_during_the_night(func):
def wrapper():
if 7 <= datetime.now().hour < 22:
func()
else:
pass
return wrapper
def say_whee():
print("Whee!")
say_whee = not_during_the_night(say_whee)
say_whee()
Whee!
裝飾器@ 語法給上面的例子提供了更加簡介的解決方案。值得注意的是,裝飾器構造函數一定要返回行爲定義內部函數的函數名,裝飾器使用的時候是在新函數定義的前一行,用@符號後面跟着裝飾器函數名,不加()。例如:
@not_during_the_night
def say_whee_in_work_hours():
print("Whee")
say_whee_in_work_hours()
Whee
def do_twice(func):
def wrapper_do_twice():
func()
func()
return wrapper_do_twice
@do_twice
def say_whee():
print("Whee")
say_whee()
Whee
Whee
被修飾函數有參數的情況
上面的例子中,被修飾的函數都是沒有輸入和輸出參數的。我們先來看看下面的這個例子:
@do_twice
def greet(name):
print(f"Hello {name}")
greet("Alice")
報錯:greet() missing 1 required positional argument: ‘name’
這是什麼情況?回憶一下裝飾器的工作原理,在函數執行的時候,實際上是傳給了do_twice裝飾器函數,在這個函數內部再執行greet這個函數,但是這時greet函數沒有輸入參數。怎麼解決問題?這時應該用到python的可變參數。
def do_twice_with_args(func):
def wrapper_do_twice(*args,**kwargs):
func(*args,**kwargs)
func(*args,**kwargs)
#return wrapper_do_twice #報錯:'NoneType' object is not callable
return wrapper_do_twice
@do_twice_with_args
def greet(name):
print(f"Hello {name}")
greet("Alice")
Hello Alice
Hello Alice
如果函數有返回值呢?
@do_twice_with_args
def greet_back(name):
return f"Hello {name}"
print(greet_back("Ben"))
None
返回的是None。顯然裝飾函數運行過程中“吞”掉了greet_back函數的輸出,就在裝飾器內部函數wrapper_do_twice()內部,調用傳入的函數時,沒有返回值。我們修改一下do_twice_with_args的定義,在wrapper_to_twice函數後面添加一個return語句。
事實上,之前返回的None是python函數在用戶沒有定義return語句時默認返回的值。
def do_twice_with_args(func):
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper_do_twice
@do_twice_complex
def greet_back(name):
print('Hello, Alice')
return f"Hello, {name}"
def no_return():
print()
print('default return should be ')
hi = greet_back("Ben")
print(hi)
ret = no_return()
print(ret)
Hello, Alice
Hello, Alice
Hello, Ben
default return should be
None
通常我們用__定義對象的內部變量和內部函數,python的這一機制便於查看對象的基本情況,還可以用內置的help函數查看對應的幫助文檔。例如:
print("print.__name__: "print.__name__)
print(help(print))
print
Help on built-in function print in module builtins:
print(...)
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
Prints the values to a stream, or to sys.stdout by default.
Optional keyword arguments:
file: a file-like object (stream); defaults to the current sys.stdout.
sep: string inserted between values, default a space.
end: string appended after the last value, default a newline.
flush: whether to forcibly flush the stream.
None
但是在裝飾器語法裝飾後,會讓被裝飾函數喪失這個特性,有的時候會給代碼編寫和測試帶來麻煩,如何修復這個問題?
可以用functools工具包解決。例如:
print('greet_back.__name__: '+greet_back.__name__)
print(help(greet_back))
print()
print("import functools toolbox to fix this. ")
import functools
def do_twice(func):
@functools.wraps(func)
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper_do_twice
@do_twice
def greet(name):
print('Hi! ')
return f"Hello, {name}"
print(greet("Ti-Ya"))
print('greet.__name__: '+greet.__name__)
greet_back.__name__: wrapper_do_twice
Help on function wrapper_do_twice in module __main__:
wrapper_do_twice(*args, **kwargs)
None
import functools toolbox to fix this.
Hi!
Hi!
Hello, Ti-Ya
greet.__name__: greet
到這裏,python裝飾器的定義和基本使用就講完了。下面給出實際中的例子。
裝飾器的應用
裝飾器函數代碼的一般結構
首先來總結下一個裝飾器函數的一般寫法:
import functools
def decorator(func):
@functools.wraps(func)
def wrapper_decorator(*args, **kwargs):
# Do something before
value = func(*args, **kwargs)
# Do something after
return value
return wrapper_decorator
記時裝飾器
記錄函數運行時間
## 記時裝飾器
import functools
import time
def timer(func):
"""Print the run time of the decorated function."""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Fininshed {func.__name__} in {run_time:.4f} secs")
return value
return wrapper_timer
import tensorflow as tf
import numpy as np
@timer
def tensor_computation():
a = tf.constant(np.random.randn(3,3))
b = tf.constant(np.random.randn(3,3))
c = tf.matmul(a,b)
return c
tensor_computation()
Fininshed tensor_computation in 0.0002 secs
<tf.Tensor: shape=(3, 3), dtype=float64, numpy=
array([[-0.67434088, 1.34905892, 0.59164407],
[-2.14192767, 1.13678956, -0.48185652],
[-0.6749793 , 1.35129415, -1.46809712]])>
debug模式裝飾器
可以定義debug裝飾器時對函數的執行情況進行檢查,這裏給個簡單的例子。repr()是python的內置函數,和str()用法類似但又有相同。詳見:https://www.geeksforgeeks.org/str-vs-repr-in-python/
此外,f""字符串中的!r代表按照兩邊帶引號的字符串進行打印。關於f-string更多的說明和幫助在這裏:
https://www.python.org/dev/peps/pep-0498/#s-r-and-a-are-redundant
import functools
def debug(func):
"""Print the function signature and return value"""
def wrapper_debug(*args,**kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k,v in kwargs.items()]
signature = ','.join(args_repr+kwargs_repr)
print(f"Calling {func.__name__}({signature})")
value = func(*args, **kwargs)
print(f"{func.__name__!r} returned {value!r}")
return value
return wrapper_debug
@debug
def make_greeting(name, age=None):
if age is None:
return f"Howdy {name}!"
else:
return f"Whoa {name}! {age} already, you are growing up!"
make_greeting("Benjamin")
make_greeting("Richard", age=112)
print()
rname = "Neptune"
print(f"{rname!r} is a cute boy!")
Calling make_greeting('Benjamin')
'make_greeting' returned 'Howdy Benjamin!'
Calling make_greeting('Richard',age=112)
'make_greeting' returned 'Whoa Richard! 112 already, you are growing up!'
'Neptune' is a cute boy!