Python基础语法练到一定程度,很多人会卡在一个尴尬的位置:列表推导式会写,但看不懂项目里那些带@的代码;知道有yield这个词,但真让自己写个生成器处理大日志,还是只会readlines硬啃内存;函数传参*args、**kwargs见过无数次,但问到闭包到底怎么保留状态,支支吾吾说不出所以然。
这篇笔记定位很明确:默认你已经有Python基础功底(变量、循环、函数、类都玩得转),想要跨过“能跑”到“写得优雅”这道坎。我整理了Python进阶语法里含金量最高的几个点——迭代器与生成器、装饰器、闭包、上下文管理器,从底层执行逻辑讲到真实业务场景,最后附上我自己踩过的坑。这些内容不管你是做后端接口、写爬虫脚本还是搞数据处理,基本天天碰得到。只要把这篇消化掉,再去读开源项目的源码,体感会完全不同。
1. 进阶之前的思维转变:一切皆协议
1.1 别再背语法,去理解Python的对象协议
学Python语法最常犯的错误,就是把它当成一个个孤立的知识点去背:decorator是一个用法,with是一种写法,yield是一个关键字。这么记确实快,但记完就忘,遇到变形题就不会了。
进阶的人需要换个视角:Python里的所谓“高级语法”,底层全是对象协议。什么叫协议?就是约定“这个类型的对象应该实现哪些行为方法”。比如你写for x in obj,Python解释器不会魔法般地遍历obj,它做的事是去找obj.__iter__()方法,找不到就找obj.__getitem__(0, 1, 2...),都找不到就抛TypeError。with语句也不是语法糖,它是去调用对象的__enter__和__exit__。@decorator本质上就是一次函数调用:func = decorator(func)。
把这个思路掰过来之后,进阶语法就是一盘棋了。你不需要记十几个孤立的所谓“高级特性”,只需要记住一条主线:每个语法背后,都是解释器在按固定步骤调用某些特殊方法。你理解了这一点,就能从“语言的使用者”变成“协议的参与者”——不仅能调用现成的迭代器,还能自己写一个可迭代对象,让自定义的类完美融入for循环。
1.2 生活类比:协议就是“插座规格”
为了把“协议”这个词说透,我打个比方。你家里买的任何电器,插头规格一定是符合国标的,插到墙上插座就能用。Python的协议就是这个“国标规格”——__iter__规定了“你必须是能产出下一个元素的东西”,__enter__规定了“你必须是能安全进入和退出的资源”。
你写一个类,只要把协议方法补齐,Python的各种内置语法就自动认你。比如你自己写一个Range类,实现了__iter__和__next__,它就能被for遍历,能被list()转换,能被sum()求和。整个Python生态都认这套接口,你写的类瞬间拥有了和内置类型一样的“公民待遇”。
这就是进阶语法的核心心法:不是学一个个孤零零的写法,而是学会如何让自己创建的对象符合Python的通行协议。
2. 迭代器与生成器:从“拿到全部”到“按需产出”
2.1 迭代器协议拆解:为什么for循环不会卡死
很多初学者有个疑惑:range(100000000)生成一个上亿的序列,为什么程序不直接内存爆炸?这就要说到迭代器的价值了。
一个对象要成为迭代器,核心是实现两个方法:__iter__(返回迭代器自身)和__next__(每次调用返回下一个值,耗尽时抛出StopIteration)。关键在于:迭代器不一次性把所有元素放进内存,它每次只产出当前需要的那一个。
我自己写代码时,最常遇到的一个场景是处理几G大小的日志文件。新手做法是content = f.read(),直接一次性把整个文件读进内存,跑一次程序电脑风扇狂转。用迭代器的思路就完全不同——for line in f一行一行读,内存占用恒定量级,和文件多大没关系。
你甚至可以自己写一个斐波那契迭代器,感受一下“按需产出”和“一次性算完”的差别:
class FibIterator: def __init__(self, n): self.n = n # 想生成前n个斐波那契数 self.a, self.b = 0, 1 self.count = 0 def __iter__(self): return self def __next__(self): if self.count >= self.n: raise StopIteration self.a, self.b = self.b, self.a + self.b self.count += 1 return self.a这个类实例化之后,就能被for直接遍历。你观察执行流程会发现:每次for循环取下一个值,__next__计算一次,算完就丢,内存里永远只保留两个整数(a和b)。这就是迭代器协议的全部精髓。
2.2 生成器:用yield把函数变成迭代器
手写一个类实现__iter__和__next__确实有点啰嗦,于是Python提供了一个更优雅的写法:只要函数里出现yield关键字,这个函数就不走普通return逻辑了,它会变成一个生成器函数。调用生成器函数不会真正执行函数体,而是返回一个生成器对象。真正执行是在for循环推进它的时候,每次遇到yield就暂停,把值抛出来,下次继续从暂停位置往后跑。
def fib_generator(n): a, b = 0, 1 count = 0 while count < n: a, b = b, a + b count += 1 yield a这段代码和上面的类写法,行为完全一致,但代码量少了一半。我自己的经验是:凡是需要“按序遍历大数据”或者“不断产生中间值”的场景,优先写生成器而不是攒一个list。比较典型的是用生成器表达式做数据管道——把读取、清洗、转换串成一条流水线,每个环节都是惰性求值,处理千万条数据内存一样稳定。
# 数据管道场景:读取日志文件,逐行清洗,再提取关键字 cleaned_lines = (line.strip() for line in open("app.log", encoding="utf-8") if line.strip()) log_tuples = (tuple(line.split("|")) for line in cleaned_lines) error_count = sum(1 for item in log_tuples if item[0] == "ERROR")这里有个细节值得注意:生成器表达式能像列表推导式一样写,但它外面是圆括号。它在循环和推导式里逐个产出值,几乎不占内存。我经常跟人强调:如果你写列表推导式只是为了在for循环里遍历一次,那就换成生成器表达式,不会错。
2.3 生成器的底层执行真相:暂停与恢复
很多教程把yield讲得神乎其神,其实你只要做一次实验就懂了:在生成器函数里加print,观察它什么时候执行。
def gen_demo(): print("第一段开始") yield 1 print("第一段结束,第二段开始") yield 2 print("全部结束") g = gen_demo() print("生成器已创建,但函数体尚未执行") print(next(g)) # 打印"第一段开始",返回1 print(next(g)) # 打印"第一段结束,第二段开始",返回2第一个next(g)调用时,“生成器已创建”已经打印了,说明构造生成器对象确实没有执行函数体。执行到yield就冻结了当前函数的状态——局部变量、执行到的行号、堆栈信息全部保存。第二个next(g)从上次冻结的地方恢复,继续往下跑,直到遇见下一个yield或函数结束。
理解了这个“暂停和恢复”机制,你才能真正体会生成器为什么能处理无限序列。比如你想遍历所有偶数,手写while True生成,理论上无穷无尽,但因为你按需取,所以内存和CPU都不会炸。这也是Python协程的雏形:函数之间来回切换执行流,而这套机制正是asyncio的基础。
3. 装饰器:给函数穿马甲的高级语法
3.1 装饰器的本质就是函数替换
装饰器是Python里被“神化”最多的语法,其实所谓@decorator,就是告诉你一件事:你定义完函数后,解释器自动帮你调一次装饰器函数,再用返回值替换掉原来的函数名。
def my_decorator(func): def wrapper(*args, **kwargs): print("调用前增强") result = func(*args, **kwargs) print("调用后增强") return result return wrapper @my_decorator def say_hello(name): return f"Hello, {name}"执行完这段代码后,say_hello这个名字指向的不再是你定义的原始函数,而是wrapper。你调用say_hello("Tom"),实际执行的是wrapper("Tom"),wrapper内部再调用原本的函数。一层套一层,所以叫“装饰器”一点没错——你给原函数加了装饰,但没有改动原函数的代码。
这里我跟大家强调一个原则:装饰器最适合做横切关注点——那些跟业务逻辑无关但每个函数都要做的公共事务,比如打印日志、统计耗时、鉴权校验、重试机制。之所以用装饰器而不是在每个函数里复制粘贴,动力就是“单一职责原则”:业务函数只关心业务,公共逻辑抽出来复用。
3.2 装饰器实战:一行代码给函数加计时
我自己做性能排查时最常用的写法就是计时装饰器,代码简单但实用性极高:
import time import functools def timer_decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) cost = (time.perf_counter() - start) * 1000 print(f"[计时] {func.__name__} 耗时 {cost:.2f} ms") return result return wrapper @timer_decorator def heavy_task(n): total = 0 for i in range(n): total += i ** 2 return total heavy_task(100000)我特别提醒一下:为什么wrapper函数上面还要加@functools.wraps(func)?因为不加的话,装饰后函数的__name__会变成wrapper,这会破坏一些依赖函数名的工具(比如Flask路由注册有些场景下会有问题,调试日志里显示的函数名也全是wrapper)。functools.wraps的作用就是把原函数的元信息(__name__、__doc__、__module__等)拷贝到wrapper上,保持“看起来还是原来的函数”。这是装饰器写法里的一个规范动作,新手最容易漏。
3.3 带参数的装饰器:三层嵌套的玩法
有时候你希望装饰器能接收参数,比如“日志级别”“重试次数”。这时需要再加一层工厂函数:
def retry(max_attempts=3): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_attempts + 1): try: return func(*args, **kwargs) except ValueError as e: print(f"第 {attempt} 次失败: {e}") raise ValueError("多次重试仍失败") return wrapper return decorator @retry(max_attempts=5) def unstable_request(): # 模拟可能失败的第三方接口调用 import random if random.random() < 0.7: raise ValueError("接口暂时不可用") return "ok"理解拆解过程:@retry(max_attempts=5)首先执行retry(5),得到decorator;然后Python把原函数传给decorator,得到wrapper;最后用wrapper替换原函数。三层嵌套第一次看确实绕,但拆分后就是简单的函数调用。记一个口诀:参数给工厂,工厂出装饰器,装饰器吃函数,返回新函数。
3.4 装饰器的叠加顺序:从下往上执行
写Flask或Django的人经常看到多个装饰器叠在一起:
@app.route("/api/user") @login_required @timer_decorator def get_user(): ...这里有一个非常容易误解的执行顺序。装饰器叠加时,先应用离函数定义最近的那个(下方),再依次向上。也就是timer_decorator先包装原始函数,然后login_required包装上一步的结果,最后app.route拿到的已经是层层包裹后的函数。调用时则正好相反,从上往下执行。我踩过的坑是:我把@app.route放在最外层,自以为“路由装饰器应该优先执行鉴权”,结果请求进来直接被timer打点然后才鉴权,日志里多了一堆未授权请求的耗时记录。
理解这个顺序后,你可以设计出清晰的层次:最外层做路由映射,中间层做权限控制,内层做技术埋点。每一层只关心自己的事,互不干扰。
4. 闭包:函数如何记住外层变量
4.1 闭包的本质:函数+环境
闭包这个概念在Python面试里出现频率极高,但很多人只背定义:“内层函数引用了外层函数的变量”。其实闭包的精髓在于理解:当外层函数结束后,内层函数依然能访问外层函数的局部变量,靠的是Python为它创建了一个“闭包环境”。
我习惯用一个计数器例子来演示:
def make_counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment counter = make_counter() print(counter()) # 1 print(counter()) # 2 print(counter()) # 3按普通的函数作用域理解,make_counter()执行完,count局部变量就该被销毁了。但increment被返回出去,而且它内部引用count,所以Python把count打包进了increment的闭包环境里,让这个变量活了下了来。每次调用counter(),操作的还是同一个count,于是计数器能持续自增。
这里必须说明nonlocal的作用。在嵌套函数里,如果只读取外层变量,不需要额外声明;但如果你想给外层变量重新赋值,Python就会认为你在定义一个新局部变量,从而与外层变量“失联”。nonlocal就是告诉解释器:这个变量不是本层的局部变量,去最近的且已绑定的外层作用域找。
4.2 闭包的经典坑:延迟绑定
闭包有一个几乎人人都会踩的坑,我在这上面debug过整整一下午。看这段代码:
def create_multipliers(): multipliers = [] for i in range(3): def multiply(x): return x * i multipliers.append(multiply) return multipliers multipliers = create_multipliers() for m in multipliers: print(m(2))很多人直觉上认为会输出0、2、4,但实际输出是4、4、4。原因是:三个multiply函数引用的i是同一个变量。for循环结束后,i的值停在2,闭包环境里保存的i就是2,所以三个函数无论谁调用,乘的都固定是2。
修法很简单,用默认参数把当前i值“钉死”到函数定义那一刻:
def create_multipliers(): multipliers = [] for i in range(3): def multiply(x, i=i): return x * i multipliers.append(multiply) return multipliers这个坑的教训值得记一辈子:闭包里引用的循环变量,最终用到的是循环结束后的值,不是定义时的值。如果需要定义时的快照,请用默认参数或者再包一层立即执行函数。
4.3 闭包与装饰器的不解之缘
现在你回头再看装饰器里那个wrapper(*args, **kwargs)函数,它在调用func时读取了外层decorator接收到的func参数,本质就是一个典型的闭包。所以在实际项目里,装饰器就是闭包最广泛的应用场景。
更进一步,闭包还能用来做“记忆化”(缓存函数结果)。经典斐波那契数列用递归写会有大量重复计算,复杂度指数级;套一层闭包做缓存,复杂度直接降到线性:
def memoize(func): cache = {} @functools.wraps(func) def wrapper(*args): if args not in cache: cache[args] = func(*args) return cache[args] return wrapper @memoize def fib(n): if n < 2: return n return fib(n - 1) + fib(n - 2)这里cache也是一个闭包变量,被wrapper长期引用,所以它的生命周期贯穿了整个程序运行。这种“闭包持有状态 + 装饰器切入函数执行”的组合拳,在实际项目里非常常见,你会在缓存中间件、限流器、ORM的懒加载机制里反复见到。
5. 上下文管理器:用with优雅地管理资源
5.1 为什么需要with:别忘了清理现场
很多人写过这样的代码:打开文件、读取、关闭。但问题在于——如果读取过程中抛出异常,close()根本执行不到,文件句柄就泄漏了。传统修法是try/finally,但写起来啰嗦。with语句存在的意义就是:让“进入-执行-退出”这三段逻辑固化成一个协议,无论中间发生什么,退出代码保证执行。
# 传统写法 file_obj = open("data.txt", encoding="utf-8") try: data = file_obj.read() finally: file_obj.close() # with 写法 with open("data.txt", encoding="utf-8") as f: data = f.read()第二条看着顺眼得多,而它的底层实现就是两个魔法方法:__enter__负责进入(返回资源对象),__exit__负责退出(清理资源)。
5.2 自己写上下文管理器:两种主流姿势
第一种是类写法,适合逻辑较重、需要封装状态的场景:
class DatabaseSession: def __enter__(self): self.conn = get_connection() return self.conn def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: self.conn.rollback() else: self.conn.commit() self.conn.close() with DatabaseSession() as conn: conn.execute("insert ...")注意__exit__的四个参数:exc_type是异常类型,exc_val是异常实例,exc_tb是traceback对象。如果代码块里没有异常,三者都是None。你可以在__exit__里根据有没有异常决定提交还是回滚,这就把事务管理收进了with里面。如果__exit__返回True,异常会被吞掉;返回False(默认)则继续向上抛。这个细节是事务逻辑是否正确的关键,我自己写的时候差点搞反。
第二种是生成器写法,只用contextlib.contextmanager就能把普通函数变成上下文管理器,代码量最少:
from contextlib import contextmanager @contextmanager def change_dir(path): import os old_cwd = os.getcwd() os.chdir(path) try: yield finally: os.chdir(old_cwd) with change_dir("/tmp"): # 在这段代码里,当前工作目录是 /tmp do_something()使用@contextmanager时,yield前面的代码相当于__enter__,yield后面的代码相当于__exit__。如果yield后面想接异常处理逻辑,需要把整个yield包在try/finally里——这个模式就是用生成器实现上下文管理器的标准模板。
5.3 上下文管理器的高级玩法:同时管理多个资源
Python允许在一个with语句里同时进入多个上下文管理器,比如同时打开两个文件做逐行对比:
with open("file_a.txt", encoding="utf-8") as f_a, open("file_b.txt", encoding="utf-8") as f_b: for x, y in zip(f_a, f_b): if x != y: print(f"差异行: {x.rstrip()} vs {y.rstrip()}")这里两个文件都会自动关闭,而且如果任何一个打开失败,前面成功打开的资源也会自动关闭,不会泄漏。这个特性在做数据对账脚本时非常实用,我以前用普通写法时总担心某个分支忘记关文件,用with之后就再没操心过。
6. 参数魔法:拆包与打包的底层逻辑
6.1 *args和**kwargs:一个星解序列,两个星解字典
函数参数里的*args与**kwargs,本质上不是“关键字”本身特殊,而是解包操作符。调用函数时,*可以把一个可迭代对象拆分成位置参数;**可以把一个字典拆分成关键字参数。
def show(a, b, c): print(a, b, c) nums = [1, 2, 3] show(*nums) # 等价于 show(1, 2, 3) info = {"a": 10, "b": 20, "c": 30} show(**info) # 等价于 show(a=10, b=20, c=30)这个语法最核心的用途有这几个:转发参数(装饰器里的wrapper(*args, **kwargs)就是干这个的)、合并配置、动态构建调用。一个函数可以接收别人传来的任意参数,再原封不动传给另一个函数——模块解耦最常用的手段。
6.2 解包不只是函数参数那里能用
很多人不知道,Python 3.5之后,普通赋值语句和列表表达式也能用*解包了:
first, *rest, last = [1, 2, 3, 4, 5] # first=1, rest=[2,3,4], last=5 merged = [*list_a, *list_b] # 合并列表,等价于 list_a + list_b combined = {**dict_a, **dict_b} # 合并字典,后面的键覆盖前面的解包语法在写配置合并、数据处理时能省出大量临时变量。但有一点我必须提醒:解包太多会影响可读性。如果一段业务逻辑里连续出现三四处**、*,那就是该抽函数了,别把代码写成迷宫。
6.3 关键字参数的地位:比位置参数更稳
进阶的道路上,一个值得刻意养成的习惯是:多参数接口尽量用关键字传参,甚至是强制关键字参数。Python里在*后面的参数,调用时只能用关键字传递:
def crew(name, *models, leader="captain"): ...更常用的是定义时直接加个独立*:
def upload_file(path, *, max_size_mb=100, overwrite=False): ...这个写法禁止了upload_file("a.txt", 200, True)这种全靠调用者记忆顺序的调用方式。参数一多,位置参数的调用就是灾难:调用方面对五个位置参数,根本分不清第几个是端口、第几个是超时值。而强制关键字参数,让每一个配置项自带名字,代码自解释性显著提高。这个习惯在大项目协作里特别重要,我review过的代码里,凡是多个布尔参数叠加的位置传参,几乎都会出现传错顺序的bug。
7. 常见问题与排查经验
7.1 生成器只能用一次:iterator没有回头路
很多人发现生成器“见鬼了”:第一次for循环正常,第二次for循环输出为空。这不叫bug,而是迭代器的本性——它像一支只能往前走的箭,耗尽后StopIteration已经抛出,游标停在终点,不会自动重置。
我实际工作中最常踩的坑是:写了一个get_rows()函数返回生成器,调用方准备分两次统计,第一次sum、第二次len,结果第二次永远得到0。解决方案也简单:要么每次调用都重新构建生成器,要么把数据物化成列表。具体取舍的依据是数据量:几十万条以下直接list,数据大就loop两次但每次都重新读源。
7.2 装饰器看不清函数名:必须补functools.wraps
试验上面不写@functools.wraps的装饰器,你会看到这样的诡异现象:
@my_decorator def greet(): """打招呼""" pass print(greet.__name__) # wrapper函数名变成wrapper之后,影响是连锁的:文档字符串丢了,调试日志里全是wrapper,某些依赖内省机制的框架行为还会变古怪(比如pytest的fixture名匹配)。所以我在自己的项目里定了条规矩:任何装饰器内层都必须加@functools.wraps(func),没有任何例外。
7.3 闭包变量泄漏到循环之外
除了上面提到的延迟绑定坑,闭包还有一个隐蔽问题:如果你在模块里直接写一个带循环的嵌套函数,循环变量会泄漏到外层作用域。在旧版Python(2.x时代)这是经典作用域bug,Python 3的列表推导式已经修掉了泄漏问题,但普通for循环依然会泄漏变量。示例:
for j in range(5): pass print(j) # 输出4,j泄漏了这个行为在代码量大的时候很难排查,你可能会在几百行下面突然发现一个j,但不知道它从哪来的。建议从一开始就养成习惯:循环变量用完即弃,别指望它“随循环结束而消失”,也别在循环外面依赖它的值。
7.4 上下文管理器里的异常被吞了
写自定义__exit__时,一个容易出问题的点是返回值。看这个伪代码:
def __exit__(self, exc_type, exc_val, exc_tb): clean_up() return True # 异常被吞__exit__返回True时,解释器会认为“异常已经被处理”,于是不再向上抛出。如果你只是想在退出时做清理,而没打算吞掉业务异常,那__exit__应该返回False或者不写return(默认就是None,相当于False)。我处理事务时特别要求:有异常主动rollback后,继续返回True并且从上下文里抛出业务异常;而普通资源清理场景,千万别乱返回True。
7.5 性能陷阱:列表推导式虽好,也要看场景
列表推导式和生成器表达式看起来只是括号区别,但这俩的内存差异极大。常见误用是:对一个极大的序列做sum([x * 2 for x in huge]),这会先生成一个完整的列表,然后sum再遍历它,等于同一份数据占了双倍内存。正确写法是sum(x * 2 for x in huge),直接用生成器惰性求和。
同理,判断是否有满足条件的元素,用any(...)配合生成器表达式,能在找到第一个匹配项时立刻短路,不用遍历全量数据。这类写法优化在数据批量处理时效果非常明显。
8. 进阶语法的联动:几个综合应用
8.1 装饰器+闭包+上下文管理器:打造一个简易重试器
学了这么多样语法,它们不是孤岛。这里给一个综合案例:一个带延迟退避的重试装饰器,集成日志功能,支持最大重试次数和退避因子。
import functools import time import random def retry_with_backoff(max_attempts=5, base_delay=0.1, backoff_factor=2): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): current_delay = base_delay for attempt in range(1, max_attempts + 1): try: return func(*args, **kwargs) except (ConnectionError, TimeoutError) as e: if attempt == max_attempts: raise sleep_time = current_delay + random.uniform(0, current_delay * 0.2) print(f"[重试] {func.__name__} 第 {attempt} 次失败: {e}, {sleep_time:.2f}s 后重试") time.sleep(sleep_time) current_delay *= backoff_factor return None return wrapper return decorator @retry_with_backoff(max_attempts=3, base_delay=0.5) def fetch_user_from_api(user_id): # 模拟不稳定的网络请求 if random.random() < 0.6: raise ConnectionError("网络暂不可用") return {"id": user_id, "name": "Tom"}这个例子融合了:带参数的装饰器(工厂模式)、*args/**kwargs参数转发、闭包保存重试状态。你在实际的爬虫、RPC客户端、消息队列消费者里都能套用这个模板。我把它写进过好几个项目,把耦合在业务代码里的重试逻辑全部抽成了装饰器,业务函数清爽了很多。
8.2 生成器+字典解包:处理配置文件的紧凑写法
读取配置文件并合并默认配置,是每个后端项目都会遇到的场景。用生成器表达式加字典解包,代码可以写得非常紧凑且易读:
def load_config(path, default_config=None): defaults = { "host": "127.0.0.1", "port": 8080, "debug": False, } if default_config: defaults = {**defaults, **default_config} with open(path, encoding="utf-8") as f: items = (line.strip().split("=", 1) for line in f if "=" in line and not line.strip().startswith("#")) parsed = {k.strip(): v.strip() for k, v in items} return {**defaults, **parsed}这里用了几个关键点:with管理文件句柄、生成器表达式按需清洗每一行、字典解包实现配置覆盖。这种组合写法的代码量比传统for循环加if判断少一半,而且每行的意图都很明确。
8.3 生成器处理大文件后接入上下文管理器
一个地道的Python进阶开发者,会自然而然地组合这些语法来解决问题。比如处理超大文件时,要把“分块读取”的逻辑做成一个生成器函数,再用上下文管理器保证打开和关闭的安全:
from contextlib import contextmanager @contextmanager def big_file_reader(file_path, chunk_size=1024 * 1024): try: f = open(file_path, "rb") def chunks(): while True: data = f.read(chunk_size) if not data: break yield data yield chunks() finally: f.close() with big_file_reader("huge_data.bin") as chunks: for one_chunk in chunks: process(one_chunk)你会发现,生成器嵌套在上下文管理器里,上下文管理器负责生命周期,生成器负责数据流,分工非常清晰。这种配合是Python语言设计里非常优雅的一面——每个工具解决自己最擅长的问题。
9. 写在笔记最后的一些个人体会
这一篇笔记本想写得更短,但写着写着还是塞了这么多内容。回头梳理整个Python所谓“进阶语法”的脉络,我个人最大的体会是:不要孤立地记语法点,要把它们放进“协议”和“组合”的框架里理解。
你只要想清楚for背后的迭代协议、@背后的函数替换、with背后的资源协议,那么无论遇到多花哨的写法,都能拆解出来。所谓进阶,其实是换了一种看代码的方式:不再把Python当成一组“命令”,而是理解成一组“对象之间的协作规则”。
还有一个习惯我想特别分享:读代码的效率远高于写代码的学习效率。找一份高质量的开源项目源码(比如requests、Flask核心源码),把里面出现的@property、yield、contextmanager、*args圈出来,试着用自己的话解释它为什么出现在这里、解决了什么问题。一遍不行就两遍,比刷十遍语法教程都管用。
另外,这些语法特性在实际业务里使用时,请遵循一条原则:不加戏。能写简单for循环解决的,就别为了炫技套三层生成器;一个普通函数能搞定的,不必非包个装饰器。语法进阶的意义是让你在真正需要的时候有工具可用,而不是让你把代码写得让人看不懂。代码清晰永远是第一优先级,一个人写得爽、三个人看得苦的“高级语法”,说到底只是自嗨。
最后再分享一个小技巧:像itertools、functools、contextlib这三个标准库模块,是Python高级语法的“武器库”。itertools.product做笛卡尔积、itertools.chain连接多个迭代器、functools.partial固化参数、functools.lru_cache做缓存,这些函数式编程工具配合本篇讲的闭包和装饰器,几乎能覆盖绝大多数需要“高级语法”解决的场景。我建议你把官方文档里这三个模块过一遍,十分钟就能掌握十几个实用工具,投入产出比远超想象。
Python这条路没有所谓的“学完”,语法进阶笔记(二)到此为止,下一篇准备写写Python的类机制与元类、描述符协议,那些才是真正触及Python灵魂的内容。先把今天这些消化透再说吧。