Python装饰器(入门)

Decorator

根据定义,装饰器是一个函数,它接受另一个函数并扩展后者的行为,而不显式地修改它。
这听起来,有点令人困惑,但它真的不是,尤其在你看到了几个例子关于修饰器如何工作,你能够找到所有的例子。

函数和修饰器密切相关

第一级对象——python将函数视为对象

在Python中,函数是第一级对象。这意味着函数可以被传递当作参数来使用。就像其他任何的对象一样。考虑下面三个函数:

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

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

def greet_bob(greeter_func):
    return greeter_func("Bob")

内联函数

就是函数内部定义的函数。

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

通过函数返回函数

Python还允许使用函数作为返回值。下面的示例从外部父()函数返回一个内部函数:

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)
>>> second = parent(2)

>>> first
<function parent.<locals>.first_child at 0x7f599f1e2e18>

>>> second
<function parent.<locals>.second_child at 0x7f599dad5268>

有些神秘的输出仅仅意味着第一个变量引用parent()内部的本地first_child()函数,而第二个变量指向second_child()。
现在可以像使用常规函数一样使用first和second,即使它们指向的函数不能直接访问:

>>> first()
'Hi, I am Emma'

>>> second()
'Call me Liam'

简单的修饰器

既然您已经了解了函数与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 = my_decorator(say_whee)

实际上,名称say_whee现在指向wrapper()内部函数。记住,当您调用my_decorator(say_whee)时,将包装器作为函数返回:

扫描二维码关注公众号,回复: 5761467 查看本文章
>>> say_whee
<function my_decorator.<locals>.wrapper at 0x7f3c5dfd42f0>

然而,wrapper()将原始say_whee()引用为func,并在两个调用之间调用该函数来打印()。
简单地说:装饰器包装一个函数,修改它的行为。
在继续之前,让我们看一下第二个例子。因为wrapper()是一个常规的Python函数,装饰器修改函数的方式可以动态地改变。为了不打扰邻居,下面的例子只会在白天运行经过修饰的代码:

from datetime import datetime

def not_during_the_night(func):
    def wrapper():
        if 7 <= datetime.now().hour < 22:
            func()
        else:
            pass  # Hush, the neighbors are asleep
    return wrapper

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

say_whee = not_during_the_night(say_whee)
>>> say_whee()
>>>

糖衣语法

上面装饰say_whee()的方法有点笨拙。首先,您需要输入三次say_whee名称。此外,装饰会隐藏在函数的定义之下。

相反,Python允许您以更简单的方式使用修饰符@符号,有时称为“pie”语法。下面的例子和第一个装饰器例子做的是一样的:
原来:

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

say_whee = not_during_the_night(say_whee)

使用@后:

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

即是将say_whee传入装饰器,返回一个新的函数(装饰器)。
所以,@my_decorator只是说say_whee = my_decorator(say_whee)的一种更简单的方式。这就是如何将装饰器应用于函数。

有参数的装饰器函数

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

问题是内部函数wrapper_do_twice()不接受任何参数,但是name="World"被传递给它。您可以通过让wrapper_do_twice()接受一个参数来解决这个问题,但是这样它就不能用于前面创建的say_whee()函数。

解决方案是在内部包装函数中使用*args和kwargs**。然后它将接受任意数量的位置和关键字参数。重写decorator .py如下:

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper_do_twice

从修饰函数返回值

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(hi_adam)
None

我们的返回值去哪了呢?
因为do_twice_wrapper()没有显式地返回值,所以调用return_greeting(“Adam”)最终没有返回值。

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper_do_twice

到此,我们可以实现了正确返回。

那么,被修饰的函数是谁?

在使用Python时,尤其是在交互式shell中,它强大的内省能力是一个极大的便利。自省是对象在运行时了解自身属性的能力。例如,一个函数知道它自己的名称和文档:

>>> print
<built-in function print>

>>> print.__name__
'print'

>>> help(print)
Help on built-in function print in module builtins:

print(...)
    <full help message>

自省也适用于你定义自己的函数:

>>> say_whee
<function do_twice.<locals>.wrapper_do_twice at 0x7f43700e52f0>

>>> say_whee.__name__
'wrapper_do_twice'

>>> help(say_whee)
Help on function wrapper_do_twice in module decorators:

wrapper_do_twice()

但是,在修饰之后,say_whee()对它的标识非常困惑。现在它报告为do_twice()装饰器中的wrapper_do_twice()内部函数。虽然在技术上是正确的,但这不是非常有用的信息

要解决这个问题,装饰器应该使用@functools。包装装饰器,它将保存关于原始函数的信息。再次更新decorators.py:

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

那么,

>> say_whee
<function say_whee at 0x7ff79a60f2f0>

>>> say_whee.__name__
'say_whee'

>>> help(say_whee)
Help on function say_whee in module whee:

say_whee()

技术细节:@functools.wrap 装饰器使用函数functools.update_wrapper()来更新内省中使用的特殊属性,就像__name__和__doc__。

一些真实的例子

让我们看一些更有用的装饰器示例。你会注意到它们主要遵循的模式与你目前所学的相同:
这是一个很好的模板。

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

运行,

>>> waste_some_time(1)
Finished 'waste_some_time' in 0.0010 secs

>>> waste_some_time(999)
Finished 'waste_some_time' in 0.3260 secs

**注意:**如果您只是想了解函数的运行时,@timer装饰器非常棒。如果希望对代码进行更精确的度量,则应该考虑标准库中的timeit模块。它暂时禁用垃圾收集,并运行多次试验来消除快速函数调用中的噪声。

调试代码

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. 创建关键字参数列表。f-string将每个参数格式化为key=value,其中!r说明符表示使用repr()表示该值。
  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!"

执行,

>>> 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装饰器只是重复您刚刚编写的内容。当它应用于您自己不直接调用的小型方便函数时,它会更强大。

数学例子

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的近似基于以下级数展开:
在这里插入图片描述
调用approate_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

在本例中,只添加5项,就得到了e = 2.718281828的近似值。

减速代码

下一个例子可能不是很有用。为什么要降低Python代码的速度?可能最常见的用例是您希望对一个函数进行速率限制,该函数不断地检查资源(如web页面)是否发生了更改。@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)

要查看@slow_down装饰器的效果,您确实需要亲自运行示例:

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

注意:countdown()函数是一个递归函数。换句话说,它是一个调用自身的函数。要了解更多关于Python中的递归函数的知识,请参阅我们的Python中递归思维指南。
@slow_down装饰器总是休眠一秒钟。稍后,您将看到如何通过向装饰器传递参数来控制速率。

注册插件

装饰师不需要包装他们正在装饰的功能。它们还可以简单地注册一个函数的存在并返回unwrapped。例如,这可以用来创建轻量级插件架构:

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装饰器只是在全局插件dict中存储对被装饰函数的引用。注意,您不必编写内部函数或使用@functools。在本例中进行包装,因为您将返回未修改的原始函数。

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

使用@register装饰器,您可以创建自己的有趣变量列表,有效地从globals()中手动选择一些函数。

用户登陆

在继续介绍一些更高级的装饰器之前,最后一个示例通常用于处理web框架。在本例中,我们使用Flask设置了一个/secret web页面,这个页面应该只对登录或者通过其他认证的用户可见:

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扩展,这增加了更多的安全性和功能。

猜你喜欢

转载自blog.csdn.net/xiabenshu/article/details/88961007