简介
在这个实验中,你将学习 Python 中的装饰器,这是一个强大的特性,可以修改函数和方法的行为。装饰器通常用于日志记录、性能测量、访问控制和类型检查等任务。
你将学习如何链式调用多个装饰器、创建接受参数的装饰器、在使用装饰器时保留函数元数据,以及将装饰器应用于不同类型的类方法。你将使用的文件有 logcall.py
、validate.py
和 sample.py
。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
在这个实验中,你将学习 Python 中的装饰器,这是一个强大的特性,可以修改函数和方法的行为。装饰器通常用于日志记录、性能测量、访问控制和类型检查等任务。
你将学习如何链式调用多个装饰器、创建接受参数的装饰器、在使用装饰器时保留函数元数据,以及将装饰器应用于不同类型的类方法。你将使用的文件有 logcall.py
、validate.py
和 sample.py
。
在 Python 中,装饰器是一个强大的工具,可用于修改函数的行为。然而,当你使用装饰器包装函数时,会出现一个小问题。默认情况下,原始函数的元数据(如函数名、文档字符串(docstring)和注解)会丢失。元数据很重要,因为它有助于内省(检查代码结构)和生成文档。让我们先来验证这个问题。
在 WebIDE 中打开你的终端。我们将运行一些 Python 命令,看看使用装饰器时会发生什么。以下命令将创建一个用装饰器包装的简单函数 add
,然后打印该函数及其文档字符串。
cd ~/project
python3 -c "from logcall import logged; @logged
def add(x,y):
'Adds two things'
return x+y
print(add)
print(add.__doc__)"
当你运行这些命令时,你会看到类似如下的输出:
<function wrapper at 0x...>
None
注意,函数名显示为 wrapper
,而不是 add
。并且文档字符串本应是 'Adds two things'
,现在却为 None
。当你使用依赖这些元数据的工具(如内省工具或文档生成器)时,这可能会成为一个大问题。
functools.wraps
解决问题Python 的 functools
模块可以解决这个问题。它提供了一个 wraps
装饰器,可帮助我们保留函数元数据。让我们看看如何修改 logged
装饰器以使用 wraps
。
logcall.py
文件。你可以在终端中使用以下命令导航到项目目录:cd ~/project
logcall.py
中的 logged
装饰器。@wraps(func)
装饰器是关键所在。它将原始函数 func
的所有元数据复制到包装函数中。from functools import wraps
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@wraps(func)
装饰器起着重要作用。它从原始函数 func
中获取所有元数据(如名称、文档字符串和注解),并将其附加到 wrapper
函数上。这样,当我们使用被装饰的函数时,它将拥有正确的元数据。
让我们测试改进后的装饰器。在终端中运行以下命令:
python3 -c "from logcall import logged; @logged
def add(x,y):
'Adds two things'
return x+y
print(add)
print(add.__doc__)"
现在你应该会看到:
<function add at 0x...>
Adds two things
太棒了!函数名和文档字符串都被保留下来了。这意味着我们的装饰器现在按预期工作,原始函数的元数据完好无损。
validate.py
中的装饰器现在,让我们对 validate.py
中的 validated
装饰器应用同样的修复。这个装饰器用于根据函数的注解验证函数参数和返回值的类型。
在 WebIDE 中打开 validate.py
文件。
用 @wraps
装饰器更新 validated
装饰器。以下代码展示了如何操作。在 validated
装饰器内部的 wrapper
函数上添加 @wraps(func)
装饰器,以保留元数据。
from functools import wraps
class Integer:
@classmethod
def __instancecheck__(cls, x):
return isinstance(x, int)
def validated(func):
@wraps(func)
def wrapper(*args, **kwargs):
## Get function annotations
annotations = func.__annotations__
## Check arguments against annotations
for arg_name, arg_value in zip(func.__code__.co_varnames, args):
if arg_name in annotations and not isinstance(arg_value, annotations[arg_name]):
raise TypeError(f'Expected {arg_name} to be {annotations[arg_name].__name__}')
## Run the function and get the result
result = func(*args, **kwargs)
## Check the return value
if 'return' in annotations and not isinstance(result, annotations['return']):
raise TypeError(f'Expected return value to be {annotations["return"].__name__}')
return result
return wrapper
validated
装饰器现在是否保留了元数据。在终端中运行以下命令:python3 -c "from validate import validated, Integer; @validated
def multiply(x: Integer, y: Integer) -> Integer:
'Multiplies two integers'
return x * y
print(multiply)
print(multiply.__doc__)"
你应该会看到:
<function multiply at 0......>
Multiplies two integers
现在,logged
和 validated
这两个装饰器都能正确保留它们所装饰函数的元数据。这确保了你在使用这些装饰器时,函数仍然会保留其原始名称、文档字符串和注解,这对代码的可读性和可维护性非常有用。
到目前为止,我们一直在使用 @logged
装饰器,它总是打印一条固定的消息。但如果你想自定义消息格式呢?在本节中,我们将学习如何创建一个可以接受参数的新装饰器,让你在使用装饰器时拥有更多的灵活性。
带参数的装饰器是一种特殊类型的函数。它并不直接修改另一个函数,而是返回一个装饰器。带参数的装饰器的一般结构如下:
def decorator_with_args(arg1, arg2, ...):
def actual_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
## Use arg1, arg2, ... here
## Call the original function
return func(*args, **kwargs)
return wrapper
return actual_decorator
当你在代码中使用 @decorator_with_args(value1, value2)
时,Python 首先调用 decorator_with_args(value1, value2)
。这个调用会返回实际的装饰器,然后该装饰器会应用到 @
语法后面的函数上。这个两步过程是带参数的装饰器工作的关键。
logformat
装饰器让我们创建一个 @logformat(fmt)
装饰器,它接受一个格式字符串作为参数。这将使我们能够自定义日志消息。
logcall.py
文件,并添加新的装饰器。下面的代码展示了如何定义现有的 logged
装饰器和新的 logformat
装饰器:from functools import wraps
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
def logformat(fmt):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(fmt.format(func=func))
return func(*args, **kwargs)
return wrapper
return decorator
在 logformat
装饰器中,外部函数 logformat
接受一个格式字符串 fmt
作为参数。然后它返回 decorator
函数,这是实际修改目标函数的装饰器。
sample.py
文件来测试我们的新装饰器。以下代码展示了如何在不同的函数上使用 logged
和 logformat
装饰器:from logcall import logged, logformat
@logged
def add(x, y):
"Adds two numbers"
return x + y
@logged
def sub(x, y):
"Subtracts y from x"
return x - y
@logformat('{func.__code__.co_filename}:{func.__name__}')
def mul(x, y):
"Multiplies two numbers"
return x * y
在这里,add
和 sub
函数使用了 logged
装饰器,而 mul
函数使用了带有自定义格式字符串的 logformat
装饰器。
sample.py
文件以查看结果。打开终端并运行以下命令:cd ~/project
python3 -c "import sample; print(sample.add(2, 3)); print(sample.mul(2, 3))"
你应该会看到类似如下的输出:
Calling add
5
sample.py:mul
6
这个输出表明,logged
装饰器按预期打印了函数名,而 logformat
装饰器使用自定义格式字符串打印了文件名和函数名。
logformat
重新定义 logged
装饰器既然我们有了一个更灵活的 logformat
装饰器,我们可以使用它来重新定义我们原来的 logged
装饰器。这将帮助我们复用代码并保持一致的日志格式。
logcall.py
文件:from functools import wraps
def logformat(fmt):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(fmt.format(func=func))
return func(*args, **kwargs)
return wrapper
return decorator
## Define logged using logformat
logged = lambda func: logformat("Calling {func.__name__}")(func)
在这里,我们使用一个 lambda 函数根据 logformat
装饰器来定义 logged
装饰器。这个 lambda 函数接受一个函数 func
,并使用特定的格式字符串应用 logformat
装饰器。
logged
装饰器是否仍然有效。打开终端并运行以下命令:cd ~/project
python3 -c "from logcall import logged; @logged
def greet(name):
return f'Hello, {name}'
print(greet('World'))"
你应该会看到:
Calling greet
Hello, World
这表明重新定义的 logged
装饰器按预期工作,并且我们成功复用了 logformat
装饰器来实现一致的日志格式。
现在,我们将探讨装饰器如何与类方法进行交互。这可能有点棘手,因为 Python 有不同类型的方法:实例方法、类方法、静态方法和属性方法。装饰器是一种函数,它接受另一个函数并扩展该函数的行为,而无需显式修改它。当将装饰器应用于类方法时,我们需要注意它们如何与这些不同类型的方法协同工作。
让我们看看将 @logged
装饰器应用于不同类型的方法时会发生什么。@logged
装饰器可能用于记录方法调用的信息。
methods.py
。这个文件将包含一个类,其中不同类型的方法都使用 @logged
装饰器进行装饰。from logcall import logged
class Spam:
@logged
def instance_method(self):
print("Instance method called")
return "instance result"
@logged
@classmethod
def class_method(cls):
print("Class method called")
return "class result"
@logged
@staticmethod
def static_method():
print("Static method called")
return "static result"
@logged
@property
def property_method(self):
print("Property method called")
return "property result"
在这段代码中,我们有一个名为 Spam
的类,它包含四种不同类型的方法。每个方法都使用 @logged
装饰器进行装饰,有些方法还使用了其他内置装饰器,如 @classmethod
、@staticmethod
和 @property
。
cd ~/project
python3 -c "from methods import Spam; s = Spam(); print(s.instance_method()); print(Spam.class_method()); print(Spam.static_method()); print(s.property_method)"
当你运行这个命令时,你可能会注意到一些问题:
@property
装饰器与 @logged
装饰器可能无法正确协同工作。@property
装饰器用于将方法定义为属性,它有特定的工作方式。当与 @logged
装饰器结合使用时,可能会产生冲突。@classmethod
和 @staticmethod
,装饰器的顺序很重要。装饰器的应用顺序会改变方法的行为。当你应用多个装饰器时,它们是从下到上依次应用的。这意味着离方法定义最近的装饰器最先应用,然后上面的装饰器按顺序依次应用。例如:
@decorator1
@decorator2
def func():
pass
这相当于:
func = decorator1(decorator2(func))
在这个例子中,decorator2
首先应用于 func
,然后 decorator1
应用于 decorator2(func)
的结果。
让我们更新 methods.py
文件来修正装饰器的顺序。通过改变装饰器的顺序,我们可以确保每个方法都能按预期工作。
from logcall import logged
class Spam:
@logged
def instance_method(self):
print("Instance method called")
return "instance result"
@classmethod
@logged
def class_method(cls):
print("Class method called")
return "class result"
@staticmethod
@logged
def static_method():
print("Static method called")
return "static result"
@property
@logged
def property_method(self):
print("Property method called")
return "property result"
在这个更新后的版本中:
instance_method
,顺序无关紧要。实例方法是在类的实例上调用的,@logged
装饰器可以以任何顺序应用,而不会影响其基本功能。class_method
,我们在 @logged
之后应用 @classmethod
。@classmethod
装饰器会改变方法的调用方式,在 @logged
之后应用它可以确保日志记录正常工作。static_method
,我们在 @logged
之后应用 @staticmethod
。与 @classmethod
类似,@staticmethod
装饰器有其自身的行为,与 @logged
装饰器的顺序需要正确。property_method
,我们在 @logged
之后应用 @property
。这可以确保在获得日志记录功能的同时,保持属性的行为。cd ~/project
python3 -c "from methods import Spam; s = Spam(); print(s.instance_method()); print(Spam.class_method()); print(Spam.static_method()); print(s.property_method)"
现在你应该会看到所有方法类型都有正确的日志记录:
Calling instance_method
Instance method called
instance result
Calling class_method
Class method called
class result
Calling static_method
Static method called
static result
Calling property_method
Property method called
property result
在使用方法装饰器时,请遵循以下最佳实践:
@classmethod
、@staticmethod
、@property
)。这可以确保自定义装饰器首先执行其日志记录或其他操作,然后内置装饰器可以按预期转换方法。在前面的步骤中,我们学习了 @validated
装饰器。这个装饰器用于在 Python 函数中强制实施类型注解。类型注解是一种指定函数参数和返回值预期类型的方式。现在,我们要更进一步。我们将创建一个更灵活的装饰器,它可以接受类型规范作为参数。这意味着我们可以更明确地为每个参数和返回值定义所需的类型。
我们的目标是创建一个 @enforce()
装饰器。这个装饰器允许我们使用关键字参数指定类型约束。以下是它的工作示例:
@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
return x + y
在这个例子中,我们使用 @enforce
装饰器指定 add
函数的 x
和 y
参数应该是 Integer
类型,返回值也应该是 Integer
类型。这个装饰器的行为与我们之前的 @validated
装饰器类似,但它让我们对类型规范有更多的控制权。
enforce
装饰器validate.py
文件。我们将把新的装饰器添加到这个文件中。以下是我们要添加的代码:from functools import wraps
class Integer:
@classmethod
def __instancecheck__(cls, x):
return isinstance(x, int)
def validated(func):
@wraps(func)
def wrapper(*args, **kwargs):
## Get function annotations
annotations = func.__annotations__
## Check arguments against annotations
for arg_name, arg_value in zip(func.__code__.co_varnames, args):
if arg_name in annotations and not isinstance(arg_value, annotations[arg_name]):
raise TypeError(f'Expected {arg_name} to be {annotations[arg_name].__name__}')
## Run the function and get the result
result = func(*args, **kwargs)
## Check the return value
if 'return' in annotations and not isinstance(result, annotations['return']):
raise TypeError(f'Expected return value to be {annotations["return"].__name__}')
return result
return wrapper
def enforce(**type_specs):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
## Check argument types
for arg_name, arg_value in zip(func.__code__.co_varnames, args):
if arg_name in type_specs and not isinstance(arg_value, type_specs[arg_name]):
raise TypeError(f'Expected {arg_name} to be {type_specs[arg_name].__name__}')
## Run the function and get the result
result = func(*args, **kwargs)
## Check the return value
if 'return_' in type_specs and not isinstance(result, type_specs['return_']):
raise TypeError(f'Expected return value to be {type_specs["return_"].__name__}')
return result
return wrapper
return decorator
让我们来分析一下这段代码的作用。Integer
类用于定义一个自定义类型。validated
装饰器根据函数的类型注解检查函数参数和返回值的类型。enforce
装饰器是我们正在创建的新装饰器。它接受关键字参数,这些参数指定了每个参数和返回值的类型。在 enforce
装饰器的 wrapper
函数内部,我们检查参数和返回值的类型是否与指定的类型匹配。如果不匹配,我们就抛出一个 TypeError
异常。
@enforce
装饰器。我们将运行一些测试用例,看看它是否按预期工作。以下是运行测试的代码:cd ~/project
python3 -c "from validate import enforce, Integer
@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
return x + y
## This should work
print(add(2, 3))
## This should raise a TypeError
try:
print(add('2', 3))
except TypeError as e:
print(f'Error: {e}')
## This should raise a TypeError
try:
@enforce(x=Integer, y=Integer, return_=Integer)
def bad_add(x, y):
return str(x + y)
print(bad_add(2, 3))
except TypeError as e:
print(f'Error: {e}')"
在这段测试代码中,我们首先使用 @enforce
装饰器定义了一个 add
函数。然后,我们用有效的参数调用 add
函数,这应该能正常工作而不会出错。接下来,我们用一个无效的参数调用 add
函数,这应该会抛出一个 TypeError
异常。最后,我们定义了一个 bad_add
函数,它返回一个错误类型的值,这也应该抛出一个 TypeError
异常。
当你运行这段测试代码时,你应该会看到类似以下的输出:
5
Error: Expected x to be Integer
Error: Expected return value to be Integer
这个输出表明我们的 @enforce
装饰器工作正常。当参数或返回值的类型与指定的类型不匹配时,它会抛出一个 TypeError
异常。
@validated
和 @enforce
装饰器都实现了强制类型约束的目标,但它们的实现方式不同。
@validated
装饰器使用 Python 的内置类型注解。以下是一个示例:
@validated
def add(x: Integer, y: Integer) -> Integer:
return x + y
使用这种方法,我们在函数定义中直接使用类型注解指定类型。这是 Python 的内置特性,在集成开发环境(IDE)中提供了更好的支持。IDE 可以使用这些类型注解来提供代码补全、类型检查和其他有用的功能。
另一方面,@enforce
装饰器使用关键字参数来指定类型。以下是一个示例:
@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
return x + y
这种方法更明确,因为我们直接将类型规范作为参数传递给装饰器。当与依赖其他注解系统的库一起使用时,这种方法很有用。
每种方法都有其优点。类型注解是 Python 的原生部分,提供了更好的 IDE 支持,而 @enforce
方法则给我们更多的灵活性和明确性。你可以根据正在进行的项目选择最适合你需求的方法。
在这个实验中,你学习了如何有效地创建和使用装饰器。你学会了使用 functools.wraps
保留函数元数据,创建接受参数的装饰器,处理多个装饰器并理解它们的应用顺序。你还学会了将装饰器应用于不同的类方法,并创建一个接受参数的类型强制装饰器。
这些装饰器模式在 Flask、Django 和 pytest 等 Python 框架中经常使用。掌握装饰器将使你能够编写更易于维护和复用的代码。为了进一步学习,你可以探索上下文管理器、基于类的装饰器、使用装饰器进行缓存,以及使用装饰器进行高级类型检查。