第一章 python数据模型

第I部分 数据结构

Python数据模型

Python数据模型是使Python对象与语言特性良好协作的API集合。它形式化了语言构建块的接口(如序列、函数、迭代器、类等),可以将数据模型视为对Python作为框架的描述。

数据模型的关键价值

  • 语言一致性:理解数据模型后,能对新功能做出合理预测
  • Pythonic代码:正确使用数据模型是编写符合Python习惯代码的关键
  • 框架式设计:与使用框架类似,我们实现被Python解释器调用的方法

特殊方法(Dunder Methods)也可以称其为魔术方法(magic method),特殊方法通常以双下划线开头和结尾(如__getitem__),由Python解释器自动调用,通常由特殊语法触发。根据Python官方文档,“任何在任何上下文中使用__*__名称的方式,如果不遵循明确记录的用法,都可能在没有警告的情况下失效。

其使用案例如:

  • obj[key]语法由__getitem__方法支持
  • 当计算my_collection[key]时,解释器实际调用my_collection.__getitem__(key)

实现特殊方法可使自定义对象支持并与以下基本语言结构交互:

语言特性相关特殊方法示例
集合操作__len__, __getitem__, __contains__
属性访问__getattr__, __setattr__, __delattr__
迭代__iter__, __next__, __aiter__, __anext__
运算符重载__add__, __eq__, __lt__, __matmul__
函数调用__call__
字符串表示__repr__, __str__, __format__
异步编程__await__, __aenter__, __aexit__
对象生命周期__new__, __init__, __del__
上下文管理__enter__, __exit__

为什么使用len(collection)而非collection.len()?

这是Python数据模型设计理念的体现:

  • 一致性:所有对象使用统一的len()函数
  • 开放性:无需修改类定义即可为现有类型添加功能
  • 清晰分离:功能与实现分离,符合"鸭子类型"哲学

Pythonic的卡牌组

collections.namedtuple 工厂函数

import collections
import random

# 定义Card类,使用namedtuple表示单张牌
# collections.namedtuple 是一个工厂函数,用于创建轻量级的、不可变的自定义元组子类
# collections.namedtuple(typename, field_names, *, rename=False, defaults=None, module=None)
# -typename: 新创建的类的名称,以字符串形式提供
# -field_names: 定义新类的字段(属性)名称,支持以下几种格式:
# 	1. 字符串,字段名用空格分隔 "rank suit"
# 	2. 字符串列表  ['rank', 'suit']
# 	3. 单个包含所有字段名的字符串(逗号分隔,可有空格)"rank, suit"
# -rename (可选): 布尔值,默认为 False 当设置为 True 时,如果提供的字段名是无效的 Python 标识符(如以数字开头、包含空格或特殊字符等),将被自动替换为位置名称 _0, _1, _2... 例如:field_names=['abc', 'def', '123'], rename=True -> 实际字段名为 ('abc', '_1', '_2')

# defaults (可选): 为字段提供默认值的可迭代对象(如元组或列表)
# 默认值从右向左应用到字段上
# 例如:fields=['x', 'y', 'z'], defaults=(0,) -> z 的默认值为 0
#      fields=['x', 'y', 'z'], defaults=(1, 0) -> y=1, z=0

# module (可选): 显式指定创建的类的 __module__ 属性 这在序列化(如 pickle)时可能有用,用于正确记录类的来源模块 如果不指定,会自动从调用栈中推断

# Card类通过collections.namedtuple创建,而namedtuple本质上是tuple的子类 通过这个实现了Card类的不可变性
Card = collections.namedtuple('Card', ['rank', 'suit'])
# 添加了两个命名字段:rank 和 sui
# 自动实现了必要的特殊方法(__init__、__repr__、__eq__ 等)
命名元组的优势

可读性增强

与普通元组相比,命名元组通过字段名访问数据,而不是索引:

# 普通元组
card = ('7', 'diamonds')
print(card[0])  # 难以理解 0 代表什么

# 命名元组
card = Card(rank='7', suit='diamonds')
print(card.rank)  # 清晰表明这是牌的点数

自动实现的有用方法

命名元组自动提供以下方法:

  • _make(iterable): 从可迭代对象创建实例

    card = Card._make(['A', 'spades'])
    
  • _asdict(): 将实例转换为 OrderedDict

    print(card._asdict())  # OrderedDict([('rank', 'A'), ('suit', 'spades')])
    
  • _replace(**kwargs): 创建新实例,替换指定字段

    new_card = card._replace(rank='K')
    
  • _fields: 返回字段名元组

    print(Card._fields)  # ('rank', 'suit')
    

良好的字符串表示

card = Card('7', 'diamonds')
print(card)  # Card(rank='7', suit='diamonds')
# 这比普通元组的表示 ('7', 'diamonds') 更具可读性。

这比普通元组的表示 ('7', 'diamonds') 更具可读性。

与普通元组兼容

# 命名元组仍然是元组的子类,所以可以像使用普通元组一样使用:
card = Card('7', 'diamonds')
print(card[0])  # '7'
print(len(card))  # 2
rank, suit = card  # 解包

核心示例

class FrenchDeck:
    """一副标准的法式扑克牌"""
    ranks = [str(n) for n in range(2, 11)] + list('JOKA')  # 2-10, J, O, K, A
    suits = 'spades diamonds clubs hearts'.split()  # 黑桃, 方块, 梅花, 红心
    
    def __init__(self):
        """初始化牌组,创建52张牌"""
        self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks]
    # 通过实现两个特殊方法构建了一个Pythonic对象
    def __len__(self):
        """返回牌组中的牌数"""
        return len(self._cards)
    
    def __getitem__(self, position):
        """支持索引访问"""
        return self._cards[position]

# 花色排序值,用于自定义排序
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)

def spades_high(card):
    """自定义排序函数:先按等级排序(A最高),然后按花色排序(黑桃>红心>方块>梅花)"""
    rank_value = FrenchDeck.ranks.index(card.rank)
    return rank_value * len(suit_values) + suit_values[card.suit]

def main():
    # 创建牌组实例
    deck = FrenchDeck()
    
    # 演示len()函数
    print(f"牌组总共有 {len(deck)} 张牌") # 牌组总共有 52 张牌
    
    # 演示索引访问
    print("\n第一张牌:", deck[0]) # 第一张牌: Card(rank='2', suit='spades')
    print("最后一张牌:", deck[-1]) # 最后一张牌: Card(rank='A', suit='hearts')
    
    # 演示random.choice 与标准库无缝衔接
    print("\n随机抽取三张牌:")
    for _ in range(3):
        print(random.choice(deck))
    # 随机抽取三张牌:
	# Card(rank='J', suit='clubs')
	# Card(rank='9', suit='clubs')
	# Card(rank='J', suit='diamonds')    
    
    # 自动支持切片操作
    print("\n前三张牌(切片操作):")
    for card in deck[:3]:
        print(card)
    print("\n所有A(切片操作):")
    for card in deck[12::13]:
        print(card)
    
    # 支持迭代和反向迭代
    print("\n前5张牌(迭代):")
    for i, card in enumerate(deck):
        if i >= 5:
            break
        print(card)
    print("\n最后3张牌(反向迭代):")
    for i, card in enumerate(reversed(deck)):
        if i >= 3:
            break
        print(card)
    
    # 支持in操作符
    # 由于实现了迭代协议,即使没有__contains__方法,in运算符也能工作
    print("\nin操作符测试:")
    print("Card('Q', 'hearts') in deck:", Card('Q', 'hearts') in deck)
    print("Card('7', 'beasts') in deck:", Card('7', 'beasts') in deck)
    
    # 支持自定义排序
    print("\n按花色和等级排序(前5张和最后5张):")
    sorted_deck = sorted(deck, key=spades_high)
    
    print("排序后的前5张牌:")
    for card in sorted_deck[:5]:
        print(card)
    
    print("\n排序后的最后5张牌:")
    for card in sorted_deck[-5:]:
        print(card)

if __name__ == "__main__":
    main()

上述笔记通过一个简单的扑克牌示例,教会我们如何利用 Python 数据模型 来创建真正“Pythonic”的代码:通过实现少量的特殊方法(如 __len____getitem__),我们可以让自定义对象无缝地融入 Python 的生态系统,使其表现得像内置序列类型一样,从而自然地支持 len()、索引、切片、迭代和 in 操作等核心语言特性;这体现了 Python 的核心哲学,一致性组合优于继承,即无需复杂的继承体系,只需遵循数据模型的协议,就能充分利用 Python 强大的标准库(如 random.choicesorted),用简洁、直观的方式构建功能丰富的类。FrenchDeck 的例子完美地诠释了 Pythonic 编程的核心思想:通过实现数据模型定义的少量特殊方法,就能让你的自定义对象获得强大的、符合直觉的行为。

FrenchDeck 类虽然简单,但通过 __len____getitem__ 两个方法,自动获得了以下核心语言特性的支持:

  1. 序列协议 (Sequence Protocol)
    • 对象可以像列表、元组等内置序列一样被处理。
  2. 长度查询 (len() 函数)
    • 可以使用内置函数 len(deck) 来获取牌组中牌的数量。
  3. 索引和切片访问 ([] 操作符)
    • 可以通过索引访问特定位置的元素:deck[0], deck[-1]
    • 可以使用切片操作:deck[:3], deck[12::13]
  4. 可迭代性 (Iterability)
    • 对象可以直接在 for 循环中使用:for card in deck: ...
    • 支持反向迭代:for card in reversed(deck): ...
  5. 成员资格测试 (in 操作符)
    • 可以使用 Card('Q', 'hearts') in deck 来检查某张牌是否在牌组中。
  6. 与标准库集成
    • 能够直接使用如 random.choice(deck) 这样的标准库函数,因为 random.choice 需要一个序列。

关于 __contains__ 方法的说明

笔记中提到,即使没有实现 __contains__ 方法,in 操作符也能工作。这是因为 Python 有一个优雅的降级机制:如果一个类没有定义 __contains__ 方法,解释器会退而求其次,使用 __iter__ 方法进行顺序扫描来检查成员资格。正是因为 __getitem__ 的存在,Python 将 FrenchDeck 视为一个序列,从而提供了默认的迭代行为,这间接支持了 in 操作。

特殊方法的使用方式

**特殊方法主要由Python解释器自动调用,而非用户直接调用。**例如应使用内置函数(如 len(my_object)),而不是直接调用特殊方法(如 my_object.__len__())。

对于内置类型(如 list, str 等),len() 会直接访问底层C结构体(PyVarObject)中的 ob_size 字段,速度极快。对于用户定义的类,len() 会通过解释器调用 __len__ 方法。

大多数特殊方法的调用是隐式的,例如:

for i in x:
     # 隐式调用 iter(x)
     # 进而可能调用 x.__iter__() 或 x.__getitem__()

**通常应更多地实现特殊方法,而非在代码中显式调用它们。**唯一常见的直接调用是在子类的 __init__ 中调用父类的 __init__。需要调用时,优先使用 len(), iter(), str() 等内置函数,它们不仅调用特殊方法,还提供额外服务且性能更优。

二维向量类

创建一个表示二维欧几里得向量的类,支持向量加法、计算模长、标量乘法。

import math

class Vector:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return f'Vector({self.x!r}, {self.y!r})'
    
    def __abs__(self):
        return math.hypot(self.x, self.y)
    
    def __bool__(self):
        return bool(abs(self))
    # 返回一个新的Vector实例,而不会修改任一操作数
    def __add__(self, other):
        x = self.x + other.x
        y = self.y + other.y
        return Vector(x, y)
    # 同上
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
方法功能
__init__初始化向量的x和y分量。
__repr__提供对象的“官方”字符串表示,用于调试和开发。!r 确保使用 repr() 表示值。
__abs__计算并返回向量的模(长度),使用 math.hypot(x, y) 计算直角三角形的斜边。
__bool__定义向量的布尔值。零向量为 False,非零向量为 True
__add__实现 + 运算符。创建并返回一个新 Vector 实例,不修改原操作数。
__mul__实现 * 运算符(右乘标量)。创建并返回一个新 Vector 实例。

当前Vector 类示例中,只实现了右乘(__mul__),而没有实现左乘。

在表达式 v * 3 中:

  • vVector 类的实例,位于运算符 *左边
  • 3 是一个标量(整数或浮点数),位于运算符 *右边

当 Python 解释器遇到 v * 3 时,它会尝试调用 v 对象的 __mul__ 方法,并将 3 作为参数传入。因此,__mul__ 方法处理的是 self * other 这种情况,即向量对象在左,标量在右

所以,虽然我们称其为“标量乘法”,但从方法调用的角度看,__mul__ 实现的是左操作数是向量、右操作数是标量的乘法,也就是 vector * scalar

# 当前实现支持的操作
v = Vector(3, 4)
print(v * 3)  # 输出: Vector(9, 12) ✅ 正常工作

# 当前实现不支持的操作
print(3 * v)  # 报错: TypeError: unsupported operand type(s) for *: 'int' and 'Vector'

当执行 3 * v 时:

  1. Python 首先尝试调用左操作数(即 3,一个 int)的 __mul__ 方法。
  2. int 类型的 __mul__ 方法知道如何与另一个 intfloat 相乘,但它不知道如何与一个 Vector 对象相乘。
  3. 因此,int.__mul__(v) 失败,返回 NotImplemented
  4. Python 接着检查右操作数(即 v,一个 Vector)是否定义了 __rmul__ 方法。
  5. 在当前的 Vector 类中,没有定义 __rmul__ 方法。
  6. 由于两种尝试都失败了,Python 就抛出了 TypeError

为了使标量乘法具有交换律(即 v * s == s * v),我们需要实现 __rmul__ 特殊方法。

__rmul__ 的含义是 “right-side multiplication” 或 “reflected multiplication”。它在以下情况下被调用:

当本类的对象出现在 * 运算符的右边,而左边的操作数无法处理该运算时。

# 对算法进行修正
class Vector:
    # ... 其他方法保持不变 ...
    
    def __rmul__(self, scalar):
        # 反射乘法,当标量在左边时调用
        return self * scalar  # 等同于调用 __mul__
        # 或者直接写成: return Vector(self.x * scalar, self.y * scalar)
v = Vector(3, 4)
print(v * 3)  # Vector(9, 12)
print(3 * v)  # Vector(9, 12) 

仅仅实现 __mul__ 而不实现 __rmul__ 会导致 API 设计上的缺陷。它破坏了标量乘法的交换律,使得用户的代码不够灵活,也违背了数学直觉。一个完整的、用户友好的数值类型模拟,通常需要同时实现 __mul____rmul__

字符串表示

Python 提供了两种特殊方法来控制对象的字符串表示,它们服务于不同的目的。

__repr__ 方法(开发者视角)

  • 调用方式:由内置函数 repr() 调用。
  • 使用场景
    • 交互式控制台显示求值结果
    • 调试器
    • %r 格式化占位符
    • f-string 中的 !r 转换字段(如 {obj!r}
  • 设计原则
    • 返回的字符串应明确无歧义
    • 最好能匹配重新创建该对象所需的源代码。例如,Vector(3, 4)__repr__ 返回 'Vector(3, 4)',这非常清晰。
    • __repr__ 中,建议对属性使用 !r 来显示其标准表示,以区分 Vector(1, 2)Vector('1', '2') 这样的情况。
def __repr__(self):
    return f'Vector({self.x!r}, {self.y!r})'

[!NOTE]

!r 是 Python 中的一种转换标志 (conversion flag),主要用于字符串格式化操作中。它的作用是将一个对象通过 repr() 函数转换为其“官方”或“开发者友好”的字符串表示形式。

当你在格式化字符串(如 f-string 或 .format() 方法)中使用 !r 时,Python 会调用该对象的 __repr__ 特殊方法来获取其字符串表示。

  1. 在 f-string (格式化字符串字面量) 中
name = "Alice"
print(f"Hello, {name!r}")
# 输出: Hello, 'Alice'

# 对比不使用 !r
print(f"Hello, {name}")
# 输出: Hello, Alice
  • {name}: 直接插入变量值,结果为 Alice
  • {name!r}: 插入 repr(name) 的结果,即带引号的 'Alice',这清晰地表明它是一个字符串。
  1. 在 str.format() 方法中
name = "Bob"
print("Hello, {!r}".format(name))
# 输出: Hello, 'Bob'
  1. 在 % 格式化中 (经典方式)
name = "Charlie"
print("Hello, %r" % name)
# 输出: Hello, 'Charlie'
转换标志等效函数说明
!rrepr()获取对象的“官方”表示,通常明确无歧义,适合调试。
!sstr()获取对象的“可读”表示,适合向最终用户显示。
!aascii()类似于 !r,但会使用 \x, \u\U 转义序列替换任何非 ASCII 字符。

假设我们有一个自定义的 Vector 类:

class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y
    
    def __repr__(self):
        return f'Vector({self.x}, {self.y})'
    
    def __str__(self):
        return f'({self.x}, {self.y})'

v = Vector(3, 4)

使用不同的转换标志会产生不同的输出:

print(f"Vector repr: {v!r}") # Vector repr: Vector(3, 4) <- 调用 __repr__
print(f"Vector str: {v!s}")  # Vector str: (3, 4)     <- 调用 __str__
print(f"Vector: {v}")        # Vector: (3, 4)         <- 默认调用 __str__

__str__ 方法(用户视角)

  • 调用方式:由内置函数 str() 调用,并被 print() 函数隐式使用。
  • 使用场景:返回适合向最终用户显示的、友好的字符串。
  • 继承关系:如果一个类没有实现 __str__,Python 会退而求其次,使用 __repr__ 的返回值作为后备。

实践建议

如果你只能实现其中一个,请选择 __repr__

有 Java 或 C# 背景的程序员可能习惯于实现类似 toString() 的方法,倾向于只写 __str__。但在 Python 中,__repr__ 对于调试和开发至关重要。一个良好的 __repr__ 可以极大提升开发效率。

自定义类型的布尔值

在 Python 中,任何对象都可以用于布尔上下文(如 if 语句、and/or 操作)。

确定一个对象是真值还是假值时,Python 会按以下顺序进行:

  1. 如果实现了 __bool__ 方法,则调用它并返回其结果(必须是 TrueFalse)。
  2. 如果未实现 __bool__,但实现了 __len__,则检查 __len__ 的返回值:
    • 如果长度为零,返回 False
    • 否则,返回 True
  3. 如果以上两者都未实现,默认所有用户定义的实例都是真值

Vector 类中的实现

def __bool__(self):
    return bool(abs(self))
  • 概念清晰:如果向量的模(长度)为零,则为假值(即零向量),否则为真值。
  • 计算开销:需要计算平方和再开方,相对耗时。
def __bool__(self):
    return bool(self.x or self.y)
  • 性能更优:避免了数学运算,直接检查 x 或 y 分量是否非零。
  • 注意:必须用 bool() 显式转换,因为 or 操作符可能返回非布尔值(如数字本身)。

容器API

在这里插入图片描述

上图展示了 Python 基本集合类型的抽象基类 (ABCs) 结构。

抽象基类 (ABC)特殊方法功能
Sized__len__支持 len(obj)
Iterable__iter____getitem__支持 for item in obj: 迭代
Container__contains__支持 item in obj

Collection ABC 统一了这三个接口。只要一个类实现了相应的特殊方法,就被认为“满足”了这些接口,无需显式继承。

  1. Sequence (序列)

    • 代表list, str, tuple
    • 关键方法__getitem__, __len__
    • 特性:支持索引、切片、任意排序。
    • 可逆性:是 Reversible 的,因此支持 reversed(seq)
  2. Mapping (映射)

    • 代表dict, collections.defaultdict
    • 关键方法__getitem__, __iter__, __len__
    • 特性:键值对存储。自 Python 3.7 起,dict 正式保证插入顺序,但这不等同于可以像序列一样随意重排。
  3. Set (集合)

    • 代表set, frozenset
    • 关键方法__contains__, __iter__, __len__
    • 中缀运算符:所有集合操作都通过特殊方法实现:
      • a & ba.__and__(b) (交集)
      • a | ba.__or__(b) (并集)
      • a - ba.__sub__(b) (差集)
      • a ^ ba.__xor__(b) (对称差)

Python 的集合类型并非通过复杂的继承树来强制行为,而是通过一套简单、一致的协议(即特殊方法)来定义。这体现了 Python 数据模型的核心思想:鸭子类型。**如果一个对象走起来像鸭子,叫起来像鸭子,那么它就是鸭子。**只要你的类实现了 __len____getitem__,它就是一个“序列”,就能享受序列的所有便利。

特殊方法概述

The Python Language Reference(Python语言参考手册)的"Data Model"章节列出了许多特殊方法名称。需要再去查了。

当第一个操作数无法使用相应的特殊方法时,Python会在第二个操作数上调用反向运算符特殊方法。

为什么 len() 不是方法?

这是一个关于Python设计哲学的经典问题,答案根植于 “实用胜于纯粹” 的准则。

  1. 性能优先:对于内置类型(如 list, str, memoryview),len(x) 并非调用方法。它直接从底层C结构体(PyVarObject)的 ob_size 字段读取长度,这比方法调用快得多。获取长度是一个极其频繁的操作,必须对基础类型高效。
  2. 一致性折衷:通过提供 __len__ 方法,Python 在保证内置类型高性能的同时,也允许用户自定义对象使用 len() 函数,实现了效率与一致性的完美平衡。**"一致性"指的是为所有开发者提供一个统一、直观的编程接口。**用户不需要记住不同的API,只需要调用len()即可。只要你实现了__len__方法,len()函数就会将其视为一个有长度的对象来对待。
  3. 概念类比:可以将 lenabs 视为一元“运算符”,其函数式语法(len(obj))比面向对象的方法调用(obj.len())更自然。这一点源于Python的祖先语言ABC,其中使用 # 作为长度运算符(如 #s)。

本章的核心思想是:通过实现特殊方法,你可以让你的自定义对象表现得像内置类型一样,从而编写出真正的 “Pythonic” 代码。 例如这我们需要为对象提供可用的字符串表示,使用__repr__用于调试,使用__str__用于用户展示。

  • “python数据模型"也可以被称作"python对象模型”
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值