[Python進階]Python的@Decoractor

今天這篇文章,我們來學習一下 Python 的 Decorator 裝飾器。

Decorator 在 Python 是一個非常經典的功能,在工程中應用廣泛,例如日誌 (Log)、快取 (Cache)、多執行緒 (Threading) 等等。

Function Decorator

其實 Decorator 就是對函數的封裝,可以理解為在函數的前後做一點“裝飾”。我們會從 Python 的 lambda 切入講解,介紹 Decorator 的基本概念和用法,最後透過一個實際的例子加深理解。

前面說過,Python 的一切皆為物件,連函數也不例外。我們來看下面的例子:

1
2
3
4
5
6
7
def func():
print("hello world")

helloworld = func
helloworld()
# Output:
# hello world

從上面的例子,我們把 func 作為一個變數賦值給 helloworld,然後呼叫 helloworld,相當於呼叫了 func。所以我們也可以把函數作為一個參數傳到另一個函數裡面:

1
2
3
4
5
6
7
8
9
def print_hello_world():
print("hello world")

def printer(func):
func()

printer(print_hello_world)
# Output:
# hello world

有了這些基礎概念後,我們接下來可以深入挖掘 Decorator。按照 Decorator 的思路,就是對某個函數做前後包裝,例如我們想要計算每個傳進來的函數執行花了多少時間,可以這樣寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time

def print_hello_world():
print("hello world")

def printer(func):
start = time.time()
func()
print('Time consumed: %s secs' % (time.time()-start))

printer(print_hello_world)
# Output:
# hello world
# Time consumed: 4.220008850097656e-05 secs

更通用一點,我們可以把 printer 封裝成更通用的函數直接返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time

def print_hello_world():
print("hello world")

def decorator_benchmark(func):
def wrapper():
start = time.time()
func()
print('Time consumed: %s secs' % (time.time()-start))
return wrapper

printer = decorator_benchmark(print_hello_world)
printer()
# Output:
# hello world
# Time consumed: 5.2928924560546875e-05 secs

我們把原本的 print_hello_world 封裝成 decorator_benchmark 的內部函數,這樣在外面呼叫就會非常簡潔。不過這樣還是有點麻煩,如果我們總是需要對 print_hello_world 測量性能,呼叫之前都需要對它封裝一次,那有沒有更簡潔的方法呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time

def decorator_benchmark(func):
def wrapper():
start = time.time()
func()
print('Time consumed: %s secs' % (time.time()-start))
return wrapper

@decorator_benchmark
def print_hello_world():
print("hello world")

print_hello_world()
# Output:
# hello world
# Time consumed: 4.38690185546875e-05 secs

我們在 print_hello_world 上面加了 @decorator_benchmark,其中 @ 是 Python 裡的語法糖。我們可以對一些常見的功能,例如效能測量 (benchmark)、日誌 (log) 等等寫成一個 Decorator 函數,然後再對其他函數進行“裝飾”,這樣就大大提高了程式的重複利用性和可讀性。

當然,Decorator 具有強大的靈活性,我們也可以對其傳入參數,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def repeat(num):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(num):
func(*args, **kwargs)
return wrapper
return decorator

@repeat(4)
def print_hello_world():
print('hello world')

print_hello_world()
# Output:
# hello world
# hello world
# hello world
# hello world

不過這樣寫有個副作用是,我們裝飾後的 print_hello_world 的元數據 (metadata) 就被改變了:

1
2
3
4
help(print_hello_world)
# Output:
# Help on function wrapper in module __main__:
# wrapper(*args, **kwargs)

它告訴我們函數不再是原來的 print_hello_world,而是被 wrapper 取代了。不過俗話說得好,見招拆招。為了解決這個問題,我們可以使用 Python 已有的 Decorator @functools.wraps,它會保留原本函數的元數據(也就是將原本函數的元數據複製到 Decorator 裡面):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import functools

def repeat(num):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(num):
func(*args, **kwargs)
return wrapper
return decorator

@repeat(4)
def print_hello_world():
print('hello world')

help(print_hello_world)
# Output:
# Help on function print_hello_world in module __main__:
# print_hello_world()

Class Decorator

最後來說說 Class Decorator。前面提到的 Decorator 是以函數為形式的,其實 class 也可以作為 Decorator,這樣可以持久化存一些資料。Class Decorator 藉由函數 __call__,每當呼叫一次被裝飾的函數時,就會呼叫一次 __call__。我們以“計算函數被呼叫的次數”作為例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Count:
def __init__(self, func):
self.num_call = 0
self.func = func

def __call__(self, *args, **kwargs):
self.num_call += 1
print("Num of call is %s" % self.num_call)
return self.func(*args, **kwargs)

@Count
def print_hello_world():
print('hello world')

print_hello_world()
print_hello_world()
# Output:
# Num of call is 1
# hello world
# Num of call is 2
# hello world

總結

所謂的 Decorator,就是透過去“裝飾”函數,增加或改變已有函數的功能,使得原有函數不需要修改。有如下優點:

  • 封裝原有程式碼
  • 程式碼簡潔
  • 易讀