Decorator進階指南
在[python裝飾器完全指南基礎篇中],我們已經知道了python中的裝飾器本質上只是一個接受一個函數對象作為輸入,在該函數前后增加一些新邏輯,并返回包裝過后的新函數對象的函數。
問題一: 有輸入參數的函數的裝飾器
在基礎篇中,我們所討論的所有被裝飾器裝飾的函數都是不接收輸入參數的函數。那么對于有輸入參數的函數,我們應該如何定義適用于它們的裝飾器呢?請看如下代碼片段。
def decorator_for_func_with_arguments(func_to_decorate):
def wrapper_that_passes_through_arguments(arg1, arg2):
print('I got args! Look: {} {}'.format(arg1, arg2))
func_to_decorate(arg1, arg2)
return wrapper_that_passes_through_arguments
@ decorator_for_func_with_arguments
def print_full_name(first_name, last_name):
print('I am {} {}'.format(first_name, last_name))
print_full_name('Tony', 'Stark')
# output:
# I got args! Look: Tony Stark
# I am Tony Stark
可以看出,由于裝飾器事實上返回了一個新的函數來代替我們需要裝飾的函數,所以我們只需要確保在裝飾器中返回的新的函數與原函數所接受的參數格式一致即可。
問題二:類方法的裝飾器
Python中的類方法事實上與函數一樣,只是固定接收當前實例的引用作為第一個參數,一般標記為self
。那么能夠裝飾類方法的裝飾器事實上也可以用與問題一中一致的方法實現,只不過我們總是要確保返回的函數所接收的第一個參數也是當前實例的引用(self
)即可。如下所示。
def decorator_for_instance_method(method_to_decorate):
def wrapper(self, bonus):
# 升職加薪,獎金增加一倍 d=====( ̄▽ ̄*)b
bonus = bonus * 2
return method_to_decorate(self, bonus)
return wrapper
class Salary(object):
def __init__(self):
self.base = 666
@decorator_for_instance_method
def total_compensation(self, bonus):
print('Congrats! You got a total compensation of {}'.format(self.base * 12 + bonus))
salary_instance = Salary()
salary_instance.total_compensation(2048)
# output: Congrats! You got a total compensation of 12088
類似地,我們也可以用python中的args和*kwargs來實現一個能夠裝飾接收任意數目參數函數的裝飾器。如下所示。
def decorator_passing_arbitrary_arguments(function_to_decorate):
def wrapper_with_arbitrary_arguments(*args, **kwargs):
print('Received arguments as following')
print(args)
print(kwargs)
function_to_decorate(*args, **kwargs)
return wrapper_with_arbitrary_arguments
@decorator_passing_arbitrary_arguments
def function_with_no_argument():
print('This function does not have any argument')
function_with_no_argument()
# output:
# Received arguments as following
# ()
# {}
# This function does not have any argument
@decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
print('This function has arguments')
function_with_arguments(1,2,3)
# output:
# Received arguments as following
# (1, 2, 3)
# {}
# This function has arguments
@decorator_passing_arbitrary_arguments
def function_with_named_arguments(a, b, c, name)
print('{}, {}, {}'.format(a, b, c))
print('{}'.format(name))
function_with_named_arguments(1, 2, 3, name='robot')
# output:
# Received arguments as following
# (1, 2, 3)
# {'name': 'robot'}
# 1, 2, 3
# robot
class Salary(object):
def __init__(self):
self.base = 666
@decorator_passing_arbitrary_arguments
def total_compensation(self, bonus):
print('Congrats! You got a total compensation of {}'.format(self.base * 12 + bonus))
salary = Salary()
salary.total_compensation(2048)
# salary.total_compensation(2048)
# Received arguments as following
# (<__main__.Salary object at 0x1070b5f28>, 2048)
# {}
# Congrats! You got a total compensation of 10040
問題三:給裝飾器傳入參數
上面我們討論了裝飾器所裝飾的函數參數有關的問題。接下來我們討論如何實現能夠接收參數的裝飾器。
看過之前文章內容的讀者相比已經知道,所謂裝飾器其實就是接收一個函數作為輸入,并返回另一個函數的函數。這種情況下,由于裝飾器的函數簽名已經固定,所以我們無法直接傳入除輸入函數之外的參數。如下面代碼所示。
# 裝飾器就是接收一個函數作為輸入的函數
def my_decorator(func):
print('This is an ordinary function')
def wrapper():
print('This is the wrapper function that will be returned')
func()
# 只有上述格式簽名的函數才能作為裝飾器
# 下面代碼等于
# lazy_function = my_decorator(lazy_function)
@my_decorator
def lazy_function():
print('zzzzz')
# output:
# This is an ordinary function
lazy_function()
# output:
# This is the wrapper function that will be returned
# zzzzz
上面代碼說明當我們使用@my_decorator
的時候,事實上就是執行了lazy_function = my_decorator(lazy_function)
。因此在無法直接改變裝飾器簽名的情況下,我們需要采用一些別的辦法來實現我們的目標——實現一個能夠返回裝飾器的函數來替裝飾器接收參數,并使用閉包(對閉包概念不熟悉的讀者,請參閱這篇文章
)的方法來將這些參數傳遞到裝飾器中。換句話說,我們需要一個裝飾器工廠函數來替我們動態生成裝飾器。
首先我們實現一個裝飾器工廠函數,如下面的代碼所示。
def decorator_maker():
print('This is a factory generating decorators on the fly. This function is called once we want a fresh decorator.')
def my_decorator(func):
print('This is the decorator generated on the fly. This function is called when the decoration happens.')
# 類似地,我們還是在裝飾器中定義一個wrapper還包裝原函數
def wrapper():
print('This is the wrapper around the decorated function. This function is called once the decorated function is called.')
return func()
return wrapper
print('The decorator function created on the fly is returned.')
return my_decorator
def func():
print('This is the function decorated.')
fresh_decorator = decorator_maker()
# output:
# This is a factory generating decorators on the fly. This function is called once we want a fresh decorator.
# The decorator function created on the fly is returned.
func = fresh_decorator(func)
# output:
# This is the decorator generated on the fly. This function is called when the decoration happens.
func()
# output:
# This is the wrapper around the decorated function. This function is called once the decorated function is called.
# This is the function decorated.
基于如上代碼,我們可以用更加pythonic的方式去使用這個裝飾器工廠函數。如下所示。
@decorator_maker()
def func():
print('This is the function decorated.')
# output:
# This is a factory generating decorators on the fly. This function is called once we want a fresh decorator.
# The decorator function created on the fly is returned.
# This is the decorator generated on the fly. This function is called when the decoration happens.
func()
# output:
# This is the wrapper around the decorated function. This function is called once the decorated function is called.
# This is the function decorated.
上面的例子說明,我們是事實上可以用一個裝飾器工廠返回的函數作為@
語法中的裝飾器使用。既然如此,我們自然也就可以給這個工廠函數傳入參數。如下面代碼所示。
def decorator_maker_with_arguments(decorator_arg1, decorator_arg2):
print('This is the decorator factory. Input arguments are: {}, {}.'.format(decorator_arg1, decorator_arg2))
def my_decorator(func):
# 利用閉包特性來將工廠函數接收的參數傳遞給動態生成的裝飾器
print('This is the decorator function created on the fly.')
print('Arguments are passed in from outer function using closure: {}, {}.'.format(decorator_arg1, decorator_arg2))
# 注意這里wrapper函數接收的參數是被該裝飾器裝飾的函數
# 不要和上面工廠函數接收的參數混淆
def wrapper(function_arg1, function_arg2):
print('This is the wrapper around the decorated function.')
print('This function can access all the variables.')
print('Variables from the decorator factory: {}, {}.'.format(decorator_arg1, decorator_arg2))
print('Variables from the decorated function: {}, {}.'.format(function_arg1, function_arg2))
return func(function_arg1, function_arg2)
return wrapper
return my_decorator
@decorator_maker_with_arguments('Avengers', 'Justice League')
def func(function_arg1, function_arg2):
print('This is the function decorated.')
print('It accepts two arguments: {}, {}.'.format(function_arg1, function_arg2))
# output:
# This is the decorator factory. Input arguments are: Avengers, Justice League.
# This is the decorator function created on the fly.
# Arguments are passed in from outer function using closure: Avengers, Justice League.
func('Captain America', 'Bat Man')
# output:
# This is the wrapper around the decorated function.
# This function can access all the variables.
# Variables from the decorator factory: Avengers, Justice League.
# Variables from the decorated function: Captain America, Bat Man.
# This is the function decorated.
# It accepts two arguments: Captain America, Bat Man.
上面例子中,我們將字符串常量作為參數傳遞給了裝飾器工廠函數。與平常的python函數一樣,這一函數也可以接收變量作為參數。如下面所示。
a1 = 'Avenagers'
a2 = Justice League'
b1 = 'Captain America'
b2 = 'Bat Man'
@decorator_maker_with_arguments(a1, a2)
def func(function_arg1, function_arg2):
print('This is the function decorated.')
print('It accepts two arguments: {}, {}.'.format(function_arg1, function_arg2))
# output:
# This is the decorator factory. Input arguments are: Avengers, Justice League.
# This is the decorator function created on the fly.
# Arguments are passed in from outer function using closure: Avengers, Justice League.
func(b1, b2)
# output:
# This is the wrapper around the decorated function.
# This function can access all the variables.
# Variables from the decorator factory: Avengers, Justice League.
# Variables from the decorated function: Captain America, Bat Man.
# This is the function decorated.
# It accepts two arguments: Captain America, Bat Man.
通過上述討論,可以看出對于裝飾器工廠函數而言,調用它與調用普通函數完全相同。我們甚至也可以使用*args
及**kwargs
來接受可變長度的參數。但有一點需要注意,那就是裝飾器裝飾函數的過程只發生一次,那就是python編譯器import當前代碼文件的時候。因此一旦文件已經被import過,那我們就無法修改裝飾器工廠接收的參數,也無法修改已經生成的裝飾器裝飾過的函數了。
Reference
文中部分內容翻譯自如下文章。翻譯部分版權歸原作者所有。
https://gist.github.com/Zearin/2f40b7b9cfc51132851a