工作中用到了 Decorator,正好看到一篇比较好的文章,翻译作为笔记。
装饰器是一种常用的设计模式,允许你为已存在的对象添加新功能而不改变对象的内部结构。
- Python 的装饰器和 Hook 函数和”面向切面编程”起相同的效果
首先要重点说明的是在 Python 中函数是一等公民。这意味着它们可以支持如下操作:被作为参数传递、作为一个函数的返回值返回、修改、设置给一个函数变量。这是接下来理解装饰器模式的基础概念。
将函数赋值给一个变量
def plus_one(number):
return number + 1
add_one = plus_one # 这里将函数赋值给变量
add_one(5) # 在这里执行函数
# 6
在函数内定义另一个函数
def plus_one(number):
def add_one(number):
return number + 1
result = add_one(number) # 执行内部定义的函数
return result
plus_one(4)
# 5
把函数作为其他函数的参数传递
def plus_one(number):
return number + 1
def function_call(function):
number_to_add = 5
return function(number_to_add) # 执行传入的函数,并将函数执行结果作为本函数结果返回
function_call(plus_one)
# 6
将函数内部定义的函数作为返回值返回(生产函数的函数)
def hello_function():
def say_hi():
return "Hi"
return say_hi
hello = hello_function()
hello()
# 'Hi'
嵌套函数中,内部函数可以共享外部函数的变量(这种模式也叫做”闭包”)
def print_message(message):
"Enclosong Function"
def message_sender():
"Nested Function"
print(message)
message_sender()
print_message("Some random message")
# 'Some random message'
创建装饰器
有了前面的准备,终于可以实现一个装饰器了。我们的装饰器只实现一个简单地功能:将被装饰的函数的返回的字符串改写为大写。 为了实现这个功能,我们需要利用到前面说的闭包结构:
def uppercase_decorator(function): # 这里传入被装饰的函数
def wrapper():
result = function() # 在内部函数执行被装饰的函数,结果是一个字符串
make_uppercase = result.upper() # 将结果字符串转为大写
return make_uppercase
return wrapper # 将闭包函数返回
接下来定义一个返回字符串的函数,用上面定义好的装饰器函数进行装饰:
def say_hi():
return 'hello there'
decorate = uppercase_decorator(say_hi)
decorate()
# 'HELLO THERE'
然而,Python 提供了更方便的方法进行装饰。我们只要简单的在装饰器函数前加上@
并放在被装饰函数上面即可:
@uppercase_decorator
def say_hi():
return 'hello there'
say_hi()
# 'HELLO THERE'
在被装饰函数上使用多个装饰器
基于前面的代码,我们再定义一个新的装饰器:将被装饰函数返回的字符串用空格分割为字符串数组。
def split_string(function):
def wrapper():
result = function()
splitted_string = result.split()
return splitted_string
return wrapper
@split_string
@uppercase_decorator
def say_hi():
return 'hello there'
say_hi()
# ['HELLO', 'THERE']
你可能注意到了,上面两个装饰器的执是需要有顺序的,执行的顺序和wrapper
中function()
的调用位置相关。
上面的顺序为split_string 调用 function()前
、uppercase_decorator 调用 function()前
、function()调用
、uppercase_decorator 调用 function()后
、split_string 调用 function()后
可以看出就像一个剥洋葱的过程,这种模式也叫责任链模式(Chain of Responsibility Pattern)。
同一个装饰器也可以被装饰在一个被装饰函数上多次,每一次装饰都是独立的。这也是符合装饰器模式的。
在装饰器中截取被装饰函数的参数
装饰器被调用时,传给被装饰函数的参数会先传给内部的 wrapper 函数:
def decorator_with_arguments(function):
def wrapper_accepting_arguments(arg1, arg2):
print("My arguments are: {0}, {1}".format(arg1,arg2)) # 截获并打印参数
function(arg1, arg2) # 调用被装饰函数时传入截获的参数,中间可以做修改
return wrapper_accepting_arguments
@decorator_with_arguments
def cities(city_one, city_two):
print("Cities I love are {0} and {1}".format(city_one, city_two))
cities("Nairobi", "Accra")
# My arguments are: Nairobi, Accra
# Cities I love are Nairobi and Accra
在装饰器中截取更通用的参数
定义内部 wrapper 函数时,可以用(*args, **kwargs)
截取通用参数。
其中*args
是顺序传入参数;**kwargs
是通过 key-value 方式指定的参数。
def a_decorator_passing_arbitrary_arguments(function_to_decorate):
def a_wrapper_accepting_arbitrary_arguments(*args,**kwargs):
print('The positional arguments are', args) # 本质是一个数组
print('The keyword arguments are', kwargs) # 本质是一个dict
function_to_decorate(*args)
return a_wrapper_accepting_arbitrary_arguments
@a_decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
print(a, b, c)
function_with_arguments(1, 2, 3)
# The positional arguments are (1, 2, 3)
# The keyword arguments are {}
# 1 2 3
function_with_arguments(c=1, a=2, b=3)
# The positional arguments are ()
# The keyword arguments are {'c':1, 'a':2, 'b'=3}
# 2 3 1
带有参数的装饰器
有时需要给装饰器增加参数,实现方案同样基于”闭包”。装饰器本质上就是一个函数,这个函数执行时会返回一个可执行的函数。 为了添加装饰器参数,我们只需要在外层再套一层函数添加参数,这层的参数将被作为闭包变量使用。
def decorator_maker_with_arguments(decorator_arg1, decorator_arg2, decorator_arg3):
def decorator(func):
def wrapper(function_arg1, function_arg2, function_arg3) :
"This is the wrapper function"
print("The wrapper can access all the variables\n"
"\t- from the decorator maker: {0} {1} {2}\n"
"\t- from the function call: {3} {4} {5}\n"
"and pass them to the decorated function"
.format(decorator_arg1, decorator_arg2,decorator_arg3,
function_arg1, function_arg2,function_arg3))
return func(function_arg1, function_arg2,function_arg3)
return wrapper
return decorator
pandas = "Pandas"
@decorator_maker_with_arguments(pandas, "Numpy","Scikit-learn")
def decorated_function_with_arguments(function_arg1, function_arg2,function_arg3):
print("This is the decorated function and it only knows about its arguments: {0}"
" {1}" " {2}".format(function_arg1, function_arg2,function_arg3))
decorated_function_with_arguments(pandas, "Science", "Tools")
# The wrapper can access all the variables
# - from the decorator maker: Pandas Numpy Scikit-learn
# - from the function call: Pandas Science Tools
# and pass them to the decorated function
# This is the decorated function, and it only knows about its arguments: Pandas Science Tools
调试装饰器
你应该已经注意到了,装饰器会把原函数封装起来。这导致原函数的名称、文档字符串和参数列表被封装在闭包中。例如当你想获取decorated_function_with_arguments
的元数据时,我们只能看到 warapper 的闭包元数据。这将导致难以调试。
decorated_function_with_arguments.__name__
# 'wrapper'
decorated_function_with_arguments.__doc__
# 'This is the wrapper function'
为了解决这个问题,Python 提供了一个functools.wraps
装饰器。这个装饰器可以将原函数的元数据拷贝到闭包中。示例如下:
import functools
def uppercase_decorator(func):
@functools.wraps(func)
def wrapper():
return func().upper()
return wrapper
@uppercase_decorator
def say_hi():
"This will say hi"
return 'hello there'
say_hi()
# 'HELLO THERE'
say_hi.__name__
# 'say_hi'
say_hi.__doc__
# 'This will say hi'
建议你尽量在所有定义装饰器的地方使用functools.wraps
,这将节省你大量的令人头疼的调试时间。
Python 装饰器总结
装饰器可以动态的改变原函数/方法/类而不必使用继承或修改原函数。使用装饰器可以确保你的代码DRY(Don’t Repeat Yourself)。
装饰器的常用场景:
- 在 Python 框架中进行授权处理
- 打 log
- 度量函数执行时间
- 同步处理
想要了解更多装饰器知识,可以查看 Python 官方文档Decorator Library