858 lines
30 KiB
Markdown
858 lines
30 KiB
Markdown
---
|
||
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]] |