详解Python装饰器(Decorators)

365 阅读14分钟

在本篇文章中,我们将了解什么是装饰器以及如何创建和使用它们。装饰器能够让我们以简单的方式构建高阶函数。

根据定义,装饰器是一个函数,它接受另一个函数并扩展了后者的行为,而无需对其进行显式修改。

或许现在对您来说这听起来令人困惑,但我相信通过阅读本篇文章,您会对装饰器有一个全新的认识。

函数(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__()方法.

realpython.com/primer-on-p…