返回介绍

19.4 定义一个特性工厂函数

发布于 2024-02-05 21:59:47 字数 3439 浏览 0 评论 0 收藏 0

我们将定义一个名为 quantity 的特性工厂函数,取这个名字是因为,在这个应用中要管理的属性表示不能为负数或零的量。示例 19-23 是 LineItem 类的简洁版,用到了 quantity 特性的两个实例:一个用于管理 weight 属性,另一个用于管理 price 属性。

示例 19-23 bulkfood_v2prop.py:使用特性工厂函数 quantity

class LineItem:
  weight = quantity('weight')  ➊
  price = quantity('price')  ➋

  def __init__(self, description, weight, price):
    self.description = description
    self.weight = weight  ➌
    self.price = price

  def subtotal(self):
    return self.weight * self.price  ➍

❶ 使用工厂函数把第一个自定义的特性 weight 定义为类属性。

❷ 第二次调用,构建另一个自定义的特性,price。

❸ 这里,特性已经激活,确保不能把 weight 设为负数或零。

❹ 这里也用到了特性,使用特性获取实例中存储的值。

前文说过,特性是类属性。构建各个 quantity 特性对象时,要传入 LineItem 实例属性的名称,让特性管理。可惜,这一行要两次输入单词 weight:

  weight = quantity('weight')

这里很难避免重复输入,因为特性根本不知道要绑定哪个类属性名。记住,赋值语句的右边先计算,因此调用 quantity() 时,weight 类属性还不存在。

 如果想改进 quantity 特性,避免用户重复输入属性名,那么对元编程来说是个挑战。第 20 章会介绍一种变通方法,真正的解决方法在第 21 章说明,因为要么得使用类装饰器,要么得使用元类。

示例 19-24 列出 quantity 特性工厂函数的实现。13

13这段代码改编自 David Beazley 与 Brian K. Jones 的《Python Cookbook(第 3 版)中文版》一书中的“9.21 避免出现重复的属性方法”一节。

示例 19-24 bulkfood_v2prop.py:quantity 特性工厂函数

def quantity(storage_name):  ➊

  def qty_getter(instance):  ➋
    return instance.__dict__[storage_name]  ➌

  def qty_setter(instance, value):  ➍
    if value > 0:
      instance.__dict__[storage_name] = value  ➎
    else:
      raise ValueError('value must be > 0')

  return property(qty_getter, qty_setter)  ➏

❶ storage_name 参数确定各个特性的数据存储在哪儿;对 weight 特性来说,存储的名称是 'weight'。

❷ qty_getter 函数的第一个参数可以命名为 self,但是这么做很奇怪,因为 qty_getter 函数不在类定义体中;instance 指代要把属性存储其中的 LineItem 实例。

❸ qty_getter 引用了 storage_name,把它保存在这个函数的闭包里;值直接从 instance.__dict__ 中获取,为的是跳过特性,防止无限递归。

❹ 定义 qty_setter 函数,第一个参数也是 instance。

❺ 值直接存到 instance.__dict__ 中,这也是为了跳过特性。

❻ 构建一个自定义的特性对象,然后将其返回。

示例 19-24 中值得仔细分析的代码是与 storage_name 变量相关的部分。使用传统方式定义特性时,用于存储值的属性名硬编码在读值方法和设值方法中。但是,这里的 qty_getter 和 qty_setter 函数是通用的,要依靠 storage_name 变量判断从 __dict__ 中获取哪个属性,或者设置哪个属性。每次调用 quantity 工厂函数构建属性时,都要把 storage_name 参数设为独一无二的值。

在工厂函数的最后一行,我们使用 property 对象包装 qty_getter 和 qty_setter 函数。需要运行这两个函数时,它们会从闭包中读取 storage_name,确定从哪里获取属性的值,或者在哪里存储属性的值。

在示例 19-25 中,我创建并审查了一个 LineItem 示例,说明存储值的是哪个属性。

示例 19-25 bulkfood_v2prop.py:quantity 特性工厂函数

>>> nutmeg = LineItem('Moluccan nutmeg', 8, 13.95)
>>> nutmeg.weight, nutmeg.price  ➊
(8, 13.95)
>>> sorted(vars(nutmeg).items())  ➋
[('description', 'Moluccan nutmeg'), ('price', 13.95), ('weight', 8)]

➊ 通过特性读取 weight 和 price,这会遮盖同名实例属性。

➋ 使用 vars 函数审查 nutmeg 实例,查看真正用于存储值的实例属性。

注意,工厂函数构建的特性利用了 19.3.1 节所述的行为:weight 特性覆盖了 weight 实例属性,因此对 self.weight 或 nutmeg.weight 的每个引用都由特性函数处理,只有直接存取 __dict__ 属性才能跳过特性的处理逻辑。

示例 19-25 中的代码有点难理解,不过够简洁,与示例 19-17 中使用装饰器声明读值方法和设值方法的代码行数一样,但是那里只定义了 weight 特性。示例 19-23 中定义的 LineItem 类没有干扰人的读值方法和设值方法,看起来舒服多了。

在真实的系统中,分散在多个类中的多个字段可能要做同样的验证,此时最好把 quantity 工厂函数放在实用工具模块中,以便重复使用。最终可能要重构那个简单的工厂函数,改成更易扩展的描述符类,然后使用专门的子类执行不同的验证。在第 20 章中,我们会这么做。

下面要分析删除属性的问题,以此结束对特性的讨论。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文