第2章 类以下层级的语法最佳实践

第2章 类以下层级的语法最佳实践

学习目标

  • 能为映射/过滤、惰性取用、横切增强和资源清理分别选出推导式、生成器、装饰器或上下文管理器,并说出选错时出错的位置
  • 能用 @wraps 保留装饰函数的元数据、用 @contextmanager 把获取与释放绑成作用域,并说明异常时清理是否仍执行
  • 自测:生成器表达式为什么只能遍历一次?把它当列表按下标取值会在哪一步报错?

为什么要在函数这一层挑语法

写 Python 时,同样一件事往往有好几种写法。把一段处理写成一行紧凑的表达,还是写成显式的循环;把“用到时才拿数据”写成按需取用,还是一次性全装进内存;把“进入前做准备、退出后收拾”写成固定形状,还是散落在各处。选哪一种,决定了别人能不能一眼看懂这段代码在做什么、出错时好不好追。

好的语法选择不是炫技,而是把责任讲清楚:谁负责取数据、谁负责存状态、谁负责善后。选错了,代码不会立刻报错,但会在别人接手、改需求或排查故障时露出破绽——一段看似聪明的写法,读起来像谜语;一处本该自动清理的资源,因为写法散乱而漏掉。

本章把“类以下”这一层的几样工具拆开看:它们各自替你管哪一段责任,什么时候该用,什么时候用了反而更糟。先建立这张全景,再去看具体写法,你就不会把“短”当成“好”,也不会为了显得高级而把简单的事写复杂。

先看全景:五种工具各管哪段责任

本章要拆的是“类以下”这一层——还没到类,光是函数和表达式这一层——的几样语法工具。它们各自替你管一段责任:谁负责算、谁负责存状态、谁负责善后。下面这张交互图把五种工具串成一条线,先点一遍每个节点看看它管什么、出什么错,再逐项拆开看。

类以下层级的语法最佳实践
推导式、生成器、迭代器、装饰器、with与contextlib推导式列表推导与生成器表达式迭代器迭代器与生成器协程协程装饰器装饰器上下文with与contex…
列表推导与生成器表达式

推导式适合单一可读的映射或过滤,生成器表达式惰性传给消费者。复杂副作用时改回显式循环。

点开“注入故障”开关,能看到每种工具最常踩的那个坑——这正是后面“常见误区”要展开的内容。

推导式与生成器表达式

把一段映射或过滤写紧凑时,最常用的是 ,它把“对每个元素做变换并收集”压成一行,读的人一眼能看出输入到输出的形状。它的惰性兄弟是生成器表达式——把方括号换成圆括号,元素按需产出而不一次装满内存。

判断该用哪种,看的是可读性和副作用。单一、可读的映射或过滤,推导式最合适;一旦出现多层嵌套、分支或复杂异常,继续硬塞进一行只会让代码变成谜语,这时应改回显式循环。生成器表达式适合把结果惰性地传给下游消费者(求和、拼接、入库),但当消费者需要多次访问或按下标取值时,它就得先物化成列表。

先写出这段处理“正常输入做什么、空输入做什么、出错时怎么传播”的契约,再决定用哪种写法。这样即使日后把实现换成别的工具,调用者仍能依据同一份契约判断结果对不对。

迭代器与生成器

推导式把数据一次性算完,而有些场景需要“边走边产”。这时用 ,它不预先装满结果,而是在每次被取用时算出下一个值并交出,取完即停。它依赖的是迭代协议——iter() 拿到一个迭代器,next() 推进它,耗尽就抛 StopIteration

生成器把暂停点和局部变量一起保存下来,所以像滑窗这种“记住前面几个元素”的逻辑写起来很自然。下面这段滑窗生成器两种视角对比同一段逻辑:

def windows(values, size):
    iterator = iter(values)
    window = []
    for value in iterator:
        window.append(value)
        if len(window) == size:
            yield tuple(window)
            window.pop(0)

要记住一个边界:惰性只降低“同时驻留在内存里的元素数”,它不会自动限制上游无限生产,也不会让你对生成器按下标取值。需要多次遍历或随机访问,就先用 list() 物化。抽象不能消除成本,它只是把成本挪了位置——生成器把内存成本换成时间成本,这笔账要写在明处。

协程

原书用生成器的 send 把数据推进一个暂停的执行体,这种 思路,把“消费—暂停—再消费”做成了协议。它和普通函数的区别在于:函数被调用就一路跑到底,协程可以在中间停住,等外面喂进来一个值再往下走。

现代 Python 的 async 协程语法已经不一样了——它由事件循环调度,而不是靠手动 send。但问题类别没变:消息怎么进、怎么取消、怎么关闭、异常怎么往外传,仍是同一类协议问题。所以看原书的 send 协程时,要提取的是“暂停点 + 输入 + 关闭”这套不变量,而不是照搬它的 API。

正因如此,把生成器 send 协程和 async 协程混用会直接报错:两者的调度协议不同,一个靠手动推值,一个靠事件循环。需要 I/O 并发就用 async/await,生成器 send 只留给惰性拉取场景。

装饰器

有些横切的需求——记日志、计时、缓存、权限检查——不想写进每个函数里。这时用 ,它在函数定义的那一刻就把原函数换成一个包装器,调用者看到的接口不变,行为却被增强了一层。

包装器要做两件事:透传参数和返回值,以及保留原函数的身份。如果不保留元数据,help() 和调试器看到的就只剩 wrapper,原始函数名和文档都丢了。下面这段用 @wraps 复制元数据的装饰器两种视角对比同一段逻辑:

from functools import wraps
 
def traced(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"call={func.__name__}")
        return func(*args, **kwargs)
    return wrapper

装饰器适合“接口不变、只加一层”的横切契约;一旦把业务控制流藏进不可见的全局状态,调试就会变得困难。包装器必须说明它改了什么、吞了什么异常——透传是默认,拦截是例外。

上下文管理器

资源用完必须释放——文件要关、锁要放、事务要回滚。散落在各处的 close() 一遇异常就会漏。这时用 ,它把“进入前获取、退出后释放”绑成一个词法作用域,无论正常退出还是抛异常,清理都会执行。

最轻量的写法是用 @contextmanager 把一个生成器变成上下文管理器:yield 之前是获取,yield 把资源交给 with 块,finally 里是释放。下面这段打开文件的上下文管理器两种视角对比同一段逻辑:

from contextlib import contextmanager
 
@contextmanager
def opened(path):
    handle = open(path, encoding="utf-8")
    try:
        yield handle
    finally:
        handle.close()

退出方法必须明确是否吞掉异常:finally 里只做释放,不要顺手 except 把异常静默掉;__exit__ 返回 False 让异常继续传播。上下文管理器适合文件、锁、事务和临时资源,它的价值不在“少写一行 close()”,而在“异常路径也保证释放”。

常见误区

误区 1

现象 → 生成器表达式当列表用,取下标报错 原因 → 生成器是惰性迭代器,无 __getitem__,且只能遍历一次 修法 → 需要多次访问或下标取值时用 list(gen) 物化,或直接用列表推导

误区 2

现象 → 装饰后函数 help() 显示的是 wrapper 而非原函数 原因 → 装饰器替换了 __name__/__doc__,元数据丢失 修法 →@functools.wraps(func) 复制被装饰函数的元数据到包装器

误区 3

现象 → with 块内抛异常后资源没释放 原因 → 上下文管理器的 __exit__/finally 被另一层 try 吞掉,或用了裸 except 静默异常 修法 → finally 中只做释放,不捕获异常;__exit__ 返回 False 让异常继续传播

误区 4

现象 → 生成器 send 协程与 async 协程混用,运行时报错 原因 → 两者协议不同:send 推入值到生成器,async 由事件循环调度 修法 → 现代 I/O 并发用 async/await,生成器 send 仅用于惰性拉取场景

小结

  • 推导式用于可读映射/过滤,复杂副作用时改回显式循环
  • 生成器保存暂停点与局部状态,惰性只降低驻留元素数
  • 装饰器在定义阶段替换可调用对象,@wraps 保留元数据
  • 上下文管理器把获取与释放绑成作用域,异常也执行清理
  • async/await 取代生成器 send 协程,协议不同不可混用

练习与验收

练习

问题 1: 为什么生成器表达式只能遍历一次,而列表可以反复遍历?

问题 2: 装饰器不加 @wraps 会丢失什么?

问题 3:@contextmanager 实现一个计时上下文:进入时记录开始时间,退出时打印耗时毫秒,要求异常时也打印并重新抛出。(独立实现)

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

列表推导式

一行表达式把循环里的映射和过滤浓缩成一个新列表。

生成器

用 yield 暂停并把局部状态保存下来的函数,恢复后接着执行。

协程

能在某一点暂停、等待外部推入数据后再继续执行的执行体。

装饰器

在定义阶段用另一个可调用对象替换原函数的语法,常用于横切关注点。

上下文管理器

把资源获取与释放绑成一段作用域、即使异常也执行清理的协议。

资料与写作方式声明

本章以Tarek Ziade《Expert Python Programming》合法公开试读核定可见范围,并以目录限定未公开部分,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…