引言
这是第二章的第 9 篇。前 8 篇我们写了大量变量:scores、perm、rgb、total……运行都正确,但这只完成了一半。程序有两个读者:机器执行它,人维护它——六周后的你、接手你代码的同事、以及代码评审中的审查者。本篇讨论变量与代码风格的工程规范:标识符的语法底线(哪些名字干脆非法)、PEP 8 的命名约定(哪些名字是惯例)、遮蔽内置名的危险,以及一套实用的命名决策流程。这是本章偏”软”的一篇,但它的收益最持久:从今天起,你每一次写变量名都在为未来省时间。先决条件:第一章的环境与 PEP 8 概述(001 章的既有知识),以及本篇用到的前 8 篇全部语法。
概念与原理:名字的两个层面
一个变量名要过两道关。第一道是语法:如果名字违反语言规则,代码根本无法运行。规则如下:标识符(identifier)由字母、数字与下划线组成,且不能以数字开头、不能是关键字;大小写敏感(Score 与 score 是两个名字);Python 3 还允许把 Unicode 字母当标识符——变量 = 42 完全合法,只是按社区惯例不鼓励。关键字(keyword)是语言保留词,Python 3.12 共有 35 个,用 keyword 模块可以随时查看:
import keyword
print(len(keyword.kwlist)) # 35
print(keyword.kwlist[:6]) # ['False', 'None', 'True', 'and', 'as']
第二道是语义:名字要用得符合惯例,这没有编译器管,靠的是规范。Python 社区的事实标准是 PEP 8(Python Enhancement Proposal 8,Python 官方代码风格指南)。它规定的最重要命名约定如下:
- 变量与函数:snake_case(小写单词以下划线连接),如
total_score、is_valid; - 类名:CapWords(每个单词首字母大写),如
ShoppingCart; - 常量:ALL_CAPS(全大写加下划线),如
MAX_RETRIES; - 模块名:简短小写,如
utils.py、parse_log.py; - 私有约定:
_name表示”内部使用,外部别碰”; - 避免冲突:
name_(尾部下划线)用于绕开与关键字的撞名,如class_、type_; - 双下划线
__name触发名称改写(name mangling),类内私有成员防被子类意外覆盖; __name__(双下划线开头结尾)是保留给语言的特殊方法名,如__init__,普通人不要发明新的。
为什么约定如此重要?因为阅读成本是开发成本的大头。一个叫 data 的变量等于没命名;一组命名混乱的代码,审查时每个地方都要停下来推断语义。而一致的命名让代码库成为自解释的文档——看到 has_permission 就知道它是布尔,看到 user_list 就知道它是集合。
由此引出一条注释分工原则:名字能表达的,注释不要重复。total = total + score # 累加总分 是废话注释——名字已经说了;注释的领地是”名字表达不了的东西”:为什么这么取整、为什么这个边界条件是 20、这个数据来自哪个上游接口。指挥得当的话,好命名 + 少量注释的组合,让注释行数直线下降,而可读性反而上升。
操作与实现:从反例到规范
反例与重写
先看一段”能运行但没法维护”的代码,再逐行重写:
# 反例:每个名字都在逼读者猜
l = [90, 75, 60] # l 是什么?成绩?负载?灯泡亮度?
t = 0
for x in l:
t += x
a = t / len(l)
print(a)
重写时把三个诉求绑定到名字上:内容(l → exam_scores)、含义(t → total_score)、单位与角色(a → average_score):
exam_scores = [90, 75, 60]
total_score = sum(exam_scores)
average_score = total_score / len(exam_scores)
print(f"平均分:{average_score:.1f}") # 平均分:75.0
第二版没有注释,但任何人三秒内能读出全部逻辑。注意 sum 是内置函数,直接可用——反例里手写累加既慢又隐晦,用内置既是风格问题也是正确性问题(见易错点 2)。
再对照一个”缩命名”反例——把变量压短不会让程序变快,只会让读者变慢:
# 反例:u / p / n 是什么意思?
u = "alice"
p = "p@ss123"
n = 3
if len(p) >= n:
print("注册成功:", u)
# 规范:三个名字自带语义
username = "alice"
password = "p@ss123"
MIN_PASSWORD_LEN = 3 # 常量:全大写
if len(password) >= MIN_PASSWORD_LEN:
print("注册成功:", username)
第二版把”最少几位密码”从魔法数字 3 提升为命名常量 MIN_PASSWORD_LEN,改需求时只动一处;u/p/n 则逼读者回到赋值行核对,六个字符省下的空间,远远抵不上每次阅读多花的时间。
布尔、集合、常量与时间单位
命名要携带”类型信息”。布尔变量用 is_、has_、can_ 前缀;多个元素的集合用复数;常量全大写;带单位的时间一定要写单位,timeout 迟早被误解成秒还是毫秒:
MAX_RETRIES = 3 # 常量:全大写
DEFAULT_TIMEOUT_SECONDS = 30 # 单位写进名字
def is_valid_username(username: str) -> bool:
return 3 <= len(username) <= 20
registered_users = ["alice", "bob"] # 复数:集合
has_admin_permission = True # has_ 前缀:布尔
print(is_valid_username("alice"), registered_users, MAX_RETRIES)
配合第 5 篇的类型注解习惯(username: str),名字 + 注解双保险,读者拿到手的语义是完整的。对比:d = 30 与 default_timeout_seconds = 30,前者在三个月后的 bug 排查里毫无线索,后者直接自证。
关键字与非法标识符的真面目
class、if、for 这类词做变量名会直接 SyntaxError。用 exec 演示各类非法名字的报错形态(很多初学者以为只是”IDE 标红”):
for bad in ("1st_grade = 3", "total-score = 3", "class = 3"):
try:
exec(bad)
except SyntaxError as e:
print(f"{bad!r:24} -> SyntaxError: {e.msg}")
三种错误各有理由:1st_grade 以数字开头,Python 按”非法十进制字面量”处理;total-score 中的 - 被当作减法表达式,赋值左侧不合法;class 是保留关键字。避开它们的现代姿势简单粗暴:keyword.kwlist 查一遍 + 后缀 _(如 class_)绕行。
避免遮蔽内置函数
内置命名空间里有 158 个名字(len(dir(builtins)) 实测),它们不是关键字,但遮蔽它们同样会带来崩溃。所谓遮蔽(shadowing),就是你在局部重新定义了与内置同名的变量或函数,之后这个作用域里的”原名”就指向你的版本:
# 演示:全局遮蔽 list 后,原来的 list() 调用崩了
code = "list = 100\nprint(list('abc'))"
try:
exec(code)
except TypeError as e:
print("TypeError:", e) # 'int' object is not callable
真实工程里最常见的遮蔽对象是 list、dict、sum、id、type、input、max、min——恰巧都是最高频的内置。修复动作永远是”换名”而非”记住别用”:ids 替代 id,config 替代 dict,total 替代 sum。IDE 的 lint 工具(如 ruff)开箱即用就能标出遮蔽警告,新项目建议直接从第一天启用。
命名三步流程
把以上全部规则压缩成每次写变量名都走一遍的三步检查,成本不到三秒钟,但能挡住 90% 的命名事故:
- 语义:这个名字让一个陌生的读者在 5 秒内知道”存的是什么、什么单位、是不是集合”?说不清就换词——
d说不清,delay_seconds说得清。 - 语法:过一遍
keyword.kwlist,数字开头、连字符、全角字符这些雷区逐个排除。 - 冲突:与内置名或上一级作用域的名字撞车没有?撞了立刻加后缀或换词,绝不寄希望于”这里暂时用不到内置”。
三步都过的名字,大概率也是可搜索的:grep "MAX_RETRIES" 能在全库找到唯一语义,而 grep "data" 会捞起两百个无关命中——名字的可搜索性,是评审与调试效率的隐形杠杆。
下划线体系一览
一个下划线位置,三种含义,全部是约定俗成:
class Account:
def __init__(self):
self.balance = 100 # 公开字段
self._pending = 0 # 约定私有:本模块内部使用
self.__locked = False # 名称改写:实际存为 _Account__locked
acc = Account()
print(acc.balance, acc._pending) # 100 0:公开与约定私有仍可访问
print(acc._Account__locked) # False:改写后的真实名字
__locked 在类外访问 acc.__locked 会 AttributeError——名字被改写成了 _Account__locked,防止子类里同名属性意外冲突。注意 __ 是对”名称改写”的硬机制,_ 则纯粹是软约定(不阻止访问,只表达意图),两者别混用;而 __init__、__eq__ 这类”双下划线夹名字”是语言保留的特殊方法(第 7 篇提到过 __add__),普通变量一律不要碰这个样式。
易错点与陷阱
1. 用 class、if 等关键字当名字。 直接 SyntaxError,程序都跑不起来。报错信息对新人不友好(“invalid syntax” 不会告诉你”class 是保留字”),遇到莫名语法错误时先怀疑命名。keyword.kwlist 是排雷清单;实在想表达”类型”、“类”这类概念,用 class_、type_ 结尾下划线。
2. 遮蔽内置函数,运行时才爆炸。 def id(x): ... 之后就再也调不到真的 id();list = 100 之后 list("abc") 抛 TypeError: 'int' object is not callable。它比关键字更阴险的是不报错直到调用点——代码可能运行几周才在某条路径触发。养成习惯:看到变量名与内置同名,立刻改名,不要抱侥幸。
3. l、o、I 与数字 1/0 的视觉灾难。 单字母 l(小写 L)在等宽字体里与数字 1 几乎无法区分;O 与 0 同理。PEP 8 明确建议避免 l 与 O 作为单字母变量。同理,缩写过度(usruid?totsc?)让名字需要解码,比不缩写更累。缩写白名单只保留无歧义的惯例:idx、msg、tmp。
4. 常量不是”常量”。 Python 没有真正的常量,MAX_RETRIES = 3 只是全大写约定,代码里依然可以 MAX_RETRIES = 5 覆盖它且不报错。不要因此随手改它——全大写名字在审查中承担”不可变契约”的语义,随意改写等于对读者撒谎。真正的”防改”要靠后续章节的守卫手段,现阶段维持契约靠自觉。
顺带说清短名的合法场合,免得矫枉过正:循环计数 i、j、k 是几十年来的惯例(作用域只有三行,读者绝不误解);数学公式里的 x、y、n 对应课本符号;lambda 表达式(lambda x: x * 2)的临时参数。判断标准就一条:这个名字的读者,能否在目光所及的几行内确定它的含义。离开这三类场合,短名就是欠债。
小结
命名是第二层语法:第一层保证能跑(避开数字开头、关键字),第二层保证好读(PEP 8 的 snake_case 变量、CapWords 类、ALL_CAPS 常量)。下划线三态要分清:_x 是软私有约定,__x 触发名称改写,__x__ 是语言保留。最大的两个坑是遮蔽内置函数(延迟爆炸)与缩写过度的名字(永久解码成本)。每次取名走完”语义—语法—冲突”三步,配合”名字能表达的注释别重复”的分工原则,你的代码库就会慢慢长成不需要注释也能读懂的文档。本系列的后续章节还会遇到函数、类、模块三个更大粒度的命名场景,但底层规则与今天完全相同。下一篇文章把本章全部知识收进一份综合测验,并用”进制转换器”这个小工具做一次完整的工程演练。
练习与思考题
- 找出下列命名中的风格问题并改正:
tmp = 3.5(临时存储温度)、listOfStudents、isDone、N(表示总人数)、data2。 - 下面代码的运行结果与修复方式是什么:
for dict in range(3): print(dict)之后调用dict(a=1)会怎样?先推理,再运行验证。 - 为第 6 篇「解析成绩文件」片段里的变量(
line、parts、scores、total)逐个说明:它属于 snake_case 变量、ALL_CAPS 常量还是函数名,以及它携带了什么语义;再为这个场景补充两个你认为更好的名字。 - 思考题:为什么 PEP 8 要求模块名不推荐混合大小写?结合”导入语句
from x import Y的视觉混淆”给出你的解释。