可变性、复制与迭代

本节目标

理解“变量绑定对象”、可变性与复制层级,避免 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 history

Agent 会并行处理多个任务时,共享可变状态尤其危险。状态应按 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 循环更清楚。

练习

  1. 预测下面输出,再运行验证:
a = [[1], [2]]
b = a.copy()
b[0].append(9)
print(a)
  1. 写函数 last_n(messages, n):返回最后 n 条,不修改原列表;n <= 0 返回空列表。
  2. 用元组表示 (tool_name, call_id),用拆包分别取出两个字段。
  3. 解释为什么把同一个 history 列表给两个并发任务会串话。
  4. 写 non_empty(lines) 生成器,证明它只能消费一次;再写一个工厂函数让两位消费者各自获得新生成器。
  5. 迭代字典时删除键,观察错误;改成“先收集、后删除”。

自测

  1. 切片 items[1:3] 包含哪些下标?
  2. tuple 中能否包含 list?该列表能否改变?
  3. copy() 为什么不能隔离所有嵌套对象?
  4. 何时应优先用普通循环而非列表推导式?
  5. iterable 与 iterator 的区别是什么?
  6. 为什么生成器只有在下游保持流式时才可能降低峰值内存?
  7. 一个生成器被 list() 后还能从头再读吗?

下一步与相关概念

下一节:栈、队列与双端队列。

参考资料

获取日期:2026-07-14。