- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
21.1 类工厂函数
本书多次提到标准库中的一个类工厂函数——collections.namedtuple。我们把一个类名和几个属性名传给这个函数,它会创建一个 tuple 的子类,其中的元素通过名称获取,还为调试提供了友好的字符串表示形式(__repr__)。
有时,我觉得应该有类似的工厂函数,用于创建可变对象。假设我在编写一个宠物店应用程序,我想把狗的数据当作简单的记录处理。编写下面的样板代码让人厌烦:
class Dog: def __init__(self, name, weight, owner): self.name = name self.weight = weight self.owner = owner
无趣……各个字段名称出现了三次。写了这么多样板代码,甚至字符串表示形式都不友好:
>>> rex = Dog('Rex', 30, 'Bob') >>> rex <__main__.Dog object at 0x2865bac>
参考 collections.namedtuple,下面我们创建一个 record_factory 函数,即时创建简单的类(如 Dog)。这个函数的用法如示例 21-1。
示例 21-1 测试 record_factory 函数,一个简单的类工厂函数
>>> Dog = record_factory('Dog', 'name weight owner') ➊ >>> rex = Dog('Rex', 30, 'Bob') >>> rex ➋ Dog(name='Rex', weight=30, owner='Bob') >>> name, weight, _ = rex ➌ >>> name, weight ('Rex', 30) >>> "{2}'s dog weighs {1}kg".format(*rex) ➍ "Bob's dog weighs 30kg" >>> rex.weight = 32 ➎ >>> rex Dog(name='Rex', weight=32, owner='Bob') >>> Dog.__mro__ ➏ (<class 'factories.Dog'>, <class 'object'>)
❶ 这个工厂函数的签名与 namedtuple 类似:先写类名,后面跟着写在一个字符串里的多个属性名,使用空格或逗号分开。
❷ 友好的字符串表示形式。
❸ 实例是可迭代的对象,因此赋值时可以便利地拆包。
❹ 传给 format 等函数时也可以拆包。
❺ 记录实例是可变的对象。
❻ 新建的类继承自 object,与我们的工厂函数没有关系。
record_factory 函数的代码在示例 21-2 中。2
2感谢我的朋友 J.S. Bueno 的建议。
示例 21-2 record_factory.py:一个简单的类工厂函数
def record_factory(cls_name, field_names): try: field_names = field_names.replace(',', ' ').split() ➊ except AttributeError: # 不能调用.replace或.split方法 pass # 假定field_names本就是标识符组成的序列 field_names = tuple(field_names) ➋ def __init__(self, *args, **kwargs): ➌ attrs = dict(zip(self.__slots__, args)) attrs.update(kwargs) for name, value in attrs.items(): setattr(self, name, value) def __iter__(self): ➍ for name in self.__slots__: yield getattr(self, name) def __repr__(self): ➎ values = ', '.join('{}={!r}'.format(*i) for i in zip(self.__slots__, self)) return '{}({})'.format(self.__class__.__name__, values) cls_attrs = dict(__slots__ = field_names, ➏ __init__ = __init__, __iter__ = __iter__, __repr__ = __repr__) return type(cls_name, (object,), cls_attrs) ➐
❶ 这里体现了鸭子类型:尝试在逗号或空格处拆分 field_names;如果失败,那么假定 field_names 本就是可迭代的对象,一个元素对应一个属性名。
❷ 使用属性名构建元组,这将成为新建类的 __slots__ 属性;此外,这么做还设定了拆包和字符串表示形式中各字段的顺序。
❸ 这个函数将成为新建类的 __init__ 方法。参数有位置参数和(或)关键字参数。
❹ 实现 __iter__ 函数,把类的实例变成可迭代的对象;按照 __slots__ 设定的顺序产出字段值。
❺ 迭代 __slots__ 和 self,生成友好的字符串表示形式。
❻ 组建类属性字典。
❼ 调用 type 构造方法,构建新类,然后将其返回。
通常,我们把 type 视作函数,因为我们像函数那样使用它,例如,调用 type(my_object) 获取对象所属的类——作用与 my_object.__class__ 相同。然而,type 是一个类。当成类使用时,传入三个参数可以新建一个类:
MyClass = type('MyClass', (MySuperClass, MyMixin), {'x': 42, 'x2': lambda self: self.x * 2})
type 的三个参数分别是 name、bases 和 dict。最后一个参数是一个映射,指定新类的属性名和值。上述代码的作用与下述代码相同:
class MyClass(MySuperClass, MyMixin): x = 42 def x2(self): return self.x * 2
让人觉得新奇的是,type 的实例是类,例如这里的 MyClass 类或示例 21-1 中的 Dog 类。
总之,示例 21-2 中 record_factory 函数的最后一行会构建一个类,类的名称是 cls_name 参数的值,唯一的直接超类是 object,有 __slots__、__init__、__iter__ 和 __repr__ 四个类属性,其中后三个是实例方法。
我们本可以把 __slots__ 类属性的名称改成其他值,不过要是那样的话,就要实现 __setattr__ 方法,为属性赋值时验证属性的名称,因为对于记录这样的类,我们希望属性始终是固定的那几个,而且顺序相同。然而 9.8 节说过,__slots__ 属性的主要特色是节省内存,能处理数百万个实例,不过也有一些缺点。
把三个参数传给 type 是动态创建类的常用方式。如果查看 collections.namedtuple 函数的源码,你会发现另一种方式:先声明一个 _class_template 变量,其值是字符串形式的源码模板;然后在 namedtuple 函数中调用 _class_template.format(...) 方法,填充模板里的空白;最后,使用内置的 exec 函数计算得到的源码字符串。
在 Python 中做元编程时,最好不用 exec 和 eval 函数。如果接收的字符串(或片段)来自不可信的源,那么这两个函数会带来严重的安全风险。Python 提供了充足的内省工具,大多数时候都不需要使用 exec 和 eval 函数。然而,Python 核心开发者实现 namedtuple 函数时选择了使用 exec 函数,这样做是为了让生成的类代码能通过 ._source 属性获取。
record_factory 函数创建的类,其实例有个局限——不能序列化,即不能使用 pickle 模块里的 dump/load 函数处理。这个示例是为了说明如何使用 type 类满足简单的需求,因此不会解决这个问题。如果想了解完整的方案,请分析 collections.nameduple 函数的源码,搜索“pickling”这个词。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论