python进阶系列 - 12 装饰器

61 阅读7分钟

装饰器的目的是为了扩展函数的功能,而不是修改函数本身。 它是一个非常强大的工具,它允许为现有的函数添加新的功能。

本文主要分享有两种装饰器:

  • 函数装饰器
  • 类装饰器

一个函数可以被修饰为一个装饰器,使用 @ 号。

示列代码:

@my_decorator
def my_function():
    pass

看完上面的示例代码,我们再来看看这个函数是如何被修饰的。

函数装饰器

为了理解装饰器模式,我们需要先强调下Python中一切都皆为对象。

函数也是一种对象,它可以被赋值给变量,还可以在另一个函数内定义,作为另一个函数的参数,或者从另一个函数返回。

装饰器是一个接受另一个函数作为参数的函数,它包装了函数的行为,并返回包装后的函数。所以装饰器的目的是为了扩展函数的功能,而不是修改函数本身。

练习下面代码:

def start_end_decorator(func):
    def wrapper():
        print("Start")
        func()
        print("End")

    return wrapper


def print_name():
    print("Alex")


print_name()
print()
# 使用装饰器得到一个新的函数
print_name_new = start_end_decorator(print_name)
print_name_new()

结果:

Alex

Start
Alex
End

解释

上面的代码中定义了两个函数,其中print_name()函数是一个普通函数,它的功能是打印Alex。

我们重点来看start_end_decorator()函数:

  1. 它的参数func是一个函数。
  2. 它在内部定义了一个函数wrapper()作为返回值。当然wrapper就是一个名字而已,可以随意😊。
  3. wrapper()首先打印出"Start",然后调用func(),最后打印"End"。

总起来看,start_end_decorator()函数的是扩展了print_name()函数的功能:

  • 在其运行之前打印Start,
  • 运行结束之后打印End。

所以我们可以用start_end_decorator()函数来修饰print_name()函数,得到一个新的函数print_name_new

思考题,请写一个装饰器函数,来打印函数的执行时长。

装饰器的语法

上面的函数我们必须重新定义一个函数,这比较麻烦,所以Python提供了一种语法来定义装饰器。 它可以将修饰后函数赋值给自己,我们可以通过@修饰我们的函数来实现相同的功能。

请练习下面代码:

def start_end_decorator(func):
    def wrapper():
        print("Start")
        func()
        print("End")

    return wrapper


@start_end_decorator
def print_name():
    print('Alex')


print_name()

结果:

Start
Alex
End

解释

首先我们定义了一个装饰器start_end_decorator()。 然后在定义新的函数print_name()时,我们使用了@装饰器。 从而print_name()函数具备了新的功能!

是不是@非常简单,这也是真实工作中最常用的!!!

修饰带参数的函数

如果我们的函数有输入参数,上面的装饰器就无法实现,因为Python不允许装饰器接受参数。

那我们该怎么办呢?

我们可以在内部函数中使用 *args**kwargs 来实现。

请尝试下面代码:

def start_end_decorator_2(func):
    def wrapper(*args, **kwargs):
        print('Start')
        func(*args, **kwargs)
        print('End')

    return wrapper


@start_end_decorator_2
def add_5(x):
    return x + 5


result = add_5(10)
print(result)

查看结果:

Start
End
None

解释

跟上面装饰器不同的是,内部定义函数时使用了*args**kwargs。 从而这个装饰器可以修饰需要输入参数的函数。 但是我们会发现输出结果并不是我们想要的,因为我们没有指定输出结果。

下面我们继续深入,来解决这个问题。

装饰器返回值

请练习下面代码:

def start_end_decorator_3(func):
    def wrapper(*args, **kwargs):
        print('Start')
        result = func(*args, **kwargs)
        print('End')
        return result

    return wrapper


@start_end_decorator_3
def add_5(x):
    return x + 5


result = add_5(10)
print(result)

结果:

Start
End
15

函数的身份问题

如果我们查看我们修饰后函数的名字,或者使用内置的help函数,我们会发现,它的名字是wrapper

Python认为现在的函数是装饰器函数的内部函数。

代码:

def start_end_decorator_3(func):
    def wrapper(*args, **kwargs):
        print('Start')
        result = func(*args, **kwargs)
        print('End')
        return result

    return wrapper


@start_end_decorator_3
def add_5(x):
    return x + 5


print(add_5.__name__)
help(add_5)

结果:

wrapper
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

为了解决这个问题,我们可以使用functools.wraps装饰器,它将保留原函数的信息。 这可以帮助我们在运行时获取对象的信息,比如函数的名字,参数等。

修正后的代码:

import functools


def start_end_decorator_4(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('Start')
        result = func(*args, **kwargs)
        print('End')
        return result

    return wrapper


@start_end_decorator_4
def add_5(x):
    return x + 5


result = add_5(10)
print(result)
print(add_5.__name__)
help(add_5)

结果:

Start
End
15
add_5
Help on function add_5 in module __main__:

add_5(x)

最终的自定义装饰器模板

现在我们已经拥有了所有的部件,我们的自定义装饰器模板如下(🍓):

import functools


def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 定义额外功能
        result = func(*args, **kwargs)
        # 定义额外功能。
        return result

    return wrapper

装饰器参数

为了更好地理解装饰器参数的必要性, 我们看看另一个例子:一个repeat装饰器,它接受一个数字作为输入。

这个装饰器的功能是:重复执行输入函数给定的次数。

你理解了么?装饰器的装饰器...

练习下面的代码:

import functools


def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result

        return wrapper

    return decorator_repeat


@repeat(num_times=3)
def greet(name):
    print(f"Hello {name}")


greet("Alex")

结果:

Hello Alex
Hello Alex
Hello Alex

嵌套装饰器

我们可以通过堆叠的方式将多个装饰器应用到一个函数上。 这些装饰器按照列出的顺序中执行。

代码:

import functools


# 一个装饰器,在执行前后打印日志
def start_end_decorator_4(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('Start')
        result = func(*args, **kwargs)
        print('End')
        return result

    return wrapper


# 一个装饰器函数,打印函数的调用信息
def debug(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"Calling {func.__name__}({signature})")
        result = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {result!r}")
        return result

    return wrapper


@debug
@start_end_decorator_4
def say_hello(name):
    greeting = f'Hello {name}'
    print(greeting)
    return greeting


say_hello(name='Alex')

结果:

Calling say_hello(name='Alex')
Start
Hello Alex
End
'say_hello' returned 'Hello Alex'

你可以尝试改变装饰器的顺序,看看结果会发生什么。

类装饰器

我们也可以使用类作为装饰器。 因此,我们必须实现__call__()方法,使我们的对象可调用。

类装饰器通常用于维护状态。例如,我们可以记录函数被调用的次数。 __call__本质上和wrapper()方法是一样的。 它添加了一些功能,执行函数,并返回其结果。

注意,这里我们使用functools.update_wrapper()而不是functools.wraps来保留我们的函数的信息。

代码:

import functools


class CountCalls:
    # 这里我们使用了`functools.update_wrapper()`来保留我们的函数的信息。
    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"{self.func.__name__!r}被执行了{self.num_calls}次")
        return self.func(*args, **kwargs)


@CountCalls
def say_hello(num):
    print("Hello!")


say_hello(5)
say_hello(5)

结果:

'say_hello'被执行了1次
Hello!
'say_hello'被执行了2次
Hello!

一些常见使用场景

  • 用于计算函数的执行时间
  • 用于打印函数的调用信息和参数
  • 用于检查参数是否符合某些要求,并且根据结果进行不同的行为
  • 注册函数(插件)
  • 使用time.sleep()函数来检查网络行为
  • 用于缓存返回值的装饰器
  • 添加或更新状态

小节

装饰器(Decorators)是 Python 的一个重要部分。简单地说:他们是修改其他函数的功能的函数。他们有助于让我们的代码更简短,也更Pythonic(Python范儿)

大多数初学者不知道在哪儿使用它们,希望上面的分享可以帮助到你!

感谢你的阅读。欢迎大家点赞、收藏、支持!

pythontip 出品,Happy Coding!

公众号: 夸克编程

我们的小目标: 让天下木有难学的Python