那天我在公司加班,晚上十一点多,正打算关电脑回家,隔壁小伙儿蹭过来一句:“东哥,你帮我看下这个 Python 脚本,老大说可读性一坨屎。” 我一打开文件,心情瞬间从“准备下班”切换成“想提桶跑路”。 大写变量、小写函数、缩进混着 tab 和空格,整整一千多行一个文件,活生生把 Python 写成了“压缩日志格式”。 有种当年看某人把 SQL 写成《天龙八部》的既视感,跟我之前吐槽数据库那篇一样,逻辑没问题,就是想打人那种。
你们有没有这种感觉啊,就是 Python 跑得挺好,但一看代码就知道:这个人以后线上一定出事。 代码能跑 ≠ 代码写得对,这话我都说吐了。我们后端踩的很多坑,真不是技术多难,而是代码规范当空气。SpringBoot 的默认配置你不改,迟早翻车是一个道理。
先说最扎眼的——命名。 那小伙的变量叫 a, b1, bb, data2,函数名叫 do, run, calc2,看着就像犯罪现场。 我直接在他电脑上敲了一段:
# 这是他原来的风格
defdo(a, b, c):
r = a + b
if c:
r = r * 1.13
return r
然后我边改边念叨:“别问 c 是啥,问就是业务逻辑。” 我给他改成这样:
defcalc_order_price(base_price: float, shipping_fee: float, need_tax: bool) -> float:
subtotal = base_price + shipping_fee
if need_tax:
subtotal *= 1.13# 13% 税率,后面再抽成配置
return subtotal
你看,变量一换,注释一写,逻辑几乎不用解释。 命名做到这三个:见名知义、统一风格、不要缩写到鬼都看不懂——已经赢一半人了。
然后是缩进和空格,这个真的会要命。 Python 的哲学就是:你缩进错了,解释器直接给你一巴掌;你空格乱用,解释器不管,但同事会给你一脚。 那哥们有一段 if…else 写成这样(我没黑他,是原样的):
if status=='success':
print('ok')
elif status == 'pending' :
print('wait')
else :
print( 'error')
我看了三秒,眼睛开始自动寻路。 规范一点就这样:
if status == "success":
print("ok")
elif status == "pending":
print("wait")
else:
print("error")
规则很啰嗦,但很好记: 运算符两边空格、逗号后面空格、括号紧贴内容、缩进四个空格,别用 tab。 你要是嫌记不住,就装个 formatter,交给工具去吵架。
真正恶心人的,是那种一千多行的“上帝函数”。 我翻他脚本的时候,看见一个 process_all(),从 50 行写到 600 多行,中间什么下单、扣库存、发短信、生成报表,全堆一起。 这种函数出 bug 的时候,你连“从哪开始看”都不知道。 我给他拆了一版简单的示例:
defprocess_order(order):
validate_order(order)
price = calc_order_price(order.base_price, order.shipping_fee, order.need_tax)
save_order(order, price)
notify_user(order)
defvalidate_order(order):
ifnot order.user_id:
raise ValueError("user_id is required")
# ... 其他校验
defsave_order(order, price):
# TODO: 这里本来应该是入库,我就随便写一下
print("saving order:", order.id, "price:", price)
defnotify_user(order):
# 这里假装发个消息
print("notify user:", order.user_id)
你看一眼这些函数名,大概就知道发生了什么。 单一职责这个词听着有点官方,翻译成人话就是:一个函数只干一件事,出事了好甩锅。
还有一个经常被忽略的东西:docstring。 不是每个函数都要写长篇大论,但关键的能力边界一定要说明白。 尤其那种对外暴露的工具函数,你不写文档,三个月后连你自己都忘了当初咋想的。
给你个我平时会写的版本:
defload_config(path: str) -> dict:
"""
加载 JSON 格式配置文件.
:param path: 配置文件路径
:return: 解析后的配置字典
:raises FileNotFoundError: 文件不存在
:raises ValueError: JSON 解析失败
"""
import json
from pathlib import Path
text = Path(path).read_text(encoding="utf-8")
try:
return json.loads(text)
except Exception as exc:
raise ValueError(f"invalid json config: {path}") from exc
你看,谁调用都不用点进函数体,光看 docstring 就知道能不能放心用、会抛啥错。 这比代码里堆一堆“// todo 以后处理”靠谱多了。
再往上一个等级,就是类型注解。 我知道你们很多人觉得 type hint 很烦,写起来像 Java;但是项目一大,没有类型提示的 Python,就跟没加校验的 MQ 一样,问题到了线上才知道。
简单用法其实就那样:
from typing import List, Dict
defgroup_users_by_city(users: List[dict]) -> Dict[str, List[dict]]:
result: Dict[str, List[dict]] = {}
for user in users:
city = user.get("city", "UNKNOWN")
result.setdefault(city, []).append(user)
return result
不需要一上来把全项目加满,太累。 你就从公共函数开始,入参和返回值标一下,等哪天你换 IDE,发现自动补全变聪明了,那就是类型注解在帮你赚钱。
说完“写得清楚”,得说说“挂了好查”。 异常处理这块,如果你一直 except Exception: pass,那真的不是写代码,是埋地雷。 我给那小伙找到一段:
try:
do_something()
except:
pass
我当场给他念悼词。 正常一点的姿势至少要这样:
import logging
logger = logging.getLogger(__name__)
defrun_task():
try:
do_something()
except ValueError as exc:
logger.warning("参数错误: %s", exc)
raise
except Exception as exc:
logger.error("未知异常,task 终止", exc_info=exc)
# 看业务决定是重试、降级还是直接抛出
raise
日志里要有上下文信息,比如哪个用户、哪个订单、关键参数是多少,这样你 Kibana 一搜就能把链路串起来,不至于像查 Feign 超时那次一样绕半天。
还有一个细节很多人忽略:常量和配置。 不要在代码里满地跑魔法数字、硬编码字符串,改起来非常痛苦。 我现在基本都会搞一个 settings.py 或者单独的配置模块:
# settings.py
TAX_RATE = 0.13
DEFAULT_PAGE_SIZE = 20
MAX_PAGE_SIZE = 100
# service.py
from .settings import TAX_RATE, DEFAULT_PAGE_SIZE
defcalc_tax(price: float) -> float:
return round(price * TAX_RATE, 2)
你后面要做 A/B 实验、按国家切税率,直接在配置层搞就行了,不用到处搜 1.13 这个鬼东西。
最后再啰嗦一句团队协作的事。 规范这玩意儿,靠大家嘴上说是没用的,必须落到工具上。 最简单的套路: 本地装一个 black 或 ruff 之类的代码工具,配个 pre-commit 钩子,不符合规范直接拦截。 你要是嫌黑盒 formatter 不顺眼,可以至少把 flake8/pylint 这种检查类打开,PR 不过就别合。 这个感觉就跟我以前排查 TCP 那个 1024 字节的诡异 Bug 一样,没工具辅助你,一堆字节流看得人要吐。
行了,我一口气说了这么多,你们先挑一两条在自己项目里用起来就行,别全记。 真的,哪怕只是从明天开始把变量名取正常、tab 换成四个空格、别再 except: pass,你们组的幸福指数都会直线上升。 我先去泡个面,等会儿要把那个一千行的脚本拆了,不然代码审查我得被气出高血压。