Skip to content

第3章 元组与不可变序列理论

本章导读

元组是Python中实现不可变序列的核心数据结构。本章从类型理论和函数式编程角度深入分析元组的理论基础,包括乘积类型的数学定义、不可变性的语义保证、可哈希性的形式化条件,以及命名元组的代数规范。

学习目标

完成本章学习后,读者将能够:

  • 理解元组作为乘积类型的数学本质
  • 掌握不可变性的语义保证与形式化定义
  • 理解可哈希性的数学条件与哈希函数设计
  • 应用势能方法分析不可变数据结构的性能
  • 掌握命名元组的代数规范与类型推导

3.1 元组的类型理论基础

3.1.1 乘积类型的数学定义

定义 3.1(乘积类型) 给定类型 $T_1, T_2, \ldots, T_n$,它们的乘积类型 $T_1 \times T_2 \times \cdots \times T_n$ 是所有形如 $(v_1, v_2, \ldots, v_n)$ 的 $n$-元组的集合,其中 $v_i \in T_i$。

定义 3.2(元组的基数) 设类型 $T_1, T_2, \ldots, T_n$ 的基数分别为 $|T_1|, |T_2|, \ldots, |T_n|$,则乘积类型的基数为:

$$|T_1 \times T_2 \times \cdots \times T_n| = \prod_{i=1}^{n} |T_i|$$

定理 3.1(元组的同构性) 对于任意类型 $A, B, C$,有:

  1. $(A \times B) \times C \cong A \times (B \times C) \cong A \times B \times C$(结合律)
  2. $A \times B \cong B \times A$(交换律,但语义不同)
  3. $A \times 1 \cong A$,其中 $1$ 是单元类型

证明:通过构造双射证明。

  1. 定义 $f: (A \times B) \times C \to A \times B \times C$ 为 $f((a, b), c) = (a, b, c)$,其逆为 $f^{-1}(a, b, c) = ((a, b), c)$。显然 $f$ 是双射。

  2. 定义 $g: A \times B \to B \times A$ 为 $g(a, b) = (b, a)$,其逆为 $g^{-1}(b, a) = (a, b)$。

  3. 定义 $h: A \times 1 \to A$ 为 $h(a, ()) = a$,其逆为 $h^{-1}(a) = (a, ())$。

3.1.2 元组ADT规范

ADT 3.1(Tuple)

ADT Tuple[T₁, T₂, ..., Tₙ]
  Types: Tuple[T₁, T₂, ..., Tₙ]
  
  Operations:
    tuple: T₁ × T₂ × ... × Tₙ → Tuple[T₁, T₂, ..., Tₙ]
    fst: Tuple[T₁, T₂, ..., Tₙ] → T₁
    snd: Tuple[T₁, T₂, ..., Tₙ] → T₂
    get: Tuple[T₁, T₂, ..., Tₙ] × ℕ → Tᵢ
    size: Tuple[T₁, T₂, ..., Tₙ] → ℕ
    concat: Tuple[T₁, ..., Tₘ] × Tuple[Tₘ₊₁, ..., Tₙ] → Tuple[T₁, ..., Tₙ]
  
  Axioms:
    ∀t: Tuple[T₁, T₂, ..., Tₙ], ∀i: ℕ, 1 ≤ i ≤ n
    (A1) get(tuple(v₁, v₂, ..., vₙ), i) = vᵢ
    (A2) size(tuple(v₁, v₂, ..., vₙ)) = n
    (A3) fst(tuple(v₁, v₂, ..., vₙ)) = v₁
    (A4) snd(tuple(v₁, v₂, ..., vₙ)) = v₂
    (A5) concat(tuple(a₁, ..., aₘ), tuple(b₁, ..., bₖ)) = tuple(a₁, ..., aₘ, b₁, ..., bₖ)

定理 3.2(元组ADT的一致性) 元组ADT的公理系统是一致的。

证明:构造具体模型。设元组为索引函数 $t: {1, 2, \ldots, n} \to \bigcup_{i=1}^{n} T_i$,满足 $t(i) \in T_i$。

  • $tuple(v_1, \ldots, v_n) = \lambda i. v_i$
  • $get(t, i) = t(i)$
  • $size(t) = n$

可直接验证所有公理成立。∎

3.1.3 不可变性的形式化定义

定义 3.3(不可变性) 类型 $T$ 是不可变的,当且仅当对于该类型的任意对象 $o$,不存在操作能够改变 $o$ 的观测行为。

形式化地,设 $obs(o)$ 为对象 $o$ 的所有可观测行为的集合。类型 $T$ 不可变当且仅当:

$$\forall o: T, \forall op: Operation, obs(o) = obs(op(o))$$

定义 3.4(浅不可变性) 对象 $o$ 是浅不可变的,如果 $o$ 的直接字段不能被修改,但字段引用的对象可能可变。

定义 3.5(深不可变性) 对象 $o$ 是深不可变的,如果 $o$ 及其所有可达对象都是不可变的。

定理 3.3(Python元组的不可变性) Python元组是浅不可变的。

证明:元组 $t = (v_1, v_2, \ldots, v_n)$ 存储的是引用 $r_1, r_2, \ldots, r_n$。元组保证这些引用不变,即:

$$\forall t, \forall i, \forall op \in {assignment, deletion}, op(t[i]) \text{ raises TypeError}$$

但如果 $r_i$ 指向可变对象 $m$,则 $m$ 可以被修改。因此元组是浅不可变的。∎


3.2 可哈希性的数学理论

3.2.1 哈希函数的形式化定义

定义 3.6(哈希函数) 哈希函数是从任意大小数据到固定大小值的映射:

$$h: U \to {0, 1, \ldots, m-1}$$

其中 $U$ 是全集,$m$ 是哈希表大小。

定义 3.7(理想哈希函数性质) 理想的哈希函数应满足:

  1. 确定性:$\forall x, h(x)$ 总是返回相同值
  2. 均匀分布:$\forall i \in {0, \ldots, m-1}, P(h(x) = i) \approx \frac{1}{m}$
  3. 高效计算:$h(x)$ 可在 $O(|x|)$ 时间内计算

定义 3.8(可哈希对象) 对象 $o$ 是可哈希的,当且仅当:

  1. $o$ 在其生命周期内哈希值不变
  2. $o$ 实现了 __hash____eq__ 方法
  3. 相等对象的哈希值相同:$a = b \Rightarrow hash(a) = hash(b)$

定理 3.4(哈希一致性约束) 若 $a = b$,则必须 $hash(a) = hash(b)$。

证明:反证法。设 $a = b$ 但 $hash(a) \neq hash(b)$。将 $a$ 插入哈希表 $T$,则 $a$ 存储在桶 $hash(a)$。查询 $b$ 时,在桶 $hash(b)$ 查找,无法找到 $a$,但 $a = b$,矛盾。∎

推论 3.1 可变对象不能是可哈希的。

证明:设 $o$ 是可变对象,存在操作使 $o$ 从状态 $s_1$ 变为 $s_2$。若 $o$ 可哈希,则 $hash(o)$ 应不变。但若 $s_1 \neq s_2$,可能 $o_{s_1} = o' \neq o_{s_2}$,此时 $hash(o_{s_1}) = hash(o') \neq hash(o_{s_2})$,与哈希值不变矛盾。∎

3.2.2 元组的可哈希条件

定理 3.5(元组可哈希条件) 元组 $t = (v_1, v_2, \ldots, v_n)$ 是可哈希的,当且仅当所有 $v_i$ 都是可哈希的。

证明

必要性:若存在 $v_i$ 不可哈希,则 $v_i$ 可变。修改 $v_i$ 会改变 $t$ 的观测行为,从而 $t$ 不可哈希。

充分性:若所有 $v_i$ 可哈希,定义:

$$hash(t) = \bigoplus_{i=1}^{n} (hash(v_i) \times p_i) \mod m$$

其中 $p_i$ 是第 $i$ 个质数,$\oplus$ 是异或操作。

由于所有 $v_i$ 的哈希值不变,$hash(t)$ 也不变。若 $t_1 = t_2$,则所有对应元素相等,故 $hash(t_1) = hash(t_2)$。∎

3.2.3 Python哈希实现分析

python
import sys
from typing import Any, Tuple

def analyze_tuple_hash(t: Tuple) -> int:
    """分析元组哈希计算"""
    h = 0x345678
    mult = 1000003
    
    for i, item in enumerate(t):
        h = (h ^ hash(item)) * mult
        mult += 82520 + i + i
    
    h += 97531
    
    return h

def demonstrate_hash_properties():
    """演示哈希性质"""
    t1 = (1, 2, 3)
    t2 = (1, 2, 3)
    t3 = (1, 2, 4)
    
    print(f"hash((1, 2, 3)) = {hash(t1)}")
    print(f"hash((1, 2, 3)) = {hash(t2)}")
    print(f"hash((1, 2, 4)) = {hash(t3)}")
    print(f"相等元组哈希相等: {hash(t1) == hash(t2)}")
    print(f"不等元组哈希可能不等: {hash(t1) != hash(t3)}")
    
    print(f"\n元组大小与哈希值:")
    for n in [1, 2, 3, 5, 10]:
        t = tuple(range(n))
        print(f"  {n}元素: hash={hash(t)}, sizeof={sys.getsizeof(t)}")

if __name__ == '__main__':
    demonstrate_hash_properties()

3.3 元组的内存模型与性能分析

3.3.1 CPython内存布局

CPython PyTupleObject 结构
┌─────────────────────────────────────────────────────────────┐
│  PyObject_VAR_HEAD                                          │
│  ├─ ob_refcnt: Py_ssize_t     │ 引用计数                    │
│  └─ ob_type: PyTypeObject*    │ 类型指针 → &PyTuple_Type    │
├─────────────────────────────────────────────────────────────┤
│  ob_size: Py_ssize_t          │ 元素数量 (len())            │
│  ob_item[0]: PyObject*        │ 第一个元素指针              │
│  ob_item[1]: PyObject*        │ 第二个元素指针              │
│  ...                         │                              │
│  ob_item[n-1]: PyObject*      │ 最后一个元素指针            │
└─────────────────────────────────────────────────────────────┘

3.3.2 内存效率定理

定理 3.6(元组内存效率) 对于相同元素数量 $n$,元组的内存占用严格小于列表。

证明

列表存储:

  • PyObject_VAR_HEAD:16字节(64位系统)
  • ob_size:8字节
  • ob_item:8字节(指针)
  • allocated:8字节(容量)
  • 元素指针数组:$8n$ 字节

总计:$40 + 8n$ 字节

元组存储:

  • PyObject_VAR_HEAD:16字节
  • ob_size:8字节
  • 元素指针数组:$8n$ 字节(内联)

总计:$24 + 8n$ 字节(约节省16字节)∎

定理 3.7(元组创建效率) 常量元组的创建时间为 $O(1)$。

证明:CPython对常量元组进行常量折叠(Constant Folding)。编译时,常量元组被存储在代码对象的常量表中。运行时直接返回预构造的元组对象,无需分配内存和初始化元素。∎

3.3.3 性能基准测试

python
import sys
import timeit
from typing import List, Tuple

def benchmark_tuple_vs_list():
    """元组与列表性能对比"""
    n = 1000000
    
    print("=== 内存占用 ===")
    for size in [1, 5, 10, 100]:
        lst = list(range(size))
        tpl = tuple(range(size))
        print(f"  {size}元素: 列表={sys.getsizeof(lst)}字节, 元组={sys.getsizeof(tpl)}字节")
    
    print("\n=== 创建速度 ===")
    list_time = timeit.timeit('[1, 2, 3, 4, 5]', number=n)
    tuple_time = timeit.timeit('(1, 2, 3, 4, 5)', number=n)
    print(f"  列表: {list_time:.4f}秒")
    print(f"  元组: {tuple_time:.4f}秒")
    print(f"  元组快 {list_time/tuple_time:.1f}倍")
    
    print("\n=== 索引访问速度 ===")
    lst = list(range(10000))
    tpl = tuple(range(10000))
    
    list_access = timeit.timeit('lst[5000]', globals=globals(), number=n)
    tuple_access = timeit.timeit('tpl[5000]', globals=globals(), number=n)
    print(f"  列表: {list_access:.4f}秒")
    print(f"  元组: {tuple_access:.4f}秒")
    
    print("\n=== 哈希计算速度 ===")
    hash_time = timeit.timeit('hash((1, 2, 3, 4, 5))', number=n)
    print(f"  hash((1,2,3,4,5)): {hash_time:.4f}秒")

if __name__ == '__main__':
    benchmark_tuple_vs_list()

3.4 序列解包的理论基础

3.4.1 模式匹配语义

定义 3.9(模式匹配) 模式匹配是将数据结构分解为其组成部分的过程。对于元组,模式匹配定义为:

$$match(t, p) = \begin{cases} \theta & \text{if } t \sim p \ \text{fail} & \text{otherwise} \end{cases}$$

其中 $t$ 是目标元组,$p$ 是模式,$\theta$ 是替换(绑定变量的映射)。

定义 3.10(元组模式) 元组模式的形式化定义为:

$$p ::= (p_1, p_2, \ldots, p_n) \mid x \mid _ \mid p_1 \mid p_2$$

其中:

  • $(p_1, \ldots, p_n)$ 是元组模式
  • $x$ 是变量模式(绑定值)
  • $_$ 是通配符模式(忽略值)
  • $p_1 \mid p_2$ 是或模式

定理 3.8(解包完备性) 若模式 $p$ 与元组 $t$ 匹配,则解包操作产生唯一的变量绑定。

证明:归纳证明。

基础情况:变量模式 $x$ 匹配任意值 $v$,产生绑定 ${x \mapsto v}$,唯一。

归纳步骤:设元组模式 $p = (p_1, \ldots, p_n)$ 匹配 $t = (v_1, \ldots, v_n)$。由归纳假设,每个 $p_i$ 匹配 $v_i$ 产生唯一绑定 $\theta_i$。由于变量名不相交,$\theta = \bigcup_i \theta_i$ 是唯一的。∎

3.4.2 扩展解包的形式化

定义 3.11(星号模式) 星号模式 $*x$ 匹配零个或多个元素,绑定为列表:

$$match((v_1, \ldots, v_n), (p_1, *x, p_2)) = {p_1 \mapsto v_1, x \mapsto [v_2, \ldots, v_{n-1}], p_2 \mapsto v_n}$$

定理 3.9(星号模式的唯一性) 在一个模式中,星号模式最多出现一次。

证明:反证法。设模式中有两个星号模式 $*x$ 和 $*y$。匹配元组 $(v_1, \ldots, v_n)$ 时,无法确定 $x$ 和 $y$ 各自匹配多少元素,产生歧义。∎

3.4.3 解包实现

python
from typing import Tuple, List, Any, TypeVar, Generic

T = TypeVar('T')

class PatternMatcher(Generic[T]):
    """模式匹配器实现"""
    
    @staticmethod
    def match_tuple(tpl: Tuple[T, ...], pattern: Tuple) -> dict:
        """
        元组模式匹配
        
        pattern 可以包含:
        - 具体值:精确匹配
        - 变量名(字符串):绑定值
        - '_':通配符
        - '*name':星号模式
        """
        bindings = {}
        tpl_idx = 0
        pat_idx = 0
        star_var = None
        star_start = None
        
        while pat_idx < len(pattern):
            p = pattern[pat_idx]
            
            if isinstance(p, str) and p.startswith('*'):
                star_var = p[1:]
                star_start = tpl_idx
                pat_idx += 1
                continue
            
            if star_var is not None and pat_idx == len(pattern) - 1:
                bindings[star_var] = list(tpl[star_start:tpl_idx])
                star_var = None
            
            if tpl_idx >= len(tpl):
                return None
            
            val = tpl[tpl_idx]
            
            if isinstance(p, str):
                if p == '_':
                    pass
                elif p.startswith('*'):
                    pass
                else:
                    bindings[p] = val
            elif p == val:
                pass
            else:
                return None
            
            tpl_idx += 1
            pat_idx += 1
        
        if star_var is not None:
            bindings[star_var] = list(tpl[star_start:])
        
        return bindings if tpl_idx == len(tpl) else None

def demo_pattern_matching():
    """演示模式匹配"""
    matcher = PatternMatcher()
    
    tpl = (1, 2, 3, 4, 5)
    
    print(f"元组: {tpl}")
    
    result = matcher.match_tuple(tpl, ('a', 'b', 'c', 'd', 'e'))
    print(f"模式 ('a', 'b', 'c', 'd', 'e'): {result}")
    
    result = matcher.match_tuple(tpl, ('first', '*rest'))
    print(f"模式 ('first', '*rest'): {result}")
    
    result = matcher.match_tuple(tpl, ('*head', 'last'))
    print(f"模式 ('*head', 'last'): {result}")
    
    result = matcher.match_tuple(tpl, ('first', '*middle', 'last'))
    print(f"模式 ('first', '*middle', 'last'): {result}")

if __name__ == '__main__':
    demo_pattern_matching()

3.5 命名元组的代数规范

3.5.1 命名元组ADT

ADT 3.2(NamedTuple)

ADT NamedTuple[N: ℕ, Fields: (String × Type)^N]
  Types: NamedTuple[Fields]
  
  Operations:
    create: (v₁: T₁, ..., vₙ: Tₙ) → NamedTuple[Fields]
    get: NamedTuple[Fields] × String → Tᵢ
    get_by_index: NamedTuple[Fields] × ℕ → Tᵢ
    fields: → (String)^N
    asdict: NamedTuple[Fields] → Dict[String, Tᵢ]
    replace: NamedTuple[Fields] × String × Tᵢ → NamedTuple[Fields]
  
  Axioms:
    ∀nt: NamedTuple[Fields], ∀f: String, ∀v: Tᵢ
    (A1) get(create(v₁, ..., vₙ), fᵢ) = vᵢ
    (A2) get_by_index(create(v₁, ..., vₙ), i) = vᵢ
    (A3) get(nt, fᵢ) = get_by_index(nt, i)
    (A4) asdict(create(v₁, ..., vₙ)) = {f₁: v₁, ..., fₙ: vₙ}
    (A5) get(replace(nt, fᵢ, v), fᵢ) = v
    (A6) fⱼ ≠ fᵢ → get(replace(nt, fᵢ, v), fⱼ) = get(nt, fⱼ)

3.5.2 类型安全的命名元组

python
from typing import NamedTuple, TypeVar, Generic, Optional, Dict, Any
from dataclasses import dataclass
from abc import ABC, abstractmethod

T = TypeVar('T')

class TypedNamedTuple(ABC):
    """类型安全的命名元组基类"""
    
    @classmethod
    @abstractmethod
    def field_names(cls) -> Tuple[str, ...]:
        """返回字段名列表"""
        pass
    
    @classmethod
    @abstractmethod
    def field_types(cls) -> Dict[str, type]:
        """返回字段类型映射"""
        pass
    
    @abstractmethod
    def as_dict(self) -> Dict[str, Any]:
        """转换为字典"""
        pass
    
    @abstractmethod
    def validate(self) -> bool:
        """验证类型约束"""
        pass


class Point2D(NamedTuple):
    """二维点 - 类型安全命名元组"""
    x: float
    y: float
    
    def distance_to(self, other: 'Point2D') -> float:
        """计算两点距离"""
        return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
    
    def distance_to_origin(self) -> float:
        """到原点距离"""
        return self.distance_to(Point2D(0, 0))
    
    def translate(self, dx: float, dy: float) -> 'Point2D':
        """平移(返回新点)"""
        return Point2D(self.x + dx, self.y + dy)
    
    def scale(self, factor: float) -> 'Point2D':
        """缩放"""
        return Point2D(self.x * factor, self.y * factor)


class Vector3D(NamedTuple):
    """三维向量"""
    x: float
    y: float
    z: float
    
    def dot(self, other: 'Vector3D') -> float:
        """点积"""
        return self.x * other.x + self.y * other.y + self.z * other.z
    
    def cross(self, other: 'Vector3D') -> 'Vector3D':
        """叉积"""
        return Vector3D(
            self.y * other.z - self.z * other.y,
            self.z * other.x - self.x * other.z,
            self.x * other.y - self.y * other.x
        )
    
    def magnitude(self) -> float:
        """模长"""
        return (self.dot(self)) ** 0.5
    
    def normalize(self) -> 'Vector3D':
        """单位化"""
        m = self.magnitude()
        if m == 0:
            raise ValueError("Cannot normalize zero vector")
        return Vector3D(self.x / m, self.y / m, self.z / m)
    
    def __add__(self, other: 'Vector3D') -> 'Vector3D':
        return Vector3D(self.x + other.x, self.y + other.y, self.z + other.z)
    
    def __sub__(self, other: 'Vector3D') -> 'Vector3D':
        return Vector3D(self.x - other.x, self.y - other.y, self.z - other.z)
    
    def __mul__(self, scalar: float) -> 'Vector3D':
        return Vector3D(self.x * scalar, self.y * scalar, self.z * scalar)


def demo_named_tuple():
    """演示命名元组"""
    p1 = Point2D(3, 4)
    p2 = Point2D(6, 8)
    
    print(f"点1: {p1}")
    print(f"点2: {p2}")
    print(f"p1到原点距离: {p1.distance_to_origin()}")
    print(f"p1到p2距离: {p1.distance_to(p2)}")
    print(f"p1平移(1, 1): {p1.translate(1, 1)}")
    
    v1 = Vector3D(1, 0, 0)
    v2 = Vector3D(0, 1, 0)
    
    print(f"\nv1 × v2 = {v1.cross(v2)}")
    print(f"v1 · v2 = {v1.dot(v2)}")


if __name__ == '__main__':
    demo_named_tuple()

3.6 不可变数据结构的设计模式

3.6.1 值对象模式

定义 3.12(值对象) 值对象是通过其属性值而非身份来定义的对象。两个值对象相等当且仅当它们的所有属性相等。

定理 3.10(值对象的不变性) 值对象必须是不可变的,以保证相等性语义的一致性。

证明:设值对象 $v_1, v_2$ 相等。若 $v_1$ 可变,修改后 $v_1 \neq v_2$,但 $v_2$ 未变,矛盾。∎

python
from dataclasses import dataclass, field
from typing import Tuple, Optional, List
from functools import total_ordering
import math

@dataclass(frozen=True, order=True)
class Money:
    """货币值对象"""
    amount: float
    currency: str = "USD"
    
    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("Amount cannot be negative")
    
    def add(self, other: 'Money') -> 'Money':
        if self.currency != other.currency:
            raise ValueError("Cannot add different currencies")
        return Money(self.amount + other.amount, self.currency)
    
    def multiply(self, factor: float) -> 'Money':
        return Money(self.amount * factor, self.currency)
    
    def __str__(self) -> str:
        return f"{self.amount:.2f} {self.currency}"


@dataclass(frozen=True)
class DateRange:
    """日期范围值对象"""
    start: Tuple[int, int, int]
    end: Tuple[int, int, int]
    
    def __post_init__(self):
        if self.start > self.end:
            raise ValueError("Start date must be before end date")
    
    def contains(self, date: Tuple[int, int, int]) -> bool:
        return self.start <= date <= self.end
    
    def overlaps(self, other: 'DateRange') -> bool:
        return self.start <= other.end and other.start <= self.end
    
    def duration_days(self) -> int:
        return (self._to_days(self.end) - self._to_days(self.start))
    
    @staticmethod
    def _to_days(date: Tuple[int, int, int]) -> int:
        y, m, d = date
        return y * 365 + m * 30 + d


@dataclass(frozen=True)
class Coordinate:
    """地理坐标值对象"""
    latitude: float
    longitude: float
    
    def __post_init__(self):
        if not -90 <= self.latitude <= 90:
            raise ValueError("Latitude must be between -90 and 90")
        if not -180 <= self.longitude <= 180:
            raise ValueError("Longitude must be between -180 and 180")
    
    def distance_to(self, other: 'Coordinate') -> float:
        """Haversine公式计算球面距离(公里)"""
        R = 6371
        
        lat1, lon1 = math.radians(self.latitude), math.radians(self.longitude)
        lat2, lon2 = math.radians(other.latitude), math.radians(other.longitude)
        
        dlat = lat2 - lat1
        dlon = lon2 - lon1
        
        a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
        c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
        
        return R * c
    
    def __str__(self) -> str:
        ns = 'N' if self.latitude >= 0 else 'S'
        ew = 'E' if self.longitude >= 0 else 'W'
        return f"{abs(self.latitude):.4f}°{ns}, {abs(self.longitude):.4f}°{ew}"

3.6.2 不可变构建器模式

python
from typing import TypeVar, Generic, Optional, Dict, Any, Callable
from dataclasses import dataclass, field
from copy import deepcopy

T = TypeVar('T')

@dataclass(frozen=True)
class ImmutableConfig:
    """不可变配置"""
    host: str
    port: int
    database: str
    username: Optional[str] = None
    password: Optional[str] = None
    timeout: int = 30
    ssl: bool = False
    pool_size: int = 10
    
    class Builder:
        """配置构建器"""
        
        def __init__(self, config: Optional['ImmutableConfig'] = None):
            self._data = {}
            if config:
                self._data = {
                    'host': config.host,
                    'port': config.port,
                    'database': config.database,
                    'username': config.username,
                    'password': config.password,
                    'timeout': config.timeout,
                    'ssl': config.ssl,
                    'pool_size': config.pool_size,
                }
        
        def host(self, value: str) -> 'ImmutableConfig.Builder':
            self._data['host'] = value
            return self
        
        def port(self, value: int) -> 'ImmutableConfig.Builder':
            self._data['port'] = value
            return self
        
        def database(self, value: str) -> 'ImmutableConfig.Builder':
            self._data['database'] = value
            return self
        
        def credentials(self, username: str, password: str) -> 'ImmutableConfig.Builder':
            self._data['username'] = username
            self._data['password'] = password
            return self
        
        def timeout(self, value: int) -> 'ImmutableConfig.Builder':
            self._data['timeout'] = value
            return self
        
        def ssl(self, enabled: bool = True) -> 'ImmutableConfig.Builder':
            self._data['ssl'] = enabled
            return self
        
        def pool_size(self, value: int) -> 'ImmutableConfig.Builder':
            self._data['pool_size'] = value
            return self
        
        def build(self) -> 'ImmutableConfig':
            required = ['host', 'port', 'database']
            for field in required:
                if field not in self._data:
                    raise ValueError(f"Missing required field: {field}")
            return ImmutableConfig(**self._data)
    
    def with_port(self, new_port: int) -> 'ImmutableConfig':
        """返回修改端口的新配置"""
        return ImmutableConfig(
            host=self.host,
            port=new_port,
            database=self.database,
            username=self.username,
            password=self.password,
            timeout=self.timeout,
            ssl=self.ssl,
            pool_size=self.pool_size,
        )
    
    def to_builder(self) -> Builder:
        """转换为构建器"""
        return ImmutableConfig.Builder(self)


def demo_immutable_config():
    """演示不可变配置"""
    config = (ImmutableConfig.Builder()
              .host("localhost")
              .port(5432)
              .database("mydb")
              .credentials("user", "pass")
              .ssl()
              .build())
    
    print(f"原始配置: {config}")
    
    new_config = config.with_port(5433)
    print(f"修改端口: {new_config}")
    print(f"原配置不变: {config}")
    
    updated = (config.to_builder()
               .port(5434)
               .timeout(60)
               .build())
    print(f"更新配置: {updated}")


if __name__ == '__main__':
    demo_immutable_config()

3.7 持久化数据结构

3.7.1 持久化数据结构理论

定义 3.13(持久化数据结构) 持久化数据结构在修改时保留之前的版本,支持对历史版本的访问。

定义 3.14(完全持久化) 数据结构支持对任意版本的查询和修改,形成版本树。

定理 3.11(不可变数据结构的持久化) 不可变数据结构天然支持持久化,因为修改操作创建新版本而不破坏旧版本。

证明:设不可变数据结构 $D$,修改操作 $modify(D, op)$ 返回新结构 $D'$。由于 $D$ 不可变,$D$ 在 $modify$ 后保持不变,因此可以同时访问 $D$ 和 $D'$。∎

3.7.2 持久化列表实现

python
from typing import TypeVar, Generic, Optional, List, Iterator, Tuple
from dataclasses import dataclass

T = TypeVar('T')

@dataclass(frozen=True)
class PersistentList(Generic[T]):
    """
    持久化列表(基于二叉树)
    
    操作复杂度:
    - get: O(log n)
    - set: O(log n)
    - push: O(log n)
    - pop: O(log n)
    
    空间复杂度:每次修改 O(log n) 新节点
    """
    
    _size: int
    _tree: Optional[Tuple[T, 'PersistentList[T]', 'PersistentList[T]']]
    
    @classmethod
    def empty(cls) -> 'PersistentList[T]':
        return cls(0, None)
    
    @classmethod
    def from_list(cls, items: List[T]) -> 'PersistentList[T]':
        result = cls.empty()
        for item in items:
            result = result.push(item)
        return result
    
    def is_empty(self) -> bool:
        return self._size == 0
    
    def __len__(self) -> int:
        return self._size
    
    def push(self, value: T) -> 'PersistentList[T]':
        """在头部添加元素"""
        return PersistentList(self._size + 1, (value, self, PersistentList.empty()))
    
    def head(self) -> Optional[T]:
        """获取头部元素"""
        if self._tree is None:
            return None
        return self._tree[0]
    
    def tail(self) -> 'PersistentList[T]':
        """获取尾部列表"""
        if self._tree is None:
            return self
        return self._tree[1]
    
    def get(self, index: int) -> Optional[T]:
        """获取指定位置元素"""
        if not 0 <= index < self._size:
            return None
        return self._get(index, self._size)
    
    def _get(self, index: int, size: int) -> T:
        if size == 1:
            return self._tree[0]
        
        left_size = size // 2
        if index < left_size:
            return self._tree[1]._get(index, left_size)
        else:
            return self._tree[2]._get(index - left_size, size - left_size)
    
    def set(self, index: int, value: T) -> 'PersistentList[T]':
        """设置指定位置元素(返回新列表)"""
        if not 0 <= index < self._size:
            raise IndexError("Index out of range")
        return self._set(index, value, self._size)
    
    def _set(self, index: int, value: T, size: int) -> 'PersistentList[T]':
        if size == 1:
            return PersistentList(1, (value, PersistentList.empty(), PersistentList.empty()))
        
        left_size = size // 2
        if index < left_size:
            new_left = self._tree[1]._set(index, value, left_size)
            return PersistentList(size, (self._tree[0], new_left, self._tree[2]))
        else:
            new_right = self._tree[2]._set(index - left_size, value, size - left_size)
            return PersistentList(size, (self._tree[0], self._tree[1], new_right))
    
    def to_list(self) -> List[T]:
        """转换为Python列表"""
        result = []
        self._to_list(result, self._size)
        return result
    
    def _to_list(self, result: List[T], size: int) -> None:
        if size == 0:
            return
        if size == 1:
            result.append(self._tree[0])
            return
        
        left_size = size // 2
        self._tree[1]._to_list(result, left_size)
        self._tree[2]._to_list(result, size - left_size)
    
    def __iter__(self) -> Iterator[T]:
        return iter(self.to_list())
    
    def __repr__(self) -> str:
        return f"PersistentList({self.to_list()})"


def demo_persistent_list():
    """演示持久化列表"""
    l1 = PersistentList.from_list([1, 2, 3, 4, 5])
    print(f"l1 = {l1}")
    
    l2 = l1.set(2, 100)
    print(f"l2 = l1.set(2, 100) = {l2}")
    print(f"l1 不变 = {l1}")
    
    l3 = l2.push(0)
    print(f"l3 = l2.push(0) = {l3}")
    print(f"l2 不变 = {l2}")
    
    print(f"\n历史版本:")
    print(f"  l1[2] = {l1.get(2)}")
    print(f"  l2[2] = {l2.get(2)}")
    print(f"  l3[0] = {l3.get(0)}")


if __name__ == '__main__':
    demo_persistent_list()

3.8 本章小结

3.8.1 核心定理总结

定理内容应用
定理 3.1元组同构性类型推导
定理 3.4哈希一致性约束哈希表设计
定理 3.5元组可哈希条件字典键设计
定理 3.6元组内存效率性能优化
定理 3.10值对象不变性设计模式

3.8.2 元组 vs 列表对比

特性元组列表
可变性不可变可变
可哈希性是(元素可哈希时)
内存占用$O(n)$$O(n)$ + 容量开销
创建速度$O(1)$(常量折叠)$O(n)$
作为字典键可以不可以
线程安全需要锁
语义值类型引用类型

3.8.3 选择指南

python
# 使用元组的场景
coordinates = (x, y)           # 固定结构数据
config = ("localhost", 5432)   # 配置常量
return min_val, max_val        # 函数返回多值
d[(key1, key2)] = value        # 复合键

# 使用命名元组的场景
Point = namedtuple("Point", "x y")  # 需要字段名
Employee = namedtuple("Employee", "id name dept")  # 记录类型

# 使用frozen dataclass的场景
@dataclass(frozen=True)
class Money:                        # 需要方法和验证
    amount: float
    currency: str

3.9 习题

理论题

  1. 证明:对于类型 $A, B, C$,有 $A \times (B + C) \cong A \times B + A \times C$(分配律)。

  2. 证明:若 $a = b$,则 $hash(a) = hash(b)$,但逆命题不成立。

  3. 分析持久化列表的 set 操作空间复杂度为 $O(\log n)$。

  4. 证明:不可变数据结构天然线程安全。

设计题

  1. 设计一个不可变的二叉搜索树,支持 $O(\log n)$ 的查找、插入和删除。

  2. 实现一个持久化字典,支持历史版本查询。

实现题

  1. 实现一个支持模式匹配的元组解包函数。

  2. 实现一个不可变的优先队列。


3.10 参考文献

  1. Pierce, B. C. (2002). Types and Programming Languages. MIT Press. Chapter 11: Simple Extensions.

  2. Okasaki, C. (1999). Purely Functional Data Structures. Cambridge University Press.

  3. Abelson, H., & Sussman, G. J. (1996). Structure and Interpretation of Computer Programs (2nd ed.). MIT Press.

  4. Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley. Item 17: Minimize mutability.

  5. CPython Source Code. (2024). Objects/tupleobject.c. https://github.com/python/cpython/blob/main/Objects/tupleobject.c

  6. Fowlie, M. (2015). Pattern matching in Python. PyCon 2015.


下一章:第4章 字典与哈希表理论

Python技术丛书 - 江苏省宿城中等专业学校