CHAPTER 12 Special Methods for Sequences
1、介绍
-
创建一个多维向量类 (
Vector
):- 表现为不可变 (immutable) 的扁平序列(flat sequence)。
- 元素为浮点数 (
float
)。 - 支持丰富的标准 Python 序列协议和自定义行为。
-
功能支持:
- 基本序列协议:
- 实现
__len__
和__getitem__
方法。
- 实现
- 实例字符串的安全表示:
- 适用于包含大量元素的实例。
- 切片支持:
- 完善切片功能,返回新的
Vector
实例。
- 完善切片功能,返回新的
- 哈希支持:
- 基于每个元素值的聚合散列值。
- 自定义格式化:
- 支持扩展的格式化语言(如 f-string 格式的自定义扩展)。
- 基本序列协议:
-
动态属性访问:
- 使用
__getattr__
实现动态属性访问,替代早前Vector2d
使用的只读属性。 - 注意:这不是典型的序列类型行为,但是一种功能增强。
- 使用
-
概念补充:以 Python 的协议(Protocol)为主题进行深入探讨。
- 协议是非正式接口的一种体现,和鸭子类型紧密相关。
- 探讨协议的实际应用和意义,包括类型提示支持(如
typing.Protocol
)。
2、Vector: A User-Defined Sequence Type
1. Vector
类的设计策略
- 使用组合(composition),避免继承:
Vector
的实现采用组合设计,将向量的各个元素存储在一个浮点数组中。- 目标是实现一个不可变的扁平序列类型(immutable flat sequence),类似于标准的 Python 容器类型。
- 兼容性考虑:
- 基线实现需要与之前的
Vector2d
类兼容,但不适用之处除外。
- 基线实现需要与之前的
2. 高维向量的实际应用
- 高维向量的需求:
- N 维向量(可能有数千维)广泛用于信息检索领域,例如文档和文本查询表示。
- Vector Space Model(向量空间模型):
- 文本查询和文档可以通过向量表示,每个维度代表一个单词。
- 关键度量 - 余弦相似度:
- 使用向量间夹角的余弦值来衡量文本相关性。
- 当余弦值接近 1(夹角越小)时,查询与文档的相关性更高。
3. 在 Vector
中的实际数学计算非重点
- 这个
Vector
类的主要目的是教学和示例,展示如何通过 Python 的特殊方法(如魔术方法)创建一个自定义的序列类型。 - 真实应用工具:
- 数学计算推荐使用科学计算工具:
- NumPy 和 SciPy:处理高效向量和矩阵计算。
- Gensim:实现自然语言处理(NLP)中的向量空间模型,依赖于 NumPy 和 SciPy。
- 数学计算推荐使用科学计算工具:
4. 学习目标
- 通过
Vector
类的实现,理解 Python 面向对象设计的特性,特别是如何通过魔术方法来构建一个自定义序列类型容器。 - 理解向量在数学建模、搜索引擎与自然语言处理中的基础作用。
3、Vector Take #1: Vector2d Compatible
3.1 构造器设计的最佳实践
传统的 Vector2d
接受两个参数的构造方式:
Vector2d(3, 4)
新的 Vector
类以一种更通用的方式构造对象,通过接受任意可迭代对象作为参数,使它可以表示任意维度的数据:
Vector([3.1, 4.2]) # 2D 向量
Vector((3, 4, 5)) # 3D 向量
Vector(range(10)) # 10D 向量
这种设计形式更符合 Python 内建数列类型(如 list
、tuple
)的习惯,同时避免了一些不必要的参数控制逻辑。
3.2 代码实现和功能解析
以下是实现的核心代码及详解:
from array import array
import reprlib
import mathclass Vector:typecode = 'd' # 表示数组存储的元素类型为双精度浮点数def __init__(self, components): """构造函数,接受任意可迭代对象作为 components"""self._components = array(self.typecode, components)def __iter__(self):"""实现迭代器协议,支持遍历操作"""return iter(self._components)def __repr__(self):"""返回类似 'Vector([3.0, 4.0, 5.0])' 的字符串表示"""components = reprlib.repr(self._components)components = components[components.find('['):-1]return f'Vector({components})'def __str__(self):"""用户友好的字符串形式,返回元组形式"""return str(tuple(self))def __bytes__(self):"""支持以字节序列表示,用于数据的序列化"""return (bytes([ord(self.typecode)]) + bytes(self._components))def __eq__(self, other):"""判断两个 Vector 是否相等"""return tuple(self) == tuple(other)def __abs__(self):"""计算向量的模长"""return math.hypot(*self) # N 维度支持从 math.hypot(3.8 起)def __bool__(self):"""判断向量是否为零向量"""return bool(abs(self))@classmethoddef frombytes(cls, octets):"""从字节序列重建 Vector 对象"""typecode = chr(octets[0])memv = memoryview(octets[1:]).cast(typecode)return cls(memv)
3.3 关键点逐一解析
-
类变量
typecode
- 使用
array
模块存储浮点数,需要指定类型。例如'd'
表示双精度浮点。 - 将其定义为类变量可以在多个实例间复用,并且尽可能地减少硬编码。
- 使用
-
构造函数
__init__
- 接收一个任意可迭代对象,并将其存储为
array
类型,提供高效的存储和数值操作。 - 例子:
- 接收一个任意可迭代对象,并将其存储为
v = Vector([3.1, 4.2]) # 2D 向量
print(v._components) # array('d', [3.1, 4.2])
-
定制
__repr__
方法- 用于调试和开发阶段,提供精简但信息丰富的字符串形式。
- 使用
reprlib.repr
来适配较大的数据集,当向量元素较多时仅显示部分,剩余部分以...
表示。 - 例子:
Vector(range(10)) # 输出: Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
-
定制
__str__
方法- 提供用户友好型的输出,将向量显示为元组形式。
- 例子:
print(Vector((3, 4, 5))) # 输出: (3.0, 4.0, 5.0)
-
实现序列化:
__bytes__
和frombytes
__bytes__
将向量转化为字节序列,对于数据的持久化、网络传输等应用场景非常有用。frombytes
提供了从字节序列还原向量的能力。- 例子:
v = Vector([3.1, 4.2]) v_bytes = bytes(v) print(v_bytes) # b'd@\t\x99\x99\x99\x99\x99\x9a@!\x99\x99\x99\x99\x99\x9a' print(Vector.frombytes(v_bytes)) # Vector([3.1, 4.2])
-
模长计算
__abs__
- 利用
math.hypot
求解 N 维向量的模长,比手动求平方和更高效。 - 例子:
v = Vector([3, 4, 5]) print(abs(v)) # 输出: 7.0710678118654755
- 利用
-
布尔值表示
__bool__
- 模长为 0 时返回
False
,否则True
。 - 例子:
v1 = Vector([0, 0, 0]) v2 = Vector([1, 2, 3]) print(bool(v1)) # False print(bool(v2)) # True
- 模长为 0 时返回
3.4 工程中容易忽略的细节
reprlib.repr
的效率
from array import array
import reprlibcomponents = array('d', range(100)) # 使用 array 存储 100 个双精度浮点数
print(type(components)) # <class 'array.array'># 使用 reprlib.repr(list(components)) 截断并生成字符表示
print(reprlib.repr(list(components)))
# 输出: '[0.0, 1.0, 2.0, ...]'
-
如果
_components
是一个很大的数组,比如包含数百万个元素,这个操作就会使用大量的内存来生成相同内容的列表,而这个新的列表仅仅是为了调用reprlib.repr()
。 -
这种行为是 不必要的内存浪费,因为我们可以直接对
_components
调用reprlib.repr()
,无需先将其转为列表。 -
在
Vector
类中,原作者设计为直接对_components
调用reprlib.repr
:
components = reprlib.repr(self._components)
components = components[components.find('['):-1] # 找到 '[' 开始,并切除末尾的 ')'
- 直接操作原生数据结构:例如,直接基于
array
、memoryview
、numpy.array
,而不是盲目转换为更轻量级的形式。 - 量体裁衣使用工具:在本例中,
reprlib.repr
支持任意对象,因此没必要为了适配它而引入开销昂贵的转换操作。
-
abs
与bool
的逻辑联系__bool__
的判断依赖于__abs__
方法,只要模长为 0,则认为这是一个零向量。
-
兼容性与扩展性
- 如果直接从
Vector2d
类继承,构造器的参数可能带来复杂的兼容性问题。通过实现独立的Vector
类,精细化控制每一种逻辑。
- 如果直接从
3.5 总结
通过这一版 Vector
类的实现,我们能够:
- 支持任意维度的向量;
- 通过定制化的特殊方法(如
__repr__
、__str__
、__eq__
等),丰富了类的功能; - 提供了高效的计算方式(如基于
math.hypot
的模长计算)。
这种设计方式非常贴合 Python 的数据类型设计哲学(例如 list
或 tuple
),同时通过模块化和类方法便捷地实现了序列化操作,值得在工程中深刻实践和学习。
4、Protocols and Duck Typing
4.1 什么是协议(Protocol)?
定义
在面向对象编程的上下文中,协议是一种非正式接口,它通过文档定义了对象必须遵守的规则,但在代码中这种规则是非强制的(不像 Java 的接口 interface
或 C++ 的具体抽象类)。协议的核心思想是,只需要实现与协议相关的方法且保证它们符合约定的行为签名,就可以让类表现出符合协议的行为。
Python 中的协议
- 很多 Python 的内置功能和标准库都会基于协议工作。
- 协议本身并不要求类显式继承某个特定的父类,只要实现了相关方法,这个类型的实例就可以用在期待协议对象的位置。
例子:
Python 的序列协议 (Sequence Protocol) 要求对象提供以下方法:
__len__
:返回对象的长度。__getitem__
:支持通过索引获取元素。
一个类只要实现了这些方法,就可以被视为一个序列,可以在很多期望序列的上下文中使用,如 for
循环、支持切片操作等。
例子:FrenchDeck
类实现序列协议
以下是一个简单的扑克牌类 FrenchDeck
,通过实现序列协议的方法来表现为一个序列类型:
import collections# 命名元组表示一张牌
Card = collections.namedtuple('Card', ['rank', 'suit'])class FrenchDeck:# 扑克牌的序号和花色ranks = [str(n) for n in range(2, 11)] + list('JQKA')suits = 'spades diamonds clubs hearts'.split()def __init__(self):# 使用列表生成式构造一副扑克牌self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks]def __len__(self):# 返回扑克牌的数量return len(self._cards)def __getitem__(self, position):# 根据位置返回具体的扑克牌return self._cards[position]
运行结果示例
我们可以像操作序列一样使用 FrenchDeck
:
deck = FrenchDeck()# 序列长度
print(len(deck)) # Output: 52# 通过索引访问
print(deck[0]) # Output: Card(rank='2', suit='spades')# 支持切片
print(deck[:3]) # Output: [Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades')]# 支持迭代
for card in deck:print(card)
4.2 鸭子类型(Duck Typing)
定义
鸭子类型是 Python 中的一种多态表现方式,其哲学是:“如果某个对象像鸭子一样走路、像鸭子一样叫声、像鸭子一样游泳,那么我们就可以把它当成鸭子。”——也就是说,它的类型并不重要,只要行为符合预期即可。
关键思想:
- 动态类型化:Python 不关心对象的具体类型,而关心它是否实现了某些特定的方法。
- 这种动态类型的设计使得代码更加灵活,但也需要开发者主动确保协议符合预期。
例子:FrenchDeck
类的鸭子类型行为
# 检查类型
print(isinstance(deck, list)) # False - 它不是 list 的子类# 然而它可以当作序列来使用
from collections.abc import Sequence
print(isinstance(deck, Sequence)) # False - 它的行为符合序列
尽管 FrenchDeck
看起来确实像一个序列(因为实现了 __len__
和 __getitem__
方法),但它并没有显式地继承 collections.abc.Sequence
。在 Python 中,isinstance
检查是否为某个类的实例时,实际会以以下方式工作:
- 检查对象是否直接继承指定的类型。
- 检查对象是否是该类型的子类的实例。
- 对于协议类型,检查该对象的类型是否"注册到"了相应的抽象基类中。
由于 FrenchDeck
只是动态实现了序列的行为,而不是显式地继承或注册到 Sequence
抽象基类,因此 isinstance(deck, Sequence)
会返回 False
,尽管从行为上它确实类似序列。
4.3 协议的灵活性与潜在问题
由于协议是非正式的,并没有强制检查,因此:
- 优点:灵活性高,任何类实现方法即可符合。
- 缺点:在复杂代码中,可能导致潜在的错误。例如:
- 没有实现协议所需的某些方法。
- 方法实现不符合预期的行为或返回值类型。
例子:未完整实现协议导致的问题
假设我们遗漏了实现 __getitem__
方法,再尝试使用 FrenchDeck
:
class BrokenDeck:def __len__(self):return 52
尝试:
deck = BrokenDeck()
print(len(deck)) # 52 - 没问题
print(deck[0]) # TypeError: 'BrokenDeck' object is not subscriptable
4.4 静态协议(Static Protocols)
Python 3.8 引入了 PEP 544,将协议形式化为类型检查工具。这些协议以 typing
模块中的 Protocol
类为基础,可以用来指定结构化的类型。静态协议弥补了传统协议未明确声明接口或缺乏强制的不足。
静态协议示例
基于上面的 FrenchDeck
,如果我们编写一个明确的静态协议来定义序列:
from typing import Protocol, runtime_checkable@runtime_checkable
class SequenceProtocol(Protocol):def __len__(self) -> int: ...def __getitem__(self, index: int): ...# 检查是否符合
print(isinstance(FrenchDeck(), SequenceProtocol)) # True
静态协议确保类型安全,并可与工具如 mypy
配合进行静态代码检查。
4.5. 工程实践中的要点
-
灵活性与可读性
- 鸭子类型的灵活性强,但文档和代码的清晰度必须保证。
- 推荐在注释或文档中描述协议要求(即方法签名与行为)。
-
默认实现
- 如果协议要求多个方法,但某些方法调试使用频率低,可以提供合理的默认实现,以简化多类开发。
-
静态类型检查工具
- 使用
Protocol
和typing
模块定义接口,可以让代码在大型项目中更安全、更易维护。
- 使用
-
优先协议而非继承
- 除非明确需要类的通用父类型(如
dict
或list
),否则,更推荐基于行为(protocol)而非继承树设计类。
- 除非明确需要类的通用父类型(如
5、Vector Take #2: A Sliceable Sequence
Python 中内置的序列类型(如 list
, tuple
, str
等),在支持切片操作时表现良好。比如对切片 my_list[1:4]
操作,返回的结果仍然是 list
类型;"hello"[1:3]
返回的仍然是字符串。
因此,在自定义序列类时,一个良好的设计应当模仿内置序列类的行为,当用切片操作时,返回的是自定义类的实例,而非简单的数组或列表。重点以 Vector
类为例,展示如何实现一个支持切片的自定义序列,同时解析切片的底层实现。
5.1 自定义基础序列类 Vector 的初步实现
Vector
类包含一组数值(存储在数组 self._components
中)。我们首先通过实现 __len__
和 __getitem__
,让 Vector
类成为一个基本序列类型:
示例代码:
class Vector:def __init__(self, data):self._components = list(data)def __len__(self):return len(self._components)def __getitem__(self, index):return self._components[index]
测试用例:
>>> v1 = Vector([3, 4, 5])
>>> len(v1)
3
>>> v1[0], v1[-1]
(3, 5)
>>> v2 = Vector(range(7))
>>> v2[1:4] # 切片
[1, 2, 3] # 输出为普通列表类型
结果及问题:
- 上述实现虽然支持简单的索引与切片操作,但返回切片结果时,仅输出了一个普通的
list
或数组array
,没有利用Vector
类本身的优势。 - 目标:希望切片操作返回的结果仍然是类型为
Vector
的实例,例如Vector
的切片仍然是Vector([1, 2, 3])
。
5.2 切片的工作原理
在执行 my_seq[1:4]
这样的切片操作时,Python 会将其转化为对特殊方法 __getitem__
的调用;切片的法则如下:
- 如果是单一索引(如
my_seq[1]
),index
的值会直接传递给__getitem__
。 - 如果是切片语法(如
[1:4:2]
),Python 会将其转化为slice
对象,如:slice(1, 4, 2)
对象代表从索引 1 开始,步长为 2,直到索引 4 的切片。
- 如果切片操作中包含多个逗号,则
__getitem__
接收到的参数是一个tuple
,例如my_seq[1:4:2, 9]
会传递(slice(1, 4, 2), 9)
。
示例代码:探究 __getitem__
的行为
class MySeq:def __getitem__(self, index):return indexs = MySeq()
print(s[1]) # 输出:1
print(s[1:4]) # 输出:slice(1, 4, None)
print(s[1:4:2]) # 输出:slice(1, 4, 2)
print(s[1:4:2, 9]) # 输出:(slice(1, 4, 2), 9)
总结:
slice
是 Python 内置类型,拥有start
,stop
,step
等属性;- 切片中的多个逗号或组合会形成一个
tuple
;- 理解这些机制是正确解析切片语法的关键。
5.3 slice
类型的强大功能
使用 slice
对象可以清晰表达切片规则,并具有以下特性:
slice.indices(len)
方法能够根据序列长度,将切片范围和步长规范化。- 不需手动处理负索引或超出范围的边界。
示例代码:
print(slice(None, 10, 2).indices(5)) # 对长度为5的序列:结果为 (0, 5, 2)
print(slice(-3, None, None).indices(5)) # 结果为 (2, 5, 1)
解释:
- ‘ABCDE’[None:10:2] 等价于 ‘ABCDE’[0:5:2]
- ‘ABCDE’[-3:] 等价于 ‘ABCDE’[2:5]
5.4 改善 Vector
的切片行为
为了让切片操作的返回值仍是一个 Vector
实例,我们需要:
- 在
__getitem__
方法中检查参数key
是否为slice
对象; - 如果是
slice
,则用self._components
的切片结果创建并返回一个新的Vector
实例; - 如果是单一索引,直接从
_components
中返回结果。
改进后的代码:
from operator import indexclass Vector:def __init__(self, data):self._components = list(data)def __len__(self):return len(self._components)def __getitem__(self, key):if isinstance(key, slice):cls = type(self) # 获取类的构造函数return cls(self._components[key])key = index(key) # 确保 key 为整数return self._components[key]
代码解析:
- 处理
slice
的逻辑:判断key
是否为slice
类型,若是切片,则用self._components[key]
计算切片,并用结果创建新的Vector
; operator.index
的作用:确保单一索引是整数(避免传入浮点数)。
测试用例:
>>> v7 = Vector(range(7))
>>> v7[-1] # 单一索引
6
>>> v7[1:4] # 切片
Vector([1, 2, 3])
>>> v7[-1:] # 切片长度为1
Vector([6])
>>> v7[1, 2] # 不支持多维索引
Traceback (most recent call last):...
TypeError: 'tuple' object cannot be interpreted as an integer
解释:
v7[-1]
: 返回单个数值;v7[1:4]
: 返回一个新的Vector
,这是预期目标;v7[1, 2]
: 抛出错误,因为不支持多维索引。
cls(self._components[key])用法思考
1. cls
的基本概念
在 Python 中,cls
通常用于表示类本身。它在不同的上下文中可能有不同的获取方式,但本质上都是指向类本身。
2. cls
在 type(self)
中的使用
解释
在实例方法或特殊方法(如 __getitem__
)中,cls
可以通过 type(self)
动态获取。这里的 type(self)
返回的是当前实例 self
所属的类。
示例
class Vector:def __init__(self, data):self._components = list(data)def __getitem__(self, key):if isinstance(key, slice):cls = type(self) # 动态获取类类型,即 Vector 或其子类return cls(self._components[key]) # 构造新的 Vector 对象return self._components[key]class SubVector(Vector):passv = SubVector(range(5))
print(type(v)) # 输出:<class '__main__.SubVector'>
print(type(v[1:3])) # 输出:<class '__main__.SubVector'>
解析
- 当
v
是SubVector
的实例时,type(self)
返回SubVector
类。 - 因此,
cls(self._components[key])
实际上调用的是SubVector(self._components[key])
,确保返回的对象类型与调用实例一致。
3. cls
在 @classmethod
中的使用
解释
在类方法中,cls
作为第一个参数自动传递,表示调用该方法的类本身。这使得类方法可以独立于实例进行操作,并且可以动态地引用调用它的类。
示例
class BOSUtils:_global_bos_utils = Nonedef __init__(self, name):self.name = name@classmethoddef global_utils(cls, name):if not cls._global_bos_utils:cls._global_bos_utils = cls(name) # 调用当前类的构造函数return cls._global_bos_utilsclass MyBOSUtils(BOSUtils):passprint(BOSUtils.global_utils("A")) # 输出:<__main__.BOSUtils object>
print(MyBOSUtils.global_utils("B")) # 输出:<__main__.MyBOSUtils object>
解析
- 当通过
BOSUtils.global_utils("A")
调用时,cls
是BOSUtils
,因此返回的是BOSUtils
的实例。 - 当通过
MyBOSUtils.global_utils("B")
调用时,cls
是MyBOSUtils
,因此返回的是MyBOSUtils
的实例。 - 这种方式确保了无论调用者是哪个类,
cls
都会指向正确的类,从而返回相应类的实例。
4. cls
的相同点和区别
相同点
- 目标一致:无论是在
type(self)
动态获取的场景,还是在类方法中作为参数隐式传递,cls
的目标都是表示“当前类”。 - 动态性:两者都能动态地指向当前所属的类。
区别
- 获取方式:
- 在实例方法中,
cls
是通过type(self)
动态推断来的。 - 在类方法中,
cls
是作为参数自动传入的。
- 在实例方法中,
- 使用场景:
type(self)
适用于需要动态创建与当前实例对应的新实例的场景。- 类方法中的
cls
适用于类相关的操作,如创建单例或操作类属性。
!
operator.index
用法
1. 查找元素的索引
示例:查找列表中某个元素的索引
# 示例列表
fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry']# 查找 'cherry' 的索引
try:index = fruits.index("cherry")print(f"'cherry' 的索引位置为: {index}")
except ValueError:print("'cherry' 不在列表中")# 'cherry' 的索引位置为: 2
2. 检查元素是否存在
示例:检查元素是否存在,如果不存在则处理异常
# 示例列表
vowels = ['a', 'e', 'i', 'o', 'u']# 要查找的字符
char = 'p'# 查找字符的索引
try:index2 = vowels.index('a')print(f"'a' 在列表中的索引位置为: {index2}")index = vowels.index(char)print(f"'{char}' 在列表中的索引位置为: {index}")
except ValueError:print(f"'{char}' 不在列表中")# 'a' 在列表中的索引位置为: 0
# 'p' 不在列表中
3. 在指定范围内查找元素
示例:在列表的特定范围内查找元素
# 示例列表
test = ['a', 'e', 'i', 'o', 'g', 'l', 'i', 'u']# 要查找的字符及其范围
char = 'i'
start = 3
end = 7# 查找字符的索引
try:index = test.index(char, start, end)print(f"从索引 {start} 到 {end} 查找 '{char}',其索引位置为: {index}")
except ValueError:print(f"'{char}' 在指定范围内不存在")# 从索引 3 到 7 查找 'i',其索引位置为: 6
4. 在机器学习中的应用:独热编码
示例:使用 index()
方法进行独热编码
# 定义类别列表
categories = ['猫', '狗', '鸟', '鱼']# 要进行独热编码的动物列表
animals = ['狗', '鸟', '猫', '鱼', '狗', '猫']# 获取类别数量
num_categories = len(categories)# 初始化独热编码矩阵
one_hot_encoded = []# 进行独热编码
for animal in animals:try:# 使用 index() 找到动物在类别列表中的索引index = categories.index(animal)# 创建一个独热编码向量vector = [0] * num_categoriesvector[index] = 1one_hot_encoded.append(vector)except ValueError:# 如果动物不在类别列表中,处理异常(例如,忽略或添加一个新类别)print(f"'{animal}' 不在类别列表中,跳过或添加新类别。")# 这里选择跳过continue# 输出独热编码结果
for i, vec in enumerate(one_hot_encoded):print(f"{animals[i]} 的独热编码为: {vec}")
输出:
狗 的独热编码为: [0, 1, 0, 0]
鸟 的独热编码为: [0, 0, 1, 0]
猫 的独热编码为: [1, 0, 0, 0]
鱼 的独热编码为: [0, 0, 0, 1]
狗 的独热编码为: [0, 1, 0, 0]
猫 的独热编码为: [1, 0, 0, 0]
5. 在数据分析中的应用:查找特定数据点的索引
示例:使用 Pandas 查找满足条件的行的索引
import pandas as pd# 创建示例 DataFrame
data = {'A': [1, 2, 3, 4, 5],'B': [10, 20, 30, 40, 50]
}
df = pd.DataFrame(data)# 查找 'A' 列中值为 3 的行的索引
try:index = df.index[df['A'] == 3][0]print(f"满足条件 'A' == 3 的行的索引为: {index}")
except IndexError:print("没有满足条件的行")
输出:
满足条件 'A' == 3 的行的索引为: 2
工程中的注意点
- 避免返回错误的数据类型:
- 在
Vector
的切片中返回list
会丢失类的功能,因此必须返回Vector
实例。
- 在
- 使用
operator.index
:- 比直接用
int()
更严格,确保传入参数为整数,避免浮点数误用。
- 比直接用
- 不支持多维索引:
- 默认的
__getitem__
设计只支持一维索引或一维切片,若需支持高维数据,需另外实现逻辑。
- 默认的
额外示例:实现二维切片支持
如果需要支持二维切片,比如 matrix[1:3, 2:4]
,需改写 __getitem__
方法,将 key
判定为元组:
class Matrix:def __init__(self, data):self.data = datadef __getitem__(self, key):if isinstance(key, tuple):rows, cols = keyreturn [row[cols] for row in self.data[rows]]return self.data[key]
示例数据与测试:
matrix = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(matrix[1:3, 1:]) # 输出:[[5, 6], [8, 9]]
总结
- 自定义类需尽量贴近 Python 标准库的行为,提升可用性;
- 切片背后的
slice
机制非常强大,可以帮助开发者处理复杂的索引逻辑; Vector
类通过检测切片并返回自身实例,避免了功能损失,是自定义序列类的良好实践;
希望你能熟练掌握相关技巧,提升 Python 面向对象设计能力!
6、Vector Take #3: Dynamic Attribute Access
6.1 动态属性访问的需求
背景:
在从 Vector2d
类进化到 Vector
类的过程中,我们希望能够通过类似 v.x
, v.y
这样的属性名称来访问向量的分量,而不仅仅是通过索引 v[0]
, v[1]
等。这种方式对于具有大量分量的向量来说,可以提高代码的可读性和简洁性。
目标:
提供一种替代语法,使得用户可以通过 v.x
, v.y
, v.z
, v.t
访问向量的前四个分量。
示例:
>>> v = Vector(range(10))
>>> v.x
0.0
>>> v.y, v.z, v.t
(1.0, 2.0, 3.0)
6.2 实现动态属性访问:使用 __getattr__
6.2.1 __getattr__
的作用
__getattr__
是一个特殊方法,当属性查找失败时,Python 会调用它。换句话说,当用户尝试访问一个不存在的属性时,__getattr__
会被触发。
工作流程:
-
属性查找顺序:
- 首先,Python 在实例中查找属性。
- 如果未找到,则在类中查找。
- 如果仍未找到,则在继承链中查找。
- 如果最终未找到,则调用
__getattr__
方法。
-
__getattr__
的参数:self
: 当前实例。name
: 尝试访问的属性名称(字符串形式)。
示例:
# vector_v3.py 部分代码:向 Vector 类添加 __getattr__ 方法
__match_args__ = ('x', 'y', 'z', 't')def __getattr__(self, name):cls = type(self)try:pos = cls.__match_args__.index(name)except ValueError:pos = -1if 0 <= pos < len(self._components):return self._components[pos]msg = f'{cls.__name__!r} object has no attribute {name!r}'raise AttributeError(msg)
解释:
__match_args__
: 一个元组,存储了允许动态访问的属性名称(x
,y
,z
,t
)。- 查找属性位置:
- 使用
index()
方法在__match_args__
中查找属性名称的位置。 - 如果属性名称不在
__match_args__
中,index()
会引发ValueError
,此时将pos
设置为-1
。
- 使用
- 返回对应分量:
- 如果
pos
在self._components
的索引范围内,则返回对应的分量。 - 例如,
v.x
会返回self._components[0]
。
- 如果
- 属性不存在:
- 如果属性名称不在
__match_args__
中,则引发AttributeError
,提示属性不存在。
- 如果属性名称不在
示例运行:
>>> v = Vector(range(5))
>>> v.x
0.0
>>> v.y, v.z, v.t
(1.0, 2.0, 3.0)
>>> v.a
AttributeError: 'Vector' object has no attribute 'a'
6.3 动态属性访问的问题:属性赋值导致的不一致性
6.3.1 问题描述
尽管 __getattr__
实现了动态属性访问,但它只处理属性读取。当用户尝试对动态属性进行赋值时,会出现不一致的行为。
示例:
>>> v = Vector(range(5))
>>> v.x
0.0
>>> v.x = 10
>>> v.x
10
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])
解释:
- 初始状态:
v
是一个包含 5 个分量的向量,v.x
返回第一个分量0.0
。 - 赋值操作:
v.x = 10
试图将x
属性设置为10
。 - 结果:
v.x
返回10
,因为x
属性已经被赋值为10
。- 然而,
v
对象的_components
列表并未改变,仍然是[0.0, 1.0, 2.0, 3.0, 4.0]
。 - 这导致
v.x
和向量分量之间出现了不一致。
原因分析:
- 当用户对
v.x
进行赋值时,Python 会在实例中创建一个新的属性x
,并将其值设置为10
。 - 由于
x
属性已经存在于实例中,__getattr__
不会被调用,因此v.x
返回的是新赋值的10
,而不是向量分量。
6.3.2 解决方案:实现 __setattr__
为了避免这种不一致性,我们需要自定义 __setattr__
方法,控制属性的设置行为。
目标:
- 禁止对所有单字母小写属性名称进行赋值,以避免与受支持的只读属性
x
,y
,z
,t
混淆。 - 如果用户尝试赋值,则引发
AttributeError
。
实现方法:
# vector_v3.py 部分代码:向 Vector 类添加 __setattr__ 方法
def __setattr__(self, name, value):cls = type(self)if len(name) == 1:if name in cls.__match_args__:error = 'readonly attribute {attr_name!r}'elif name.islower():error = "can't set attributes 'a' to 'z' in {cls_name!r}"else:error = ''else:error = ''if error:msg = error.format(cls_name=cls.__name__, attr_name=name)raise AttributeError(msg)super().__setattr__(name, value)
解释:
- 单字母属性处理:
- 只读属性: 如果属性名称是
__match_args__
中的一员,则使用特定的错误消息'readonly attribute {attr_name!r}'
。 - 小写字母: 如果属性名称是小写字母,则使用通用的错误消息
"can't set attributes 'a' to 'z' in {cls_name!r}"
。 - 其他情况: 允许赋值。
- 只读属性: 如果属性名称是
- 错误处理:
- 如果存在非空错误消息,则引发
AttributeError
,并显示相应的错误消息。
- 如果存在非空错误消息,则引发
- 默认行为:
- 对于其他属性名称,调用
super().__setattr__
以执行标准赋值行为。
- 对于其他属性名称,调用
示例运行:
>>> v = Vector(range(5))
>>> v.x
0.0
>>> v.x = 10
AttributeError: "readonly attribute 'x'"
>>> v.a = 5
AttributeError: "can't set attributes 'a' to 'z' in 'Vector'"
>>> v.B = 5 # 允许赋值,因为 'B' 不是单字母小写
>>> v.B
5
解释:
- 尝试对
v.x
进行赋值会引发AttributeError
,因为x
是只读属性。 - 尝试对
v.a
进行赋值也会引发AttributeError
,因为a
是单字母小写属性。 - 尝试对
v.b
进行赋值是允许的,因为b
不是单字母小写属性。
6.4 进一步解释:为什么需要 __setattr__
__setattr__
是控制属性赋值的核心方法。当用户对对象进行赋值操作时,Python 会调用 __setattr__
方法。因此,通过重写 __setattr__
,我们可以控制哪些属性可以被赋值,哪些不可以。
对比示例:
假设我们没有实现 __setattr__
,而是仅仅依赖 __getattr__
:
# vector_v2.py 部分代码:只有 __getattr__,没有 __setattr__
__match_args__ = ('x', 'y', 'z', 't')def __getattr__(self, name):cls = type(self)try:pos = cls.__match_args__.index(name)except ValueError:pos = -1if 0 <= pos < len(self._components):return self._components[pos]msg = f'{cls.__name__!r} object has no attribute {name!r}'raise AttributeError(msg)
问题:
- 用户可以成功对
v.x
进行赋值,但不会影响向量分量,导致数据不一致。
示例:
>>> v = Vector(range(5))
>>> v.x
0.0
>>> v.x = 10
>>> v.x
10
>>> v._components
[0.0, 1.0, 2.0, 3.0, 4.0]
解释:
v.x = 10
实际上是在实例中创建了一个新的属性x
,而不是修改向量分量。- 这导致
v.x
返回10
,但向量分量仍然是[0.0, 1.0, 2.0, 3.0, 4.0]
,造成了数据不一致。
解决方案:
- 通过实现
__setattr__
,我们可以阻止这种赋值行为,确保v.x
始终反映向量分量。
6.5 总结
- 动态属性访问: 通过
__getattr__
可以实现动态属性访问,但需要谨慎处理属性赋值。 - 属性赋值控制: 通过
__setattr__
可以控制哪些属性可以被赋值,哪些不可以,从而避免数据不一致。 - 只读属性: 通过自定义
__setattr__
可以有效地将某些属性设置为只读,确保对象状态不被意外修改。 - 避免过度使用
__slots__
: 仅在需要节省内存时使用__slots__
,而不是为了控制属性设置。
7、Vector Take #4: Hashing and a Faster ==
7.1 哈希方法 __hash__
的实现
7.1.1 背景与需求
在 Python 中,如果希望对象能够被存储在集合(如 set
或 dict
)中,需要实现 __hash__
和 __eq__
方法。__hash__
方法用于生成对象的哈希值,而 __eq__
方法用于定义对象的相等性。
对于 Vector
类,我们希望能够对包含大量分量的向量进行高效哈希计算。
7.1.2 原始方法回顾
在 Vector2d
类中,__hash__
方法的实现如下:
def __hash__(self):return hash((self.x, self.y))
缺点:
- 对于包含数千个分量的向量,构建一个元组并对其进行哈希计算会非常耗时且占用大量内存。
7.1.3 改进方法:使用 functools.reduce
和 operator.xor
为了提高效率,我们采用以下策略:
- 逐个计算每个分量的哈希值,而不是一次性构建整个元组。
- 使用异或(
^
)运算符 将所有哈希值累积起来,最终得到一个单一的哈希值。
实现步骤:
-
导入必要的模块:
import functools import operator
-
实现
__hash__
方法:def __hash__(self):hashes = (hash(x) for x in self._components) # 1. 生成器表达式逐个计算哈希值return functools.reduce(operator.xor, hashes, 0) # 2. 使用 reduce 和 xor 聚合哈希值
- 生成器表达式
hashes
:惰性计算每个分量的哈希值,节省内存。 functools.reduce(operator.xor, hashes, 0)
:operator.xor
作为聚合函数。hashes
作为可迭代对象。0
作为初始值,确保在空序列时不会抛出异常。
- 生成器表达式
reduce方法
简介
functools.reduce
是一个函数,它将一个二元函数(接受两个参数)依次应用于一个序列的元素,累积结果。例如,对序列 [a, b, c, d]
,reduce
会计算 f(f(f(a, b), c), d)
。
例子讲解
在你的 __hash__
方法中:
def __hash__(self):hashes = (hash(x) for x in self._components) # 生成每个元素的哈希值return functools.reduce(operator.xor, hashes, 0) # 使用异或操作累积哈希值
- 生成哈希值:
(hash(x) for x in self._components)
生成一个生成器,逐个计算self._components
中每个元素的哈希值。 - 使用
reduce
和xor
:functools.reduce(operator.xor, hashes, 0)
将operator.xor
(按位异或)应用于hashes
中的所有哈希值,初始值为0
。因为 0 ^ x = x,不会改变任何元素的值。结果仍然是该数本身。例如,0 ^ x = x。- 具体过程:
0 ^ hash1 = hash1
hash1 ^ hash2 = hash1 ^ hash2
(hash1 ^ hash2) ^ hash3 = hash1 ^ hash2 ^ hash3
- 依此类推,最终得到所有哈希值的按位异或结果。
- 具体过程:
这种做法通过按位异或将多个哈希值组合成一个单一的哈希值,确保组合后的哈希值能反映所有组成部分的变化。
优点:
- 效率更高:避免了构建整个元组的过程。
- 内存占用更低:使用生成器表达式而不是列表。
7.1.4 优化 __hash__
方法:使用 map
替代生成器表达式
生成器表达式和 map 函数都返回迭代器
简洁性: 对于简单的函数应用,map 更简洁。例如,map(str, range(5)) 比 (str(x) for x in range(5)) 更简洁。
可读性: 对于更复杂的操作,生成器表达式可能更具可读性,因为它允许更复杂的表达式和逻辑。
函数式编程: map 更符合函数式编程风格,强调函数的映射和组合。
在 Python 3 中,map
函数返回一个惰性迭代器,类似于生成器表达式,因此可以用 map
替代生成器表达式:
def __hash__(self):hashes = map(hash, self._components) # 使用 map 替代生成器表达式return functools.reduce(operator.xor, hashes)
注意: 在 Python 2 中,map
会构建一个完整的列表,效率较低,因此推荐使用生成器表达式。
7.1.5 完整示例
from array import array
import reprlib
import math
import functools
import operatorclass Vector:typecode = 'd'def __init__(self, components):self._components = array(self.typecode, components)def __eq__(self, other):return len(self) == len(other) and all(a == b for a, b in zip(self, other)) # 更高效的比较方法def __hash__(self):hashes = map(hash, self._components) # 使用 map 替代生成器表达式return functools.reduce(operator.xor, hashes) # 使用 reduce 和 xor 聚合哈希值def __len__(self):return len(self._components)def __iter__(self):return iter(self._components)def __repr__(self):components = reprlib.repr(self._components)return 'Vector({})'.format(components)
7.2 高效比较方法 __eq__
的实现
7.2.1 原始方法回顾
最初的 __eq__
方法如下:
def __eq__(self, other):return tuple(self) == tuple(other)
tuple()
函数会尝试将任何可迭代对象转换为一个元组。当我们调用 tuple(self)
时:
-
可迭代性:
tuple()
函数会调用传入对象的迭代器协议,即__iter__()
方法或__getitem__()
方法。 -
在7.1.5中,我们已经实现了
__iter__()
方法,使得Vector
实例可以被迭代。 -
__iter__()
方法返回self._components
的迭代器。 -
对于
Vector([1, 2])
,self._components
是[1, 2]
,所以__iter__()
返回的是[1, 2]
的迭代器。
缺点:
- 对于包含数千个分量的向量,构建两个元组并进行比较会非常耗时且占用大量内存。
- 这种方法还会导致
Vector([1, 2]) == (1, 2)
返回True
,这可能不是我们想要的结果。
7.2.2 改进方法:使用 zip
和 for
循环
为了提高效率,我们采用以下策略:
- 逐个比较对应分量,而不是一次性构建整个元组。
- 使用
zip
函数 将两个向量的分量配对进行迭代。
实现步骤:
-
检查长度是否相同:
if len(self) != len(other):return False
-
使用
zip
迭代对应分量并比较:for a, b in zip(self, other):if a != b:return False return True
完整实现:
def __eq__(self, other):if len(self) != len(other):return Falsefor a, b in zip(self, other):if a != b:return Falsereturn True
不会将 Vector
和元组视为相等,除非明确实现。
if not isinstance(other, Vector):return False
7.2.3 进一步优化:使用 all
函数
我们可以使用 all
函数将上述逻辑简化为一行:
def __eq__(self, other):return len(self) == len(other) and all(a == b for a, b in zip(self, other))
zip
函数的用法
zip
是 Python 内置的一个函数,用于将多个可迭代对象(如列表、元组、字符串等)“压缩”在一起,生成一个由元组组成的迭代器。每个元组包含来自每个可迭代对象的对应元素。
示例:
list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']zipped = zip(list1, list2)
print(list(zipped)) # 输出: [(1, 'a'), (2, 'b'), (3, 'c')]
在 __eq__
方法中,zip(self, other)
会将 self
和 other
的对应元素配对。例如:
v1 = Vector([1, 2, 3])
v2 = Vector([1, 2, 3])zipped = zip(v1, v2)
print(list(zipped)) # 输出: [(1, 1), (2, 2), (3, 3)]
all
函数的用法
all
是 Python 内置的一个函数,用于判断给定的可迭代对象中的所有元素是否都为 True
。如果所有元素都为 True
,则返回 True
;否则返回 False
。
语法:
all(iterable)
示例:
# 所有元素都为 True
result1 = all([True, True, True])
print(result1) # 输出: True# 有一个元素为 False
result2 = all([True, False, True])
print(result2) # 输出: False
在 __eq__
方法中,all(a == b for a, b in zip(self, other))
的工作原理如下:
- 生成比较结果:
a == b for a, b in zip(self, other)
会生成一个生成器,依次产生self
和other
中对应元素的比较结果。例如,对于Vector([1, 2, 3])
和Vector([1, 2, 3])
,生成器会依次产生True
,True
,True
。 - 检查所有结果:
all
函数会检查生成器中的所有结果。如果所有比较结果都是True
,则all
返回True
;否则返回False
。
示例:
v1 = Vector([1, 2, 3])
v2 = Vector([1, 2, 3])result = all(a == b for a, b in zip(v1, v2))
print(result) # 输出: True
8、Vector Take #5: Formatting
8.1 背景与目标
在之前的章节中,我们已经实现了一个兼容 Vector2d
的多维 Vector
类。本章的目标是进一步完善 Vector
类,使其支持自定义格式化输出,特别是使用超球坐标系(也称为“超球坐标系”)来展示向量。
超球坐标系是笛卡尔坐标系在高维空间中的推广,适用于 4 维及更高维度空间中的球体表示。与 Vector2d
中使用的极坐标表示法(‘p’)不同,Vector
类将使用超球坐标表示法,并采用新的格式代码 'h'
来表示。
8.2 关键概念解析
8.2.1 格式规范迷你语言(Format Specification Mini-Language)
Python 的字符串 format()
方法支持一种迷你语言,用于定义格式化输出的细节。在扩展这种迷你语言时,需要注意以下几点:
-
避免使用内置类型已支持的格式代码:例如,整数使用
'bcdoxXn'
,浮点数使用'eEfFgGn%'
,字符串使用's'
。为了避免冲突,应选择未被占用的格式代码。示例对比:
# 使用内置类型支持的格式代码
v = Vector([1, 2, 3])
print(format(v, 'e')) # 可能引发冲突或意外行为# 使用自定义格式代码 'h'
print(format(v, 'h')) # 输出超球坐标表示
- 选择合适的自定义格式代码:原文选择了
'h'
作为超球坐标的格式代码,这是一个合理的选择,因为它与向量(Vector)的概念相关,并且未被其他内置类型使用。
8.2.2 超球坐标系与笛卡尔坐标系的区别
-
笛卡尔坐标系:使用沿各轴的坐标值表示点的位置,例如
(x, y, z)
。 -
超球坐标系:使用半径(向量长度)和一系列角度来表示点的位置,例如
<r, Φ₁, Φ₂, Φ₃>
。r
是向量的长度,计算方式为abs(v)
,即sqrt(x² + y² + z² + ...)
Φ₁, Φ₂, Φ₃
是各个角度分量,具体计算方法可参考维基百科的“n-sphere”词条。
示例:
v = Vector([-1, -1, -1, -1])
print(format(v, 'h'))
# 输出: '<2.0, 2.0943951023931957, 2.186276035465284, 3.9269908169872414>'
"""
A ``Vector`` is built from an iterable of numbers::>>> Vector([3.1, 4.2])Vector([3.1, 4.2])>>> Vector((3, 4, 5))Vector([3.0, 4.0, 5.0])>>> Vector(range(10))Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])Tests with two dimensions (same results as ``vector2d_v1.py``)::>>> v1 = Vector([3, 4])>>> x, y = v1>>> x, y(3.0, 4.0)>>> v1Vector([3.0, 4.0])>>> v1_clone = eval(repr(v1))>>> v1 == v1_cloneTrue>>> print(v1)(3.0, 4.0)>>> octets = bytes(v1)>>> octetsb'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'>>> abs(v1)5.0>>> bool(v1), bool(Vector([0, 0]))(True, False)Test of ``.frombytes()`` class method:>>> v1_clone = Vector.frombytes(bytes(v1))>>> v1_cloneVector([3.0, 4.0])>>> v1 == v1_cloneTrueTests with three dimensions::>>> v1 = Vector([3, 4, 5])>>> x, y, z = v1>>> x, y, z(3.0, 4.0, 5.0)>>> v1Vector([3.0, 4.0, 5.0])>>> v1_clone = eval(repr(v1))>>> v1 == v1_cloneTrue>>> print(v1)(3.0, 4.0, 5.0)>>> abs(v1) # doctest:+ELLIPSIS7.071067811...>>> bool(v1), bool(Vector([0, 0, 0]))(True, False)Tests with many dimensions::>>> v7 = Vector(range(7))>>> v7Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])>>> abs(v7) # doctest:+ELLIPSIS9.53939201...Test of ``.__bytes__`` and ``.frombytes()`` methods::>>> v1 = Vector([3, 4, 5])>>> v1_clone = Vector.frombytes(bytes(v1))>>> v1_cloneVector([3.0, 4.0, 5.0])>>> v1 == v1_cloneTrueTests of sequence behavior::>>> v1 = Vector([3, 4, 5])>>> len(v1)3>>> v1[0], v1[len(v1)-1], v1[-1](3.0, 5.0, 5.0)Test of slicing::>>> v7 = Vector(range(7))>>> v7[-1]6.0>>> v7[1:4]Vector([1.0, 2.0, 3.0])>>> v7[-1:]Vector([6.0])>>> v7[1,2]Traceback (most recent call last):...TypeError: 'tuple' object cannot be interpreted as an integerTests of dynamic attribute access::>>> v7 = Vector(range(10))>>> v7.x0.0>>> v7.y, v7.z, v7.t(1.0, 2.0, 3.0)Dynamic attribute lookup failures::>>> v7.kTraceback (most recent call last):...AttributeError: 'Vector' object has no attribute 'k'>>> v3 = Vector(range(3))>>> v3.tTraceback (most recent call last):...AttributeError: 'Vector' object has no attribute 't'>>> v3.spamTraceback (most recent call last):...AttributeError: 'Vector' object has no attribute 'spam'Tests of hashing::>>> v1 = Vector([3, 4])>>> v2 = Vector([3.1, 4.2])>>> v3 = Vector([3, 4, 5])>>> v6 = Vector(range(6))>>> hash(v1), hash(v3), hash(v6)(7, 2, 1)Most hash codes of non-integers vary from a 32-bit to 64-bit CPython build::>>> import sys>>> hash(v2) == (384307168202284039 if sys.maxsize > 2**32 else 357915986)TrueTests of ``format()`` with Cartesian coordinates in 2D::>>> v1 = Vector([3, 4])>>> format(v1)'(3.0, 4.0)'>>> format(v1, '.2f')'(3.00, 4.00)'>>> format(v1, '.3e')'(3.000e+00, 4.000e+00)'Tests of ``format()`` with Cartesian coordinates in 3D and 7D::>>> v3 = Vector([3, 4, 5])>>> format(v3)'(3.0, 4.0, 5.0)'>>> format(Vector(range(7)))'(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'Tests of ``format()`` with spherical coordinates in 2D, 3D and 4D::>>> format(Vector([1, 1]), 'h') # doctest:+ELLIPSIS'<1.414213..., 0.785398...>'>>> format(Vector([1, 1]), '.3eh')'<1.414e+00, 7.854e-01>'>>> format(Vector([1, 1]), '0.5fh')'<1.41421, 0.78540>'>>> format(Vector([1, 1, 1]), 'h') # doctest:+ELLIPSIS'<1.73205..., 0.95531..., 0.78539...>'>>> format(Vector([2, 2, 2]), '.3eh')'<3.464e+00, 9.553e-01, 7.854e-01>'>>> format(Vector([0, 0, 0]), '0.5fh')'<0.00000, 0.00000, 0.00000>'>>> format(Vector([-1, -1, -1, -1]), 'h') # doctest:+ELLIPSIS'<2.0, 2.09439..., 2.18627..., 3.92699...>'>>> format(Vector([2, 2, 2, 2]), '.3eh')'<4.000e+00, 1.047e+00, 9.553e-01, 7.854e-01>'>>> format(Vector([0, 1, 0, 0]), '0.5fh')'<1.00000, 1.57080, 0.00000, 0.00000>'
"""
from array import array
import reprlib
import math
import functools
import operator
import itertoolsclass Vector:typecode = 'd'def __init__(self, components):self._components = array(self.typecode, components)def __iter__(self):return iter(self._components)def __repr__(self):components = reprlib.repr(self._components)components = components[components.find('['):-1]return f'Vector({components})'def __str__(self):return str(tuple(self))def __bytes__(self):return (bytes([ord(self.typecode)]) +bytes(self._components))def __eq__(self, other):return (len(self) == len(other) andall(a == b for a, b in zip(self, other)))def __hash__(self):hashes = (hash(x) for x in self)return functools.reduce(operator.xor, hashes, 0)def __abs__(self):return math.hypot(*self)def __bool__(self):return bool(abs(self))def __len__(self):return len(self._components)def __getitem__(self, key):if isinstance(key, slice):cls = type(self)return cls(self._components[key])index = operator.index(key)return self._components[index]__match_args__ = ('x', 'y', 'z', 't')def __getattr__(self, name):cls = type(self)try:pos = cls.__match_args__.index(name)except ValueError:pos = -1if 0 <= pos < len(self._components):return self._components[pos]msg = f'{cls.__name__!r} object has no attribute {name!r}'raise AttributeError(msg)def angle(self, n):r = math.hypot(*self[n:])a = math.atan2(r, self[n-1])if (n == len(self) - 1) and (self[-1] < 0):return math.pi * 2 - aelse:return adef angles(self):return (self.angle(n) for n in range(1, len(self)))def __format__(self, fmt_spec=''):if fmt_spec.endswith('h'): # 判断是否为超球面坐标格式fmt_spec = fmt_spec[:-1] # 移除 'h',获取后续的格式规范coords = itertools.chain([abs(self)], self.angles()) # 生成器:先添加径向距离,再添加所有角度坐标outer_fmt = '<{}>' # 超球面坐标使用尖括号包围else:coords = self # 笛卡尔坐标直接使用向量自身outer_fmt = '({})' # 笛卡尔坐标使用圆括号包围# 使用生成器表达式按需格式化每个坐标项components = (format(c, fmt_spec) for c in coords)# 组装最终字符串return outer_fmt.format(', '.join(components))@classmethoddef frombytes(cls, octets):typecode = chr(octets[0])memv = memoryview(octets[1:]).cast(typecode)return cls(memv)v = Vector([-1, -1, -1, -1])
print(format(v, 'h'))
<2.0, 2.0943951023931957, 2.186276035465284, 3.9269908169872414>
8.2.3 关于__abs__
的解读
def __abs__(self):return math.hypot(*self)
math.hypot 函数的作用
math.hypot 是 Python 数学模块中的一个函数,用于计算直角三角形的斜边长度(即欧几里得范数)。它可以接受多个参数,计算这些数的平方和的平方根。
数学表达式为:
hypot(x, y, z, ...) = sqrt(x² + y² + z² + ...)
*self 的含义–为什么 v = Vector([3, 4, 0]) 能被解包为 3, 4, 0?
关键在于两个 Python 特性的结合:
- iter 方法的作用
在Vector 类中实现了 iter 方法:
def __iter__(self):return iter(self._components)
这使得 Vector 实例成为可迭代对象。当 Python 遇到需要迭代的场景时(比如 * 解包操作、for 循环等),就会调用这个方法。
- *解包操作的工作原理
当你在函数调用中使用 * 解包操作时:
math.hypot(*self)
Python 会:
- 检查 self 是否可迭代(即是否有 iter 方法)
- 调用 iter(self) 获取迭代器
- 逐个取出元素作为单独的参数
- 具体执行流程
- *v 触发解包操作
- Python 调用
v.__iter__()
- 得到 self._components 的迭代器(这是一个 array(‘d’, [3.0, 4.0, 0.0]) 的迭代器)
- 逐个解包出 3.0, 4.0, 0.0
- 最终相当于调用 math.hypot(3.0, 4.0, 0.0)
iter(obj) 调用 obj.__iter__()
,返回迭代器,这个不就是自己调用自己吗
1. 关键区别:iter(obj)
vs obj.__iter__()
虽然 iter(obj)
会调用 obj.__iter__()
,但它们是不同层级的操作:
iter(obj)
是Python内置函数(高级接口)obj.__iter__()
是对象的方法实现(底层接口)
2. 实际执行流程
以 Vector
类为例:
v = Vector([1, 2, 3])
for x in v: # 触发 iter(v)print(x)
执行步骤:
for
循环首先调用iter(v)
→ 这会寻找v.__iter__()
- 执行你定义的
__iter__
方法:def __iter__(self):return iter(self._components) # 注意这里!
- 这个
iter(self._components)
调用的是array
类型的迭代器,不是Vector
的__iter__
3. 为什么不会无限递归?
因为 self._components
是一个 array.array
对象,它有自己独立的 __iter__
实现:
# 伪代码展示array的迭代实现
class array:def __iter__(self):return array_iterator(self) # 返回专门的迭代器对象
所以调用链是:
Vector.__iter__() → iter(array) → array.__iter__()
没有形成循环调用。
4. 类比:文件读取的迭代器
想象你有一个文件类:
class TextFile:def __iter__(self):return iter(self._fileobj) # 委托给底层的文件对象# 不是 return iter(self) ! 这才会导致递归
这里的 self._fileobj
是真正的文件对象,有自己的迭代逻辑。
5. 如果错误实现会导致递归
如果错误地写成:
def __iter__(self):return iter(self) # 错误!这会真的导致无限递归
这时调用链就是:
iter(self) → self.__iter__() → iter(self) → ...
Python会检测到这种递归并抛出 RecursionError
。
6. 正确的设计模式
这种写法是迭代器委托模式(Iterator Delegation):
- 你的类(
Vector
)声明自己可迭代 - 但实际迭代工作委托给底层数据结构(
array
) - 符合"组合优于继承"的原则
总结表
代码 | 实际调用 | 结果 |
---|---|---|
iter(v) | v.__iter__() | 获取Vector的迭代器 |
iter(self._components) | array.__iter__() | 获取array的迭代器 |
iter(self) ❌ | 循环调用 __iter__ | 递归错误 |
这种设计让 Vector
既保持了可迭代性,又把实际迭代工作交给更适合的 array
类型来处理,是Python中非常优雅的实现方式。
8.2.4 itertools.chain([abs(self)], self.angles())
解读
itertools.chain([abs(self)], self.angles())
是Python中用于将多个可迭代对象连接成一个连续迭代器的操作。下面通过具体示例讲解其工作原理和应用
- 基本结构解析
itertools.chain([abs(self)], self.angles())
- [abs(self)]:将向量的模(标量值)转换为单元素列表
- self.angles():生成器,产生向量的所有角度值
- chain():将上述两部分连接为单一迭代器
- 典型应用场景(以3D向量为例)
import itertools
import mathclass Vector:# ... (其他方法省略)def angles(self):return (math.atan2(math.hypot(self.y, self.z), self.x), math.atan2(self.z, self.y))v = Vector([1, 1, 1])
combined = itertools.chain([abs(v)], v.angles())
print(list(combined))
输出结果:
[1.7320508075688772, 0.6154797086703874, 0.7853981633974483]
解析:
- abs(v) = √(1²+1²+1²) ≈ 1.732(模长)
- 第一个角度:xy平面与x轴的夹角 ≈ 0.615弧度
- 第二个角度:z轴与y轴的夹角 ≈ 0.785弧度(π/4)
- 与直接列表拼接的区别
chain() 惰性求值,不创建中间列表 处理大型数据集或生成器时更高效
[abs(v)] + list(v.angles()) 立即创建完整列表 需要立即获取所有结果时使用
这种组合方式常用于:
科学计算中的坐标转换
避免临时列表的内存开销
流式处理大规模几何数据
8.2.5 (format(c, fmt_spec) for c in coords
代码解析
components = (format(c, fmt_spec) for c in coords)
return outer_fmt.format(', '.join(components))
这两行代码做了以下事情:
-
生成器表达式:
(format(c, fmt_spec) for c in coords)
创建了一个生成器,它会:- 遍历
coords
中的每个元素c
- 对每个
c
应用format(c, fmt_spec)
进行格式化 - 按需生成格式化后的字符串(惰性求值)
- 遍历
-
字符串拼接:
', '.join(components)
将生成器产生的所有格式化后的字符串用逗号和空格连接起来 -
最终格式化:
outer_fmt.format(...)
将拼接好的字符串放入外层格式(尖括号或圆括号)中
示例1:笛卡尔坐标格式化
v = Vector([1.23456, 2.34567, 3.45678])
print(format(v, '.2f')) # 使用两位小数格式化
执行过程:
coords
是[1.23456, 2.34567, 3.45678]
(因为不是’h’格式)- 生成器表达式对每个元素应用
format(c, '.2f')
:- 1.23456 → “1.23”
- 2.34567 → “2.35”
- 3.45678 → “3.46”
', '.join(...)
→ “1.23, 2.35, 3.46”outer_fmt
是圆括号,所以最终输出:`(1.23, 2.35, 3.46)
示例2:超球面坐标格式化
v = Vector([1, 1, 1]) # 3D向量
print(format(v, '.3fh')) # 超球面坐标,保留3位小数
执行过程:
- fmt_spec是’.3f’(去掉’h’)
- coords是生成器itertools.chain([abs(self)], self.angles()):
- 先计算模长:√(1²+1²+1²) ≈ 1.732
- 然后计算角度(这里简化说明)
- 生成器表达式对每个坐标值应用.3f格式化。结果放入尖括号中,如:<1.732, 0.785, 0.955>
为什么使用生成器表达式?
内存效率:对于大型向量,生成器不会一次性创建所有格式化字符串的列表,而是按需生成
代码简洁:一行代码完成了遍历、格式化和生成
延迟计算:只有在join()需要时才会实际执行格式化
章节总结
本章中的Vector
示例旨在与Vector2d
兼容,但构造函数的签名有所不同,它像内置序列类型一样接受单个可迭代参数。Vector
仅通过实现__getitem__
和__len__
方法就表现得像一个序列,这一事实引发了对协议的讨论,协议是鸭子类型语言中使用的非正式接口。
然后,我们研究了my_seq[a:b:c]
语法在幕后的工作原理,即创建一个slice(a, b, c)
对象并将其传递给__getitem__
。有了这些知识,我们让Vector
能够正确响应切片操作,返回新的Vector
实例,就像Python序列所期望的那样。
接下来,我们通过实现__getattr__
方法,提供了使用my_vec.x
等表示法对Vector
的前几个分量进行只读访问的功能。这样做可能会诱使用户通过my_vec.x = 7
这样的赋值操作来修改这些特殊分量,从而暴露出一个潜在的问题。我们通过实现__setattr__
方法来修复这个问题,禁止对单字母属性进行赋值。通常,在编写__getattr__
方法时,也需要添加__setattr__
方法,以避免出现不一致的行为。
实现__hash__
函数为使用functools.reduce
提供了完美的场景,因为我们需要对Vector
的所有分量的哈希值依次应用异或运算符^
,以生成整个向量的聚合哈希码。在__hash__
中使用reduce
之后,我们使用内置的all
函数创建了一个更高效的__eq__
方法。
对Vector
的最后一项增强是重新实现Vector2d
中的__format__
方法,支持使用球面坐标作为默认笛卡尔坐标的替代方案。我们在编写__format__
及其辅助函数时使用了大量数学运算和几个生成器,但这些都是实现细节 —— 我们将在第17章中再次讨论生成器。最后这部分的目标是支持自定义格式,从而实现Vector
能够完成Vector2d
的所有功能,甚至更多。
就像我们在第11章中所做的那样,在本章中我们也经常研究标准Python对象的行为,以模仿它们,为Vector
提供一种 “Python风格” 的外观和感觉。
在第16章中,我们将为Vector
实现几个中缀运算符。这里涉及的数学运算将比本章中angle()
方法的运算简单得多,但探索Python中中缀运算符的工作原理是面向对象设计中的重要一课。但在进行运算符重载之前,我们将暂时放下对单个类的研究,转而探讨如何通过接口和继承来组织多个类,这是第13章和第14章的主题。