几个有用的python装饰器,先收藏再学习

通用装饰器模板

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Do something before
        value = func(*args, **kwargs)
        # Do something after
        return value
    return wrapper_decorator

计时

import functools
import time

def timer(func):
    """Print the runtime of the decorated function"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()    # 1
        value = func(*args, **kwargs)
        end_time = time.perf_counter()      # 2
        run_time = end_time - start_time    # 3
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return value
    return wrapper_timer

@timer
def waste_some_time(num_times):
    for _ in range(num_times):
        sum([i**2 for i in range(10000)])

这个装饰器的工作原理是存储函数开始运行之前(标记为 的行# 1)和函数完成之后(标记 # 2)的时间。函数所用的时间就是两者之间的差值 (标记 # 3)。我们使用该time.perf_counter()函数,它可以很好地测量时间间隔。以下是一些示例:

>>> waste_some_time(1)
Finished ‘waste_some_time’ in 0.0010 secs

>>> waste_some_time(999)
Finished ‘waste_some_time’ in 0.3260 secs

如果你想对代码做更精确的测量,你应该考虑标准库中的timeit模块。它暂时禁用垃圾收集并运行多次试验以去除快速函数调用中的时间误差。

调试

以下@debug装饰器将在每次调用函数时打印调用函数的参数及其返回值:

import functools

def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]                      # 1
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
        signature = ", ".join(args_repr + kwargs_repr)           # 3
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")           # 4
        return value
    return wrapper_debug

签名是通过连接所有参数的字符串表示来创建的。以下列表中的数字对应于代码中的编号注释:

  1. 创建位置参数列表。使用repr()来获取表示每个参数的字符串。
  2. 创建关键字参数列表。
  3. 位置参数和关键字参数列表连接到一个签名字符串中,每个参数用逗号分隔。
  4. 函数执行后打印返回值。

让我们通过将装饰器应用于具有一个位置和一个关键字参数的简单函数来看看装饰器在实践中是如何工作的:

@debug
def make_greeting(name, age=None):
    if age is None:
        return f"Howdy {name}!"
    else:
        return f"Whoa {name}! {age} already, you are growing up!"

请注意@debug装饰器如何打印make_greeting()函数的签名和返回值:

>>> make_greeting(“Benjamin”)
Calling make_greeting(‘Benjamin’)
‘make_greeting’ returned ‘Howdy Benjamin!’
‘Howdy Benjamin!’

>>> make_greeting(“Richard”, age=112)
Calling make_greeting(‘Richard’, age=112)
‘make_greeting’ returned ‘Whoa Richard! 112 already, you are growing up!’
‘Whoa Richard! 112 already, you are growing up!’

>>> make_greeting(name=“Dorrisile”, age=116)
Calling make_greeting(name=‘Dorrisile’, age=116)
‘make_greeting’ returned ‘Whoa Dorrisile! 116 already, you are growing up!’
‘Whoa Dorrisile! 116 already, you are growing up!’

这个例子可能看起来不是立即有用,因为@debug装饰器只是重复你刚刚写的东西。当应用于您自己不直接调用的小型便利功能时,它会更加强大。

以下示例计算数学常数e的近似值:

import math
from decorators import debug

# Apply a decorator to a standard library function
math.factorial = debug(math.factorial)

def approximate_e(terms=18):
    return sum(1 / math.factorial(n) for n in range(terms))

调用approximate_e()函数时,可以看到@debug装饰器在工作:

>>> approximate_e(5)
Calling factorial(0)
‘factorial’ returned 1
Calling factorial(1)
‘factorial’ returned 1
Calling factorial(2)
‘factorial’ returned 2
Calling factorial(3)
‘factorial’ returned 6
Calling factorial(4)
‘factorial’ returned 24
2.708333333333333

降低运行速度

这一个示例可能看起来不是很有用。为什么要减慢 Python 代码的速度?最常见的用例可能是您想要对一个不断检查资源(如网页)是否已更改的函数进行速率限制。该@slow_down装饰再调用之前将暂停一秒:

import functools
import time

def slow_down(func):
    """Sleep 1 second before calling the function"""
    @functools.wraps(func)
    def wrapper_slow_down(*args, **kwargs):
        time.sleep(1)
        return func(*args, **kwargs)
    return wrapper_slow_down

@slow_down
def countdown(from_number):
    if from_number < 1:
        print("Liftoff!")
    else:
        print(from_number)
        countdown(from_number - 1)

注册函数

装饰器不必包装他们正在装饰的函数。他们也可以简单地注册一个函数存在并返回它解包。例如,这可用于创建轻量级插件架构:

import random
PLUGINS = dict()

def register(func):
    """Register a function as a plug-in"""
    PLUGINS[func.__name__] = func
    return func

@register
def say_hello(name):
    return f"Hello {name}"

@register
def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

def randomly_greet(name):
    greeter, greeter_func = random.choice(list(PLUGINS.items()))
    print(f"Using {greeter!r}")
    return greeter_func(name)

@register装饰简单地存储在全局变量PLUGINS字典中。请注意,您不必编写内部函数或@functools.wraps在此示例中使用,因为您将返回未修改的原始函数。

randomly_greet()函数随机选择一个已注册的函数来使用。请注意,PLUGINS字典已经包含对每个注册为插件的函数对象的引用:

>>> PLUGINS
{‘say_hello’: <function say_hello at 0x7f768eae6730>,
‘be_awesome’: <function be_awesome at 0x7f768eae67b8>}

>>> randomly_greet(“Alice”)
Using ‘say_hello’
‘Hello Alice’

这种简单的插件架构的主要好处是您不需要维护存在哪些插件的列表。该列表是在插件自行注册时创建的。这使得添加新插件变得微不足道:只需定义函数并用@register.

如果您熟悉globals(),您可能会发现与插件架构的工作方式有一些相似之处。globals()允许访问当前范围内的所有全局变量,包括您的插件:

>>> globals()
{…, # Lots of variables not shown here.
‘say_hello’: <function say_hello at 0x7f768eae6730>,
‘be_awesome’: <function be_awesome at 0x7f768eae67b8>,
‘randomly_greet’: <function randomly_greet at 0x7f768eae6840>}

使用@register装饰器,可以创建自己精心挑选的有趣变量列表。

判断用户是否登录

在使用 Web 框架时,通常会使用一些更高级的装饰器之前的最后一个示例。在这个例子中,我们使用Flask来设置一个/secret网页,该网页应该只对登录或以其他方式进行身份验证的用户可见:

from flask import Flask, g, request, redirect, url_for
import functools
app = Flask(__name__)

def login_required(func):
    """Make sure user is logged in before proceeding"""
    @functools.wraps(func)
    def wrapper_login_required(*args, **kwargs):
        if g.user is None:
            return redirect(url_for("login", next=request.url))
        return func(*args, **kwargs)
    return wrapper_login_required

@app.route("/secret")
@login_required
def secret():
    ...

虽然这提供了有关如何向 Web 框架添加身份验证的想法,但您通常不应该自己编写这些类型的装饰器。对于 Flask,您可以改用Flask-Login 扩展,这增加了更多的安全性和功能。

带参数的装饰器

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处理装饰器参数的附加模式中。让我们从最里面的函数开始:

def wrapper_repeat(*args, **kwargs):
    for _ in range(num_times):
        value = func(*args, **kwargs)
    return value

此wrapper_repeat()函数接受任意参数并返回装饰函数的值,func()。这个包装函数还包含调用装饰函数num_times次数的循环。这与您之前看到的包装器函数没有什么不同,只是它使用了num_times必须从外部提供的参数。

走出一步,你会发现装饰器函数:

def decorator_repeat(func):
    @functools.wraps(func)
    def wrapper_repeat(*args, **kwargs):
        ...
    return wrapper_repeat

同样,decorator_repeat()看起来与您之前编写的装饰器函数完全一样,只是它的命名不同。那是因为我们repeat()为最外层的函数保留了基本名称,即用户将调用的函数。

正如您已经看到的,最外层的函数返回对装饰器函数的引用:

def repeat(num_times):
    def decorator_repeat(func):
        ...
    return decorator_repeat

repeat()函数中发生了一些微妙的事情:

  1. 定义decorator_repeat()为内部函数意味着repeat()将引用一个函数对象—— decorator_repeat。早些时候,我们使用repeat不带括号来引用函数对象。在定义带参数的装饰器时,添加的括号是必要的。
  2. 这个num_times论点repeat()本身似乎并没有被使用。但是通过传递num_times一个闭包被创建,其中num_times存储了 的值,直到稍后被 使用wrapper_repeat()。

一切都设置好后,让我们看看结果是否如预期的那样:

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

>>> greet(“World”)
Hello World
Hello World
Hello World
Hello World

灵活参数装饰器

稍加注意,您还可以定义既可以使用也可以不使用参数的装饰器。最有可能的是,您不需要这个,但是具有灵活性是很好的。

正如您在上一节中看到的,当装饰器使用参数时,您需要添加一个额外的外部函数。您的代码面临的挑战是确定装饰器是否已被调用并带参数或不带参数。

由于仅在不带参数调用装饰器时才直接传入要装饰的函数,因此该函数必须是可选参数。这意味着装饰器参数必须全部由关键字指定。您可以使用特殊*语法强制执行此操作(在*之后的参数必须是关键字参数,在/之后的参数必须是位置参数),这意味着以下所有参数都是仅关键字的:

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。如果使用参数调用它,则_funcNone,并且某些关键字参数可能已从其默认值更改。该*参数列表意味着其余的参数不能被称为位置参数。
  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唯一的变化是添加的_func参数和if-else判断。

这些示例表明@repeat现在可以带或不带参数使用:

@repeat
def say_whee():
    print("Whee!")

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

在不带参数的情况下,默认值为2。

>>> say_whee()
Whee!
Whee!

>>> greet(“Penny”)
Hello Penny
Hello Penny
Hello Penny

上一篇:【Web前端HTML5&CSS3】13-背景


下一篇:pytest重复运行所有或指定测试用例(pytest-repeat插件)