讓程式碼更具 Python 範兒的裝飾器
在 Python 中,裝飾器的作用是在不改變函式或類的程式碼的前提下,改變函式或類的功能。在介紹裝飾器之前,我們先來複習下 Python 中的函式。
函式
1. 函式也是物件
def foo():
print("Hello World!")
bar = foo
bar()
output:
Hello World!
在上面的例子中,首先定義了函式 foo()
,然後將函式 foo
賦給變數 bar
,這樣我們便可以通過 bar()
來呼叫函式。
2. 函式作為引數
def foo():
print("I am foo" )
def bar(func):
print("I am bar")
func()
bar(foo)
output:
I am bar
I am foo
在上面的例子中,函式 foo()
作為引數傳入到 bar()
函式,然後再 bar()
函式內部通過引數來呼叫函式 foo()
。
3. 函式巢狀
def parent():
print("I am parent")
def foo_child():
print("I am foo")
def bar_child( ):
print("I am bar")
foo_child()
bar_child()
parent()
output:
I am parent
I am foo
I am bar
在上面的例子中,函式 foo_child()
和 bar_child()
巢狀在函式 parent()
的內部。巢狀在函式內部的函式只能在函式內部呼叫,從外部呼叫會報錯。例如:
def parent():
print("I am parent")
def foo_child():
print("I am foo" )
def bar_child():
print("I am bar")
foo_child()
bar_child()
foo_child()
output:
Traceback (most recent call last):
File "/Users/weisong/PycharmProjects/pythonProject/greet.py", line 14, in <module>
foo_child()
NameError: name 'foo_child' is not defined
4. 函式作為返回值
def parent(name):
def foo_child():
print("I am foo")
def bar_child():
print("I am bar")
if name == 'foo':
return foo_child
else:
return bar_child
foo = parent("foo")
bar = parent("bar")
foo()
bar()
output:
I am foo
I am bar
在上面的例子中,parent()
函式根據傳入的引數返回函式 foo_child
或者 bar_child
,得到 parent()
函式的返回值之後,我們可以使用返回值呼叫相應的函式。
裝飾器
1. 一個簡單的裝飾器
介紹完函式的各種用法後,我們來看一個簡單的裝飾器(decorator)。
def my_decorator(func):
def wrapper():
print("Do something before function is called")
func()
print("Do something after function is called")
return wrapper
def foo():
print("I am foo")
foo = my_decorator(foo)
foo()
output:
Do something before function is called
I am foo
Do something after function is called
在上面的例子中,語句 foo = my_decorator(foo)
將 my_decorator
函式的返回值 wrapper
函式賦給 foo
,這樣我們便可以使用 foo
來呼叫 wrapper
函式,在 wrapper
函式中包含著作為引數傳入的函式的呼叫,所以會輸出 I am foo
。
2. 語法糖
上面使用裝飾器的方式有點笨重,Python 提供了一種更簡單的方式來使用裝飾器,這便是使用 @ 符號,我們稱之為語法糖。使用 @ 符號,將上面的裝飾器修改如下:
def my_decorator(func):
def wrapper():
print("Do something before function is called")
func()
print("Do something after function is called")
return wrapper
@my_decorator
def foo():
print("I am foo")
foo()
output:
Do something before function is called
I am foo
Do something after function is called
@my_decorator
相當於前面的 foo = my_decorator(foo)
,使用方式較前面簡潔了許多。另外,如果程式中有其他的函式需要類似的裝飾,只需要在它們的上方加上 @my_decorator
就可以了,大大提高了程式的可複用性和可讀性。
3. 帶引數的裝飾器
def my_decorator(func):
def wrapper(greet):
print("Do something before function is called")
func(greet)
print("Do something after function is called")
return wrapper
@my_decorator
def foo(greet):
print(f"{greet}, I am foo")
foo("Hello")
output:
Do something before function is called
Hello, I am foo
Do something after function is called
在為函式 foo()
加了引數 greet
之後,呼叫函式 foo()
時便可以傳入引數。當然裝飾器也要做相應的修改,為函式 wrapper
也添加了引數 greet
。但是上述加引數的方式有一個缺點,當使用這個裝飾器來裝飾一個不帶引數的函式時,呼叫便會發生錯誤。例如:
def my_decorator(func):
def wrapper(greet):
print("Do something before function is called")
func(greet)
print("Do something after function is called")
return wrapper
@my_decorator
def foo():
print("I am foo")
foo()
output:
Traceback (most recent call last):
File "/Users/weisong/PycharmProjects/pythonProject/greet.py", line 14, in <module>
foo()
TypeError: wrapper() missing 1 required positional argument: 'greet'
為了相容帶引數的呼叫和不帶引數的呼叫,可以將裝飾器函式修改如下:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Do something before function is called")
func(*args, **kwargs)
print("Do something after function is called")
return wrapper
@my_decorator
def foo():
print("I am foo")
@my_decorator
def bar(greet):
print(f"{greet}, I am bar")
foo()
bar("Hello")
output:
Do something before function is called
I am foo
Do something after function is called
Do something before function is called
Hello, I am bar
Do something after function is called
4. 帶自定義引數的裝飾器
def repeat(num):
def my_decorator(func):
def wrapper(*args, **kwargs):
print("do something before function is called")
for i in range(num):
func(*args, **kwargs)
print("do something after function is called")
return wrapper
return my_decorator
@repeat(2)
def foo():
print("I am foo")
@repeat(3)
def bar(greet):
print(f"{greet}, I am bar")
foo()
bar("Hello")
output:
Do something before function is called
I am foo
I am foo
Do something after function is called
Do something before function is called
Hello, I am bar
Hello, I am bar
Hello, I am bar
Do something after function is called
上面的例子中,使用裝飾器時傳入了引數 num
,用來表示內部函式執行的次數。
5. 被裝飾的函式還是它自己嗎?
還是使用前面的例子,我們打出函式 foo
和 bar()
的元資訊,看看被裝飾器修飾後還是不是它們自己。
def repeat(num):
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Do something before function is called")
for i in range(num):
func(*args, **kwargs)
print("Do something after function is called")
return wrapper
return my_decorator
@repeat(2)
def foo():
print("I am foo")
@repeat(3)
def bar(greet):
print(f"{greet}, I am bar")
print(foo.__name__)
help(foo)
print("**************************")
print(bar.__name__)
help(bar)
output:
wrapper
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
**************************
wrapper
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
從輸出結果可以看出,在被裝飾器修飾後,被修飾函式的元資訊改變了。變成了 wrapper()
函式。可以使用內建的裝飾器@functools.wrap
來解決這個問題,它會保留被修飾函式的元資訊。
import functools
def repeat(num):
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Do something before function is called")
for i in range(num):
func(*args, **kwargs)
print("Do something after function is called")
return wrapper
return my_decorator
@repeat(2)
def foo():
print("I am foo")
@repeat(3)
def bar(greet):
print(f"{greet}, I am bar")
print(foo.__name__)
help(foo)
print("**************************")
print(bar.__name__)
help(bar)
output:
foo
Help on function foo in module __main__:
foo()
**************************
bar
Help on function bar in module __main__:
bar(greet)
在使用@functools.wrap
之後,被修飾函式保留了原有的元資訊。
裝飾器的應用例項
1. 記錄函式執行的時間
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return wrapper_timer
@timer
def waste_some_time(num_times):
for _ in range(num_times):
sum([i**2 for i in range(10000)])
waste_some_time(1)
waste_some_time(1000)
output:
Finished 'waste_some_time' in 0.0037 secs
Finished 'waste_some_time' in 3.3343 secs
2. 控制函式相鄰兩次執行的時間間隔
import functools
import time
def slow_down(func):
@functools.wraps(func)
def wrapper_slow_down(*args, **kwargs):
time.sleep(1)
return func(*args, **kwargs)
return wrapper_slow_down
@slow_down
def countdown(from_number):
if from_number < 1:
print("Liftoff!")
else:
print(from_number)
countdown(from_number - 1)
countdown(6)
output:
6
5
4
3
2
1
Liftoff!
3. 程式碼 Debug
import functools
def debug(func):
"""Print the function signature and return value"""
@functools.wraps(func)
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("foo")
make_greeting("foo", 18)
output:
Calling make_greeting('foo')
'make_greeting' returned 'Howdy foo!'
Calling make_greeting('foo', 18)
'make_greeting' returned 'Whoa foo! 18 already, you are growing up!'
總結
本文講述了裝飾器的原理以及用法,裝飾器的存在大大提高了程式碼的可複用性以及簡潔性。