- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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 术语表
- 作者简介
- 关于封面
9.6 可散列的 Vector2d
按照定义,目前 Vector2d 实例是不可散列的,因此不能放入集合(set)中:
>>> v1 = Vector2d(3, 4) >>> hash(v1) Traceback (most recent call last): ... TypeError: unhashable type: 'Vector2d' >>> set([v1]) Traceback (most recent call last): ... TypeError: unhashable type: 'Vector2d'
为了把 Vector2d 实例变成可散列的,必须使用 __hash__ 方法(还需要 __eq__ 方法,前面已经实现了)。此外,还要让向量不可变,详情参见第 3 章的附注栏“什么是可散列的数据类型”。
目前,我们可以为分量赋新值,如 v1.x = 7,Vector2d 类的代码并不阻止这么做。我们想要的行为是这样的:
>>> v1.x, v1.y (3.0, 4.0) >>> v1.x = 7 Traceback (most recent call last): ... AttributeError: can't set attribute
为此,我们要把 x 和 y 分量设为只读特性,如示例 9-7 所示。
示例 9-7 vector2d_v3.py:这里只给出了让 Vector2d 不可变的代码,完整的代码清单在示例 9-9 中
class Vector2d: typecode = 'd' def __init__(self, x, y): self.__x = float(x) ➊ self.__y = float(y) @property ➋ def x(self): ➌ return self.__x ➍ @property ➎ def y(self): return self.__y def __iter__(self): return (i for i in (self.x, self.y)) ➏ # 下面是其他方法(排版需要,省略了)
❶ 使用两个前导下划线(尾部没有下划线,或者有一个下划线),把属性标记为私有的。6
6根据本章开头引用的那句话,这不符合 Ian Bicking 的建议。私有属性的优缺点参见后面的 9.7 节。
❷ @property 装饰器把读值方法标记为特性。
❸ 读值方法与公开属性同名,都是 x。
❹ 直接返回 self.__x。
❺ 以同样的方式处理 y 特性。
❻ 需要读取 x 和 y 分量的方法可以保持不变,通过 self.x 和 self.y 读取公开特性,而不必读取私有属性,因此上述代码清单省略了这个类的其他代码。
Vector.x 和 Vector.y 是只读特性。读写特性在第 19 章讨论,届时会深入说明 @property 装饰器。
注意,我们让这些向量不可变是有原因的,因为这样才能实现 __hash__ 方法。这个方法应该返回一个整数,理想情况下还要考虑对象属性的散列值(__eq__ 方法也要使用),因为相等的对象应该具有相同的散列值。根据特殊方法 __hash__ 的文档,最好使用位运算符异或(^)混合各分量的散列值——我们会这么做。Vector2d.__hash__ 方法的代码十分简单,如示例 9-8 所示。
示例 9-8 vector2d_v3.py:实现 __hash__ 方法
# 在Vector2d类中定义 def __hash__(self): return hash(self.x) ^ hash(self.y)
添加 __hash__ 方法之后,向量变成可散列的了:
>>> v1 = Vector2d(3, 4) >>> v2 = Vector2d(3.1, 4.2) >>> hash(v1), hash(v2) (7, 384307168202284039) >>> set([v1, v2]) {Vector2d(3.1, 4.2), Vector2d(3.0, 4.0)}
要想创建可散列的类型,不一定要实现特性,也不一定要保护实例属性。只需正确地实现 __hash__ 和 __eq__ 方法即可。但是,实例的散列值绝不应该变化,因此我们借机提到了只读特性。
如果定义的类型有标量数值,可能还要实现 __int__ 和 __float__ 方法(分别被 int() 和 float() 构造函数调用),以便在某些情况下用于强制转换类型。此外,还有用于支持内置的 complex() 构造函数的 __complex__ 方法。Vector2d 或许应该提供 __complex__ 方法,不过我把它留作练习给读者。
我们一直在定义 Vector2d 类,也列出了很多代码片段,示例 9-9 是整理后的完整代码清单,保存在 vector2d_v3.py 文件中,包含开发时我编写的全部 doctest。
示例 9-9 vector2d_v3.py:完整版
""" A two-dimensional vector class >>> v1 = Vector2d(3, 4) >>> print(v1.x, v1.y) 3.0 4.0 >>> x, y = v1 >>> x, y (3.0, 4.0) >>> v1 Vector2d(3.0, 4.0) >>> v1_clone = eval(repr(v1)) >>> v1 == v1_clone True >>> print(v1) (3.0, 4.0) >>> octets = bytes(v1) >>> octets b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@' >>> abs(v1) 5.0 >>> bool(v1), bool(Vector2d(0, 0)) (True, False) Test of ``.frombytes()`` class method: >>> v1_clone = Vector2d.frombytes(bytes(v1)) >>> v1_clone Vector2d(3.0, 4.0) >>> v1 == v1_clone True Tests of ``format()`` with Cartesian coordinates: >>> format(v1) '(3.0, 4.0)' >>> format(v1, '.2f') '(3.00, 4.00)' >>> format(v1, '.3e') '(3.000e+00, 4.000e+00)' Tests of the ``angle`` method:: >>> Vector2d(0, 0).angle() 0.0 >>> Vector2d(1, 0).angle() 0.0 >>> epsilon = 10**-8 >>> abs(Vector2d(0, 1).angle() - math.pi/2) < epsilon True >>> abs(Vector2d(1, 1).angle() - math.pi/4) < epsilon True Tests of ``format()`` with polar coordinates: >>> format(Vector2d(1, 1), 'p') # doctest:+ELLIPSIS '<1.414213..., 0.785398...>' >>> format(Vector2d(1, 1), '.3ep') '<1.414e+00, 7.854e-01>' >>> format(Vector2d(1, 1), '0.5fp') '<1.41421, 0.78540>' Tests of `x` and `y` read-only properties: >>> v1.x, v1.y (3.0, 4.0) >>> v1.x = 123 Traceback (most recent call last): ... AttributeError: can't set attribute Tests of hashing: >>> v1 = Vector2d(3, 4) >>> v2 = Vector2d(3.1, 4.2) >>> hash(v1), hash(v2) (7, 384307168202284039) >>> len(set([v1, v2])) 2 """ from array import array import math class Vector2d: typecode = 'd' def __init__(self, x, y): self.__x = float(x) self.__y = float(y) @property def x(self): return self.__x @property def y(self): return self.__y def __iter__(self): return (i for i in (self.x, self.y)) def __repr__(self): class_name = type(self).__name__ return '{}({!r}, {!r})'.format(class_name, *self) def __str__(self): return str(tuple(self)) def __bytes__(self): return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self))) def __eq__(self, other): return tuple(self) == tuple(other) def __hash__(self): return hash(self.x) ^ hash(self.y) def __abs__(self): return math.hypot(self.x, self.y) def __bool__(self): return bool(abs(self)) def angle(self): return math.atan2(self.y, self.x) def __format__(self, fmt_spec=''): if fmt_spec.endswith('p'): fmt_spec = fmt_spec[:-1] coords = (abs(self), self.angle()) outer_fmt = '<{}, {}>' else: coords = self outer_fmt = '({}, {})' components = (format(c, fmt_spec) for c in coords) return outer_fmt.format(*components) @classmethod def frombytes(cls, octets): typecode = chr(octets[0]) memv = memoryview(octets[1:]).cast(typecode) return cls(*memv)
小结一下,前两节说明了一些特殊方法,要想得到功能完善的对象,这些方法可能是必备的。当然,如果你的应用用不到,就没必要全部实现这些方法。客户并不关心你的对象是否符合 Python 风格。
示例 9-9 中定义的 Vector2d 类只是为了教学,我们为它定义了许多与对象表示形式有关的特殊方法。不是每个用户自定义的类都要这样做。
下一节暂时不继续定义 Vector2d 类了,我们将讨论 Python 对私有属性(带两个下划线前缀的属性,如 self.__x)的设计方式及其缺点。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论