元类实战

Beginner

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

简介

在这个实验中,你将了解元类(metaclasses),这是 Python 最强大、最高级的特性之一。元类使你能够自定义类的创建过程,让你可以控制类的定义和实例化方式。你将通过实际示例来探索元类。

本实验的目标是理解什么是元类以及它们的工作原理,实现一个元类来解决实际编程问题,并探索元类在 Python 中的实际应用。本实验中修改的文件是 structure.pyvalidate.py

理解问题

在开始探索元类之前,理解我们要解决的问题很重要。在编程中,我们经常需要为属性创建具有特定类型的结构。在之前的工作中,我们开发了一个用于类型检查结构的系统。该系统允许我们定义类,其中每个属性都有特定的类型,并且分配给这些属性的值会根据该类型进行验证。

以下是我们如何使用这个系统创建 Stock 类的示例:

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

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

在这段代码中,我们首先从 validate 模块导入验证器类型(StringPositiveIntegerPositiveFloat),并从 structure 模块导入 Structure 类。然后我们定义了 Stock 类,它继承自 Structure。在 Stock 类内部,我们定义了具有特定验证器类型的属性。例如,name 属性必须是字符串,shares 必须是正整数,price 必须是正浮点数。

然而,这种方法存在一个问题。我们需要在文件顶部导入所有的验证器类型。在实际场景中,随着我们添加越来越多的验证器类型,这些导入语句会变得很长且难以管理。这可能会导致我们使用 from validate import *,但这通常被认为是一种不良实践,因为它可能会导致命名冲突并使代码的可读性降低。

为了了解我们的起点,让我们看一下 Structure 类。你需要在编辑器中打开 structure.py 文件并查看其内容。这将帮助你了解在添加元类功能之前,基本的结构处理是如何实现的。

code structure.py

当你打开文件时,你会看到 Structure 类的基本实现。这个类负责处理属性初始化,但目前还没有任何元类功能。

接下来,让我们检查验证器类。这些类在 validate.py 文件中定义。它们已经具备描述符(descriptor)功能,这意味着它们可以控制属性的访问和设置方式。但我们需要对它们进行增强,以解决前面讨论的导入问题。

code validate.py

通过查看这些验证器类,你将更好地理解验证过程是如何工作的,以及我们需要做出哪些更改来改进代码。

收集验证器类型

在 Python 中,验证器(validators)是帮助我们确保数据符合特定标准的类。本实验的首要任务是修改基础的 Validator 类,使其能够收集所有的子类。为什么要这么做呢?通过收集所有的验证器子类,我们可以创建一个包含所有验证器类型的命名空间。之后,我们会将这个命名空间注入到 Structure 类中,这样就能更轻松地管理和使用不同的验证器。

现在,让我们开始编写代码。打开 validate.py 文件。你可以在终端中使用以下命令来打开它:

code validate.py

文件打开后,我们需要为 Validator 类添加一个类级别的字典和一个 __init_subclass__() 方法。类级别的字典将用于存储所有的验证器子类,而 __init_subclass__() 方法是 Python 中的一个特殊方法,每当定义当前类的子类时,它都会被调用。

Validator 类定义之后,添加以下代码:

## Add this to the Validator class in validate.py
validators = {}  ## Dictionary to collect all validator subclasses

@classmethod
def __init_subclass__(cls):
    """Register each validator subclass in the validators dictionary"""
    Validator.validators[cls.__name__] = cls

添加代码后,修改后的 Validator 类应该如下所示:

class Validator:
    validators = {}  ## Dictionary to collect all validator subclasses

    @classmethod
    def __init_subclass__(cls):
        """Register each validator subclass in the validators dictionary"""
        Validator.validators[cls.__name__] = cls

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        self.validate(value)
        instance.__dict__[self.name] = value

    def validate(self, value):
        pass

现在,每当定义一个新的验证器类型,如 StringPositiveInteger 时,Python 会自动调用 __init_subclass__() 方法。该方法会将新的验证器子类添加到 validators 字典中,使用类名作为键。

让我们测试一下代码是否正常工作。我们将创建一个简单的 Python 脚本,检查 validators 字典的内容。你可以在终端中运行以下命令:

python3 -c "from validate import Validator; print(Validator.validators)"

如果一切正常,你应该会看到类似以下的输出,显示所有的验证器类型及其对应的类:

{'Typed': <class 'validate.Typed'>, 'Positive': <class 'validate.Positive'>, 'NonEmpty': <class 'validate.NonEmpty'>, 'String': <class 'validate.String'>, 'Integer': <class 'validate.Integer'>, 'Float': <class 'validate.Float'>, 'PositiveInteger': <class 'validate.PositiveInteger'>, 'PositiveFloat': <class 'validate.PositiveFloat'>, 'NonEmptyString': <class 'validate.NonEmptyString'>}

现在我们有了一个包含所有验证器类型的字典,接下来就可以用它来创建元类了。

创建 StructureMeta 元类

现在,让我们来谈谈接下来要做的事情。我们已经找到了收集所有验证器类型的方法。下一步是创建一个元类。但元类究竟是什么呢?在 Python 中,元类是一种特殊的类。它的实例本身就是类。这意味着元类可以控制类的创建方式,还能管理定义类属性的命名空间。

在我们的场景中,我们希望创建一个元类,以便在定义 Structure 子类时可以直接使用验证器类型,而无需每次都显式导入这些验证器类型。

让我们再次打开 structure.py 文件。你可以使用以下命令打开它:

code structure.py

文件打开后,我们需要在 Structure 类定义之前的文件顶部添加一些代码,这些代码将用于定义我们的元类。

from validate import Validator
from collections import ChainMap

class StructureMeta(type):
    @classmethod
    def __prepare__(meta, clsname, bases):
        """Prepare the namespace for the class being defined"""
        return ChainMap({}, Validator.validators)

    @staticmethod
    def __new__(meta, name, bases, methods):
        """Create the new class using only the local namespace"""
        methods = methods.maps[0]  ## Extract the local namespace
        return super().__new__(meta, name, bases, methods)

现在我们已经定义了元类,需要修改 Structure 类以使用它。这样,任何继承自 Structure 的类都能受益于元类的功能。

class Structure(metaclass=StructureMeta):
    _fields = []

    def __init__(self, *args, **kwargs):
        if len(args) > len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')

        ## Set all of the positional arguments
        for name, val in zip(self._fields, args):
            setattr(self, name, val)

        ## Set the remaining keyword arguments
        for name, val in kwargs.items():
            if name not in self._fields:
                raise TypeError(f'Invalid argument: {name}')
            setattr(self, name, val)

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

让我们详细分析一下这段代码的作用:

  1. __prepare__() 方法是 Python 中的一个特殊方法,它在类创建之前被调用,其作用是准备定义类属性的命名空间。我们在这里使用了 ChainMap,它是一个很有用的工具,可以创建一个分层字典。在我们的例子中,它包含了我们的验证器类型,使得这些类型在类的命名空间中可以被访问。

  2. __new__() 方法负责创建新的类。我们只提取了局部命名空间,也就是 ChainMap 中的第一个字典。我们舍弃了验证器字典,因为我们已经让验证器类型在命名空间中可用了。

通过这样的设置,任何继承自 Structure 的类都可以访问所有的验证器类型,而无需显式导入它们。

现在,让我们测试一下我们的实现。我们将使用增强后的 Structure 基类创建一个 Stock 类。

cat > stock.py << EOF
from structure import Structure

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
EOF

如果我们的元类正常工作,我们应该能够在不导入验证器类型的情况下定义 Stock 类。这是因为元类已经让这些类型在命名空间中可用了。

测试我们的实现

现在我们已经实现了元类并修改了 Structure 类,是时候测试我们的实现了。测试至关重要,因为它能帮助我们确保一切正常运行。通过运行测试,我们可以尽早发现潜在问题,并确保代码按预期工作。

首先,让我们运行单元测试,看看 Stock 类是否按预期工作。单元测试是小型的、独立的测试,用于检查代码的各个部分。在这种情况下,我们要确保 Stock 类功能正常。要运行单元测试,你可以在终端中使用以下命令:

python3 teststock.py

如果一切正常,所有测试都应该顺利通过,没有错误。当测试成功运行时,输出应该类似于以下内容:

........
----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

这些点代表每个通过的测试,最后的 OK 表示所有测试都成功了。

现在,让我们用一些实际数据和表格格式化功能来测试 Stock 类。这将为我们提供一个更接近真实场景的测试,让我们了解 Stock 类如何与数据交互,以及表格格式化功能如何工作。你可以在终端中使用以下命令:

python3 -c "
from stock import Stock
from reader import read_csv_as_instances
from tableformat import create_formatter, print_table

## Read portfolio data into Stock instances
portfolio = read_csv_as_instances('portfolio.csv', Stock)
print('Portfolio:')
print(portfolio)

## Format and print the portfolio data
print('\nFormatted table:')
formatter = create_formatter('text')
print_table(portfolio, ['name', 'shares', 'price'], formatter)
"

在这段代码中,我们首先导入必要的类和函数。然后将 CSV 文件中的数据读取到 Stock 实例中。接着,我们打印投资组合数据,再将其格式化为表格并打印出来。

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

Portfolio:
[Stock('AA',100,32.2), Stock('IBM',50,91.1), Stock('CAT',150,83.44), Stock('MSFT',200,51.23), Stock('GE',95,40.37), Stock('MSFT',50,65.1), Stock('IBM',100,70.44)]

Formatted table:
      name     shares      price
---------- ---------- ----------
        AA        100       32.2
       IBM         50       91.1
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50       65.1
       IBM        100      70.44

花点时间来欣赏一下我们所取得的成果:

  1. 我们创建了一种机制来自动收集所有验证器类型。这意味着我们无需手动跟踪所有验证器,节省了时间并降低了出错的可能性。
  2. 我们实现了一个元类,将这些类型注入到 Structure 子类的命名空间中。这使得子类可以使用这些验证器,而无需显式导入它们。
  3. 我们消除了对验证器类型的显式导入需求。这使我们的代码更简洁、易读。
  4. 所有这些操作都在幕后完成,使得定义新结构的代码简洁明了。

最终的 stock.py 文件与没有使用元类时相比,显得格外简洁:

from structure import Structure

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

无需直接导入验证器类型,代码更加简洁,也更易于维护。这是元类如何提高代码质量的一个很好的例子。

总结

在本次实验中,你学习了如何利用 Python 中元类的强大功能。首先,你了解了管理验证器类型导入的挑战。接着,你修改了 Validator 类,使其能够自动收集其子类,并创建了 StructureMeta 元类,将验证器类型注入到类的命名空间中。最后,你使用 Stock 类对实现进行了测试,消除了显式导入的需求。

元类是 Python 的一项高级特性,它允许你自定义类的创建过程。尽管应该谨慎使用,但正如本次实验所示,元类能为特定问题提供优雅的解决方案。通过使用元类,你简化了定义带有验证属性的结构的代码,消除了对验证器类型的显式导入需求,并创建了一个更易于维护且优雅的 API。这种基于元类的命名空间注入模式可以应用于其他场景,以简化用户 API。