Python函数修饰器应用实例

编程 2022年10月26日 51 0

在上一篇文章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

这个公式是创建复杂修饰器的样板代码,适用于多种情况。

时间函数

让我们从创建一个@timer修饰器开始,它用于测量函数执行所需的时间,并将执行时间打印到控制台。下面是代码:

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

自己运行一下,并逐行阅读代码,以理解上面的代码。

调试代码

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

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

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

  1. 创建位置参数列表。使用repr()获取表示每个参数的可阅读字符串。
  2. 创建关键字参数列表。f-string将每个参数格式化为key=value,其中!r说明符表示调用repr()函数修饰这个值。
  3. 位置参数和关键字参数的列表被连接到一个签名字符串中,每个参数由逗号分隔。
  4. 返回值在函数执行后打印。

用一个例子来验证下@debug修饰器的效果:

@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!"

打印如下信息:

>>> 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))

这个例子还展示了如何将装饰器应用于已经定义的函数。e的近似值基于以下级数展开:

当调用approximate_e()函数时,有以下输出:

>>> 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

在这个例子中,可以得到近似值e=2.718281828,只加了5项。

减速代码

为什么要减慢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)

使用一个例子来验证下这个修饰器:

>>> countdown(3)
3
2
1
Liftoff!

@slow_down修饰器总是让函数休眠一秒钟。

注册插件

修饰器不一定都是用来包装其它函数的功能,他们也可以简单地注册一个函数并将其返回。例如,这可以用于创建轻量级插件架构:

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 dict中存储对修饰函数的引用。这里不需要在内部使用@functools,因为返回的是未修改的原始函数。

函数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修饰。

如果熟悉Python中的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>}

判断用户是否登录

最后一个示例通常在使用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():
    ...

这里只是给出了一个判断登录的思路,一般不需要我们自己去实现。对于Flask,可以使用Flask登录扩展,安全性更好,功能也更多。