引言
在本实验中,你将学习 Python 中的类装饰器 (class decorators),并回顾和扩展 Python 描述器 (descriptors)。通过结合这些概念,你可以创建强大且简洁的代码结构。
在本实验中,你将基于之前的描述器概念,并使用类装饰器进行扩展。这种组合使你能够创建更简洁、更易于维护的代码,并增强验证能力。需要修改的文件是 validate.py 和 structure.py。
使用描述器实现类型检查
在本步骤中,我们将创建一个使用描述器进行类型检查的 Stock 类。但首先,让我们了解一下什么是描述器。描述器是 Python 中一个非常强大的特性。它们让你能够控制属性在类中的访问方式。
描述器是定义属性如何在其他对象上被访问的对象。它们通过实现诸如 __get__、__set__ 和 __delete__ 等特殊方法来实现这一点。这些方法允许描述器管理属性的检索、设置和删除。描述器在实现验证、类型检查和计算属性方面非常有用。例如,你可以使用描述器来确保某个属性始终是正数或特定格式的字符串。
validate.py 文件已经包含了验证器类(String、PositiveInteger、PositiveFloat)。我们可以使用这些类来验证 Stock 类的属性。
现在,让我们创建带有描述器的 Stock 类。
首先,在你的编辑器中打开
stock.py文件。文件打开后,将占位符内容替换为以下代码:
## 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 对象将拥有的属性名称。
name、shares 和 price 属性被定义为描述器对象。String() 描述器确保 name 属性是字符串类型。PositiveInteger() 描述器确保 shares 属性是正整数类型。而 PositiveFloat() 描述器则保证 price 属性是正浮点数类型。
cost 属性是一个计算属性。它根据股票数量和每股价格计算股票的总成本。
sell 方法用于减少股票数量。当你调用此方法并传入要卖出的股票数量时,它会从 shares 属性中减去该数量。
Stock.create_init() 行动态地为我们的类创建了一个 __init__ 方法。这个方法允许我们通过传入 name、shares 和 price 属性的值来创建 Stock 对象。
添加代码后,保存文件。这将确保你的更改已保存,并在运行测试时可以使用。
现在,让我们运行测试来验证你的实现。首先,通过运行以下命令将目录更改为
~/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 中,类装饰器是一个强大的工具,可以帮助我们简化这个过程。类装饰器是一个函数,它接收一个类作为参数,对其进行某种修改,然后返回修改后的类。通过使用类装饰器,我们可以自动从描述器中提取字段信息,这将使我们的代码更简洁、更易于维护。
让我们创建一个类装饰器来简化我们的代码。以下是你需要遵循的步骤:
首先,在你的编辑器中打开
structure.py文件。接下来,在
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__方法,然后返回修改后的类。
添加代码后,保存
structure.py文件。保存文件可确保你的更改得以保留。现在,我们需要修改
stock.py文件以使用这个新的装饰器。在你的编辑器中打开stock.py文件。更新
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__方法。
因此,该类现在更简单、更简洁了。装饰器处理了我们以前手动处理的所有细节。
- 进行这些更改后,我们需要验证一切是否仍按预期工作。使用以下命令再次运行测试:
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 的类。
让我们来实现这一点:
在你的编辑器中打开
structure.py文件。此文件包含Structure类的定义,我们将修改它以使用__init_subclass__方法。将
__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 的每个子类都将自动获得验证行为。
- 保存文件。
在修改 structure.py 文件后,我们需要保存它,以便应用更改。
现在,让我们更新
stock.py文件以利用这个新特性。在你的编辑器中打开stock.py文件进行修改。此文件包含Stock类的定义,我们将使其继承自Structure类以使用自动装饰器应用。修改
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继承来获得验证行为。
- 再次运行测试以验证一切是否仍然正常工作:
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 类方法来做到这一点。
首先,在你的编辑器中打开
structure.py文件。这是我们将进行代码更改的地方。接下来,我们将修改
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 类变量中。这将在稍后将数据从行转换为正确类型时非常有用。
- 现在,我们将
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列表中的相应函数将行中的每个值转换为预期的类型。 - 然后,它使用转换后的值创建并返回该类的新实例。
进行这些更改后,保存
structure.py文件。这确保了你的代码更改得以保留。让我们测试我们的
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 方法正在正常工作。
- 最后,让我们尝试使用我们的
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 中的注解是一种为函数参数和返回值添加元数据的方式。
让我们修改代码以将此装饰器应用于带有注解的方法:
- 首先,我们需要了解
validated装饰器的工作原理。在你的编辑器中打开validate.py文件进行查看。
validated 装饰器使用函数注解来验证参数。在允许函数运行之前,它会为每个注解的参数创建一个验证器类的实例,并调用 validate 方法来检查参数。例如,如果一个参数被注解为 PositiveInteger,装饰器将创建一个 PositiveInteger 实例并验证传入的值确实是一个正整数。如果验证失败,它会收集所有错误并引发一个带有详细错误消息的 TypeError。
现在,我们将修改
structure.py中的validate_attributes函数,以使用validated装饰器包装带有注解的方法。这意味着类中任何带有注解的方法都将自动验证其参数。在你的编辑器中打开structure.py文件。更新
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
这个更新后的函数现在执行以下操作:
它像以前一样处理验证器描述符。验证器描述符用于为类属性定义验证规则。
它在类中查找所有带有注解的方法。注解被添加到方法参数中,以指定参数的预期类型。
它将
@validated装饰器应用于这些方法。这确保了传递给这些方法的参数会根据其注解进行验证。进行这些更改后保存文件。保存文件很重要,因为它确保了我们的修改被存储并且以后可以使用。
现在,让我们更新
Stock类中的sell方法以包含注解。注解有助于指定参数的预期类型,这将由@validated装饰器用于验证。在你的编辑器中打开stock.py文件。修改
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 参数必须是一个正整数。
- 再次运行测试以验证一切是否仍然正常工作。运行测试是确保我们的更改没有破坏任何现有功能的好方法。
cd ~/project
python3 teststock.py
你应该会看到所有测试都通过:
.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s
OK
- 让我们测试我们新的参数验证。我们将尝试使用有效和无效参数调用
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 不是一个正整数。
你现在已经实现了一个完整的系统,用于:
- 使用描述符验证类属性。描述符用于为类属性定义验证规则。
- 使用类装饰器自动收集字段信息。类装饰器可以修改类的行为,例如收集字段信息。
- 将行数据转换为实例。这在处理来自外部源的数据时很有用。
- 使用注解验证方法参数。注解有助于为验证指定参数的预期类型。
这展示了在 Python 中结合使用描述符和装饰器来创建富有表现力、自我验证的类的强大功能。
总结
在本实验中,你学习了如何结合强大的 Python 特性来创建简洁、自验证的代码。你掌握了关键概念,例如使用描述符进行属性验证、创建类装饰器进行代码生成自动化,以及通过继承自动应用装饰器。
这些技术是创建健壮且可维护的 Python 代码的强大工具。它们使你能够清晰地表达验证需求并在整个代码库中强制执行它们。你现在可以在自己的 Python 项目中应用这些模式,以提高代码质量并减少样板代码。