🎯 学习目标
- 理解可迭代对象、迭代器协议(
__iter__/__next__) - 掌握生成器函数
yield、yield from与生成器表达式 - 理解迭代器惰性求值、节省内存的意义
- 理解闭包及其典型用途
- 从零推导并掌握装饰器(带参数、堆叠、保留元信息)
- 熟练运用列表/字典/集合三大推导式
1可迭代对象与迭代器
可迭代对象(Iterable):能被 for 循环遍历的东西——str、list、tuple、dict、set、range、文件对象等。判定标准:实现了 __iter__()。
迭代器(Iterator):一个"带记忆的游标",实现了 __iter__() 和 __next__(),每次 next() 吐出下一个值,取完后抛 StopIteration。
nums = [1, 2, 3] # list 是可迭代对象,但不是迭代器
it = iter(nums) # iter() 取出它的迭代器
print(next(it)) # 1 游标前进
print(next(it)) # 2
print(next(it)) # 3
# next(it) # StopIteration:没有下一个了
# for 循环的本质就是这一套:
# 先 iter(x) 拿迭代器,再不断 next(),遇到 StopIteration 结束
it2 = iter(nums)
while True:
try:
print(next(it2))
except StopIteration:
break
from collections.abc import Iterable, Iterator
print(isinstance([1, 2], Iterable)) # True
print(isinstance([1, 2], Iterator)) # False 列表本身不是迭代器
print(isinstance(iter([1, 2]), Iterator)) # True iter 之后才是
# 文件对象本身就是迭代器(所以能逐行读且不回头)
# 迭代器只能往前走,不能重置、不能 len()、不能索引
自定义迭代器
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self # 自己就是迭代器
def __next__(self):
if self.current <= 0:
raise StopIteration # 到头
self.current -= 1
return self.current + 1
for n in Countdown(3):
print(n) # 3 2 1
2生成器:写迭代器的偷懒神器
手写迭代器要实现两个魔法方法,很繁琐。只要函数体里出现 yield,它就变成生成器函数,调用后返回一个自动实现迭代器协议的生成器对象。
def countdown(start):
print("开始")
while start > 0:
yield start # 每次 next 执行到这里"暂停",把值吐出去
start -= 1
print("结束")
g = countdown(3) # 函数体【并没有执行】,只是创建生成器
print(next(g)) # 打印"开始",返回 3,函数暂停
print(next(g)) # 从暂停处继续,返回 2
print(next(g)) # 1
# print(next(g)) # 打印"结束",抛 StopIteration
for n in countdown(3): # 实际使用都是 for 驱动
print(n)
理解 yield 的关键词是"暂停并返回":普通函数 return 后栈就销毁了;生成器 yield 时会冻结现场(局部变量全部保留),下次 next 从冻结点继续。
为什么生成器重要:惰性求值,内存恒定
# 普通做法:100 万条数据全部进内存
# data = [x ** 2 for x in range(10**6)] # 占用几十 MB
# 生成器做法:用一个算一个,内存几乎为 0
def gen_squares(n):
for x in range(n):
yield x ** 2
g = gen_squares(10 ** 6)
print(sum(g)) # 遍历时才逐个计算
# 处理 100G 语料文件也不会爆内存(NLP 大数据必备思想)
yield from:委托给另一个生成器
def chain_all(*iterables):
for it in iterables:
yield from it # 等价于 for x in it: yield x
print(list(chain_all([1, 2], (3, 4), "ab"))) # [1,2,3,4,'a','b']
# 实战:逐行读取多个大文件
def lines_of(paths):
for path in paths:
with open(path, encoding="utf-8") as f:
yield from f # 把该文件的每一行依次产出
生成器表达式
# 列表推导式用 [],生成器表达式用 ()
sum(x ** 2 for x in range(1, 101)) # 338350
any(x > 5 for x in [1, 2, 6]) # True
all(x > 0 for x in [1, 2, 3]) # True
# 大数据场景用生成器表达式,避免中间列表
with open("out.txt", "w", encoding="utf-8") as f:
f.writelines(f"{x}\n" for x in range(1_000_000))
3常用迭代工具
# zip:并行打包多个序列
names = ["张三", "李四"]
scores = [90, 85]
print(list(zip(names, scores))) # [('张三',90),('李四',85)]
for name, score in zip(names, scores):
print(name, score)
dict(zip(["a", "b"], [1, 2])) # {'a':1,'b':2} 两列表变字典
list(zip("abc", "123", "XY")) # 按最短的来:3组
# enumerate:编号(第4章);reversed:反向迭代
for i, ch in enumerate("abc", 1):
print(i, ch)
for x in reversed([1, 2, 3]):
print(x) # 3 2 1
# itertools 是迭代器工具宝库(第14章细讲)
from itertools import chain, groupby, islice
list(chain([1, 2], [3, 4])) # [1,2,3,4] 展平一层
list(islice(range(100), 0, 5)) # 迭代器版切片
4闭包:会"记住环境"的函数
闭包 = 内层函数 + 它记住的外层变量。外层函数已经执行完,内层函数仍能访问其局部变量:
def make_counter():
count = 0 # 外层局部变量
def counter():
nonlocal count # 修改外层变量需 nonlocal
count += 1
return count
return counter # 返回内层函数(注意不带括号)
c1 = make_counter()
print(c1(), c1(), c1()) # 1 2 3 —— count 被"记住"了
c2 = make_counter()
print(c2()) # 1 —— 各自独立的一份环境
# 实用:工厂函数批量生成相似函数
def make_power(n):
def power(x):
return x ** n
return power
square = make_power(2)
cube = make_power(3)
print(square(5), cube(5)) # 25 125
闭包常见的"晚绑定坑":在循环中创建引用循环变量的 lambda/闭包,所有函数会共享循环结束后的最终值。需要立即捕获当前值时,用默认参数 lambda x, i=i: ... 或用工厂函数包一层。
5装饰器(本章高潮)
装饰器是一个"接收函数、返回增强后新函数"的函数,在不修改原函数代码的前提下给它增加功能(日志、计时、权限、缓存、重试……),是 AOP(面向切面编程)思想的体现,也是 Flask/FastAPI/pytest 等框架的核心语法。
第一步:理解"函数也是对象,能被传递替换"
def say_hi():
print("hi")
def log(func):
def wrapper():
print(f"调用 {func.__name__} 之前")
func()
print("调用之后")
return wrapper
say_hi = log(say_hi) # 手动装饰:用增强版替换原函数
say_hi()
# 调用 say_hi 之前
# hi
# 调用之后
第二步:@ 语法糖
def log(func):
def wrapper(*args, **kwargs): # 接收任意参数
print(f"[日志] 开始执行 {func.__name__}")
result = func(*args, **kwargs) # 把参数原样转给原函数
print(f"[日志] 结束执行 {func.__name__}")
return result # 原样返回结果
return wrapper
@log # 等价于 add = log(add)
def add(a, b):
return a + b
print(add(2, 3))
# [日志] 开始执行 add
# [日志] 结束执行 add
# 5
第三步:保留原函数信息 functools.wraps
# 不处理的话,add.__name__ 会变成 'wrapper',docstring 也丢失
# 标准做法:
import functools
def log(func):
@functools.wraps(func) # 把 func 的名字/文档复制给 wrapper
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
第四步:带参数的装饰器(三层嵌套)
import functools, time
def repeat(times): # 多一层:接收装饰器自己的参数
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3) # 打招呼重复 3 次
def greet(name):
print(f"hi {name}")
greet("小明")
第五步:实用装饰器合集
# 1) 计时器
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__} 耗时 {time.perf_counter()-start:.4f}s")
return result
return wrapper
# 2) 缓存(标准库自带 functools.lru_cache,递归/重复计算神器)
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
return n if n < 2 else fib(n-1) + fib(n-2)
print(fib(100)) # 瞬间出结果(朴素递归到 40 就要等很久)
# 3) 多个装饰器可以堆叠(从下往上依次包装)
# @timer
# @lru_cache
# def fib(n): ...
类装饰器与 functools 全家桶(了解)
# 用类实现装饰器(靠 __call__)
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"第 {self.count} 次调用")
return self.func(*args, **kwargs)
@CountCalls
def hello():
print("hello")
hello(); hello() # 第1次… 第2次…
6推导式大全
# 列表推导式:[表达式 for 变量 in 可迭代 if 条件]
[x * x for x in range(1, 8)] # 平方
[x for x in range(20) if x % 2 == 0] # 筛选偶数
[x * y for x in [1, 2] for y in [10, 20]] # 双重循环:4个结果
[(x, y) for x in range(3) for y in range(3) if x != y]
# 嵌套推导式:矩阵转置
matrix = [[1, 2, 3], [4, 5, 6]]
[[row[i] for row in matrix] for i in range(3)] # [[1,4],[2,5],[3,6]]
# 字典推导式
{w: len(w) for w in ["a", "bb", "ccc"]} # {'a':1,'bb':2,'ccc':3}
{v: k for k, v in {"a": 1}.items()} # 键值反转
# 集合推导式(自动去重)
{w[0] for w in ["apple", "ant", "banana"]} # {'a','b'}
# 生成器表达式(圆括号,惰性)
total = sum(x for x in range(101))
推导式适合一句话能表达清楚的变换+筛选。嵌套超过两层、逻辑复杂时请改写成普通循环——可读性永远比炫技重要。
7NLP 案例:流式处理大语料
import json, re
def iter_jsonl(path):
"""生成器:逐行读取 JSONL,一行一个 dict,内存恒定。"""
with open(path, encoding="utf-8") as f:
for line in f:
if line.strip():
yield json.loads(line)
def build_pipeline(paths, min_len=5):
"""多文件 → 清洗 → 过滤 → 产出 (id, 文本) 流水线。"""
for path in paths:
for rec in iter_jsonl(path):
text = re.sub(r"\s+", "", rec["text"])
if len(text) >= min_len:
yield rec["id"], text
# 使用:即使语料有几十 GB,也是读一条处理一条
# for doc_id, text in build_pipeline(["news1.jsonl", "news2.jsonl"]):
# tokens = jieba.lcut(text)
# ... 后续分词 / 统计 / 入库
8本章小结与练习
📌 关键结论
- 迭代器 =
__iter__+__next__,单向、惰性、省内存。 - 带 yield 的函数是生成器函数;yield 暂停保留现场,yield from 委托产出。
- 闭包让内层函数记住外层变量;修改外层变量用 nonlocal。
- 装饰器 = 入参为函数、返回增强函数的函数;标配
*args, **kwargs+@functools.wraps。 - 大文件/大数据用生成器流水线,不要一次性读入。
练习1:写一个生成器 fib_gen(),不断产出斐波那契数 1,1,2,3,5…,并用它打印前 10 项。
def fib_gen():
a, b = 1, 1
while True:
yield a
a, b = b, a + b
g = fib_gen()
print([next(g) for _ in range(10)]) 无限生成器靠调用方控制取多少,内存恒定。练习2:写一个 @timer 装饰器,打印被装饰函数的执行耗时。
用 time.perf_counter() 在 wrapper 内记录前后时间差,结构:def timer(func): @functools.wraps(func) def wrapper(*a,**kw): start=...; r=func(*a,**kw); print(耗时); return r; return wrapper。本章 5.5 有完整代码。
练习3:列表推导式和生成器表达式在内存使用上有何区别?处理百万行文件应选哪个?
列表推导式立即生成包含全部结果的列表,数据量大时占用大量内存;生成器表达式只保存算法状态,每次迭代才计算一个值,内存恒定。处理百万行文件应选生成器(或 yield 生成器函数)逐条处理,避免内存溢出。