Decorators-Primer on Python Decorators 装饰器入门

英文原文(from REAL PYTHON)链接:

Primer on Python Decorators – Real Pythonicon-default.png?t=N7T8https://realpython.com/primer-on-python-decorators/

Notion链接(我一开始是在notion翻译的可能格式更好一点):

Decorators-Primer on Python Decorators -labmem012A new tool that blends your everyday work apps into one. It's the all-in-one workspace for you and your teamicon-default.png?t=N7T8https://labmem012.notion.site/Decorators-Primer-on-Python-Decorators-57c01c9f3c5341a88e91a0d574b7df4b?pvs=4

Python Functions

为了了解装饰器(Decorators),必须先了解函数工作原理的一些细节。函数有很多方面,但是在装饰器种,一个函数根据给定的参数值返回一个值,以下是一个基本的例子:

>>> def add_one(number):
...     return number + 1
...

>>> add_one(2)
3

总体来说,Python中的函数除了将输入转为输出外还有其他的作用(side effect)。print()函数就是一个例子:它返回None ,同时会向console输出一些信息。但是,将函数想成一个转换参数到输出值的工具已足以了解装饰器(Decorators)

Fist-Class Objects 一级对象

在函数式编程 functional programming中,几乎完全使用没有其他作用(side effect)的纯函数。Python 虽然不是纯函数式语言,但它支持许多函数式编程概念,包括将函数视为一级对象 first-class objects

这意味着**函数(**function)可以作为参数传递和使用,就像任何其他的对象(object)一样,如str,int,float,list等等。

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

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

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

以上例子中,say_hello()be_awesome() 是常规函数, 需要获取一个string类型的name。而 greet_bob() 函数需要获取一个函数作为参数,例如你可以将say_hello()be_awesome()函数作为输入。

为了测试你的函数,你可以在交互模式(interactive mode)中运行你的代码。这需要使用 -i。例如,如果你的代码在一个名为greeters.py的文件里,那么你应该运行 python -i greeters.py

>>> greet_bob(say_hello)
'Hello Bob'

>>> greet_bob(be_awesome)
'Yo Bob, together we're the awesomest!'

注意 greet_bob(say_hello) 指向两个函数,greet_bob()say_hello() ,但是是以不同的方式。say_hello 函数的命名没有括号,这表示只传递对函数的引用,未执行该函数。greet_bob() 函数写了括号,所以它会被正常调用。

这是一个重要的区别,对函数如何以一级对象(first-class objects)工作至关重要。不带括号的函数名称是对函数的引用,而带括号的函数名称调用函数并引用其返回值。

Inner Functions 内部函数

在函数中可以定义函数,这样的函数称为内部函数(inner functions)。以下是包含两个内部函数的函数的例子:

def parent():
    print("Printing from parent()")

    def first_child():
        print("Printing from first_child()")

    def second_child():
        print("Printing from second_child()")

    second_child()
    first_child()

在你调用 parent() 函数的时候发生了什么?

在交互模式下运行 inner_functions.py 时输出如下:

>>> parent()
Printing from parent()
Printing from second_child()
Printing from first_child()

注意内部函数定义的顺序不重要。像其他函数一样,打印只发生在函数被执行时。

除此之外,内部函数在母函数(parentfunction)被调用前不被定义。它们的局部作用域(locally scope)为 parent(),这意味着它们仅作为局部变量存在于 parent() 函数中。尝试调用 first_child()。你会得到一个错误:

>>> first_child()
Traceback (most recent call last):
  ...
NameError: name 'first_child' is not defined

无论何时调用parent() ,内部函数first_child()second_child() 也会被调用。但是因为他们的局部作用域,他们在parent() 函数外不可用。

Functions as Return Values 函数作为返回值

python允许将函数作为函数的返回值。在以下例子中,重写parent() 以返回其中一个内部函数:

def parent(num):
    def first_child():
        return "Hi, I'm Elias"

    def second_child():
        return "Call me Ester"

    if num == 1:
        return first_child
    else:
        return second_child

注意返回的first_child 不带括号。回想一下,这意味着要返回对函数first_child的引用。相反,带括号的 first_child() 表示计算函数的结果。你可以在以下示例中看到这一点:

>>> first = parent(1)
>>> second = parent(2)

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

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

输出表示first变量指向parent() 中的first_child() 函数,second变量指向second_child()

现在你可以将firstsecond作为常规函数使用,尽管你不能访问他们直接指向的函数。

>>> first()
'Hi, I'm Elias'

>>> second()
'Call me Ester'

最后,注意在上一个例子中,你在母函数中执行了内部函数,例如first_child() 。然而在这个例子中,你没有在返回时给内部函数添加括号,例如first_child 。这样,你就可以获得对每个函数的引用,在未来你可以调用他们。

Simple Decorators in Python 简单装饰器

现在你知道了函数就像Python中其他任何对象一样,你已经准备好学习神奇的Python装饰器了(decorators),我们从一个例子开始。

def 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 = decorator(say_whee)

这里你定义了两个常规函数,decorator()say_whee(),以及一个内部函数wrapper()。接下来你重定义了say_whee() ,将decorator()应用于原始的say_whee()

你可以猜到调用say_whee()时会发生什么吗?在REPL(Read-Eval-Print Loop,即交互式环境)

中进行尝试。除了在文件后加 -i运行外,你还可以手动import函数(假设上一段代码写在hello_decorator.py中):

>>> from hello_decorator import say_whee

>>> say_whee()
Something is happening before the function is called.
Whee!
Something is happening after the function is called.

为了理解这里发生了什么,回看上一个例子,你正在运用迄今为止学到的所有知识。

所谓的装饰(decoration)发生在这一行:

say_whee = decorator(say_whee)

实际上,say_whee现在指向内部函数wapper()。记住当你调用decorator(say_whee)时,你将wapper() 作为函数返回:

>>> say_whee
<function decorator.<locals>.wrapper at 0x7f3c5dfd42f0>

但是,wapper() 将原始 say_whee() 引用为 func,并在对 print() 的两次调用之间调用该函数。

简单地说,装饰器包装一个函数,修改它的行为( a decorator wraps a function, modifying its behavior

在继续之前,看一下第二个例子,因为wapper()是一个常规函数,装饰器修改函数的方式可以动态改变。我们做一个更改,以下例子将只在白天运行装饰器的代码:

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() 会无事发生 😋。

>>> from quiet_night import say_whee

>>> say_whee()

在这里,say_whee()不打印任何输出。因为if检验没过,所以wapper没有调用func(),即原始say_whee()

Adding Syntactic Sugar 添加语法糖

回顾hello_decorator.py 中的代码,你装饰say_whee() 的方式有点“笨重”。首先,你一共会输入三遍say_whee() ,其次,装饰器隐藏在函数定义之下。

Python允许你以更简单的方法(用@符号)使用装饰器,有时这也被称为pie syntax。以下的例子与第一个装饰器例子做的事完全相同:

def 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

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

所以, @decorator 只是 say_whee = decorator(say_whee)的简短写法。这是将装饰器应用于函数的方式

Reusing Decorators 重用装饰器

回想一下,装饰器只是一个常规的python函数,所有常用的可重用性工具都可用。现在,你将创建一个module,来存储可用于很多其他函数的装饰器。

创建一个名为 decorators.py 的文件,写入以下内容:

def do_twice(func):
    def wrapper_do_twice():
        func()
        func()
    return wrapper_do_twice

<aside> <img src="/icons/exclamation-mark_red.svg" alt="/icons/exclamation-mark_red.svg" width="40px" /> 你可以随意命名你的内部函数,像 wrapper() 这样的通用名称通常是可以的。在本教程中,您将看到很多装饰器。为了将它们分开,您需要使用与装饰器相同的名称来命名内部函数,但使用wrapper_前缀。

</aside>

你下载可以在其他文件里用这个新的装饰器了:

>>> from decorators import do_twice

>>> @do_twice
... def say_whee():
...     print("Whee!")
...

输出:

>>> say_whee()
Whee!
Whee!

Decorating Functions With Arguments 用参数装饰函数

假设你有一个接受一些参数的函数。你还能装饰它吗?试一试:

>>> from decorators import do_twice

>>> @do_twice
... def greet(name):
...     print(f"Hello {name}")
...

你现在将@do_twice应用于greet()greet()需要一个name。不幸的是,调用这个函数会抛出一个错误:

>>> greet(name="World")
Traceback (most recent call last):
  ...
TypeError: wrapper_do_twice() takes 0 positional arguments but 1 was given

问题在于内部函数wrapper_do_twice() 不获取任何参数,但是你向它传递了name="World” 。你可以通过让wrapper_do_twice() 接受一个参数来修复这个问题,但是这样你之前创建的say_whee()函数就不可用了。

解决方法是在内部函数中使用*args*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() 都可以正常运行了:

>>> from decorators import do_twice

>>> @do_twice
... def say_whee():
...     print("Whee!")
...

>>> say_whee()
Whee!
Whee!

>>> @do_twice
... def greet(name):
...     print(f"Hello {name}")
...

>>> greet("World")
Hello World
Hello World

你使用了同一个装饰器去装饰两个不同的函数,这展现了装饰器强大的功能之一,他们添加的行为可以应用于许多不同的函数。

Returning Values From Decorated Functions从装饰函数中返回值

装饰函数的返回值会发生什幺变化?这取决于装饰器的决定。假设你如下方式装饰一个简单的函数:

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

返回了None

因为wrapper_do_twice() 没有显式返回值,所以调用return_greeting("Adam") 最终返回None

为了修复这个,你需要确保包装函数返回装饰函数的返回值(make sure the wrapper function returns the return value of the decorated function)。更改 decorators.py

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return 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}"
...

>>> return_greeting("Adam")
Creating greeting
Creating greeting
'Hi Adam'

Finding Yourself

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

>>> print
<built-in function print>

>>> print.__name__
'print'

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

print(...)
    <full help message>

当您检查 print() 时,您可以看到它的名称和文档。introspection也适用于你自己定义的函数:

>>> 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.wraps](<https://docs.python.org/library/functools.html#functools.wraps>) 装饰器,这将保留有关原始函数的信息。再次更新 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() 做任何改变,但是你需要重置你的REPL来看效果:

>>> from decorators import do_twice

>>> @do_twice
... def say_whee():
...     print("Whee!")
...

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

好多了,现在在经过装饰后say_whee() 仍然是它自己。

<aside> <img src="/icons/exclamation-mark_red.svg" alt="/icons/exclamation-mark_red.svg" width="40px" /> 装饰器 @functools.wraps 使用 [functools.update_wrapper()](<https://docs.python.org/3/library/functools.html#functools.update_wrapper>) 来更新 __name__ 和 __doc__ 等 special attributes ,这在introspection被使用

</aside>

您现在已经了解了如何创建装饰器的基础知识。然而,@do_twice 并不是一个非常令人兴奋的装饰器,而且它的用例也不多。在下一节中,你将实现几个装饰器,这些装饰器说明了你目前所知道的内容,并且可以在自己的代码中使用这些装饰器。

A Few Real World Examples 一些真实例子

现在你将看到一些真实的例子。你将注意到他们基本遵从相同的模式:

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

🔺 以上公式是构建更复杂装饰器的良好模板。

你将继续在 decorators.py 中存储你的装饰器。

Timing Functions

你将从创建一个@timer装饰器开始,它将计算执行一个函数所需的时间,然后在console中打印,代码如下:

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()
        value = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"Finished {func.__name__}() in {run_time:.4f} secs")
        return value
    return wrapper_timer

现在,添加 waste_some_time() 作为花费一些时间的函数的示例,以便测试@timer。以下是一些计时示例:

>>> from decorators import timer

>>> @timer
... def waste_some_time(num_times):
...     for _ in range(num_times):
...         sum([number**2 for number in range(10_000)])
...

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

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

<aside> <img src="/icons/exclamation-mark_red.svg" alt="/icons/exclamation-mark_red.svg" width="40px" /> 注意:如果你只想了解函数的运行时,@timer装饰器非常有用。如果要对代码进行更精确的测量,则应考虑标准库中的 timeit 模块。它暂时禁用垃圾回收 garbage collection,并运行多个试验,以去除短函数调用中的噪音。

</aside>

如果你对学习时间函数有兴趣,请阅读Python Timer Functions: Three Ways to Monitor Your Code

Debugging Code

下面的 @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]
        kwargs_repr = [f"{k}={repr(v)}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__}() returned {repr(value)}")
        return value
    return wrapper_debug

签名(signature)是通过连接所有参数的字符串来创建的:

  • Line 9: 创建了一个位置参数的列表。使用 repr()来获取每个参数的字符串
  • Line 10: 创建了一个关键字参数的列表。 f-string 会将每个参数格式化为 key=value,同样,你需要使用 repr() 来表示值。
  • Line 11: 将位置参数和关键字合成一个签名字符串,每个参数用逗号分隔。
  • Line 14: 在函数执行后打印返回值

现在,我们将装饰器应用于一个包含一个位置参数和一个关键字参数的简单函数,看看装饰器在实践中是如何工作的:

>>> from decorators import debug

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

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

>>> make_greeting("Benjamin")
Calling make_greeting('Benjamin')
make_greeting() returned 'Howdy Benjamin!'
'Howdy Benjamin!'

>>> make_greeting("Juan", age=114)
Calling make_greeting('Juan', age=114)
make_greeting() returned 'Whoa Juan! 114 already, you're growing up!'
'Whoa Juan! 114 already, you're growing up!'

>>> make_greeting(name="Maria", age=116)
Calling make_greeting(name='Maria', age=116)
make_greeting() returned 'Whoa Maria! 116 already, you're growing up!'
'Whoa Maria! 116 already, you're growing up!'

这个例子可能现在看起来不是很有用,因为装饰器@debug只是打印了你写的东西。当它应用于不直接调用的小型便利函数(small convenience functions)时,功能会更加强大。

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

import math
from decorators import debug

math.factorial = debug(math.factorial)

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

这里你对已经定义的函数应用了装饰器。在line 4,你装饰(decorate)了来自math标准库的函数factorial() 。您不能使用派语法(pie syntax),但仍可以手动应用装饰器。e 的近似值基于以下数列展开:

\begin{aligned}e=\sum_{n=0}^\infty\frac{1}{n!}=\frac{1}{0!}+\frac{1}{1!}+\frac{1}{2!}+\cdots=\frac{1}{1}+\frac{1}{1}+\frac{1}{1\cdot2}+\cdots\end{aligned}

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

>>> from calculate_e import approximate_e

>>> approximate_e(terms=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

在这个例子中,你只用五项(terms)就得到真实值 e ≈ 2.718281828 的近似值。

Slowing Down Code

这小节中,你将创建一个减慢代码速度的求解器。这或许看起来没用。。

可能最常见的用例是,你希望对一个函数进行速率限制,该函数会持续检查资源(如网页)是否已更改。装饰器@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 中,调用 time.sleep() 让代码在调用修饰函数之前暂停。要查看@slow_down装饰器的工作原理,请创建一个 countdown() 函数。若要查看减慢代码速度的效果,应自行运行该示例:

>>> from decorators import 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装饰器始终会休眠一秒钟。稍后,你将了解如何通过将参数传递给装饰器来控制速率。

Registering Plugins

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

# ...

PLUGINS = dict()

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

装饰器@register仅在全局PLUGINS 字典中存储对修饰函数的引用。注意在这个例子中你不必写一个内部函数或者使用@functools.wraps,因为你返回了未修改的原始函数。

你现在可以注册函数:

>>> from decorators import register, PLUGINS

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

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

注意PLUGINS 字典已经包含对注册为插件的各个函数对象的引用

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

Python在你定义函数时应用装饰器,所以 say_hello() 和be_awesome() 被立刻注册,你可以用PLUGINS 来调用这些函数:

>>> import random

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

>>> randomly_greet("Alice")
Using 'say_hello'
'Hello Alice'

The randomly_greet() function randomly chooses one of the registered functions to use. In the f-string, you use the [!r flag](Python's F-String for String Interpolation and Formatting – Real Python). This has the same effect as calling repr(greeter).

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

如果你熟悉 Python 中的 globals(),那么你可能会发现插件架构的工作方式有一些相似之处。使用 globals(),你可以访问当前范围内的所有全局变量,包括你的插件:

>>> globals()
{..., # Many variables that aren't 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()中手动挑选一些函数。

Authenticating Users

在讲更高级的装饰前的最后一个例子是在使用网络框架(web framework)时常用的装饰器。在这个例子中,你将用 Flask 来设置一个 /secret 网页,只有登录或通过其他方式验证的用户才能看到该网页:

import functools
from flask import Flask, g, request, redirect, url_for

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,你可以改用 the Flask-Login extension ,这增加了更多的安全性和功能。

Fancy Decorators 高级装饰器

至此,你已经了解了如何创建简单的装饰器。 You already have a pretty good understanding of what decorators are and how they work. Feel free to take a break from this tutorial to practice everything that you’ve learned. (这篇文真**长啊 🙉)

在这个教程的第二部分,你将探索更多高级功能,包括:

  • 向类添加装饰器
  • 向一个函数添加多个装饰器
  • 创建带有参数的装饰器
  • 创建可选择性获取参数的装饰器
  • 定义有状态的装饰器
  • 定义可充当装饰器的类

Decorating Classes 装饰类

在类上使用装饰器有两种不同的方式。第一种非常接近于你已经用函数完成的工作:你可以装饰一个类的方法。这是当年引入装饰器的动机之一。

一些常用的装饰器内置于python,包括[@classmethod@staticmethod](Python's Instance, Class, and Static Methods Demystified – Real Python), 和 [@property](<https://realpython.com/python-property/>) 。 @classmethod 和 @staticmethod 用于定义类命名空间中未连接到该类的特定实例的方法。@property装饰器用于自定义类属性的 getter 和 setter。展开下面的框,查看使用这些装饰器的示例:

  • Example using built-in class decorators

    The following definition of a Circle class uses the @classmethod@staticmethod, and @property decorators:

    class Circle:
        def __init__(self, radius):
            self.radius = radius
    
        @property
        def radius(self):
            """Get value of radius"""
            return self._radius
    
        @radius.setter
        def radius(self, value):
            """Set radius, raise error if negative"""
            if value >= 0:
                self._radius = value
            else:
                raise ValueError("radius must be non-negative")
    
        @property
        def area(self):
            """Calculate area inside circle"""
            return self.pi() * self.radius**2
    
        def cylinder_volume(self, height):
            """Calculate volume of cylinder with circle as base"""
            return self.area * height
    
        @classmethod
        def unit_circle(cls):
            """Factory method creating a circle with radius 1"""
            return cls(1)
    
        @staticmethod
        def pi():
            """Value of π, could use math.pi instead though"""
            return 3.1415926535
    

    Inside Circle you can see several different kinds of methods. Decorators are used to distinguish them:

    • .cylinder_volume() is a regular method.
    • .radius is a mutable property. It can be set to a different value. However, by defining a setter method, you do some error testing to make sure .radius isn’t set to a nonsensical negative number. Properties are accessed as attributes without parentheses.
    • .area is an immutable property. Properties without .setter() methods can’t be changed. Even though it’s defined as a method, it can be retrieved as an attribute without parentheses.
    • .unit_circle() is a class method. It’s not bound to one particular instance of Circle. Class methods are often used as factory methods that can create specific instances of the class.
    • .pi() is a static method. It’s not really dependent on the Circle class, except that it’s part of its namespace. You can call static methods on either an instance or the class.

    You can use Circle as follows:

    Python

    >>> from circle import Circle
    
    >>> c = Circle(5)
    >>> c.radius
    5
    
    >>> c.area
    78.5398163375
    
    >>> c.radius = 2
    >>> c.area
    12.566370614
    
    >>> c.area = 100
    Traceback (most recent call last):
        ...
    AttributeError: 'can't set attribute
    
    >>> c.cylinder_volume(height=4)
    50.265482456
    
    >>> c.radius = -1
    Traceback (most recent call last):
        ...
    ValueError: radius must be non-negative
    
    >>> c = Circle.unit_circle()
    >>> c.radius
    1
    
    >>> c.pi()
    3.1415926535
    
    >>> Circle.pi()
    3.1415926535
    

    In these examples, you explore the different methods, attributes, and properties of Circle.

接下来,定义一个类,使用之前写的@debug和@timer装饰器来装饰类中的方法:

from decorators import debug, timer

class TimeWaster:
    @debug
    def __init__(self, max_num):
        self.max_num = max_num

    @timer
    def waste_time(self, num_times):
        for _ in range(num_times):
            sum([number**2 for number in range(self.max_num)])

使用这个类,你可以看到装饰器的作用:

>>> from class_decorators import TimeWaster

>>> tw = TimeWaster(1000)
Calling __init__(<time_waster.TimeWaster object at 0x7efccce03908>, 1000)
__init__() returned None

>>> tw.waste_time(999)
Finished waste_time() in 0.3376 secs

另一种在类上使用装饰器的方式是装饰整个类。例如:

>>> from dataclasses import dataclass

>>> @dataclass
... class PlayingCard:
...     rank: str
...     suit: str
...

类装饰器的一个常见用途是作为元类(metaclasses)某些用例的更简单的替代方案。在这两种情况下,你都是动态更改类的定义。

写一个类装饰器与写一个函数装饰器非常相似,唯一的区别是装饰器将接受一个类而不是函数作为参数。事实上,以上所有装饰器都将作为类装饰器工作。当你在类上而不是函数上使用装饰器时,他们的作用或许和你想的不一样。在下一个例子中,装饰器@timer应用于类。

from decorators import timer

@timer
class TimeWaster:
    def __init__(self, max_num):
        self.max_num = max_num

    def waste_time(self, num_times):
        for _ in range(num_times):
            sum([i**2 for i in range(self.max_num)])

装饰一个类不会装饰它的方法,@timer只是TimeWaster = timer(TimeWaster)的缩写。在这里,@timer只计算实例化类所需的时间:

>>> from class_decorators import TimeWaster

>>> tw = TimeWaster(1000)
Finished TimeWaster() in 0.0000 secs

>>> tw.waste_time(999)

@timer的输出只在tw创建时显示,调用 .waste_time() 并不会计算时间。

接下来,你将看到一个定义一个 proper class decorator 的例子,名为 @singleton ,用于保证一个类只有一个实例。

Nesting Decorators 嵌套装饰器

你可以通过将多个装饰器堆叠在一起以一次将多个装饰器应用于函数:

>>> from decorators import debug, do_twice

>>> @debug
... @do_twice
... def greet(name):
...     print(f"Hello {name}")
...

想象一下装饰器按照它们被列出的顺序执行。换句话说,@debug 调用 @do_twice,然后调用 greet(),等价于 debug(do_twice(greet()))

>>> greet("Yadi")
Calling greet('Yadi')
Hello Yadi
Hello Yadi
greet() returned None

由于@do_twice,问候语被打印了两次。但是,@debug的输出仅显示一次,因为它是在@do_twice装饰器之前调用的。更改@debug@do_twice的顺序,观察差异,:

>>> from decorators import debug, do_twice

>>> @do_twice
... @debug
... def greet(name):
...     print(f"Hello {name}")
...

>>> greet("Yadi")
Calling greet('Yadi')
Hello Yadi
greet() returned None
Calling greet('Yadi')
Hello Yadi
greet() returned None

在这里,@do_twice 也被应用到 @debug 上了。你可以看到,greet() 的两次调用都带有调试信息。

Defining Decorators With Arguments 定义带有参数的装饰器

有的时候,向装饰器中传递函数很有用。例如, @do_twice 可以拓展为一个 @repeat(num_times) 装饰器,这样就可以通过参数指定要执行装饰的函数的次数。

定义@repeat

>>> from decorators import repeat

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

>>> greet("World")
Hello World
Hello World
Hello World
Hello World

想想你能怎么用@repeat

至此,@符号后面写的名称指的是一个可以用另一个函数调用的函数对象。为了保持一致性,你需要 repeat(num_times=4) 返回一个可以作为装饰器的函数对象。幸运的是,你已经知道如何返回函数!通常,你希望得到类似下面的结构:

def repeat(num_times):
    def decorator_repeat(func):
        ...  # Create and return a wrapper function
    return decorator_repeat

通常,装饰器会创建并返回一个内部包装函数,因此完整编写这个例子时,你会看到一个内部函数嵌套在另一个内部函数中。虽然这听起来可能像是编程界的“盗梦空间”,但你很快就会明白这一切:

import functools

# ...

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()函数中有一些微妙的事发生:

  • 定义 decorator_repeat() 作为内部函数表示repeat() 会引用函数对象decorator_repeat 。之前,你使用装饰器像@do_twice 一样不带括号。现在,你需要在设置装饰器时加上括号,如@repeat() ,为了加入参数这是必要的。
  • repeat() 中,num_times 参数似乎没有直接使用。但通过传递 num_times,创建了一个闭包,其中 num_times 的值被保存,直到稍后由 wrapper_repeat() 使用。

所有事都准备好后,测试你的代码验证结果是否符合预期:

>>> from decorators import repeat

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

>>> greet("World")
Hello World
Hello World
Hello World
Hello World

Creating Decorators With Optional Arguments 创建带有可选参数的装饰器

Tracking State in Decorators 在装饰器中跟踪状态

Using Classes as Decorators 将类用作装饰器

More Real-World Examples 更多的真实例子

Conclusion 结论

第一部分:

  • 装饰器可以重用
  • 装饰器可以装饰有参数的函数并返回值
  • 装饰器可以用 @functools.wraps 使装饰器看起来更像被装饰的函数

第二部分:

  • 装饰类
  • 嵌套装饰器
  • 为装饰器添加参数
  • 在装饰器中保持状态 Keep state within decorators
  • 使用类作为装饰器

  • 19
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
引用\[1\]:报错:Parsing error: This experimental syntax requires enabling one of the following parser plugin(s): "decorators-legacy", "decorators". 引用\[2\]:在学习mobx时,遇到了“This experimental syntax requires enabling one of the following parser plugin(s): "decorators", "decorators-legacy". (5:0)“的错误,每次在@observer那里报错。 引用\[3\]:但是发现还是不稳定,可以试试这个React的decorators装饰器报错@_程序燕的博客-CSDN博客_react 装饰器报错Parsing error: This experimental syntax requires enabling one of the following parser plugin(s): "decorators-legacy", "decorators".对修饰器的实验支持功能在将来的版本中可能更改。在 "tsconfig" 或 "jsconfig" 中https://blog.csdn.net/lfy_wybss/article/details/122079178?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-1-122079178-blog-111914160.pc_relevant_scanpaymentv1&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-1-122079178-blog-111914160.pc_relevant_scanpaymentv1&utm_relevant_index=1。 问题:Parsing error: This experimental syntax requires enabling one of the following parser plugin(s): "decorators", "decorators-legacy"是什么意思?如何解决这个错误? 回答: 当你遇到这个错误时,它意味着你的代码中使用了修饰器语法,但是你的解析器没有启用修饰器插件。修饰器是一种实验性的语法,需要通过启用相应的插件来支持。在你的代码中,你可以尝试启用"decorators"或"decorators-legacy"插件来解决这个错误。你可以在你的"tsconfig"或"jsconfig"文件中进行配置。具体的配置方法可以参考这篇博客文章:https://blog.csdn.net/lfy_wybss/article/details/122079178。 #### 引用[.reference_title] - *1* *3* [mobx装饰器报错](https://blog.csdn.net/qq_45679015/article/details/124803555)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [react之装饰器报错:This experimental syntax requires enabling one of the following parser plugin(s):...](https://blog.csdn.net/liuye066/article/details/127889894)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值