Files
coding-mentor-agent/kb/python-course-kb-practical-python/wiki/concepts/Python-容器.md
T

30 KiB
Raw Blame History

sources, brief
sources brief
summaries/07_Objects.md
summaries/06_Generators__00_Overview.md
summaries/02_Working_with_data__00_Overview.md
summaries/04_More_generators.md
summaries/02_Customizing_iteration.md
summaries/01_Iteration_protocol.md
summaries/03_Special_methods.md
summaries/06_List_comprehension.md
summaries/05_Collections.md
summaries/03_Formatting.md
summaries/02_Containers.md
summaries/01_Datatypes.md
summaries/00_Overview.md
Python 容器通过统一协议组织、访问、迭代和封装多个对象。

Python 容器

Python 容器是用于保存、组织和访问多个对象的数据结构。它们既包括内置类型,例如 list、tuple、set、dict,也包括标准库 collections模块 中的专用容器,以及用户通过特殊方法实现的自定义容器。

容器不仅是“装数据”的结构,也是 Python 统一语法和协议体系的一部分。一个对象如果实现了适当的特殊方法,就可以像内置容器一样支持 for 循环、len()、索引、切片、成员测试、赋值和删除等操作。这一点把容器与 Python迭代协议、Python特殊方法、Python数据模型、容器协议 和 Pythonic设计 紧密联系起来。

在 summaries/00_Overview 中,“Working With Data”章节将容器作为处理数据的重要主题之一,并介绍 Python 的核心数据结构:tuples、lists、sets 和 dictionaries。summaries/02_Containers 说明了这些容器在真实数据处理中的选择方式:列表适合有序数据,字典适合通过键快速查找,集合适合唯一元素和成员测试。summaries/05_Collections 展示了标准库 collections 模块如何在内置容器之上提供更专门的数据处理工具,例如 Counter、defaultdict 和 deque。summaries/03_Special_methods 和 summaries/01_Iteration_protocol 则从底层机制说明:容器行为由特殊方法和迭代协议驱动,自定义对象也可以通过这些协议融入 Python 语言。

核心含义

容器的作用是把多个值组合在一起,使程序能够:

  • 存储一组相关数据
  • 按顺序或按键访问数据
  • 遍历多个元素
  • 增加、删除或更新元素
  • 表达集合关系或映射关系
  • 支持更复杂的数据组织方式
  • 从文件、表格或其他外部数据源构造内存中的数据结构
  • 对数据进行统计、分组、索引或保留历史记录
  • 通过统一协议支持 for x in obj、len(x)、x[a]、x[a] = v、del x[a]、x in obj 等语法

Python 中常见的内置容器包括:

  • tuple:元组
  • list:列表
  • set:集合
  • dict:字典

此外,标准库 collections模块 还提供了若干专用容器或容器变体,例如:

  • Counter:用于计数和汇总
  • defaultdict:用于带默认值的映射,尤其适合一对多分组
  • deque:用于双端队列和固定长度历史记录

这些容器与 Python数据类型 密切相关,也构成了 序列、concepts/列表推导式、CSV文件处理、字典与映射、Python对象模型、Python特殊方法、容器协议 和 Python迭代协议 等主题的基础。

主要内置容器类型

1. 列表 list

列表是有序、可变的容器,适合保存一组需要动态修改的数据。当数据顺序有意义,并且需要追加、删除或替换元素时,通常应选择列表。

常见用途包括:

  • 保存一批项目
  • 按位置访问元素
  • 追加、删除或替换元素
  • 配合循环或 concepts/列表推导式 进行数据转换
  • 保存从文件中读取出来的一组记录
  • 作为自定义容器内部的底层存储

例如,股票投资组合可以表示为“元组的列表”:

portfolio = [
    ('GOOG', 100, 490.1),
    ('IBM', 50, 91.3),
    ('CAT', 150, 83.44)
]

可以通过整数索引访问列表元素:

portfolio[0]    # ('GOOG', 100, 490.1)
portfolio[2]    # ('CAT', 150, 83.44)

列表也可以从空列表开始构造,并通过 .append() 添加项目:

records = []
records.append(('GOOG', 100, 490.10))
records.append(('IBM', 50, 91.3))

在 summaries/02_Containers 的练习中,read_portfolio(filename) 读取 Data/portfolio.csv,并把每一行转换为一个持仓记录,再追加到列表中。这体现了列表在 CSV文件处理 和 数据建模 中的常见用法。

2. 元组 tuple

元组是有序、不可变的容器。它与列表类似,也可以按位置访问元素,但创建后通常不能修改。

常见用途包括:

  • 表示固定结构的数据
  • 从函数返回多个值
  • 用作不可变记录
  • 在需要稳定结构时替代列表
  • 作为字典中的复合键

例如,一条股票持仓可以用三元组表示:

holding = ('IBM', 50, 91.1)

列表中的每个元素也可以是元组,从而形成类似二维表的数据结构:

portfolio[row][column]

不过,使用数字列号访问字段有时可读性较差。因此后续常会用字典或对象来表示结构化记录。

元组属于 序列 类型的一种,因此支持索引、切片和迭代等序列操作。它也与 序列解包 密切相关,例如:

for name, shares, price in portfolio:
    total += shares * price

这种写法比反复使用 s[0]、s[1]、s[2] 更清晰。

3. 字典 dict

字典是键值对容器,用于建立 key 到 value 的映射关系。它适合需要通过名称、编号或其他键进行快速随机查找的场景。

常见用途包括:

  • 通过名称、编号或其他键快速查找值
  • 表示结构化记录
  • 统计数据
  • 构建查找表或配置对象
  • 将外部表格数据转换为可查询的数据结构
  • 作为更专门映射容器的基础,例如 Counter 和 defaultdict

例如,股票价格表可以表示为字典:

prices = {
    'GOOG': 513.25,
    'CAT': 87.22,
    'IBM': 93.37,
    'MSFT': 44.12
}

访问时使用键,而不是整数位置:

prices['IBM']
prices['GOOG']

在 summaries/02_Containers 中,read_prices(filename) 读取 Data/prices.csv,并构造一个“股票代码到当前价格”的字典。这种结构非常适合后续根据投资组合中的股票名快速查找当前价格。

字典查找与默认值

字典可以用 in 判断键是否存在:

if key in d:
    # key 存在
else:
    # key 不存在

也可以用 .get() 在键不存在时提供默认值:

value = d.get(key, default)

例如:

prices.get('IBM', 0.0)    # 93.37
prices.get('SCOX', 0.0)   # 0.0

这在处理不完整数据时很有用,也与 健壮文件读取 和 数据清洗 有关。标准库中的 defaultdict 进一步扩展了这种“默认值”思想:访问不存在的键时,它会自动创建默认值,从而减少显式判断代码。

字典表示结构化记录

除了作为查找表,字典也可以表示一条结构化记录。例如,一条股票持仓可以写成:

holding = {
    'name': 'IBM',
    'shares': 50,
    'price': 91.1
}

由多条记录组成的投资组合就可以表示为“字典的列表”:

portfolio = [
    {'name': 'AA', 'shares': 100, 'price': 32.2},
    {'name': 'IBM', 'shares': 50, 'price': 91.1}
]

访问字段时使用字段名:

portfolio[1]['shares']

这种形式通常比元组列表更易读,因为代码直接表达了字段含义,而不是依赖列号。

复合键与不可变性

Python 字典的键必须是不可变对象。字符串、数字、元组等可以作为键;列表、集合和字典不能作为键,因为它们是可变对象。

元组可用于表示复合键:

holidays = {
    (1, 1): 'New Years',
    (3, 14): 'Pi day',
    (9, 13): "Programmer's day"
}

访问时可以写作:

holidays[3, 14]

这体现了 可变性与不可变性 对容器设计的重要影响。

4. 集合 set

集合是无序、不重复元素的容器,主要用于表达数学意义上的集合关系。

常见用途包括:

  • 去除重复值
  • 判断成员是否存在
  • 求并集、交集、差集等集合运算
  • 表达唯一元素集合
  • 比较两个数据源中的元素差异

集合强调“元素是否存在”,而不是“元素位于哪个位置”。

示例:

tech_stocks = {'IBM', 'AAPL', 'MSFT'}

集合非常适合成员测试:

'IBM' in tech_stocks    # True
'FB' in tech_stocks     # False

也常用于去重:

names = ['IBM', 'AAPL', 'GOOG', 'IBM', 'GOOG', 'YHOO']
unique = set(names)

集合还支持常见集合运算:

s1 = {'a', 'b', 'c'}
s2 = {'c', 'd'}

s1 | s2    # 并集
s1 & s2    # 交集
s1 - s2    # 差集

这些操作使集合适合用于比较名称列表、股票代码集合或文件中的唯一记录。

容器与迭代协议

summaries/01_Iteration_protocol 说明了 Python 容器最重要的共同能力之一:可迭代。许多对象都支持迭代,包括字符串、字典、列表、元组、集合和文件对象。

例如:

for c in 'hello':
    ...       # 逐字符迭代

for k in {'name': 'Dave', 'password': 'foo'}:
    ...       # 默认逐键迭代

for i in [1, 2, 3, 4]:
    ...       # 逐元素迭代

for line in open('foo.txt'):
    ...       # 逐行迭代

for 循环背后依赖 Python迭代协议。语句:

for x in obj:
    # statements

大致等价于:

_iter = obj.__iter__()
while True:
    try:
        x = _iter.__next__()
        # statements
    except StopIteration:
        break

关键点包括:

  • obj.__iter__() 返回一个迭代器对象。
  • 迭代器通过 __next__() 逐个返回元素。
  • 当没有更多元素时,__next__() 抛出 StopIteration。
  • for 循环自动捕获 StopIteration 并结束。
  • 内置函数 next(it) 是调用 it.__next__() 的简写。

因此,可迭代性不是 for 循环的表面特性,而是容器接口的重要组成部分。一个对象只要实现合适的 __iter__(),就可以参与 for 循环和许多依赖迭代的 Python 工具。

文件对象也是迭代协议的典型例子。对文件调用 next(f) 会读取下一行;读到文件末尾时抛出 StopIteration。这说明“容器式”行为并不局限于内存中的列表或字典,也可以用于流式数据源。

容器协议与特殊方法

summaries/03_Special_methods 从 Python 数据模型的角度说明:容器操作本质上会调用对象上的特殊方法。也就是说,len(x)、x[a]、x[a] = v、del x[a]、x in obj 并不是只适用于内置容器的语法糖;只要一个类实现了相应的特殊方法,它就可以表现得像容器。

常见容器操作与特殊方法的对应关系是:

iter(x)     x.__iter__()
next(it)    it.__next__()
len(x)      x.__len__()
x[a]        x.__getitem__(a)
x[a] = v    x.__setitem__(a, v)
del x[a]    x.__delitem__(a)
y in x      x.__contains__(y)  # 如果定义了该方法

一个自定义容器类通常会实现类似结构:

class Sequence:
    def __iter__(self):
        ...

    def __len__(self):
        ...

    def __getitem__(self, a):
        ...

    def __setitem__(self, a, v):
        ...

    def __delitem__(self, a):
        ...

    def __contains__(self, item):
        ...

这体现了 Python 的协议式设计:对象不一定要继承某个特定的容器基类,只要实现约定的特殊方法,就可以参与相应语法。这一思想与 Python协议、Python特殊方法、Python数据模型 和 容器协议 密切相关。

从使用者角度看,容器协议带来的好处是统一性:

  • 不同容器都可以用 for 遍历。
  • 不同容器都可以使用 len() 获取长度。
  • 序列、映射或自定义容器都可以使用 [] 访问元素。
  • 可变容器可以通过 x[a] = v 修改元素。
  • 可变容器可以通过 del x[a] 删除元素。
  • 容器可以通过 in 表达成员测试。

从设计者角度看,容器协议使类能够自然融入 Python 语言。例如,一个类如果表示某种记录集合、表格、缓存、队列、索引结构或业务对象集合,就可以通过实现这些特殊方法来提供熟悉的容器接口。

自定义容器:Portfolio 示例

summaries/01_Iteration_protocol 使用 Portfolio 展示了如何把一个普通列表封装成更高级的业务容器。最初,投资组合可能只是 Stock 对象的列表:

portfolio = [Stock('AA', 100, 32.2), Stock('IBM', 50, 91.1)]

后来可以引入一个 Portfolio 类,在内部保存这个列表,并添加业务方法:

class Portfolio:
    def __init__(self, holdings):
        self._holdings = holdings

    @property
    def total_cost(self):
        return sum([s.shares * s.price for s in self._holdings])

    def tabulate_shares(self):
        from collections import Counter
        total_shares = Counter()
        for s in self._holdings:
            total_shares[s.name] += s.shares
        return total_shares

这样做体现了 对象封装:内部仍然使用列表,但对外暴露的是更符合业务语义的对象,例如 portfolio.total_cost。

不过,如果原有程序依赖:

for s in portfolio:
    ...

那么 Portfolio 必须支持迭代。修复方式是实现 __iter__(),并把迭代委托给内部列表:

class Portfolio:
    def __init__(self, holdings):
        self._holdings = holdings

    def __iter__(self):
        return self._holdings.__iter__()

这样,Portfolio 实例就可以像普通列表一样用于 for 循环,但同时又保留了封装和业务方法。

更完整的容器还可以实现:

class Portfolio:
    def __init__(self, holdings):
        self._holdings = holdings

    def __iter__(self):
        return self._holdings.__iter__()

    def __len__(self):
        return len(self._holdings)

    def __getitem__(self, index):
        return self._holdings[index]

    def __contains__(self, name):
        return any([s.name == name for s in self._holdings])

    @property
    def total_cost(self):
        return sum([s.shares * s.price for s in self._holdings])

此时可以使用标准容器语法:

len(portfolio)
portfolio[0]
portfolio[0:3]
'IBM' in portfolio

这个例子说明:自定义容器不只是“包一层列表”,而是通过协议决定它如何参与 Python 语言。一个容器越能使用 Python 的通用词汇,例如迭代、索引、切片、长度和成员测试,就越符合 Pythonic设计。

容器与对象表示

容器经常在交互式环境、日志和调试输出中被直接打印或查看。此时,容器中元素的表示方式会显著影响可读性。

summaries/03_Special_methods 中的 Stock 示例要求为对象实现更有用的 __repr__():

>>> goog = Stock('GOOG', 100, 490.1)
>>> goog
Stock('GOOG', 100, 490.1)

当多个 Stock 对象被放入列表后,查看整个列表时,列表会使用每个元素的 repr() 表示。因此,如果对象的 __repr__() 写得清晰,那么包含这些对象的容器也会更容易检查和调试。

这说明容器和 对象表示 之间存在实际联系:

  • 容器负责组织多个对象。
  • 元素对象的 __repr__() 决定容器显示时的可读性。
  • 好的对象表示能改善列表、字典、集合和自定义容器的调试体验。

这一点在数据处理程序中很重要,因为投资组合、记录列表、查找表等结构经常需要在交互式解释器中直接查看。

容器与动态属性访问

容器既可以保存元组或字典,也可以保存普通对象。例如投资组合可以是 Stock 对象的列表,或者是一个封装了 Stock 对象列表的 Portfolio 容器。此时,如果想根据用户指定的字段名输出表格,就需要动态读取对象属性。

summaries/03_Special_methods 介绍了 getattr():

getattr(obj, 'name')          # 等同于 obj.name
setattr(obj, 'name', value)   # 等同于 obj.name = value
delattr(obj, 'name')          # 等同于 del obj.name
hasattr(obj, 'name')          # 判断属性是否存在

这使容器中的对象可以被通用代码处理。例如:

columns = ['name', 'shares']
for colname in columns:
    print(colname, '=', getattr(s, colname))

在练习中,这个思想被扩展为 print_table():它接收一组对象、用户指定的属性名列表,以及一个 TableFormatter,然后打印表格。这与 concepts/动态属性访问、表格格式化 和 对象属性驱动设计 有关。

因此,容器不仅能保存基础数据结构,也能保存对象;而 getattr() 等机制让“对象列表”可以像“记录表”一样被通用处理。

collections 模块中的专用容器

内置容器已经能够解决大量问题,但某些常见数据处理任务如果只用普通 dict 或 list,代码会显得重复或不够直接。summaries/05_Collections 介绍的 collections模块 正是为这些专门场景提供更合适的容器。

1. Counter:计数和汇总

Counter 是一种专门用于计数的映射容器。它很适合统计某个键出现的次数,或把同一键对应的数值累加起来。

例如,投资组合中同一只股票可能出现多次:

from collections import Counter

total_shares = Counter()
for name, shares, price in portfolio:
    total_shares[name] += shares

这样,重复出现的 IBM、GOOG 等股票会自动被合并到同一个统计项中:

total_shares['IBM']     # 150

Counter 可以像字典一样通过键访问值,也提供了适合统计分析的方法,例如:

holdings.most_common(3)

它还支持多个计数器相加:

combined = holdings + holdings2

相同键的计数会被自动累加。这使 Counter 特别适合 concepts/数据计数与汇总、排名、频率分析以及多个数据源的统计合并。

在 Portfolio.tabulate_shares() 中使用 Counter,正是把专用容器嵌入自定义业务容器的例子。

2. defaultdict:一对多映射和自动默认值

defaultdict 是字典的一种变体。它在访问不存在的键时会自动创建默认值,因此非常适合“一个键对应多个值”的场景。

例如,要把股票名称映射到该股票的所有持仓记录,可以写成:

from collections import defaultdict

holdings = defaultdict(list)
for name, shares, price in portfolio:
    holdings[name].append((shares, price))

如果用普通字典实现,通常需要先判断键是否存在,再决定是否创建空列表;而 defaultdict(list) 自动完成这一步。

这类容器适合:

  • 数据分组
  • 建立索引
  • 一对多映射
  • 按类别聚合记录
  • 从表格数据构造查找结构

它与 字典与映射、数据分组 和 CSV文件处理 的关系非常密切。

3. deque:队列和有限历史记录

deque 是双端队列,适合高效地在两端添加或删除元素。summaries/05_Collections 中强调了它的一个典型用法:保存最近 N 个对象。

例如,处理文件时保存最近 N 行:

from collections import deque

history = deque(maxlen=N)
with open(filename) as f:
    for line in f:
        history.append(line)
        ...

当设置 maxlen=N 后,deque 会自动保持固定长度。新元素加入且超过最大长度时,最旧的元素会被自动丢弃。

这种容器适合:

  • 最近历史记录
  • 滑动窗口
  • 日志尾部追踪
  • 流式数据处理
  • 队列类算法

它扩展了普通列表在队列场景中的表达能力,也与 序列与队列 和 滑动窗口 相关。

容器与数据处理

在 summaries/00_Overview 所概述的章节结构中,容器位于 Python 数据处理主题的早期部分。这说明容器是进一步理解以下内容的基础:

  • Python数据类型:容器本身也是数据类型。
  • 序列:列表、元组和字符串等都具有序列行为。
  • 格式化输出:容器中的数据常需要被转换为可读文本。
  • collections模块:标准库提供了更多专用容器,扩展内置容器能力。
  • concepts/列表推导式:列表等容器常通过推导式进行构造和转换。
  • CSV文件处理:文件中的行和列经常被读入列表、元组、字典或对象列表。
  • concepts/异常处理:读取真实数据时需要处理空行、缺失字段和转换错误。
  • Python对象模型:理解容器需要进一步理解对象、引用、可变性等底层机制。
  • Python特殊方法:容器语法背后由特殊方法驱动。
  • Python迭代协议:for 循环和许多数据处理模式都依赖迭代。
  • concepts/动态属性访问:对象容器可以通过属性名动态读取字段。

summaries/02_Containers 的股票投资组合示例展示了容器组合使用的典型模式:

  • 用列表保存多条持仓记录。
  • 用元组或字典表示每条持仓。
  • 用字典保存当前价格查找表。
  • 用集合进行成员测试、去重或比较。

summaries/05_Collections 则进一步展示了专用容器在类似数据上的增强用法:

  • 用 Counter 汇总每只股票的总股数。
  • 用 defaultdict(list) 把股票名映射到多条持仓记录。
  • 用 deque(maxlen=N) 保存最近 N 条记录。

summaries/01_Iteration_protocol 和 summaries/03_Special_methods 则说明了容器行为的语言机制:

  • for x in obj 调用 obj.__iter__(),并反复调用迭代器的 __next__()。
  • len(x) 调用 x.__len__()。
  • x[a] 调用 x.__getitem__(a)。
  • x[a] = v 调用 x.__setitem__(a)。
  • del x[a] 调用 x.__delitem__(a)。
  • x in obj 可由 obj.__contains__(x) 支持。

这种组合比单独使用某一种容器更接近真实程序的数据组织方式:实际程序既会选择合适的数据结构,也会依赖 Python 的统一协议让不同结构拥有一致的使用体验。

容器组合模式

Python 程序经常把容器嵌套或封装使用,以表达更复杂的数据结构。

元组列表

“元组的列表”适合表示简单表格:

portfolio = [
    ('AA', 100, 32.2),
    ('IBM', 50, 91.1)
]

优点是结构紧凑;缺点是字段含义依赖位置。

字典列表

“字典的列表”适合表示多条结构化记录:

portfolio = [
    {'name': 'AA', 'shares': 100, 'price': 32.2},
    {'name': 'IBM', 'shares': 50, 'price': 91.1}
]

优点是字段名清晰,代码可读性更好。

对象列表

当记录具有行为或需要封装逻辑时,也可以使用“对象的列表”:

portfolio = [
    Stock('AA', 100, 32.2),
    Stock('IBM', 50, 91.1)
]

这种结构与面向对象设计更接近。若 Stock 实现了清晰的 __repr__(),查看整个列表时会得到更有用的输出;若配合 getattr(),还可以根据字段名动态生成表格。这连接了 Python对象模型、对象表示 和 concepts/动态属性访问。

封装列表的业务容器

当对象列表本身具有业务意义时,可以把它封装成自定义容器。例如 Portfolio 内部保存 Stock 列表,但对外提供:

  • total_cost:计算总成本
  • tabulate_shares():汇总每只股票股数
  • __iter__():支持遍历
  • __len__():支持长度
  • __getitem__():支持索引和切片
  • __contains__():支持成员测试

这种模式比直接暴露列表更清晰,因为它把业务逻辑集中在容器对象中,同时仍然保留 Python 容器的通用用法。

查找字典

“键到值的字典”适合快速查询:

prices = {
    'IBM': 106.28,
    'MSFT': 20.89
}

在计算投资组合盈亏时,可以遍历持仓列表,并用股票名在价格字典中查找当前价格。

计数字典

当字典的值表示数量累计时,可以使用普通 dict,但 Counter 通常更直接:

from collections import Counter

holdings = Counter()
for s in portfolio:
    holdings[s.name] += s.shares

这种模式适合把多条记录汇总为每个键的总量。

一对多映射

当一个键对应多个记录时,可以使用 defaultdict(list):

from collections import defaultdict

by_name = defaultdict(list)
for name, shares, price in portfolio:
    by_name[name].append((shares, price))

这种结构适合分组和索引,比手动维护“键是否存在”的逻辑更简洁。

固定长度历史队列

当只关心最近 N 个元素时,可以使用 deque(maxlen=N):

from collections import deque

history = deque(maxlen=10)
history.append('new event')

它会自动丢弃过旧的数据,适合流式处理和日志类程序。

自定义容器

当内置容器和 collections 中的工具不足以表达某种数据结构时,可以编写自定义类,并通过容器协议让它支持标准操作。例如,一个自定义表格、缓存、稀疏数组、记录集合或业务对象集合,可以根据需要实现:

  • __iter__():定义遍历方式。
  • __len__():定义长度。
  • __getitem__():定义索引、切片或键访问。
  • __setitem__():定义元素更新。
  • __delitem__():定义元素删除。
  • __contains__():定义成员测试。

这样,用户就能用熟悉的语法操作它:

for record in records:
    ...

len(records)
records[0]
records['IBM'] = value
del records['OLD']
'IBM' in records

这说明“容器”既是数据结构选择问题,也是接口设计问题。

选择容器的基本思路

选择哪种容器,通常取决于数据的组织方式和操作需求:

  • 需要有序并且可修改:使用 list
  • 需要有序但固定不变:使用 tuple
  • 需要唯一元素和集合运算:使用 set
  • 需要键值映射和快速查找:使用 dict
  • 需要表示一条固定字段记录:可使用 tuple
  • 需要更可读的结构化记录:可使用 dict
  • 需要记录同时具有数据和行为:可使用对象,并把多个对象放入 list
  • 需要保存多条记录:常使用 list 包裹元组、字典或对象
  • 需要把名称映射到数值:常使用 dict
  • 需要统计每个键的数量或总量:使用 Counter
  • 需要一个键对应多个值:使用 defaultdict(list)
  • 需要自动创建默认值:使用 defaultdict
  • 需要保存最近 N 个元素:使用 deque(maxlen=N)
  • 需要封装业务逻辑但保留容器行为:编写自定义容器并实现迭代、长度、索引和成员测试
  • 需要自定义容器语法:实现 __iter__()、__len__()、__getitem__()、__contains__() 等特殊方法

这种选择体现了 Python 编程中的一个重要思想:根据数据关系和操作需求选择合适的数据结构;当已有结构不足时,通过协议和特殊方法让自定义对象融入语言本身。

与对象模型的关系

Python 中的容器并不是简单的“值盒子”,而是对象。容器保存的是对象引用,这一点与 Python对象模型 密切相关。

因此,在使用容器时需要理解:

  • 容器本身可以是可变或不可变对象。
  • 容器内部元素也是对象。
  • 多个变量可能引用同一个容器。
  • 修改可变容器可能影响所有引用它的位置。
  • 字典键必须满足不可变性要求。
  • 嵌套容器会让引用关系更加复杂。
  • 专用容器虽然行为更高层,但仍遵循 Python 对象和引用模型。
  • 自定义容器通过特殊方法接入 Python 的容器语法。
  • 容器中的对象如果定义了合适的 __repr__(),整体显示和调试体验会更好。
  • 容器的可迭代性由 __iter__() 和迭代器的 __next__() 支持。

这些概念对理解 Python 程序的行为非常重要,尤其是在处理列表、字典、defaultdict、Counter、对象列表和自定义容器时。

Pythonic 容器设计

summaries/01_Iteration_protocol 强调了一个重要观察:代码如果“说的是 Python 其他部分通用的语言”,通常会更 Pythonic。对于容器对象来说,这意味着不要只提供专用方法,而要尽量支持用户熟悉的操作。

一个设计良好的容器通常应该考虑:

  • 能否被 for 循环遍历?
  • 能否用 len() 获取大小?
  • 能否用 [] 访问元素?
  • 是否应该支持切片?
  • 是否应该支持 in 成员测试?
  • 是否需要支持元素更新或删除?
  • 是否应该隐藏内部实现,同时保留自然的容器接口?

例如,Portfolio 可以隐藏 _holdings 内部列表,但通过 __iter__()、__len__()、__getitem__() 和 __contains__() 提供熟悉的容器行为。这样,调用者不需要知道内部到底是列表、元组还是其他数据结构,只需要按照 Python 容器的通用方式使用它。

这体现了 Python 容器设计的核心:协议比具体类型更重要。一个对象只要遵守相应协议,就能自然地参与 Python 的语言结构和工具生态。

小结

Python 容器是处理数据的基础工具。它们帮助程序组织多个对象,并通过不同结构表达不同的数据关系:列表表达有序集合,元组表达固定结构,集合表达唯一成员,字典表达键值映射。summaries/02_Containers 通过股票投资组合、价格表和盈亏计算示例说明,实际程序往往会组合使用多种容器来完成数据读取、查询和计算。

summaries/05_Collections 在此基础上补充了标准库中的专用容器:Counter 用于计数和汇总,defaultdict 用于分组和一对多映射,deque 用于队列和有限历史记录。summaries/03_Special_methods 揭示了容器语法背后的机制:len()、索引、赋值和删除操作都通过特殊方法实现。summaries/01_Iteration_protocol 进一步说明,for 循环依赖 __iter__()、__next__() 和 StopIteration,因此迭代是容器行为的核心部分之一。

理解这些容器及其适用场景,是继续学习 序列、collections模块、concepts/列表推导式、CSV文件处理、字典与映射、Python对象模型、Python特殊方法、容器协议 和 Python迭代协议 的前提。

See also: summaries/01_Datatypes, summaries/02_Containers, summaries/03_Formatting, summaries/05_Collections, summaries/06_List_comprehension, summaries/03_Special_methods, summaries/01_Iteration_protocol

See also: summaries/02_Customizing_iteration

See also: summaries/04_More_generators

See also: summaries/02_Working_with_data__00_Overview

See also: summaries/06_Generators__00_Overview

See also: summaries/07_Objects