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

858 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
sources: [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]
brief: 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/列表推导式]] 进行数据转换
- 保存从文件中读取出来的一组记录
- 作为自定义容器内部的底层存储
例如,股票投资组合可以表示为“元组的列表”:
```python
portfolio = [
('GOOG', 100, 490.1),
('IBM', 50, 91.3),
('CAT', 150, 83.44)
]
```
可以通过整数索引访问列表元素:
```python
portfolio[0] # ('GOOG', 100, 490.1)
portfolio[2] # ('CAT', 150, 83.44)
```
列表也可以从空列表开始构造,并通过 `.append()` 添加项目:
```python
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`
元组是有序、不可变的容器。它与列表类似,也可以按位置访问元素,但创建后通常不能修改。
常见用途包括:
- 表示固定结构的数据
- 从函数返回多个值
- 用作不可变记录
- 在需要稳定结构时替代列表
- 作为字典中的复合键
例如,一条股票持仓可以用三元组表示:
```python
holding = ('IBM', 50, 91.1)
```
列表中的每个元素也可以是元组,从而形成类似二维表的数据结构:
```python
portfolio[row][column]
```
不过,使用数字列号访问字段有时可读性较差。因此后续常会用字典或对象来表示结构化记录。
元组属于 序列 类型的一种,因此支持索引、切片和迭代等序列操作。它也与 序列解包 密切相关,例如:
```python
for name, shares, price in portfolio:
total += shares * price
```
这种写法比反复使用 `s[0]`、`s[1]`、`s[2]` 更清晰。
### 3. 字典 `dict`
字典是键值对容器,用于建立 key 到 value 的映射关系。它适合需要通过名称、编号或其他键进行快速随机查找的场景。
常见用途包括:
- 通过名称、编号或其他键快速查找值
- 表示结构化记录
- 统计数据
- 构建查找表或配置对象
- 将外部表格数据转换为可查询的数据结构
- 作为更专门映射容器的基础,例如 `Counter` 和 `defaultdict`
例如,股票价格表可以表示为字典:
```python
prices = {
'GOOG': 513.25,
'CAT': 87.22,
'IBM': 93.37,
'MSFT': 44.12
}
```
访问时使用键,而不是整数位置:
```python
prices['IBM']
prices['GOOG']
```
在 [[summaries/02_Containers]] 中,`read_prices(filename)` 读取 `Data/prices.csv`,并构造一个“股票代码到当前价格”的字典。这种结构非常适合后续根据投资组合中的股票名快速查找当前价格。
#### 字典查找与默认值
字典可以用 `in` 判断键是否存在:
```python
if key in d:
# key 存在
else:
# key 不存在
```
也可以用 `.get()` 在键不存在时提供默认值:
```python
value = d.get(key, default)
```
例如:
```python
prices.get('IBM', 0.0) # 93.37
prices.get('SCOX', 0.0) # 0.0
```
这在处理不完整数据时很有用,也与 健壮文件读取 和 数据清洗 有关。标准库中的 `defaultdict` 进一步扩展了这种“默认值”思想:访问不存在的键时,它会自动创建默认值,从而减少显式判断代码。
#### 字典表示结构化记录
除了作为查找表,字典也可以表示一条结构化记录。例如,一条股票持仓可以写成:
```python
holding = {
'name': 'IBM',
'shares': 50,
'price': 91.1
}
```
由多条记录组成的投资组合就可以表示为“字典的列表”:
```python
portfolio = [
{'name': 'AA', 'shares': 100, 'price': 32.2},
{'name': 'IBM', 'shares': 50, 'price': 91.1}
]
```
访问字段时使用字段名:
```python
portfolio[1]['shares']
```
这种形式通常比元组列表更易读,因为代码直接表达了字段含义,而不是依赖列号。
#### 复合键与不可变性
Python 字典的键必须是不可变对象。字符串、数字、元组等可以作为键;列表、集合和字典不能作为键,因为它们是可变对象。
元组可用于表示复合键:
```python
holidays = {
(1, 1): 'New Years',
(3, 14): 'Pi day',
(9, 13): "Programmer's day"
}
```
访问时可以写作:
```python
holidays[3, 14]
```
这体现了 可变性与不可变性 对容器设计的重要影响。
### 4. 集合 `set`
集合是无序、不重复元素的容器,主要用于表达数学意义上的集合关系。
常见用途包括:
- 去除重复值
- 判断成员是否存在
- 求并集、交集、差集等集合运算
- 表达唯一元素集合
- 比较两个数据源中的元素差异
集合强调“元素是否存在”,而不是“元素位于哪个位置”。
示例:
```python
tech_stocks = {'IBM', 'AAPL', 'MSFT'}
```
集合非常适合成员测试:
```python
'IBM' in tech_stocks # True
'FB' in tech_stocks # False
```
也常用于去重:
```python
names = ['IBM', 'AAPL', 'GOOG', 'IBM', 'GOOG', 'YHOO']
unique = set(names)
```
集合还支持常见集合运算:
```python
s1 = {'a', 'b', 'c'}
s2 = {'c', 'd'}
s1 | s2 # 并集
s1 & s2 # 交集
s1 - s2 # 差集
```
这些操作使集合适合用于比较名称列表、股票代码集合或文件中的唯一记录。
## 容器与迭代协议
[[summaries/01_Iteration_protocol]] 说明了 Python 容器最重要的共同能力之一:可迭代。许多对象都支持迭代,包括字符串、字典、列表、元组、集合和文件对象。
例如:
```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迭代协议。语句:
```python
for x in obj:
# statements
```
大致等价于:
```python
_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` 并不是只适用于内置容器的语法糖;只要一个类实现了相应的特殊方法,它就可以表现得像容器。
常见容器操作与特殊方法的对应关系是:
```python
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) # 如果定义了该方法
```
一个自定义容器类通常会实现类似结构:
```python
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` 对象的列表:
```python
portfolio = [Stock('AA', 100, 32.2), Stock('IBM', 50, 91.1)]
```
后来可以引入一个 `Portfolio` 类,在内部保存这个列表,并添加业务方法:
```python
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`。
不过,如果原有程序依赖:
```python
for s in portfolio:
...
```
那么 `Portfolio` 必须支持迭代。修复方式是实现 `__iter__()`,并把迭代委托给内部列表:
```python
class Portfolio:
def __init__(self, holdings):
self._holdings = holdings
def __iter__(self):
return self._holdings.__iter__()
```
这样,`Portfolio` 实例就可以像普通列表一样用于 `for` 循环,但同时又保留了封装和业务方法。
更完整的容器还可以实现:
```python
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])
```
此时可以使用标准容器语法:
```python
len(portfolio)
portfolio[0]
portfolio[0:3]
'IBM' in portfolio
```
这个例子说明:自定义容器不只是“包一层列表”,而是通过协议决定它如何参与 Python 语言。一个容器越能使用 Python 的通用词汇,例如迭代、索引、切片、长度和成员测试,就越符合 Pythonic设计。
## 容器与对象表示
容器经常在交互式环境、日志和调试输出中被直接打印或查看。此时,容器中元素的表示方式会显著影响可读性。
[[summaries/03_Special_methods]] 中的 `Stock` 示例要求为对象实现更有用的 `__repr__()`:
```python
>>> goog = Stock('GOOG', 100, 490.1)
>>> goog
Stock('GOOG', 100, 490.1)
```
当多个 `Stock` 对象被放入列表后,查看整个列表时,列表会使用每个元素的 `repr()` 表示。因此,如果对象的 `__repr__()` 写得清晰,那么包含这些对象的容器也会更容易检查和调试。
这说明容器和 对象表示 之间存在实际联系:
- 容器负责组织多个对象。
- 元素对象的 `__repr__()` 决定容器显示时的可读性。
- 好的对象表示能改善列表、字典、集合和自定义容器的调试体验。
这一点在数据处理程序中很重要,因为投资组合、记录列表、查找表等结构经常需要在交互式解释器中直接查看。
## 容器与动态属性访问
容器既可以保存元组或字典,也可以保存普通对象。例如投资组合可以是 `Stock` 对象的列表,或者是一个封装了 `Stock` 对象列表的 `Portfolio` 容器。此时,如果想根据用户指定的字段名输出表格,就需要动态读取对象属性。
[[summaries/03_Special_methods]] 介绍了 `getattr()`:
```python
getattr(obj, 'name') # 等同于 obj.name
setattr(obj, 'name', value) # 等同于 obj.name = value
delattr(obj, 'name') # 等同于 del obj.name
hasattr(obj, 'name') # 判断属性是否存在
```
这使容器中的对象可以被通用代码处理。例如:
```python
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` 是一种专门用于计数的映射容器。它很适合统计某个键出现的次数,或把同一键对应的数值累加起来。
例如,投资组合中同一只股票可能出现多次:
```python
from collections import Counter
total_shares = Counter()
for name, shares, price in portfolio:
total_shares[name] += shares
```
这样,重复出现的 `IBM`、`GOOG` 等股票会自动被合并到同一个统计项中:
```python
total_shares['IBM'] # 150
```
`Counter` 可以像字典一样通过键访问值,也提供了适合统计分析的方法,例如:
```python
holdings.most_common(3)
```
它还支持多个计数器相加:
```python
combined = holdings + holdings2
```
相同键的计数会被自动累加。这使 `Counter` 特别适合 [[concepts/数据计数与汇总]]、排名、频率分析以及多个数据源的统计合并。
在 `Portfolio.tabulate_shares()` 中使用 `Counter`,正是把专用容器嵌入自定义业务容器的例子。
### 2. `defaultdict`:一对多映射和自动默认值
`defaultdict` 是字典的一种变体。它在访问不存在的键时会自动创建默认值,因此非常适合“一个键对应多个值”的场景。
例如,要把股票名称映射到该股票的所有持仓记录,可以写成:
```python
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 行:
```python
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 程序经常把容器嵌套或封装使用,以表达更复杂的数据结构。
### 元组列表
“元组的列表”适合表示简单表格:
```python
portfolio = [
('AA', 100, 32.2),
('IBM', 50, 91.1)
]
```
优点是结构紧凑;缺点是字段含义依赖位置。
### 字典列表
“字典的列表”适合表示多条结构化记录:
```python
portfolio = [
{'name': 'AA', 'shares': 100, 'price': 32.2},
{'name': 'IBM', 'shares': 50, 'price': 91.1}
]
```
优点是字段名清晰,代码可读性更好。
### 对象列表
当记录具有行为或需要封装逻辑时,也可以使用“对象的列表”:
```python
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 容器的通用用法。
### 查找字典
“键到值的字典”适合快速查询:
```python
prices = {
'IBM': 106.28,
'MSFT': 20.89
}
```
在计算投资组合盈亏时,可以遍历持仓列表,并用股票名在价格字典中查找当前价格。
### 计数字典
当字典的值表示数量累计时,可以使用普通 `dict`,但 `Counter` 通常更直接:
```python
from collections import Counter
holdings = Counter()
for s in portfolio:
holdings[s.name] += s.shares
```
这种模式适合把多条记录汇总为每个键的总量。
### 一对多映射
当一个键对应多个记录时,可以使用 `defaultdict(list)`:
```python
from collections import defaultdict
by_name = defaultdict(list)
for name, shares, price in portfolio:
by_name[name].append((shares, price))
```
这种结构适合分组和索引,比手动维护“键是否存在”的逻辑更简洁。
### 固定长度历史队列
当只关心最近 N 个元素时,可以使用 `deque(maxlen=N)`:
```python
from collections import deque
history = deque(maxlen=10)
history.append('new event')
```
它会自动丢弃过旧的数据,适合流式处理和日志类程序。
### 自定义容器
当内置容器和 `collections` 中的工具不足以表达某种数据结构时,可以编写自定义类,并通过容器协议让它支持标准操作。例如,一个自定义表格、缓存、稀疏数组、记录集合或业务对象集合,可以根据需要实现:
- `__iter__()`:定义遍历方式。
- `__len__()`:定义长度。
- `__getitem__()`:定义索引、切片或键访问。
- `__setitem__()`:定义元素更新。
- `__delitem__()`:定义元素删除。
- `__contains__()`:定义成员测试。
这样,用户就能用熟悉的语法操作它:
```python
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]]