跳转至

第 7 章 · Pythonic 惯用法

前几章你学会了"正确的语法",本章教你"地道的写法"——Pythonic。这一章是从"能用 Python 写出 Java 逻辑"到"写出真正的 Python 代码"的分水岭。很多惯用法在 Java 里要靠 Stream API、try-with-resources、注解处理器才能模拟,在 Python 里是语言级的一等公民。


7.1 推导式(comprehension)

Java 要链式 stream().filter().map().collect(),Python 用一行推导式

  • Java Stream

    List<Integer> squares = nums.stream()
        .filter(n -> n > 0)
        .map(n -> n * n)
        .collect(Collectors.toList());
    Map<Integer, String> map = list.stream()
        .collect(Collectors.toMap(
            User::getId, User::getName));
    
  • Python 推导式

    squares = [n * n for n in nums if n > 0]
    lookup = {u.id: u.name for u in users}   # 字典推导式
    unique = {x for x in items}              # 集合推导式
    

三种推导式:[... ] 生成 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 一次性)

    // Stream 是惰性的,但通常一次性消费
    IntStream.range(0, n).map(...)
    
  • Python 生成器

    def evens(n):
        for i in range(n):
            if i % 2 == 0:
                yield i          # 产出并暂停
    
    for x in evens(10):          # 0,2,4,6,8
        print(x)
    
def read_lines(path):
    with open(path) as f:
        for line in f:
            yield line.rstrip()   # 逐行产出,不把整个文件读进内存

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

    try (var r = new BufferedReader(fr)) {
        return r.readLine();
    }   // 自动 close
    
  • Python

    with open(path) as f:        # 退出 with 块自动关闭
        return f.readline()
    

实现上下文管理器:定义 __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 注解(被动)

    @Override
    @GetMapping("/users")
    public List<User> list() { ... }
    // 注解本身不做事,靠 Spring/编译器反射读取
    
  • Python 装饰器(主动)

    @app.route("/users")     # 立即调用,返回新函数
    def list_users():
        return [...]
    

@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__ 会丢失:
    from functools import wraps
    def log(func):
        @wraps(func)
        def wrapper(*args, **kwargs): ...
        return wrapper
    
  • 标准库自带常用装饰器:@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 习惯(要索引)

    for (int i = 0; i < list.size(); i++) {
        System.out.println(i + ": " + list.get(i));
    }
    
  • Python(enumerate)

    for i, item in enumerate(list_):
        print(f"{i}: {item}")
    # 还能指定起始:enumerate(list_, start=1)
    
# 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 的单词及其长度的字典。

参考答案
result = {w: len(w) for w in words if len(w) > 5}
# {'banana': 6, 'cherry': 6}
练习 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) 上下文管理器:进入时切到该目录,退出时恢复原目录(即使块内抛异常也要恢复)。

参考答案
import os
from contextlib import contextmanager

@contextmanager
def cd(path):
  old = os.getcwd()
  os.chdir(path)
  try:
      yield
  finally:
      os.chdir(old)     # finally 保证异常时也恢复

上一章:第 6 章 · 模块与包 | 下一章:第 8 章 · 类型提示