返回首页
## 引言
这是第二章的第 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` 模块可以随时查看:
```python
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、这个数据来自哪个上游接口。指挥得当的话,好命名 + 少量注释的组合,让注释行数直线下降,而可读性反而上升。
## 操作与实现:从反例到规范
### 反例与重写
先看一段"能运行但没法维护"的代码,再逐行重写:
```python
# 反例:每个名字都在逼读者猜
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):
```python
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)。
再对照一个"缩命名"反例——把变量压短不会让程序变快,只会让读者变慢:
```python
# 反例: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` 迟早被误解成秒还是毫秒:
```python
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 标红"):
```python
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),就是你在局部重新定义了与内置同名的变量或函数,之后这个作用域里的"原名"就指向你的版本:
```python
# 演示:全局遮蔽 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% 的命名事故:
1. **语义**:这个名字让一个陌生的读者在 5 秒内知道"存的是什么、什么单位、是不是集合"?说不清就换词——`d` 说不清,`delay_seconds` 说得清。
2. **语法**:过一遍 `keyword.kwlist`,数字开头、连字符、全角字符这些雷区逐个排除。
3. **冲突**:与内置名或上一级作用域的名字撞车没有?撞了立刻加后缀或换词,绝不寄希望于"这里暂时用不到内置"。
三步都过的名字,大概率也是可搜索的:`grep "MAX_RETRIES"` 能在全库找到唯一语义,而 `grep "data"` 会捞起两百个无关命中——名字的可搜索性,是评审与调试效率的隐形杠杆。
### 下划线体系一览
一个下划线位置,三种含义,全部是约定俗成:
```python
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__` 是语言保留。最大的两个坑是遮蔽内置函数(延迟爆炸)与缩写过度的名字(永久解码成本)。每次取名走完"语义—语法—冲突"三步,配合"名字能表达的注释别重复"的分工原则,你的代码库就会慢慢长成不需要注释也能读懂的文档。本系列的后续章节还会遇到函数、类、模块三个更大粒度的命名场景,但底层规则与今天完全相同。下一篇文章把本章全部知识收进一份综合测验,并用"进制转换器"这个小工具做一次完整的工程演练。
## 练习与思考题
1. 找出下列命名中的风格问题并改正:`tmp = 3.5`(临时存储温度)、`listOfStudents`、`isDone`、`N`(表示总人数)、`data2`。
2. 下面代码的运行结果与修复方式是什么:`for dict in range(3): print(dict)` 之后调用 `dict(a=1)` 会怎样?先推理,再运行验证。
3. 为第 6 篇「解析成绩文件」片段里的变量(`line`、`parts`、`scores`、`total`)逐个说明:它属于 snake_case 变量、ALL_CAPS 常量还是函数名,以及它携带了什么语义;再为这个场景补充两个你认为更好的名字。
4. 思考题:为什么 PEP 8 要求模块名不推荐混合大小写?结合"导入语句 `from x import Y` 的视觉混淆"给出你的解释。