Python 动态属性和特性(定义一个特性工厂函数)

发布于:2025-08-04 ⋅ 阅读:(16) ⋅ 点赞:(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 特性工厂函数的实现。

示例 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 章中,我们会这么做。

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


网站公告

今日签到

点亮在社区的每一天
去签到