在本篇文章中,我们将了解什么是装饰器以及如何创建和使用它们。装饰器能够让我们以简单的方式构建高阶函数。
根据定义,装饰器是一个函数,它接受另一个函数并扩展了后者的行为,而无需对其进行显式修改。
或许现在对您来说这听起来令人困惑,但我相信通过阅读本篇文章,您会对装饰器有一个全新的认识。
函数(Functions)
想要充分的了解并使用装饰器,您首先需要知道函数是如何工作的。函数能够根据给定的参数返回一个值。下面是一个非常简单的函数:
def demo_1(name):
return "Hello" + " " + name
print(demo_1("Will")) # Hello Will
在Python中一切皆为对象,函数也不例外,它是“一等公民”。这意味着函数可以像其他的对象(字符串、整型数、浮点数、列表等)用作参数传递给其他函数。一个简单的例子如下,
def say_hello(name):
return f"Hello {name}"
def greet_bob(greeter_func):
return greeter_func("Bob")
print(greet_bob(say_hello)) # Hello Bob
可以看到在上述例子中,函数say_hello就像普通对象一样作为实参传递给greet_bob,当调用greet_bob时,实际返回的是say_hello("Bob")。
需要注意的是,say_hello和say_hello()是截然不同的,say_hello是函数的引用,而say_hello()是调用函数,如下:
print(greet_bob(say_hello())) # TypeError: say_hello() missing 1 required positional argument: 'name'
另外,补充一个有意思的点,在python中函数作为对象,甚至是可以有属性的
def demo_2():
print(demo_2.a)
demo_2.a="A"
demo_2() # A
内部函数
顾名思义,内部函数指的是定义在一个函数内部的函数,一个简单的例子如下:
def parent():
print("Printing from the parent() function")
def first_child():
print("Printing from the first_child() function")
def second_child():
print("Printing from the second_child() function")
second_child()
first_child()
parent()
# Printing from the parent() function
# Printing from the second_child() function
# Printing from the first_child() function
内部函数只有在调用时才会被定义,并且它们的作用域只是外部函数内,如果在外部直接调用内部函数会提示错误:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'first_child' is not defined
有什么办法在外部调用内部函数吗?答案是肯定的:把内部函数当作返回值返回
从函数中返回函数
函数作为对象,也是可以作为函数的返回值的,下面的例子中,内部函数作为外部函数的返回值返回。
def parent(num):
def first_child():
return "Hi, I am Emma"
def second_child():
return "Call me Liam"
if num == 1:
return first_child
else:
return second_child
像前面提到的一样,需要特别注意的一点,函数作为返回值时,一定不能加(),这是因为我们想要返回的是对内部函数的引用,从而达到在外部调用内部函数的目的,而不是将调用内部函数的结果作为外部函数的返回值返回。
first = parent(1)
print(first) # <function parent.<locals>.first_child at 0x7f599f1e2e18>
first() # 'Hi, I am Emma'
如上所示,此时first指向的是内部函数first_child,所以可以通过first()的方式调用内部函数first_child
装饰器
从上面的介绍中可以看出,在Python中的函数就像其他对象一样,可以做参数,可以有属性等,时刻记住这是Python装饰器的基础。现在您已经做好准备去挑战装饰器了。
初识装饰器
让我们从一个示例开始:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
def say_whee():
print("Whee!")
say_whee = my_decorator(say_whee)
say_whee()
# Something is happening before the function is called.
# Whee!
# Something is happening after the function is called.
在上述示例中,可以神奇的看到,尽管我们仿佛只调用了say_whee(),但输出结果却和原本的功能不同,仿佛say_whee函数被装饰了一样。要了解到底发生了什么,让我们把目光聚焦到
say_whee = my_decorator(say_whee)
print(say_whee)
# <function my_decorator.<locals>.wrapper at 0x7f3c5dfd42f0>
实际上,此时的say_whee已经指向了内部函数wapper,因为在调用my_decorator(say_whee)时,函数返回了wrapper内部函数的引用,而wrapper引用了原始的say_whee,并在两次调用print()之间调用了该函数,所以当调用say_whee时返回如上所示的结果。
具体过程如下,事实上,完全可以使用其他的变量名,而不是继续使用say_whee,用say_whee只是为了更好的理解装饰器。
上面例子中的my_decorator事实上已经是一个简单的装饰器了,它装饰了其他函数,改变了原有函数的行为。了解Java的同学也许发现,这其实很像Spring中的切面编程思想。
装饰器语法糖
在上述的例子中,虽然简单的实现了装饰器但是可以看到,我们需要重复的输入say_whee三次,并且如果我们更改say_whee为其他变量名,就失去了装饰本身的含义。幸运的是,Python具有很多语法糖,其中一种语法糖允许我们以@symbo的方式使用装饰器。 语法糖(英语:Syntactic sugar)是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。
语法糖(英语:Syntactic sugar)是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_whee():
print("Whee!")
在上述例子中,@my_decorator以简单的方式实现了say_whee = my_decorator(say_whee)
重用装饰器
装饰器只是一个常规的Python函数。我们可以创建一个特殊的模块,在该模块中存有各种功能的装饰器,这样我们就拥有了类似Python中math这种功能模块。
例如,我们可以把装饰器放在decorators.py的文件中,
def do_twice(func):
def wrapper_do_twice():
func()
func()
return wrapper_do_twice
在使用时,通过import导入需要的装饰器:
from decorators import do_twice
@do_twice
def say_whee():
print("Whee!")
通过以上方式,结果变得更加清晰,装饰器的重用更加方便(您可以尝试在任何.py文件中导入提前编写的装饰器)
使用带参数的装饰器
如果我需要被装饰的函数带有参数,我们还可以继续使用上述装饰器吗?
from decorators import do_twice
@do_twice
def greet(name):
print(f"Hello {name}")
greet("World")
# Traceback (most recent call last):File "<stdin>", line 1, in <module>TypeError: wrapper_do_twice() takes 0 positional arguments but 1 was given
不幸的是,可以看到,如果继续使用上述装饰器,运行代码会报错。问题是因为,当我们企图调用被装饰后的greet函数时,事实上,我们调用的事装饰器的内部函数,即greet("World")等价于wrapper_do_twice("World"),而wrapper_do_twice函数是无参的。一个最简单的解决办法就是为wrapper_do_twice函数添加一个形参,但同理,@do_twice不再适用与无参数的say_whee函数。所以一个更加合理的方案是使用*args and**kwargs。然后它将接受任意数量的位置和关键字参数。 重写decorators.py,如下所示:
def do_twice(func):
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper_do_twice
现在,wrapper_do_twice内部函数接受任意数量的参数,并将它们传递给它装饰的函数。 现在您的say_whee和greet示例都可以使用:
say_whee()
# Whee!
# Whee!
greet("World")
# Hello World
# Hello World
装饰具有返回值的函数
当上述装饰器装饰具有返回值的函数时会发生什么呢?
from decorators import do_twice
@do_twice
def return_greeting(name):
print("Creating greeting")
return f"Hi {name}"
hi_adam = return_greeting("Adam")
# Creating greeting
# Creating greeting
print(h_adam)
# None
从上面的示例可以看到,试用装饰器装饰过的函数失去了它原本的返回值,这是因为由于do_twice_wrapper没有明确返回值,因此调用return_greeting(“ Adam”)最终返回None
时刻记住,经过装饰器装饰的函数,函数名指向了装饰器的内部函数,出现问题的话尽管去查看这个内部函数,这是解决您问题的重要途径
要解决此问题,您需要确保装饰器函数返回被装饰函数的返回值。 更改您的decorators.py文件:
def do_twice(func):
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper_do_twice
再次运行上面的代码,可以看到已经能够正常返回结果。
return_greeting("Adam")
# Creating greeting
# Creating greeting
# 'Hi Adam'
使用装饰器的小问题
首先请大家看下下面的代码:
print(say_whee)
# <function do_twice.<locals>.wrapper_do_twice at 0x7f43700e52f0>
print(say_whee.__name__)
'wrapper_do_twice'
help(say_whee)
# Help on function wrapper_do_twice in module decorators:wrapper_do_twice()
正如上面一直说的一点,使用装饰器装饰,事实上是执行了“say_whee = my_decorator(say_whee)”这样类似的代码,say_whee指向了内部函数,所以当我们执行上述代码时,say_whee实际上是my_decorator内部的wrapper_do_twice函数,这显然不是我们想要的结果,因为我们希望的仅仅通过装饰器是扩充我们的函数行为,而并不希望改变函数的一些固有身份信息。
幸运的是,Python为我们提供了十分方便的解决方法,使用 @functools.wraps
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
除了使用@functools.wraps,无需做任何其他改动,问题就可以解决:
print(say_whee)
# <function say_whee at 0x7ff79a60f2f0>
print(say_whee.__name__)
'say_whee'
help(say_whee)
# Help on function say_whee in module whee: say_whee()
装饰器进阶使用
通过阅读上述内容,我相信您已经对什么是装饰器以及如何使用装饰器有了一定得了解,接下来我将带您一起探索装饰器的进阶使用。
装饰类
在类上使用装饰器有两种不同的方法。 第一个非常类似于使用装饰器装饰函数:您同样也可以装饰类的方法。
其中,@classmethod, @staticmethod, and @property是非常常用的内置装饰器。@classmethod, @staticmethod装饰器用于装饰不与具体类的实例相关联的方法。@property用于自定义类属性的getter和setter。
在类上使用装饰器的另一种方法是装饰整个类。 如下,
from dataclasses import dataclass
@dataclass
class PlayingCard:
rank: str
suit: str
和装饰函数十分类似,在上述例子中,其等价于 PlayingCard = dataclass(PlayingCard)。
类装饰器的常见用法是替代一些元类用例。 通过类装饰器,我们可以动态地更改类的定义。 编写类装饰器与编写函数装饰器非常相似。 唯一的区别是装饰器将接收类而不是函数作为参数。 实际上,您在上面看到的所有装饰器都能偶用作类装饰器(Python动态语言的体现)。 但是当您在类而不是函数上使用它们时,它们的效果可能不是您想要的。
使用多个装饰器
您可以使用多个装饰器装饰同一个类或者函数,当这样做时,您只需要将多个装饰器向下图一样叠放在类或者函数定义前,当然您还必须注意装饰器的顺序。如下所示,为了更好的展示顺序的影响,没有添加@functools.wraps消除对__name__的影响。从输出内容可以清晰的看到,最终函数指向的是最外层装饰器的内部函数,并且__name__为最外层装饰器内部函数的名字,所以最外层装饰器的函数最后装饰,即装饰顺序为由内到外。在此例中为,当外层装饰器为@secondDecorator时,执行了如下过程:sayHello = firstDecorator(sayHello) --> sayHello = secondDecorator(sayHello)
def firstDecorator(func):
def first():
print('the first decorator')
ret = func()
return ret
return first
def secondDecorator(func):
def second():
print('the second decorator')
ret = func()
return ret
return second
@firstDecorator
@secondDecorator
def sayHello():
print('Hello')
sayHello()
print(sayHello)
print(sayHello.__name__)
# the first decorator
# the second decorator
# Hello
# <function firstDecorator.<locals>.first at 0x0000024BCD91BCA0>
# first
@secondDecorator
@firstDecorator
def sayHello():
print('Hello')
sayHello()
print(sayHello)
print(sayHello.__name__)
# the second decorator
# the first decorator
# Hello
# <function secondDecorator.<locals>.second at 0x00000257F9B7BCA0>
# second
使用带参数的装饰器
有时候装饰器需要设置参数以增加装饰器的灵活性,例如,@do_twice装饰器的功能是重复执行函数两次,但是如果能够将次数参数化,即能够通过传递参数的方式,控制重复执行的次数能够提高该装饰器的灵活性。例如下面的例子,可以通过对参数num_times进行赋值,从而控制装饰器的具体作用。
@repeat(num_times=4)
def greet(name):
print(f"Hello {name}")
greet("World")
# Hello World
# Hello World
# Hello World
# Hello World
在进行详细展开之前,一定要将func和func()区分开,后者的含义是调用函数。此外,还有一点是,要想正确的使用装饰器,要确保@后面的函数能够调用另一个函数并返回函数。有了这两点基础,可以简单的解释下@repeat(num_times=4),repeat(num_times=4)是函数调用,其需要返回一个可以作为装饰器的函数,然后加上@就和装饰器差不多了。
def repeat(num_times):
def decorator_repeat(func):
@functools.wraps(func)
def wrapper_repeat(*args, **kwargs):
for _ in range(num_times):
value = func(*args, **kwargs)
return value
return wrapper_repeat
return decorator_repeat
上面的代码可能对您来说有些困惑,下面让我为您逐一展开:
def wrapper_repeat(*args, **kwargs):
for _ in range(num_times):
value = func(*args, **kwargs)
return value
wrapper_repeat()函数和之前的装饰器中的内部函数没有什么不同,差别仅仅是他需要外部为它提供参数num_times
def decorator_repeat(func):
@functools.wraps(func)
def wrapper_repeat(*args, **kwargs):
...
return wrapper_repeat
decorator_repeat()看起来与前面的decorator函数完全相同,只是命名不同。
def repeat(num_times):
def decorator_repeat(func):
...
return decorator_repeat
如您所见,最外面的函数返回对装饰器函数的引用。
在Python中,函数的参数具有极大的灵活性,得益于此,还可以定义既可以带参数也可以不带参数使用的装饰器,这很好地提高了装饰器的灵活性。
正如您在前一节中看到的,当一个装饰器使用参数时,您需要添加一个额外的外部函数。您的代码面临的挑战是,要弄清楚调用装饰器时是否带参数。一种模板式地实现方式:
def name(_func=None, *, kw1=val1, kw2=val2, ...): # 1
def decorator_name(func):
... # Create and return a wrapper function.
if _func is None:
return decorator_name # 2
else:
return decorator_name(_func) # 3
在这里,__func参数充当标记,用来指出装饰器是否传入了参数。
1.如果name未带参数被调用,则装饰函数将以_func的形式传入。如果调用时带有实参,那么_func将为None 2.在本例中,使用参数调用装饰器。返回一个可以调用和返回函数的装饰器函数。 3.在本例中,在没有参数的情况下调用装饰器。立即对函数应用装饰器。 将该模板应用于上述的@repeat装饰器
def repeat(_func=None, *, num_times=2):
def decorator_repeat(func):
@functools.wraps(func)
def wrapper_repeat(*args, **kwargs):
for _ in range(num_times):
value = func(*args, **kwargs)
return value
return wrapper_repeat
if _func is None:
return decorator_repeat
else:
return decorator_repeat(_func)
现在@repeat装饰器可以支持参数或者不带参数使用,
@repeat
def say_whee():
print("Whee!")
@repeat(num_times=3)
def greet(name):
print(f"Hello {name}")
say_whee()
# Whee!
# Whee!
greet("Penny")
# Hello Penny
# Hello Penny
# Hello Penny
遇到装饰器不清楚其作用时,您只需要清楚装饰器的原理,例如在此例中,当使用@repeat装饰时,等价于say_whee = repeat(say_whee)此时,_func=say_whee,所以return decorator_repeat(_func),相当于say_whee = decorator_repeat(say_whee)。而使用@repeat(num_times=3)修饰时,第一步首先调用repeat(num_times=3),此时_func=None,所以返回decorator_repeat,后续即等价于@decorator_repeat
类作为装饰器
装饰器语法@my_decorator只是func = my_decorator(func)的一种简单写法,所以如果类也能被调用时,理应也能作为装饰器使用。而Python恰好为我们提供了这样的可能:通过实现.__call__()方法来实现类的可调用。使用类装饰器可以方便的维持状态。 因此,类装饰器的实现需要实现.init()和.call():
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print(f"Call {self.num_calls} of {self.func.__name__!r}")
return self.func(*args, **kwargs)
@CountCalls
def say_whee():
print("Whee!")
.__init__()方法必须存储对该函数的引用,并可以进行任何其他必要的初始化。__call__()方法将被调用,而不是被装饰的函数。它的工作本质上与前面的示例中的wrapper()函数相同。注意,您需要使用functools.update_wrapper()函数,而不是@functools.wraps。在此例中,等价于say_whee = CountCalls(say_whee),此时say_whee实际上指向一个CountCalls实例,当后续调用say_whee时,实际上时调用了CountCalls实例,而CountCalls类实现了__call__()方法,所以其是可调用的,即调用__call__()方法.