又一 Python 神奇库 -- Ruff !
那天我在公司加班,手撑着脑袋等 CI 跑完,盯着进度条一点点挪,心里只有一个声音:为啥改两行代码也要等 black + isort + flake8 一起排队跑十几秒,人生有多少个十几秒啊…
结果旁边实习生来一句:哥你咋还没上 Ruff 啊?我一开始还以为是《功夫熊猫》那个师父,后来才知道,又一个 Python 神奇库,姓 R 的。
先说人话啊,Ruff 干嘛用的?就是那个……你原来要装一堆乱七八糟的东西:flake8 查代码风格,isort 排 import,black 格式化,偶尔再来个 pycodestyle、pydocstyle……现在基本都能被 Ruff 一锅端。一个命令,全给你检查一遍,还贼快。
我第一次试是这样的,先手贱写了段最“真实”的业务代码,给你们看看:
# order_service.pyimport json, time, os # noqa: E401 # 当年就这么写过…
from datetime import datetime
defcreate_order(user_id, items, debug=False):
# 这变量根本没用过
now_time = datetime.now()
if user_id == None:
raise ValueError("user_id is required")
price = 0
for i in range(0, len(items)):
price = price + items[i]["price"]
if debug == True:
print("create order", user_id, price)
return {"userId": user_id, "items": items, "price": price}
这段你线上真有可能见到,对吧?import 一坨,== None,for 写得很原始,还顺手搞个 debug 开关。
然后我在项目根目录搞个最原始的检查:
ruff check .
Ruff 给我一顿念叨,大概意思就是:
你为啥 import json, time, os一起写,人呢?有些包导进来没用 user_id == None你能不能好好写成is Nonerange(0, len(items))这不优雅debug 这个参数你其实可以用 logging 啊 balabala…
我按它说的改了一版,顺手再缩进、格式啥的都交给 Ruff 去搞,代码就变成这样:
# order_service.pyfrom __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import List
@dataclass
classItem:
name: str
price: float
defcreate_order(user_id: int, items: List[Item], debug: bool = False) -> dict:
if user_id isNone:
msg = "user_id is required"
raise ValueError(msg)
# 用 sum + 推导式就挺干净
total_price = sum(item.price for item in items)
if debug:
print(f"[{datetime.now()}] create order {user_id=}, {total_price=}")
return {
"user_id": user_id,
"items": [item.__dict__ for item in items],
"price": total_price,
}
你看,这些改动其实也不复杂,但日常要你自己一个个挑毛病,你肯定懒得查。Ruff 就是那个嘴碎但有点靠谱的同事,改之前先给你念一遍问题,改完再帮你统统排版。
顺便说个真实场景,我之前写那篇对比 Postgres 和 MySQL 压测的东西的时候,SQL 拼接那块我手滑写过一次没用到的变量,线上压测前还好被自己翻日志翻出来,要是当时项目里已经上 Ruff,让它把“没用的变量”“死代码”全标出来,我能少熬两小时夜。
Ruff 真正牛皮的是“快”。你本地整个项目跑一圈:
ruff check .
ruff format .
那速度真的,终端刷一下就完事儿了,跟以前那种“泡杯咖啡回来 black 还在转”是两种体验。尤其你们要是像我一样爱在 pre-commit 里挂一堆钩子——每次 push 前必须全通过那种——换成 Ruff 之后,等待时间能少一半不止,心情会好很多。
有一次我们团队改一个非常大的 repo,原来 flake8 一跑 40 来秒,大家每次提交之前都开始摸鱼刷手机。后来我偷偷把配置换成 Ruff,结果第二天大家异口同声:哎?怎么一下就跑完了,是不是没生效?——典型“用习惯了慢工具,对快的产生不信任”。
上 Ruff 其实门槛很低,你甚至可以偷懒只在 Python 里写个小脚本来调它,比如我当时给自己写了个“懒人检查器”,项目根目录放一个 dev_check.py:
import subprocess
import sys
from pathlib import Pathdefrun(cmd: list[str]) -> int:
print(f"\n$ {' '.join(cmd)}")
result = subprocess.run(cmd)
return result.returncode
defmain() -> int:
root = Path(__file__).parent
checks = [
["ruff", "check", str(root)],
["ruff", "format", "--check", str(root)],
["pytest", "-q"],
]
for cmd in checks:
code = run(cmd)
if code != 0:
print(f"命令失败:{' '.join(cmd)},先把这个修了再说")
return code
print("\n全部通过,可以放心提交了~")
return0
if __name__ == "__main__":
raise SystemExit(main())
我一般就是改完代码,随手来一句:
python dev_check.py
过了就 commit,过不了就按提示回去挨 Ruff 训。这个脚本本身也可以被 Ruff 继续挑毛病,互相伤害一下。
当然,Ruff 有时候也会太“严格”。比如有些 import 虽然当前文件没用到,但为了兼容某些框架(Django signal 那种),你就得硬要写上,那它就会骂你 F401 未使用导入。这时候也别和它较劲,给它贴个“封条”:
from .signals import init_signals # noqa: F401defready() -> None:
# 虽然没直接用 init_signals,但导入时会自动注册
init_signals()
或者你可以干脆在一小段代码上关掉 Ruff 某条规则:
defdebug_print(data: dict) -> None:
# ruff: noqa: T201 # 临时允许 print
print("debug >>>", data)
这个模式很适合那种“上线前删掉”的调试代码,等你哪天忘删了,Ruff 再把你骂一顿,也算帮你兜底。
Ruff 还有个让我挺喜欢的小细节,它会顺手给你做点“语法美化”。比如我以前总写这种玩意儿:
if len(items) == 0:
return []
Ruff 会温柔(?)提醒你:哥,要不你写成更 Pythonic 一点:
ifnot items:
return []
或者你写:
value = "%s-%s" % (user_id, order_id)
它会说:都什么年代了,你咋还在用 % 格式化,来,给你变成 f-string:
value = f"{user_id}-{order_id}"
这种小改动,短期看没啥,长期看整个项目的代码风格会慢慢统一起来,不会像我当年那个 SpringBoot 项目一样,一个类一个风格,默认配置也没人改,最后线上全是坑。
再说个我个人非常在意的点:Ruff 在找“潜在 bug”上比很多人认真。你比如这种不经意的小错误:
defsave_user(user, cache={}):
cache[user.id] = user
return cache
默认参数用可变对象,这个坑网上都说烂了,你真写业务的时候照样会翻车。Ruff 会直接告诉你:这写法有问题,让你改成:
from typing import Dictdefsave_user(user, cache: Dict | None = None):
if cache isNone:
cache = {}
cache[user.id] = user
return cache
我之前排查那个 TCP 1024 字节卡 bug 的时候,日志和数据包看得人脑仁疼,要是当时 Python 端那些小边界判断能早点被 Ruff 抓出来,我估计能少掉好几缕头发。
最后说点落地的经验哈,你要真想在团队里推 Ruff,大概就三步:
先在自己分支玩一圈,把最重要的目录(比如 app/、service/)拉一份,让 Ruff 扫一遍,把明显的错误修掉。选一个“新模块”作为试点,只对新代码强制 Ruff 通过,老代码先别动,慢慢“以新带旧”。 等大家习惯了,就上 pre-commit 或 CI,直接把 ruff check .和ruff format --check .当守门员,过不了就不让合 PR。
这样搞下来,你会发现项目里很多“玄学 bug”会越来越少,那种靠肉眼挑代码风格问题的 code review 也会越来越轻松,大家终于可以把精力放在“这个算发到底对不对”上,而不是“你这逗号后面能不能加个空格”。
行,今天就唠到这,我得去把我们另一个老项目也悄悄换成 Ruff,不然每次看它跑 flake8 我都想关电脑下班了…