可变性、复制与迭代
本节目标
理解“变量绑定对象”、可变性与复制层级,避免 Agent 状态被意外共享;区分 iterable、iterator 与 generator,并在下游保持流式时降低峰值内存。
序列回顾
序列按位置排列元素,通常支持:
messages = ["system", "user", "assistant"]
print(messages[0]) # 第一个元素
print(messages[-1]) # 最后一个元素
print(messages[1:]) # 从下标 1 到末尾的新列表
print(len(messages))下标从 0 开始。切片 start:stop 包含 start,不包含 stop。访问不存在的单个下标会抛出 IndexError;超出边界的切片会安全截断。
list、tuple 与 str
| 类型 | 是否可变 | 常见用途 |
|---|---|---|
list | 可变 | 消息、步骤、批次结果 |
tuple | 元组本身不可变 | 固定坐标、不可变记录片段、拆包 |
str | 不可变 | 文本、ID、路径的字符串表示 |
“元组不可变”指不能把某个位置重新绑定;若元组内部包含列表,列表本身仍可修改:
record = ("run-1", ["created"])
record[1].append("finished") # 合法:修改内部列表因此,不可变容器不自动让整棵嵌套数据不可变。
别名:两个变量可能指向同一列表
history = ["hello"]
backup = history
backup.append("world")
print(history) # ['hello', 'world']赋值没有复制列表,只让两个名字绑定同一对象。需要独立的外层列表时:
backup = history.copy()但这是浅拷贝。若元素本身是列表或字典,内层对象仍共享:
messages = [{"role": "user", "content": "hi"}]
snapshot = messages.copy()
snapshot[0]["content"] = "changed"
print(messages[0]["content"]) # changed需要隔离嵌套状态时,可以创建新的字典、使用 copy.deepcopy,或设计不可变数据;先明确哪些层允许修改,不要无脑深拷贝大型上下文。
可变默认参数陷阱
# 错误:同一个列表会在多次调用间共享
def add_message(message, history=[]):
history.append(message)
return history
# 正确:每次未传入时创建新列表
def add_message(message, history=None):
if history is None:
history = []
history.append(message)
return historyAgent 会并行处理多个任务时,共享可变状态尤其危险。状态应按 run_id 隔离,并通过清楚的接口更新。
迭代期间修改容器
遍历列表时删除或插入元素会改变后续下标,容易漏项:
items = [1, 2, 3, 4]
for value in items.copy():
if value % 2 == 0:
items.remove(value)更清晰的做法通常是生成新列表:items = [x for x in items if x % 2 != 0]。遍历字典视图时增删条目可能抛出 RuntimeError,也可能漏遍历条目;可遍历 list(mapping) 的键快照,或先收集待删除键。并发任务仍需要更高层同步,复制键列表不是并发控制。
iterable、iterator 与 generator
- iterable(可迭代对象):能交给
iter(),例如 list、tuple、dict、文件; - iterator(迭代器):能用
next()逐项取值,并用StopIteration表示结束; - generator(生成器):由生成器表达式或含
yield的函数产生,是一种保存执行状态的迭代器。
def batched_ids(prefix: str, count: int):
for index in range(count):
yield f"{prefix}-{index}"
ids = batched_ids("run", 3)
print(next(ids)) # run-0
print(list(ids)) # ['run-1', 'run-2'];生成器继续并耗尽
print(list(ids)) # []同一个生成器对象是一次性迭代器,耗尽后不会自动复位;重新调用生成器函数可创建新对象。若多个步骤要独立读取,应各自创建生成器或有意识地物化为不可变快照;不要把一个半耗尽的 iterator 隐式共享给多个消费者。
流式不等于自动省内存
def normalized(lines):
for line in lines:
cleaned = line.strip()
if cleaned:
yield cleaned只有下游也逐项消费,才能避免同时保存所有结果:
for line in normalized(source):
process(line)list(normalized(source)) 会重新物化全部值。生成器还会把读取或解析错误推迟到消费时;需要明确资源关闭、异常位置和是否允许重复遍历。大文件分块在 文档解析 中深化,本节只讲 Python 迭代语义。
列表推导式:转换而非隐藏副作用
latencies = [120, 250, 80, 500]
slow = [value for value in latencies if value >= 200]推导式适合生成新集合。若逻辑包含网络调用、异常处理或多步状态更新,普通 for 循环更清楚。
练习
- 预测下面输出,再运行验证:
a = [[1], [2]]
b = a.copy()
b[0].append(9)
print(a)- 写函数
last_n(messages, n):返回最后n条,不修改原列表;n <= 0返回空列表。 - 用元组表示
(tool_name, call_id),用拆包分别取出两个字段。 - 解释为什么把同一个
history列表给两个并发任务会串话。 - 写
non_empty(lines)生成器,证明它只能消费一次;再写一个工厂函数让两位消费者各自获得新生成器。 - 迭代字典时删除键,观察错误;改成“先收集、后删除”。
自测
- 切片
items[1:3]包含哪些下标? tuple中能否包含list?该列表能否改变?copy()为什么不能隔离所有嵌套对象?- 何时应优先用普通循环而非列表推导式?
- iterable 与 iterator 的区别是什么?
- 为什么生成器只有在下游保持流式时才可能降低峰值内存?
- 一个生成器被
list()后还能从头再读吗?
下一步与相关概念
下一节:栈、队列与双端队列。
参考资料
获取日期:2026-07-14。