第 7 章 · Pythonic 惯用法¶
前几章你学会了"正确的语法",本章教你"地道的写法"——Pythonic。这一章是从"能用 Python 写出 Java 逻辑"到"写出真正的 Python 代码"的分水岭。很多惯用法在 Java 里要靠 Stream API、try-with-resources、注解处理器才能模拟,在 Python 里是语言级的一等公民。
7.1 推导式(comprehension)¶
Java 要链式 stream().filter().map().collect(),Python 用一行推导式:
-
Java Stream
-
Python 推导式
三种推导式:[... ] 生成 list、{k: v} 生成 dict、{x} 生成 set。
Pythonic 写法
推导式只用于简单的 map/filter。逻辑复杂(嵌套多层、带副作用)时,老老实实写普通 for 循环——可读性优先。一条经验:推导式不该超过一行半。
7.2 生成器与 yield:惰性求值¶
把推导式的 [] 换成 (),就变成生成器表达式——惰性的,不一次性建出整个列表(省内存):
# 列表推导式:立即算出全部,占内存
squares = [n * n for n in range(10**7)]
# 生成器表达式:惰性,逐个产出,几乎不占内存
squares = (n * n for n in range(10**7))
更强大的是生成器函数——用 yield 产出值,函数"暂停"在 yield 处,下次调用接着往下:
-
Java(Stream 一次性)
-
Python 生成器
Java 程序员的视角
生成器 ≈ 一个可以暂停/恢复的迭代器。Java 的 Stream 也是惰性的,但 Python 的生成器是更基础的语言特性(早于 Stream),可以无限产出、可在任意函数里 yield。
7.3 迭代器协议¶
for x in obj: 能工作,是因为 obj 实现了迭代器协议(__iter__ 返回一个带 __next__ 的对象)。让你的类可迭代:
class Countdown:
def __init__(self, start):
self.cur = start
def __iter__(self):
return self
def __next__(self):
if self.cur <= 0:
raise StopIteration # 用异常表示"结束"
self.cur -= 1
return self.cur + 1
for n in Countdown(3): # 3, 2, 1
print(n)
实战中更常用 yield 写生成器(自动满足协议),手写 __next__ 较少。
7.4 with 与上下文管理器 ↔ try-with-resources¶
with 几乎一一对应 Java 的 try-with-resources,但更通用(不只管资源,能管任何"进入/退出"成对操作,如事务、锁、计时)。
-
Java
-
Python
实现上下文管理器:定义 __enter__ 和 __exit__:
class Timer:
def __enter__(self):
import time
self.start = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
print(f"elapsed: {time.time() - self.start:.3f}s")
return False # False = 不吞异常,照常向上抛
with Timer():
do_work() # 自动打印耗时
更简洁的方式——contextlib.contextmanager(用生成器实现):
from contextlib import contextmanager
import time
@contextmanager
def timer():
start = time.time()
try:
yield # with 块的代码在此处执行
finally:
print(f"elapsed: {time.time() - start:.3f}s")
with timer():
do_work()
Pythonic 写法
__exit__ 的返回值决定是否吞掉异常:返回 True 会抑制异常(不传播),返回 False/None 照常传播。绝大多数场景返回 False——只做清理,不吞异常。
7.5 装饰器 ↔ 注解(本质不同!)¶
这是 Java 程序员最容易误解的地方。Java 注解是被动的元数据(贴标签,需框架反射处理);Python 装饰器是主动的高阶函数(接收函数、返回新函数,立即生效)。
-
Java 注解(被动)
-
Python 装饰器(主动)
@deco 是语法糖,等价于 func = deco(func):
def log(func):
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log # 等价于 greet = log(greet)
def greet(name):
return f"hi {name}"
greet("Alice") # 打印 "calling greet",再返回 "hi Alice"
带参数的装饰器(多一层):
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3) # 先 repeat(3) 得到 decorator,再装饰函数
def say(msg): print(msg)
⚠️ Java 程序员的陷阱
- 装饰器是运行时执行的代码,不是声明性元数据。理解成"高阶函数"而非"标签"。
- 用
functools.wraps(func)装饰wrapper,否则被装饰函数的__name__/__doc__会丢失: - 标准库自带常用装饰器:
@property、@staticmethod、@classmethod、@dataclass、@contextmanager、@functools.lru_cache。
7.6 切片(slice)¶
切片是 Python 处理序列的核心语法,Java 没有对应物(要 substring/subList):
s = "Hello, World"
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
s[7:] # "World" 从索引 7 到末尾
s[:5] # "Hello" 从开头到索引 5(不含)
s[7:12] # "World" [起:止) 左闭右开
nums[::2] # [0,2,4,6,8] 步长 2
nums[::-1] # [9..0] 负步长 = 反转!
nums[-3:] # [7,8,9] 负索引从末尾数
切片不越界(越界自动截断,不报错),且返回新对象(浅拷贝)。
Pythonic 写法
反转列表/字符串用 x[::-1];拷贝列表用 x[:] 或 x.copy()。
7.7 enumerate / zip / 解包¶
-
Java 习惯(要索引)
-
Python(enumerate)
# zip:并行遍历多个序列(对应 Java 的 IntStream/手动索引)
for name, age in zip(names, ages):
print(name, age)
# 解包:星号收集剩余
first, *rest = [1, 2, 3, 4] # first=1, rest=[2,3,4](星号收集剩余元素)
print(*[1, 2, 3]) # 调用时用 * 展开可迭代对象 → 等价于 print(1, 2, 3)
7.8 collections 利器¶
标准库 collections 里几个工具能省掉大量样板(第 9 章详讲):
from collections import Counter, defaultdict, namedtuple
Counter("abracadabra") # {'a': 5, 'b': 2, 'r': 2, ...} 计数
dd = defaultdict(list) # 访问不存在的 key 自动建空 list
dd["k"].append(1) # 不用先判断 key 是否存在
Point = namedtuple("Point", ["x", "y"]) # 轻量不可变类型
p = Point(1, 2); p.x # 1 (现代代码更倾向 @dataclass(frozen=True))
本章练习¶
练习 7.1
用一行推导式实现:给定 words = ["apple", "banana", "cherry"],得到每个单词长度大于 5 的单词及其长度的字典。
参考答案
练习 7.2
写一个生成器函数 fib(),无限产出斐波那契数列(1, 1, 2, 3, 5, ...)。再用 itertools.islice 取前 10 个。
参考答案
def fib():
a, b = 0, 1
while True:
yield b
a, b = b, a + b
from itertools import islice
print(list(islice(fib(), 10))) # [1,1,2,3,5,8,13,21,34,55]
islice 限制消费量。
练习 7.3
写一个装饰器 @retry(times=3),让被装饰函数在抛异常时自动重试最多 times 次。用 functools.wraps 保留原函数信息。
参考答案
from functools import wraps
def retry(times=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exc = None
for _ in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
last_exc = e
raise last_exc
return wrapper
return decorator
@retry(times=3)
def flaky(): ...
练习 7.4
用 with + contextlib.contextmanager 写一个 cd(path) 上下文管理器:进入时切到该目录,退出时恢复原目录(即使块内抛异常也要恢复)。
参考答案
上一章:第 6 章 · 模块与包 | 下一章:第 8 章 · 类型提示。