1 迭代器协议 —— for 循环的幕后真相
是什么?为什么学?
迭代器(iterator)是「一次取一个」的取值工具。你天天用的 for x in 列表,底层就是在和迭代器打交道:iter() 拿迭代器,next() 逐个取值,取完抛 StopIteration 结束。理解这套协议,你就能让任何自定义类被 for 遍历。
底层原理
「可迭代」(iterable)与「迭代器」(iterator)是两个概念:可迭代对象(如列表、字符串)实现了 __iter__,返回一个迭代器;迭代器实现了 __next__,每次返回下一个值,取完抛 StopIteration。for 循环的完整展开是:it = iter(对象) → 循环里 x = next(it) → 捕获 StopIteration 就 break。迭代器像「一次性吸管」——取过的值不保留,所以迭代器只能用一遍(list(it) 之后再 next(it) 就报错)。
生活类比
迭代器像排队取餐的窗口:你每次去问「下一个好了吗」(next),窗口给你一份餐,发完了就告诉你「没了」(StopIteration)——队伍还在原地,但你的取餐过程走完就结束了,想再取得重新排队。
注意事项总结
1 iter 拿迭代器,next 逐个取;用 try/except 接住结束信号:
nums = [1, 2, 3]
it = iter(nums)
while True:
try:
value = next(it)
except StopIteration:
print("取完了,循环结束")
break
print("取到:", value)
运行结果:
取到: 1
取到: 2
取到: 3
取完了,循环结束
iter(nums) 调用列表的 __iter__ 拿到迭代器;next(it) 调用迭代器的 __next__ 取值。这段代码和 for x in nums: 做的事一模一样——for 的完整展开就是:调 iter、循环 next、捕获 StopIteration、break、处理元素。理解这个展开,你就看穿了所有循环。
2 自己写一个「能被 for 循环遍历」的类(迭代器协议):
class CountDown:
"""从 start 倒数到 1 的迭代器"""
def __init__(self, start):
self.current = start
def __iter__(self):
return self # 返回自己即可(自己就是迭代器)
def __next__(self):
if self.current < 1:
raise StopIteration # 没有元素了,抛异常告诉 for 循环"结束"
value = self.current
self.current -= 1
return value
for n in CountDown(3):
print(n)
运行结果:
3
2
1
易错点
- 错误写法:对迭代器重复使用:
list(it)后还想再遍历 it → 问题:第二次遍历结果为空(迭代器是一次性的) → 正确写法:需要重复遍历就重新iter(原始对象)。 - 错误写法:只实现了
__next__没实现__iter__,然后for遍历 → 问题:报TypeError: 'X' object is not iterable→ 正确写法:两个方法都实现(可迭代 + 迭代器)。
记忆口诀:iter 拿工具,next 取一个,StopIteration 是句号;迭代器一次性,用完得重来。
2 惰性求值 —— 生成器为什么省内存
是什么?为什么学?
惰性求值(lazy evaluation)的意思是「用到才算,不提前全造出来」。列表推导式 [i for i in range(百万)] 一次性把百万个数全放内存;生成器表达式 (i for i in range(百万)) 只占一小块内存,需要哪个现算哪个——处理大文件、大日志、无限序列时,这是内存的「救命稻草」。
底层原理
列表推导式的结果是一个列表:创建时就遍历完整个序列,把所有值存进连续内存。生成器表达式的结果是一个生成器对象:它只记住「规则」(怎么算下一个),内存里没有任何值;每次 next() 才现场算一个出来,算完即弃。时间上生成器并不更快(甚至略慢,省的是内存),它的价值是让「无限」和「超大」成为可能。
生活类比
列表推导式像超市提前把一年的大米全搬进仓库:取用方便但仓库得够大;生成器像大米随吃随买:家里只存一小袋,需要时去米店现买一袋。
注意事项总结
1 列表 vs 生成器,内存占用对比:
import sys
big_list = [i for i in range(1000000)]
big_gen = (i for i in range(1000000))
print(sys.getsizeof(big_list)) # 列表(示例输出:8448728)
print(sys.getsizeof(big_gen)) # 生成器(示例输出:200)
运行结果(示例输出,不同机器数值不同):
8448728
200
两个对象「包含」的数据一样多(都是 100 万个数),但内存差了 4 万倍:列表把 100 万个整数全存下(约 8MB),生成器只存规则和状态(约 200 字节)。
2 生成器「现算现给」——数据是现场造出来的;还能处理「无限序列」:
gen = (x * x for x in range(5))
print(next(gen)) # 现场算出 0
print(next(gen)) # 现场算出 1
print(list(gen)) # 把剩下的 4、9、16 全部取完
def all_evens():
n = 0
while True:
yield n
n += 2
gen2 = all_evens()
for _ in range(3):
print(next(gen2)) # 0、2、4
运行结果:
0
1
[4, 9, 16]
0
2
4
all_evens 里有 while True(无限循环),调用它不会卡死——每次 next 才执行到 yield n 暂停,所以「无限序列」完全合法:要多少给多少,不要就不算;若写列表推导式 [n for n in ...无限...],内存会直接爆炸。
易错点
- 错误写法:把生成器当列表用:
gen[0]、len(gen)→ 问题:报TypeError: 'generator' object is not subscriptable(生成器不支持下标) → 正确写法:需要下标先list(gen)(但会失去省内存优势)。 - 错误写法:
sum(gen)之后再用 gen → 问题:结果是 0——生成器已被取空 → 正确写法:需要多次使用就存成列表,或用itertools.tee。
记忆口诀:惰性求值现算现卖,内存只存规则;无限序列也不怕,要多少给多少。
3 yield 的暂停与恢复 —— 生成器的执行状态机
是什么?为什么学?
yield 是生成器函数里的「暂停键」:函数执行到 yield 值 时,把值交给调用方,然后原地冻结(所有局部变量、执行位置都保留);下次 next() 时从暂停处继续执行。
底层原理
普通函数调用时:进栈、执行、返回、出栈,局部变量全部销毁。生成器函数不同:它被编译成一个状态机(Python 内部用一个对象记录执行到哪一行、各局部变量的值)。next(gen) 触发状态机「从上次位置运行到下一个 yield」,返回值并暂停。所以「函数中间能停下、还能从原地接着走」——这是普通函数做不到的。
生活类比
yield 像连续剧的「本集完」:每集(每次 next)演到关键处就停,但剧情(状态)全部保留;下集从上次停的地方接着演——普通函数则是「一场电影」:一次放完,散场清场。
注意事项总结
1 在 yield 前后放 print,观察「暂停-恢复」的精确顺序:
def counter():
print("函数开始执行")
yield 1
print("第一次恢复,继续往后")
yield 2
print("第二次恢复,继续往后")
yield 3
gen = counter() # 注意:此时函数体一行都没执行!
print("拿到生成器对象")
print(next(gen)) # 执行到第一个 yield,暂停
print(next(gen)) # 从上次暂停处继续,跑到第二个 yield
print(next(gen)) # 再继续,跑到第三个 yield
运行结果:
拿到生成器对象
函数开始执行
1
第一次恢复,继续往后
2
第二次恢复,继续往后
3
关键看打印顺序——counter() 不执行函数体,所以「拿到生成器对象」先输出;第一次 next 才从函数开头执行到第一个 yield 1 并暂停;第二次 next 从暂停处继续……三次 next 三次「跑-停」,状态不丢。对比普通函数:def f(): return 42 一调用立刻执行,而 def g(): yield 42 调用只返回生成器对象、函数体一行没跑。
2 生成器简写:生成器表达式——把列表推导式的 [] 换成 () 就是惰性的:gen = (x * x for x in range(5)),用 sum(gen) 求和不占额外内存。
易错点
- 错误写法:函数里写了
yield却当普通函数用:counter()想直接拿结果 → 问题:拿到的是生成器对象,不是第一个值 → 正确写法:用next(gen)或for x in gen取值。 - 错误写法:在循环外写 yield 后不再继续 → 问题:生成器只产出一个值,第二次 next 抛 StopIteration → 正确写法:想产多个值,yield 写在循环里。
记忆口诀:yield 是暂停键,状态全保留;next 一按接着演,演完抛句号。
4 @staticmethod 与 @classmethod —— 三种方法的区别
是什么?为什么学?
普通方法接收 self(实例);类方法接收 cls(类本身);静态方法两个都不接收。区别决定了「谁有资格调用、能访问什么」:普通方法操作实例数据,类方法适合「工厂方法」(换一种方式创建对象)和访问类属性,静态方法适合「逻辑上属于这个类、但不需要任何对象数据」的工具函数。
底层原理
三种方法的本质差异在绑定(binding):普通方法绑定到实例——obj.method() 自动把 obj 作为第一个参数;类方法绑定到类——cls.method() 自动把类作为第一个参数(且通过实例调用时,传的也是「实例的类」,不是实例本身);静态方法不绑定——就是放在类里的普通函数,怎么调用都不注入参数。@classmethod / @staticmethod 干的事,就是改变这种绑定方式。
生活类比
三种方法像员工办业务 vs 公司总机 vs 前台张贴的服务说明:员工要带工牌(self)操作自己的业务数据;打总机(cls)能办「公司级」的事;服务说明不需要身份,谁来了都能照着做。
注意事项总结
1 三种方法的签名与调用方式对比:
class Tool:
def normal(self):
return "普通方法:我是", self
@classmethod
def create(cls):
return cls() # cls 就是 Tool 类本身,调用它会创建实例
@staticmethod
def add(a, b):
return a + b
t = Tool()
print(t.normal()) # 普通方法:必须通过实例调用
print(Tool.create()) # 类方法:类和实例都能调用
print(Tool.add(1, 2)) # 静态方法:不接收 self/cls
运行结果:
普通方法:我是 <__main__.Tool object at 0x...>(示例输出)
<__main__.Tool object at 0x...>(示例输出)
3
t.normal() 里 self 自动绑定为 t;Tool.create() 里 cls 自动绑定为 Tool,cls() 就是在「用类本身创建实例」;Tool.add(1, 2) 直接调用,不注入任何参数。
2 classmethod 工厂方法——最经典的用途:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
@classmethod
def from_string(cls, s):
name, salary = s.split(",")
return cls(name, int(salary))
def __str__(self):
return f"{self.name}: {self.salary}元"
e = Employee.from_string("张三,8000")
print(e)
运行结果:
张三: 8000元
from_string 是「工厂方法」——输入字符串格式,输出 Employee 实例。关键在 return cls(...):cls 是类本身,将来有人继承 Employee 写子类,子类.from_string(...) 返回的也是子类实例,所以用 cls 而不是写死 Employee。
易错点
- 错误写法:静态方法里写
self.xxx→ 问题:报TypeError(静态方法不接收 self,self 未定义) → 正确写法:静态方法只用参数,不碰实例属性。 - 错误写法:类方法里写
self→ 问题:报NameError或参数错(第一个参数约定叫 cls) → 正确写法:类方法第一个参数写cls。 - 错误写法:工厂方法里写死类名:
return Employee(...)→ 问题:子类继承后返回的仍是父类实例,绑定错误 → 正确写法:用return cls(...)。
记忆口诀:普通带 self 绑实例,类方法带 cls 绑类,静态方法不绑定;工厂方法用 cls,继承不翻车。
5 装饰器原理 —— 函数是一等公民,包装即增强
是什么?为什么学?
装饰器(decorator)是「接收一个函数、返回一个新函数的函数」。@log 写在函数上方,等价于 add = log(add)——把原函数「包装」进一个增强版。日志、计时、权限、缓存这类「横切关注点」(与业务无关、却到处都要)用装饰器写一次、到处复用,原函数一行不改。
底层原理
先决条件是「函数是一等公民」:函数可以像变量一样被传递、被返回。@decorator 只是语法糖:Python 在定义完函数后,自动执行 func = decorator(func) 并用返回值替换原名字。所以装饰器必须返回一个「可调用的东西」(通常是内部定义的 wrapper 函数),wrapper 里 *args, **kwargs 是为了把原函数的任意参数原样转发。wrapper 必须 return 原函数的结果,否则被装饰函数调用后的返回值变成 None(最常见的坑)。
生活类比
装饰器像手机壳:手机(原函数)功能不变,套上壳就多了防摔(日志)、支架(计时)等能力;壳不改变手机内部电路(不改原函数代码),想换壳随时换。
注意事项总结
1 函数是一等公民——函数能作为返回值(「加法器工厂」):
def make_adder(n):
def adder(x): # 在函数内部再定义函数
return x + n
return adder # 函数作为返回值
add_10 = make_adder(10)
print(add_10(5))
运行结果:
15
函数还能当参数传——函数能像数据一样流转,这是装饰器能成立的地基。
2 @ 语法糖的真相——装饰器就是「手动包装」的简写:
def shout(func):
def wrapper():
print("!!!")
func()
print("!!!")
return wrapper
@shout # 语法糖:等价于 bye = shout(bye)
def bye():
print("再见")
bye()
运行结果:
!!!
再见
!!!
@shout 写在 def 上面 = 定义完立刻执行 bye = shout(bye)——bye 这个名字从此指向 wrapper(里面「包裹」着原函数)。
3 wrapper 忘记 return 的坑——返回 None 的函数:
def my_log(func):
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__}")
func(*args, **kwargs) # 忘了 return!
return wrapper
@my_log
def add(a, b):
return a + b
print(add(1, 2)) # 结果是 None 而不是 3!
运行结果:
调用 add
None
add(1, 2) 实际执行的是 wrapper:打印日志、调用原函数算出了 3,但 wrapper 没有把 3 传出去(没有 return),所以返回值是 None——函数「白算了」,而且不报错、只是结果不对。修复:return func(*args, **kwargs)。
4 装饰器带参数(外层再套一层函数,用来传配置):
def repeat(times):
"""装饰器工厂:让函数执行 times 次"""
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def hello():
print("你好")
hello()
运行结果:
你好
你好
你好
易错点
- 错误写法:wrapper 里忘了
return func(...)→ 问题:被装饰函数返回 None,逻辑「静默出错」 → 正确写法:wrapper 里return func(*args, **kwargs)。 - 错误写法:wrapper 参数写死
def wrapper(a, b)→ 问题:装饰不同参数的函数就报错 → 正确写法:用*args, **kwargs通配转发。 - 错误写法:装饰器直接返回原函数:
return func→ 问题:装饰器形同虚设,没有任何增强 → 正确写法:返回内部 wrapper(新函数)。
记忆口诀:一等公民可传递,包装替换是语法糖;wrapper 必须 return,否则结果变 None。
6 @property —— 把方法变成属性
是什么?为什么学?
@property 可以让 obj.属性 这种写法背后执行一段方法逻辑,最常见两个场景:计算属性(不存值、现算)和赋值校验(不能随便乱存)。
注意事项总结
1 计算属性 + 赋值校验:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14 * self._radius ** 2
c = Circle(2)
print(c.area) # 不加括号,像属性一样访问
class Student:
def __init__(self, score=0):
self._score = 0
self.score = score # 赋值会走下面的 setter
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not 0 <= value <= 100:
raise ValueError("分数必须在 0~100 之间")
self._score = value
s = Student(85)
print(s.score)
s.score = 60 # 写:走 setter,带校验
print(s.score)
# s.score = 999 # 取消注释会抛 ValueError
运行结果:
12.56
85
60
为什么用 @property:外部代码写 obj.area 很自然,以后内部实现想改(比如换更精确的圆周率)也不影响外部调用方。
易错点
- 错误写法:
@property方法名和内部属性同名(self.radius又定义def radius) → 问题:无限递归,报RecursionError→ 正确写法:内部存self._radius,外部属性名用radius。
记忆口诀:property 变属性,点号就能取;setter 加校验,内部存下划线。
7 动手实践:数据流水线工具
设计一条「数据流水线」:源头生成器产生数字,经过「放大」、「过滤」两个加工环节,最后收集结果——把今天学的生成器真正串联起来:
# pipeline.py —— 数据流水线工具
def numbers(limit):
"""源头:逐个产出 1~limit"""
for n in range(1, limit + 1):
yield n
def multiply(factor):
"""加工环节工厂:把上游每个数乘以 factor"""
def transform(source):
for item in source:
yield item * factor
return transform
def filter_big(threshold):
"""加工环节工厂:只保留大于 threshold 的数"""
def transform(source):
for item in source:
if item > threshold:
yield item
return transform
def collect(source):
"""末端:把数据收进列表"""
return list(source)
def run_pipeline(limit, factor, threshold):
source = numbers(limit)
source = multiply(factor)(source) # 环节1:放大
source = filter_big(threshold)(source) # 环节2:过滤
result = collect(source)
print("最终结果:", result)
run_pipeline(10, 3, 20)
运行结果:
最终结果: [21, 24, 27, 30]
1~10 乘 3 得到 3,6,...,30,再筛掉 <=20 的,就剩下 21,24,27,30。整条流水线每个环节都是生成器,数据一路「边算边流」,不会一次性造一个大列表。
升级挑战:再写一个「累加求和」环节(比如把过滤后的数全部加起来);试试把流水线环节用类组织,让每个环节是一个对象;或按知识点 5 的写法给 run_pipeline 包一个计时装饰器。
今日总结
迭代器协议:iter() 拿迭代器、next() 逐个取值、StopIteration 表示结束;for 循环就是这三步的语法糖;迭代器是一次性的(取完即空),可迭代对象 ≠ 迭代器
惰性求值:生成器「用到才算」,内存占用与数据总量无关,能处理大文件与无限序列
yield 是暂停键:状态全保留,next() 从上次暂停处继续执行;区分「调用生成器函数」与「执行生成器函数体」(前者只返回对象)
三种方法:普通方法(self,绑实例)、类方法(cls,绑类,适合工厂方法)、静态方法(不绑定)
@property 把方法变成属性:计算属性 + setter 赋值校验
装饰器 = 接收函数返回函数的函数,@x 等价于 f = x(f);wrapper 必须 return func(...)
数据流水线工具能正常跑通全部环节(源头/加工/收集都是惰性生成器)
| 报错 | 原因 | 修复 |
|---|---|---|
StopIteration | 迭代器已经取完,还继续 next() | 用 for 循环自动处理结束,或自己捕获 StopIteration |
TypeError: 'CountDown' object is not iterable | 类没实现 __iter__ 就想 for 遍历 | 实现 __iter__(返回迭代器)和 __next__ |
TypeError: 'generator' object is not subscriptable | 生成器不支持 gen[0] 这种下标取值 | 先 list(gen) 转成列表,或改用迭代 |
被装饰函数返回 None | wrapper 里忘了 return func(*args, **kwargs) | 在 wrapper 里把结果 return 出去 |
AttributeError: 'function' object has no attribute 'xxx' | 装饰器把原函数换成了 wrapper,丢失了原函数信息 | 在 wrapper 上再加 @functools.wraps(func) |