学习类装饰器

Beginner

This tutorial is from open-source community. Access the source code

引言

在本实验中,你将学习 Python 中的类装饰器 (class decorators),并回顾和扩展 Python 描述器 (descriptors)。通过结合这些概念,你可以创建强大且简洁的代码结构。

在本实验中,你将基于之前的描述器概念,并使用类装饰器进行扩展。这种组合使你能够创建更简洁、更易于维护的代码,并增强验证能力。需要修改的文件是 validate.pystructure.py

使用描述器实现类型检查

在本步骤中,我们将创建一个使用描述器进行类型检查的 Stock 类。但首先,让我们了解一下什么是描述器。描述器是 Python 中一个非常强大的特性。它们让你能够控制属性在类中的访问方式。

描述器是定义属性如何在其他对象上被访问的对象。它们通过实现诸如 __get____set____delete__ 等特殊方法来实现这一点。这些方法允许描述器管理属性的检索、设置和删除。描述器在实现验证、类型检查和计算属性方面非常有用。例如,你可以使用描述器来确保某个属性始终是正数或特定格式的字符串。

validate.py 文件已经包含了验证器类(StringPositiveIntegerPositiveFloat)。我们可以使用这些类来验证 Stock 类的属性。

现在,让我们创建带有描述器的 Stock 类。

  1. 首先,在你的编辑器中打开 stock.py 文件。

  2. 文件打开后,将占位符内容替换为以下代码:

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    _fields = ('name', 'shares', 'price')
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

## Create an __init__ method based on _fields
Stock.create_init()

让我们来分析一下这段代码的作用。_fields 元组定义了 Stock 类的属性。这些是我们 Stock 对象将拥有的属性名称。

namesharesprice 属性被定义为描述器对象。String() 描述器确保 name 属性是字符串类型。PositiveInteger() 描述器确保 shares 属性是正整数类型。而 PositiveFloat() 描述器则保证 price 属性是正浮点数类型。

cost 属性是一个计算属性。它根据股票数量和每股价格计算股票的总成本。

sell 方法用于减少股票数量。当你调用此方法并传入要卖出的股票数量时,它会从 shares 属性中减去该数量。

Stock.create_init() 行动态地为我们的类创建了一个 __init__ 方法。这个方法允许我们通过传入 namesharesprice 属性的值来创建 Stock 对象。

  1. 添加代码后,保存文件。这将确保你的更改已保存,并在运行测试时可以使用。

  2. 现在,让我们运行测试来验证你的实现。首先,通过运行以下命令将目录更改为 ~/project 目录:

cd ~/project

然后,使用以下命令运行测试:

python3 teststock.py

如果你的实现是正确的,你应该会看到类似以下的输出:

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

此输出意味着所有测试都已通过。描述器已成功验证了每个属性的类型!

让我们尝试在 Python 解释器中创建一个 Stock 对象。首先,确保你在 ~/project 目录中。然后,运行以下命令:

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

你应该会看到以下输出:

Stock('GOOG', 100, 490.1)
Cost: 49010.0

你已成功实现了用于类型检查的描述器!现在,让我们进一步改进这段代码。

创建用于验证的类装饰器

在上一个步骤中,我们的实现是有效的,但存在冗余。我们必须同时指定 _fields 元组和描述器属性。这效率不高,我们可以进行改进。在 Python 中,类装饰器是一个强大的工具,可以帮助我们简化这个过程。类装饰器是一个函数,它接收一个类作为参数,对其进行某种修改,然后返回修改后的类。通过使用类装饰器,我们可以自动从描述器中提取字段信息,这将使我们的代码更简洁、更易于维护。

让我们创建一个类装饰器来简化我们的代码。以下是你需要遵循的步骤:

  1. 首先,在你的编辑器中打开 structure.py 文件。

  2. 接下来,在 structure.py 文件的顶部,紧随任何导入语句之后,添加以下代码。这段代码定义了我们的类装饰器:

from validate import Validator

def validate_attributes(cls):
    """
    Class decorator that extracts Validator instances
    and builds the _fields list automatically
    """
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## Set _fields based on validator names
    cls._fields = [val.name for val in validators]

    ## Create initialization method
    cls.create_init()

    return cls

让我们来分析一下这个装饰器做了什么:

  • 它首先创建一个名为 validators 的空列表。然后,它使用 vars(cls).items() 遍历类的所有属性。如果某个属性是 Validator 类的实例,它就会将该属性添加到 validators 列表中。
  • 之后,它设置类的 _fields 属性。它从 validators 列表中的验证器创建名称列表,并将其赋值给 cls._fields
  • 最后,它调用类的 create_init() 方法来生成 __init__ 方法,然后返回修改后的类。
  1. 添加代码后,保存 structure.py 文件。保存文件可确保你的更改得以保留。

  2. 现在,我们需要修改 stock.py 文件以使用这个新的装饰器。在你的编辑器中打开 stock.py 文件。

  3. 更新 stock.py 文件以使用 validate_attributes 装饰器。将现有代码替换为以下内容:

## stock.py

from structure import Structure, validate_attributes
from validate import String, PositiveInteger, PositiveFloat

@validate_attributes
class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

请注意我们所做的更改:

  • 我们在 Stock 类定义上方添加了 @validate_attributes 装饰器。这告诉 Python 将 validate_attributes 装饰器应用于 Stock 类。
  • 我们删除了显式的 _fields 声明,因为装饰器将自动处理它。
  • 我们还删除了对 Stock.create_init() 的调用,因为装饰器负责创建 __init__ 方法。

因此,该类现在更简单、更简洁了。装饰器处理了我们以前手动处理的所有细节。

  1. 进行这些更改后,我们需要验证一切是否仍按预期工作。使用以下命令再次运行测试:
cd ~/project
python3 teststock.py

如果一切正常,你应该会看到以下输出:

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

此输出表明所有测试都已成功通过。

让我们也在交互式环境中测试我们的 Stock 类。在终端中运行以下命令:

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

你应该会看到以下输出:

Stock('GOOG', 100, 490.1)
Cost: 49010.0

太棒了!你已成功实现了一个类装饰器,该装饰器通过自动处理字段声明和初始化来简化我们的代码。这使得我们的代码更高效、更易于维护。

通过继承应用装饰器

在步骤 2 中,我们创建了一个类装饰器来简化我们的代码。类装饰器是一种特殊的函数,它接收一个类作为参数并返回一个修改后的类。它是 Python 中一个有用的工具,可以在不修改类原始代码的情况下为其添加功能。但是,我们仍然需要为每个类显式地应用 @validate_attributes 装饰器。这意味着每次我们创建一个需要验证的新类时,都必须记住添加这个装饰器,这可能有点麻烦。

我们可以通过继承自动应用装饰器来进一步改进这一点。继承是面向对象编程中的一个基本概念,子类可以继承父类的属性和方法。Python 的 __init_subclass__ 方法在 Python 3.6 中引入,允许父类自定义子类的初始化。这意味着当子类被创建时,父类可以对其执行一些操作。我们可以利用这个特性自动将我们的装饰器应用于任何继承自 Structure 的类。

让我们来实现这一点:

  1. 在你的编辑器中打开 structure.py 文件。此文件包含 Structure 类的定义,我们将修改它以使用 __init_subclass__ 方法。

  2. __init_subclass__ 方法添加到 Structure 类:

class Structure:
    _fields = ()
    _types = ()

    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, name, val)

    def __repr__(self):
        values = ', '.join(repr(getattr(self, name)) for name in self._fields)
        return f'{type(self).__name__}({values})'

    @classmethod
    def create_init(cls):
        '''
        Create an __init__ method from _fields
        '''
        body = 'def __init__(self, %s):\n' % ', '.join(cls._fields)
        for name in cls._fields:
            body += f'    self.{name} = {name}\n'

        ## Execute the function creation code
        namespace = {}
        exec(body, namespace)
        setattr(cls, '__init__', namespace['__init__'])

    @classmethod
    def __init_subclass__(cls):
        validate_attributes(cls)

__init_subclass__ 方法是一个类方法,这意味着它可以被类本身调用,而不是类的实例。当创建 Structure 的子类时,这个方法将被自动调用。在此方法中,我们对子类 cls 调用 validate_attributes 装饰器。这样,Structure 的每个子类都将自动获得验证行为。

  1. 保存文件。

在修改 structure.py 文件后,我们需要保存它,以便应用更改。

  1. 现在,让我们更新 stock.py 文件以利用这个新特性。在你的编辑器中打开 stock.py 文件进行修改。此文件包含 Stock 类的定义,我们将使其继承自 Structure 类以使用自动装饰器应用。

  2. 修改 stock.py 文件以移除显式装饰器:

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

请注意我们所做的:

  • 我们移除了 validate_attributes 的导入,因为我们不再需要显式导入它,装饰器将通过继承自动应用。
  • 我们移除了 @validate_attributes 装饰器,因为 Structure 类中的 __init_subclass__ 方法将负责应用它。
  • 代码现在仅依赖于从 Structure 继承来获得验证行为。
  1. 再次运行测试以验证一切是否仍然正常工作:
cd ~/project
python3 teststock.py

运行测试很重要,以确保我们的更改没有破坏任何东西。如果所有测试都通过,则意味着通过继承自动应用装饰器正在正常工作。

你应该会看到所有测试都通过:

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

让我们再次测试我们的 Stock 类,以确保它按预期工作:

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

此命令创建一个 Stock 类的实例并打印其表示形式和成本。如果输出符合预期,则表示 Stock 类通过自动装饰器应用正常工作。

输出:

Stock('GOOG', 100, 490.1)
Cost: 49010.0

这个实现更简洁了!通过使用 __init_subclass__,我们消除了显式应用装饰器的需要。任何继承自 Structure 的类都会自动获得验证行为。

添加行转换功能

在编程中,从数据行创建类的实例通常很有用,尤其是在处理来自 CSV 文件等来源的数据时。在本节中,我们将添加从数据行创建 Structure 类实例的功能。我们将通过在 Structure 类中实现 from_row 类方法来做到这一点。

  1. 首先,在你的编辑器中打开 structure.py 文件。这是我们将进行代码更改的地方。

  2. 接下来,我们将修改 validate_attributes 函数。此函数是一个类装饰器,可自动提取 Validator 实例并构建 _fields_types 列表。我们将更新它以收集类型信息。

def validate_attributes(cls):
    """
    Class decorator that extracts Validator instances
    and builds the _fields and _types lists automatically
    """
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## Set _fields based on validator names
    cls._fields = [val.name for val in validators]

    ## Set _types based on validator expected_types
    cls._types = [getattr(val, 'expected_type', lambda x: x) for val in validators]

    ## Create initialization method
    cls.create_init()

    return cls

在此更新的函数中,我们正在从每个验证器收集 expected_type 属性,并将其存储在 _types 类变量中。这将在稍后将数据从行转换为正确类型时非常有用。

  1. 现在,我们将 from_row 类方法添加到 Structure 类。此方法允许我们从数据行(可以是列表或元组)创建类的实例。
@classmethod
def from_row(cls, row):
    """
    Create an instance from a data row (list or tuple)
    """
    rowdata = [func(val) for func, val in zip(cls._types, row)]
    return cls(*rowdata)

此方法的工作原理如下:

  • 它接收一行数据,该数据可以是列表或元组的形式。
  • 它使用 _types 列表中的相应函数将行中的每个值转换为预期的类型。
  • 然后,它使用转换后的值创建并返回该类的新实例。
  1. 进行这些更改后,保存 structure.py 文件。这确保了你的代码更改得以保留。

  2. 让我们测试我们的 from_row 方法,以确保它按预期工作。我们将使用 Stock 类创建一个简单的测试。在终端中运行以下命令:

cd ~/project
python3 -c "from stock import Stock; s = Stock.from_row(['GOOG', '100', '490.1']); print(s); print(f'Cost: {s.cost}')"

你应该会看到类似以下的输出:

Stock('GOOG', 100, 490.1)
Cost: 49010.0

请注意,字符串值 '100' 和 '490.1' 已自动转换为正确的类型(整数和浮点数)。这表明我们的 from_row 方法正在正常工作。

  1. 最后,让我们尝试使用我们的 reader.py 模块从 CSV 文件读取数据。在终端中运行以下命令:
cd ~/project
python3 -c "from stock import Stock; import reader; portfolio = reader.read_csv_as_instances('portfolio.csv', Stock); print(portfolio); print(f'Total value: {sum(s.cost for s in portfolio)}')"

你应该会看到显示 CSV 文件中股票的输出:

[Stock('GOOG', 100, 490.1), Stock('AAPL', 50, 545.75), Stock('MSFT', 200, 30.47)]
Total value: 82391.5

from_row 方法使我们能够轻松地将 CSV 数据转换为 Stock 类的实例。当与 read_csv_as_instances 函数结合使用时,我们就有了一种强大的方法来加载和处理结构化数据。

添加方法参数验证

在 Python 中,验证数据是编写健壮代码的重要组成部分。在本节中,我们将通过自动验证方法参数来进一步提升我们的验证能力。validate.py 文件已经包含了一个 @validated 装饰器。Python 中的装饰器是一种可以修改另一个函数的特殊函数。这里的 @validated 装饰器可以根据其注解检查函数参数。Python 中的注解是一种为函数参数和返回值添加元数据的方式。

让我们修改代码以将此装饰器应用于带有注解的方法:

  1. 首先,我们需要了解 validated 装饰器的工作原理。在你的编辑器中打开 validate.py 文件进行查看。

validated 装饰器使用函数注解来验证参数。在允许函数运行之前,它会为每个注解的参数创建一个验证器类的实例,并调用 validate 方法来检查参数。例如,如果一个参数被注解为 PositiveInteger,装饰器将创建一个 PositiveInteger 实例并验证传入的值确实是一个正整数。如果验证失败,它会收集所有错误并引发一个带有详细错误消息的 TypeError

  1. 现在,我们将修改 structure.py 中的 validate_attributes 函数,以使用 validated 装饰器包装带有注解的方法。这意味着类中任何带有注解的方法都将自动验证其参数。在你的编辑器中打开 structure.py 文件。

  2. 更新 validate_attributes 函数:

def validate_attributes(cls):
    """
    Class decorator that:
    1. Extracts Validator instances and builds _fields and _types lists
    2. Applies @validated decorator to methods with annotations
    """
    ## Import the validated decorator
    from validate import validated

    ## Process validator descriptors
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## Set _fields based on validator names
    cls._fields = [val.name for val in validators]

    ## Set _types based on validator expected_types
    cls._types = [getattr(val, 'expected_type', lambda x: x) for val in validators]

    ## Apply @validated decorator to methods with annotations
    for name, val in vars(cls).items():
        if callable(val) and hasattr(val, '__annotations__'):
            setattr(cls, name, validated(val))

    ## Create initialization method
    cls.create_init()

    return cls

这个更新后的函数现在执行以下操作:

  1. 它像以前一样处理验证器描述符。验证器描述符用于为类属性定义验证规则。

  2. 它在类中查找所有带有注解的方法。注解被添加到方法参数中,以指定参数的预期类型。

  3. 它将 @validated 装饰器应用于这些方法。这确保了传递给这些方法的参数会根据其注解进行验证。

  4. 进行这些更改后保存文件。保存文件很重要,因为它确保了我们的修改被存储并且以后可以使用。

  5. 现在,让我们更新 Stock 类中的 sell 方法以包含注解。注解有助于指定参数的预期类型,这将由 @validated 装饰器用于验证。在你的编辑器中打开 stock.py 文件。

  6. 修改 sell 方法以包含类型注解:

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares: PositiveInteger):
        self.shares -= nshares

重要的更改是将 : PositiveInteger 添加到 nshares 参数。这告诉 Python(以及我们的 @validated 装饰器)使用 PositiveInteger 验证器来验证此参数。因此,当我们调用 sell 方法时,nshares 参数必须是一个正整数。

  1. 再次运行测试以验证一切是否仍然正常工作。运行测试是确保我们的更改没有破坏任何现有功能的好方法。
cd ~/project
python3 teststock.py

你应该会看到所有测试都通过:

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK
  1. 让我们测试我们新的参数验证。我们将尝试使用有效和无效参数调用 sell 方法,以查看验证是否按预期工作。
cd ~/project
python3 -c "
from stock import Stock
s = Stock('GOOG', 100, 490.1)
s.sell(25)
print(s)
try:
    s.sell(-25)
except Exception as e:
    print(f'Error: {e}')
"

你应该会看到类似以下的输出:

Stock('GOOG', 75, 490.1)
Error: Bad Arguments
  nshares: nshares must be >= 0

这表明我们的方法参数验证正在起作用!第一次调用 sell(25) 成功,因为 25 是一个正整数。但第二次调用 sell(-25) 失败,因为 -25 不是一个正整数。

你现在已经实现了一个完整的系统,用于:

  1. 使用描述符验证类属性。描述符用于为类属性定义验证规则。
  2. 使用类装饰器自动收集字段信息。类装饰器可以修改类的行为,例如收集字段信息。
  3. 将行数据转换为实例。这在处理来自外部源的数据时很有用。
  4. 使用注解验证方法参数。注解有助于为验证指定参数的预期类型。

这展示了在 Python 中结合使用描述符和装饰器来创建富有表现力、自我验证的类的强大功能。

总结

在本实验中,你学习了如何结合强大的 Python 特性来创建简洁、自验证的代码。你掌握了关键概念,例如使用描述符进行属性验证、创建类装饰器进行代码生成自动化,以及通过继承自动应用装饰器。

这些技术是创建健壮且可维护的 Python 代码的强大工具。它们使你能够清晰地表达验证需求并在整个代码库中强制执行它们。你现在可以在自己的 Python 项目中应用这些模式,以提高代码质量并减少样板代码。