Pythonic 代码去哪里练:题库、点评与 lint 闭环

2026-09-09 00:00    #Python   #Pythonic   #代码风格   #练习方法  

你在洛谷上 AC 了一道题,代码 60 行:for 套 for、一堆 tmp、建了三个数组。旁边有人 25 行写完,用了 defaultdict、@cache 和一个生成器。两边的评测结果一模一样——都是 AC。

这不是因为你"不会 Python",而是因为你在一个不提供风格反馈的地方练习。OJ 的判分器只回答"对不对",从不回答"能不能写得更清楚"。

想练 Pythonic,需要的是三类反馈:有人给你点评、有高手解让你对比、有工具指出"这里可以不这么写"。本文按这三类给出可用的场地,再给一套能直接跑的 lint 闭环和一张交付前自检表。

1. 为什么刷 OJ 练不出 Pythonic

先明确 Pythonic 到底指什么。它不是"用得多"、也不等于"写得短",而是三条:

  1. 用语言内建的抽象表达意图:要"取不到就返回默认值"就用 dict.get,而不是先 if key in d;
  2. 让数据形状决定容器:值到至多两个生产者,就用一个 dict 而不是两条定长数组;
  3. 把重复劳动交给标准库:@cache、itertools、推导式。

对照这三条看,OJ 的问题就很清楚了:

1# 最短,但需要读懂 setdefault 的返回值语义
2nxt[v] = person if nxt.setdefault(v, person) == person else ANY

这段代码在 golf 场上是好答案,在工程和教学里必须配一行"本轮首次出现 value、或唯一生产者还是自己 → person,否则 ANY"的注释才算合格。短和清晰是两个指标,OJ 只奖励其中一个。

2. 三类练习场

站点给的反馈适合练什么成本
Exercismmentor 逐题点评 + 读完题后能看别人的解局部 idiom、命名、测试意识免费(可申请 mentoring)
Python Morsels每周练习题,交完附参考解 + 为什么这样写标准库、迭代器、数据结构选型付费
Codewars通过后能看社区解,同题常有五六种写法表达式能力、itertools、位运算免费
Advent of Code题本身不教 Python,但 r/adventofcode 的 Python 解质量高完整小项目的结构与拆分免费
HackerRank Python语法分档练习,没有风格反馈补语法盲区免费
LeetCode / 洛谷 / Codeforces只有正确性算法,不练风格免费
Code Golf字符数与 Pythonic 目标相反免费

用法上有两个关键点,决定这些站点是"练题"还是"练风格":

3. 工具闭环:把 lint 当老师

题库给的是别人的解,工具给的是针对你自己代码的反馈,而且能重复跑、零等待。Pythonic 规则已经被几个 linter 编码成规则集了:

1pipx install ruff refurb
2
3# C4=flake8-comprehensions  SIM=flake8-simplify  PERF=perflint
4# PIE=flake8-pie  FURB=refurb  UP=pyupgrade  PLR=pylint 重构建议
5ruff check --select C4,SIM,PERF,PIE,FURB,UP,PLR main.py
6
7refurb main.py

三个它会直接抓出来的例子:

 1# C4 / SIM:for + append 换成推导式
 2squares = []
 3for x in xs:
 4    squares.append(x * x)
 5squares = [x * x for x in xs]
 6
 7# PERF:手写累加换成 sum
 8total = 0
 9for x in xs:
10    total += x
11total = sum(xs)
12
13# SIM:if/else 赋值换成条件表达式
14if on_board:
15    choices = range(8)
16else:
17    choices = (None,)
18choices = range(8) if on_board else (None,)

**但它抓不到结构级的改法。**下面两条都是真实的改进,lint 一条都不会报:

1# 之前:先枚举 9^5 种窗口,手工建表
2COLORED = bytearray(9 ** 5)
3for code in range(9 ** 5):
4    ...                     # 还要配 window_code() / decode_window() 两个进制转换函数
5
6# 之后:让表按需生成,编码转换和建表循环一起消失
7@cache
8def colored_mask(window: tuple[int | None, ...]) -> int:
9    ...
1# 之前:读者在 if 条件里当场解 prev.get 的默认值语义
2if prev.get(value, person) != person:
3    deadline = pos + k - 1
4
5# 之后:先命名再判断,条件里只留一个已经想清楚的名字
6can_start = prev.get(value, person) != person
7if can_start:
8    deadline = pos + k - 1

结论:lint 管局部 idiom,结构级改法要靠自检表(第 5 节)。

4. 最有效的练法:同题两语言

最省事的练习素材其实是你手上已有的题:先用 C++ 写一遍正确解,再把它翻成 Pythonic 短解。

这个流程强在三点:

一个真实例子:同一道五列窗口 DP,教学版先用 build_lines() 生成线表、再把 959^5 种窗口的掩码整表算进 COLORED,151 行;Pythonic 版把线表写成四个列表推导,把整表换成 @cache,行数掉到 90 行以内,而且 window_code() / decode_window() 与建表循环整体消失:

1# 窗口里所有"经过中间列(列 2)"的三连,共 16 条;每条写成 3 个 (行, 列) 格子。
2LINES = (
3    [((r, c), (r, c + 1), (r, c + 2)) for r in range(3) for c in range(3)]  # 横向 9 条
4    + [((0, 2), (1, 2), (2, 2))]                                           # 纵向 1 条
5    + [((0, c), (1, c + 1), (2, c + 2)) for c in range(3)]                  # 右下斜 3 条
6    + [((0, c + 2), (1, c + 1), (2, c)) for c in range(3)]                  # 右上斜 3 条
7)

注意这里的注释:每个"家族"一行、行尾标数量。这种注释密度是 Pythonic 短代码的一部分,不是负担——它把"这段推导式在生成什么"从读者脑内推导变成了明示。

5. 结构级改法:交付前自检表

风格这个词很难执行,除非把它拆成可否证的条目。下面这张表是我自己交付(或让 AI 交付)Python 代码前逐条核对用的,每条都能在代码里指到具体行:

#检查项不通过的样子
1每个函数都能用一句话说清"它回答什么问题"有函数说不清职责,只能写"处理一下"
2状态编码的含义只在模块级常量处解释一次哨兵值的含义散落各处,靠读者拼
3热循环里没有裸的复合表达式if nxt.setdefault(v, p) == p: 直接进 if
4只有一个调用点的单行函数数量 = 0定义了 add_producer(...) 却只调用一次
5“单人层 / 单轮层 / 主流程"各占一个函数三层 for 全塞在 main 里
6输入里的每个位置量都有名字for _ in range(next(data))、range(max(by_round) + 1)
7主流程只做读入、调用、输出主流程里出现算法判断
8能用 dict / set / 生成器的地方不写定长数组或全量预计算手写建表循环;size=V 的数组只为查一次
9谓词用局部变量承载名字,而不是抽单行函数def can_start(prev, value, person) -> bool: return ... 只被调用一次
10状态空了就停明知后面全部不可达还老实跑满 R 轮

第 4 条和第 9 条看起来矛盾,其实是一条规则的两面:名字要留在调用点,参数表不要。

1# ✅ 名字在调用点,没有参数表
2can_start = prev.get(value, person) != person
3if can_start: ...
4
5# ❌ 为一个表达式跳去读参数表
6def can_start(prev, value, person) -> bool:
7    return prev.get(value, person) != person

而"层"相反,必须留成函数——advance(prev, seqs, k) 这样的名字本身就是一个可复用的概念,它能让你在阅读主流程时不必展开细节。

6. 免费的点评渠道与阅读清单

小结

一句话:题库给题目,lint 给局部反馈,自检表给结构反馈,同题两语言给压力。

如果你的目标是"我写出来的 Python 能被人一眼看懂”,那么练习顺序建议是:

  1. 用 lint 闭环(ruff --select C4,SIM,PERF,PIE,FURB)清掉所有局部坏习惯;
  2. 用同题两语言做主动翻译练习,逼自己每处都做选择;
  3. 交付前跑一遍第 5 节的自检表,把"感觉不够 Pythonic"变成具体的待改条目。