当前位置: 首页 > java >正文

CHAPTER 12 Special Methods for Sequences

1、介绍

  1. 创建一个多维向量类 (Vector)

    • 表现为不可变 (immutable) 的扁平序列(flat sequence)。
    • 元素为浮点数 (float)。
    • 支持丰富的标准 Python 序列协议和自定义行为。
  2. 功能支持

    • 基本序列协议:
      • 实现 __len____getitem__ 方法。
    • 实例字符串的安全表示:
      • 适用于包含大量元素的实例。
    • 切片支持:
      • 完善切片功能,返回新的 Vector 实例。
    • 哈希支持:
      • 基于每个元素值的聚合散列值。
    • 自定义格式化:
      • 支持扩展的格式化语言(如 f-string 格式的自定义扩展)。
  3. 动态属性访问

    • 使用 __getattr__ 实现动态属性访问,替代早前 Vector2d 使用的只读属性。
    • 注意:这不是典型的序列类型行为,但是一种功能增强。
  4. 概念补充:以 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 的特殊方法(如魔术方法)创建一个自定义的序列类型。
  • 真实应用工具
    • 数学计算推荐使用科学计算工具:
      • NumPySciPy:处理高效向量和矩阵计算。
      • 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 内建数列类型(如 listtuple)的习惯,同时避免了一些不必要的参数控制逻辑。

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 关键点逐一解析

  1. 类变量 typecode

    • 使用 array 模块存储浮点数,需要指定类型。例如 'd'表示双精度浮点。
    • 将其定义为类变量可以在多个实例间复用,并且尽可能地减少硬编码。
  2. 构造函数 __init__

    • 接收一个任意可迭代对象,并将其存储为 array 类型,提供高效的存储和数值操作。
    • 例子:
v = Vector([3.1, 4.2])   # 2D 向量
print(v._components)     # array('d', [3.1, 4.2])
  1. 定制 __repr__ 方法

    • 用于调试和开发阶段,提供精简但信息丰富的字符串形式。
    • 使用 reprlib.repr 来适配较大的数据集,当向量元素较多时仅显示部分,剩余部分以 ... 表示。
    • 例子:
    Vector(range(10))
    # 输出: Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
    
  2. 定制 __str__ 方法

    • 提供用户友好型的输出,将向量显示为元组形式。
    • 例子:
    print(Vector((3, 4, 5)))  
    # 输出: (3.0, 4.0, 5.0)
    
  3. 实现序列化:__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])
    
  4. 模长计算 __abs__

    • 利用 math.hypot 求解 N 维向量的模长,比手动求平方和更高效。
    • 例子:
    v = Vector([3, 4, 5])
    print(abs(v))  # 输出: 7.0710678118654755
    
  5. 布尔值表示 __bool__

    • 模长为 0 时返回 False,否则 True
    • 例子:
    v1 = Vector([0, 0, 0])
    v2 = Vector([1, 2, 3])
    print(bool(v1))  # False
    print(bool(v2))  # True
    

3.4 工程中容易忽略的细节

  1. 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]  # 找到 '[' 开始,并切除末尾的 ')'
  • 直接操作原生数据结构:例如,直接基于 arraymemoryviewnumpy.array,而不是盲目转换为更轻量级的形式。
  • 量体裁衣使用工具:在本例中,reprlib.repr 支持任意对象,因此没必要为了适配它而引入开销昂贵的转换操作。
  1. absbool 的逻辑联系

    • __bool__ 的判断依赖于 __abs__ 方法,只要模长为 0,则认为这是一个零向量。
  2. 兼容性与扩展性

    • 如果直接从 Vector2d 类继承,构造器的参数可能带来复杂的兼容性问题。通过实现独立的 Vector 类,精细化控制每一种逻辑。

3.5 总结

通过这一版 Vector 类的实现,我们能够:

  1. 支持任意维度的向量;
  2. 通过定制化的特殊方法(如 __repr____str____eq__ 等),丰富了类的功能;
  3. 提供了高效的计算方式(如基于 math.hypot 的模长计算)。

这种设计方式非常贴合 Python 的数据类型设计哲学(例如 listtuple),同时通过模块化和类方法便捷地实现了序列化操作,值得在工程中深刻实践和学习。

4、Protocols and Duck Typing

4.1 什么是协议(Protocol)?

定义

在面向对象编程的上下文中,协议是一种非正式接口,它通过文档定义了对象必须遵守的规则,但在代码中这种规则是非强制的(不像 Java 的接口 interface 或 C++ 的具体抽象类)。协议的核心思想是,只需要实现与协议相关的方法且保证它们符合约定的行为签名,就可以让类表现出符合协议的行为。

Python 中的协议

  • 很多 Python 的内置功能和标准库都会基于协议工作。
  • 协议本身并不要求类显式继承某个特定的父类,只要实现了相关方法,这个类型的实例就可以用在期待协议对象的位置。

例子:
Python 的序列协议 (Sequence Protocol) 要求对象提供以下方法:

  1. __len__:返回对象的长度。
  2. __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 检查是否为某个类的实例时,实际会以以下方式工作:

  1. 检查对象是否直接继承指定的类型。
  2. 检查对象是否是该类型的子类的实例。
  3. 对于协议类型,检查该对象的类型是否"注册到"了相应的抽象基类中。

由于 FrenchDeck 只是动态实现了序列的行为,而不是显式地继承或注册到 Sequence 抽象基类,因此 isinstance(deck, Sequence) 会返回 False,尽管从行为上它确实类似序列。

4.3 协议的灵活性与潜在问题

由于协议是非正式的,并没有强制检查,因此:

  1. 优点:灵活性高,任何类实现方法即可符合。
  2. 缺点:在复杂代码中,可能导致潜在的错误。例如:
    • 没有实现协议所需的某些方法。
    • 方法实现不符合预期的行为或返回值类型。

例子:未完整实现协议导致的问题

假设我们遗漏了实现 __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. 工程实践中的要点

  1. 灵活性与可读性

    • 鸭子类型的灵活性强,但文档和代码的清晰度必须保证。
    • 推荐在注释或文档中描述协议要求(即方法签名与行为)。
  2. 默认实现

    • 如果协议要求多个方法,但某些方法调试使用频率低,可以提供合理的默认实现,以简化多类开发。
  3. 静态类型检查工具

    • 使用 Protocoltyping 模块定义接口,可以让代码在大型项目中更安全、更易维护。
  4. 优先协议而非继承

    • 除非明确需要类的通用父类型(如 dictlist),否则,更推荐基于行为(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__ 的调用;切片的法则如下:

  1. 如果是单一索引(如 my_seq[1]),index 的值会直接传递给 __getitem__
  2. 如果是切片语法(如 [1:4:2]),Python 会将其转化为 slice 对象,如:
    • slice(1, 4, 2) 对象代表从索引 1 开始,步长为 2,直到索引 4 的切片。
  3. 如果切片操作中包含多个逗号,则 __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 实例,我们需要:

  1. __getitem__ 方法中检查参数 key 是否为 slice 对象;
  2. 如果是 slice,则用 self._components 的切片结果创建并返回一个新的 Vector 实例;
  3. 如果是单一索引,直接从 _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. clstype(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'>

解析

  • vSubVector 的实例时,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") 调用时,clsBOSUtils,因此返回的是 BOSUtils 的实例。
  • 当通过 MyBOSUtils.global_utils("B") 调用时,clsMyBOSUtils,因此返回的是 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

工程中的注意点

  1. 避免返回错误的数据类型
    • Vector 的切片中返回 list 会丢失类的功能,因此必须返回 Vector 实例。
  2. 使用 operator.index
    • 比直接用 int() 更严格,确保传入参数为整数,避免浮点数误用。
  3. 不支持多维索引
    • 默认的 __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]]

总结

  1. 自定义类需尽量贴近 Python 标准库的行为,提升可用性;
  2. 切片背后的 slice 机制非常强大,可以帮助开发者处理复杂的索引逻辑;
  3. 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__ 会被触发。

工作流程:

  1. 属性查找顺序:

    • 首先,Python 在实例中查找属性。
    • 如果未找到,则在类中查找。
    • 如果仍未找到,则在继承链中查找。
    • 如果最终未找到,则调用 __getattr__ 方法。
  2. __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)

解释:

  1. __match_args__: 一个元组,存储了允许动态访问的属性名称(x, y, z, t)。
  2. 查找属性位置:
    • 使用 index() 方法在 __match_args__ 中查找属性名称的位置。
    • 如果属性名称不在 __match_args__ 中,index() 会引发 ValueError,此时将 pos 设置为 -1
  3. 返回对应分量:
    • 如果 posself._components 的索引范围内,则返回对应的分量。
    • 例如,v.x 会返回 self._components[0]
  4. 属性不存在:
    • 如果属性名称不在 __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])

解释:

  1. 初始状态: v 是一个包含 5 个分量的向量,v.x 返回第一个分量 0.0
  2. 赋值操作: v.x = 10 试图将 x 属性设置为 10
  3. 结果:
    • 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)

解释:

  1. 单字母属性处理:
    • 只读属性: 如果属性名称是 __match_args__ 中的一员,则使用特定的错误消息 'readonly attribute {attr_name!r}'
    • 小写字母: 如果属性名称是小写字母,则使用通用的错误消息 "can't set attributes 'a' to 'z' in {cls_name!r}"
    • 其他情况: 允许赋值。
  2. 错误处理:
    • 如果存在非空错误消息,则引发 AttributeError,并显示相应的错误消息。
  3. 默认行为:
    • 对于其他属性名称,调用 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 中,如果希望对象能够被存储在集合(如 setdict)中,需要实现 __hash____eq__ 方法。__hash__ 方法用于生成对象的哈希值,而 __eq__ 方法用于定义对象的相等性。

对于 Vector 类,我们希望能够对包含大量分量的向量进行高效哈希计算。

7.1.2 原始方法回顾

Vector2d 类中,__hash__ 方法的实现如下:

def __hash__(self):return hash((self.x, self.y))

缺点:

  • 对于包含数千个分量的向量,构建一个元组并对其进行哈希计算会非常耗时且占用大量内存。

7.1.3 改进方法:使用 functools.reduceoperator.xor

为了提高效率,我们采用以下策略:

  • 逐个计算每个分量的哈希值,而不是一次性构建整个元组。
  • 使用异或(^)运算符 将所有哈希值累积起来,最终得到一个单一的哈希值。

实现步骤:

  1. 导入必要的模块:

    import functools
    import operator
    
  2. 实现 __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)  # 使用异或操作累积哈希值
  1. 生成哈希值: (hash(x) for x in self._components) 生成一个生成器,逐个计算 self._components 中每个元素的哈希值。
  2. 使用 reducexor 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 改进方法:使用 zipfor 循环

为了提高效率,我们采用以下策略:

  • 逐个比较对应分量,而不是一次性构建整个元组。
  • 使用 zip 函数 将两个向量的分量配对进行迭代。

实现步骤:

  1. 检查长度是否相同:

    if len(self) != len(other):return False
    
  2. 使用 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) 会将 selfother 的对应元素配对。例如:

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)) 的工作原理如下:

  1. 生成比较结果: a == b for a, b in zip(self, other) 会生成一个生成器,依次产生 selfother 中对应元素的比较结果。例如,对于 Vector([1, 2, 3])Vector([1, 2, 3]),生成器会依次产生 True, True, True
  2. 检查所有结果: 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 特性的结合:

  1. iter 方法的作用

在Vector 类中实现了 iter 方法:

def __iter__(self):return iter(self._components)

这使得 Vector 实例成为‌可迭代对象‌。当 Python 遇到需要迭代的场景时(比如 * 解包操作、for 循环等),就会调用这个方法。

  1. *解包操作的工作原理

当你在函数调用中使用 * 解包操作时:

math.hypot(*self)

Python 会:

  • 检查 self 是否可迭代(即是否有 iter 方法)
  • 调用 iter(self) 获取迭代器
  • 逐个取出元素作为单独的参数
  1. 具体执行流程
  • *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)

执行步骤:

  1. for 循环首先调用 iter(v) → 这会寻找 v.__iter__()
  2. 执行你定义的 __iter__ 方法:
    def __iter__(self):return iter(self._components)  # 注意这里!
    
  3. 这个 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中用于将多个可迭代对象连接成一个连续迭代器的操作。下面通过具体示例讲解其工作原理和应用

  1. 基本结构解析

itertools.chain([abs(self)], self.angles())

  • [abs(self)]:将向量的模(标量值)转换为单元素列表
  • self.angles():生成器,产生向量的所有角度值
  • chain():将上述两部分连接为单一迭代器
  1. 典型应用场景(以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)
  1. 与直接列表拼接的区别

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))

这两行代码做了以下事情:

  1. 生成器表达式(format(c, fmt_spec) for c in coords) 创建了一个生成器,它会:

    • 遍历coords中的每个元素c
    • 对每个c应用format(c, fmt_spec)进行格式化
    • 按需生成格式化后的字符串(惰性求值)
  2. 字符串拼接', '.join(components) 将生成器产生的所有格式化后的字符串用逗号和空格连接起来

  3. 最终格式化outer_fmt.format(...) 将拼接好的字符串放入外层格式(尖括号或圆括号)中

示例1:笛卡尔坐标格式化

v = Vector([1.23456, 2.34567, 3.45678])
print(format(v, '.2f'))  # 使用两位小数格式化

执行过程:

  1. coords[1.23456, 2.34567, 3.45678](因为不是’h’格式)
  2. 生成器表达式对每个元素应用format(c, '.2f')
    • 1.23456 → “1.23”
    • 2.34567 → “2.35”
    • 3.45678 → “3.46”
  3. ', '.join(...) → “1.23, 2.35, 3.46”
  4. outer_fmt是圆括号,所以最终输出:`(1.23, 2.35, 3.46)

示例2:超球面坐标格式化

v = Vector([1, 1, 1])  # 3D向量
print(format(v, '.3fh'))  # 超球面坐标,保留3位小数

执行过程:

  1. fmt_spec是’.3f’(去掉’h’)
  2. coords是生成器itertools.chain([abs(self)], self.angles()):
  • 先计算模长:√(1²+1²+1²) ≈ 1.732
  • 然后计算角度(这里简化说明)
  1. 生成器表达式对每个坐标值应用.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章的主题。

http://www.xdnf.cn/news/3455.html

相关文章:

  • stm32数码管显示数字/循环
  • 2025五一杯数学建模C题:社交媒体平台用户分析问题,完整第一问模型与求解+代码
  • ‌AI与编程的范式革命:从工具到协作者的进化之路
  • 等保系列(一):网络安全等级保护介绍
  • OWASP TOP 10 2025
  • 第 11 届蓝桥杯 C++ 青少组中 / 高级组省赛 2020 年真题(选择题)
  • 408考研逐题详解:2009年第6题
  • PyTorch入门------训练图像分类器
  • 12.多边形的三角剖分 (Triangulation) : Fisk‘s proof
  • 车联网可视化:构建智能交通数字孪生
  • 全面理解 C++ 中的 `std::forward`
  • 【滑动窗口】找到字符串中所有字母异位词| 找出字符串中第一个匹配项的下标
  • 【Tool】vscode
  • C++11新特性_自动类型推导_auto
  • 使用QtCreator创建项目(3)
  • Matlab/Simulink - BLDC直流无刷电机仿真基础教程(五) - animateRotorPosition脚本讲解与使用
  • Qt connect第五个参数
  • 构建强大垂直领域AI数据能力
  • 2025年五一杯C题详细思路分析
  • 单片机-89C51部分:13、看门狗
  • 数字智慧方案5972丨智慧农业大数据平台解决方案(65页PPT)(文末有下载方式)
  • CompletableFuture
  • 【基础算法】二分查找算法 - JAVA
  • Python Cookbook-6.12 检查一个实例的状态变化
  • 【笔记】深度学习模型训练的 GPU 内存优化之旅③:内存交换篇
  • 【软件设计师:复习】上午题核心知识点总结(二)
  • C语言学习之动态内存的管理
  • VSCode插件Python Image Preview使用笔记
  • 【FreeRTOS-列表和列表项】
  • PyTorch中“原地”赋值的思考