
那天教一位新同事调代码他写了句class Python进阶班直接把我看愣住了。倒不是这句赋值有什么惊天动地的逻辑问题而是class这个单词在 Python 里压根不能用来做变量名——它是保留字。编辑器里那段红波浪线程序一运行就抛的SyntaxError: invalid syntax都是同一件事在报警。很多刚入门的同学在这儿栽过跟头而且栽了之后只知道哦不能用却不知道为什么不能用、哪些词不能用、以及除了保留字之外标识符还有哪些隐藏规则。这一篇我就一次性把 Python 保留字和标识符这件事讲透顺带把我自己这些年踩过的相关坑一并交了底。什么是保留字说白了就是 Python 语法自己圈地的那批单词。语言解释器要靠它们来解析代码结构比如if、for、while代表流程控制def代表定义一个函数class代表定义一个类。如果允许你拿这些词当变量名解释器读代码时就会陷入二义性这里的if到底是关键字还是变量代码还能不能按既有规则解析所以 Python 干脆一不做二不休把这些词全部占死任何人都不许拿来做标识符。理解了这层逻辑你就明白这不是故意给你添堵而是语法解析的必要代价。1. 保留字全景Python 到底圈了哪些地1.1 用 keyword 模块直接看官方清单与其死记硬背一份随时可能过时的清单不如直接在解释器里查。Python 官方在标准库里给你备好了工具就是keyword模块。import keyword # 输出所有硬保留字 print(keyword.kwlist) print(len(keyword.kwlist))你在自己电脑上跑一下会看到类似这样一份列表[False, None, True, and, as, assert, async, await, break, class, continue, def, del, elif, else, except, finally, for, from, global, if, import, in, is, lambda, nonlocal, not, or, pass, raise, return, try, while, with, yield]以 Python 3.11、3.12 为例kwlist里的硬保留字一共 35 个。为什么不直接背因为不同 Python 版本存在细微差别比如async和await从 Python 3.5 开始才正式成为保留字你要是背一个老版本清单很可能在判断到底能不能给变量命名为 async这种问题的时候得出错误结论。所以遇到这个词能不能用的疑问跑一下keyword.iskeyword(xxx)是最靠谱的验证方式。import keyword print(keyword.iskeyword(class)) # True用不了 print(keyword.iskeyword(Class)) # False因为 Python 区分大小写 print(keyword.iskeyword(type)) # Falsetype 不是保留字type这个例子值得多说一句。它不是保留字可你一旦把type当变量名用了type(x)这个查看对象类型的函数就在当前作用域内被你的变量遮蔽了后面再想用它就会得到TypeError: str object is not callable。这种不是保留字但最好别用的词行内常叫内置函数名我后面单独开一节讲。1.2 这些词为什么必须被圈地保留字的存在不是随机选择它们分别对应着 Python 语法里的各类结构。我习惯把它们按用途分几组来记比对着死记硬背这 35 个词高效得多字面量相关False、True、None。这三个其实更像是内置常量但解释器禁止你重新赋值所以也进了保留字名单。True 1这种写法在 Python 3 里直接抛语法错误Python 2 里倒是允许这也是当年大量脏代码的源头之一。流程控制if、elif、else、for、while、break、continue、pass。这些词定义了代码的分支和循环骨架。函数与类定义def、return、class、lambda、yield。yield用来把普通函数变成生成器函数lambda用来写匿名函数。模块与导入import、from、as。这三个配合使用可以实现模块导入和别名。异常处理try、except、finally、raise、assert。这一组负责异常捕获与抛出。逻辑与比较and、or、not、in、is。它们是表达式层面的运算符is和的区别我后面会顺带提一下。变量作用域global、nonlocal。用来在函数内部声明变量作用域。异步与上下文async、await、with。with管理资源上下文async/await是协程语法。为什么要分组因为每一组的关键字一旦允许用户自定义对应的语法结构立刻会出现歧义。举个例子pass是空语句占位符你要是能把pass定义成变量解释器读到pass时到底是什么也不做还是取出变量 pass 的值没法区分。同理from一旦变成变量名from xxx import yyy这种导入语句就无法解析了。保留字本质上是语法解析器在词法层面的保留地是语言设计者为了消灭歧义做的必然取舍。1.3 soft keywordmatch/case 实际上不算硬保留这里必须补充一个很多教程不会讲、但实战里特别容易踩的知识点Python 里还有一类软保留字。它们只在特定语法上下文里才表现得像关键字离开了那个上下文你照样可以拿来当变量名。最典型的就是 Python 3.10 加入结构化模式匹配后引入的match和case。# 这样写是合法的 match 足球比赛 # 这样写也是合法的 case 情况A # 但下面这样用match 就会按关键字来解析 match points: case [x, y]: print(f坐标为 ({x}, {y}))为什么 Python 要把match和case做成软保留字而不是直接圈死因为自 1989 年诞生以来Python 社区里老早就有大量代码用match当变量名、用case当变量名。如果 Python 3.10 直接把这两个词设成硬保留字那一大批存量代码全部要炸库的兼容性会出大问题。于是官方想了个折中方案只有在match后面跟着一个可匹配的目标对象这种语法位置时解释器才把它当作模式匹配关键字其余情况一律按普通标识符处理。你可以在自己环境里验证一下软保留字的清单import keyword print(keyword.softkwlist)Python 3.10 环境下会输出类似[_, case, match]这样的列表。_这个下划线在模式匹配里用来匹配任意内容并且不绑定变量它也算软保留字之一。所以你在 REPL 里用_接收上一个表达式结果的习惯在match语句内部要注意别产生预期之外的覆盖。2. 标识符命名规则硬性边界与隐含雷区2.1 三条铁律开头、后续、大小写保留字是不能触碰的红线但标识符本身也有一套合法性的硬性规定。Python 的变量名、函数名、类名、模块名统统要遵守这几条规则标识符由字母、数字、下划线构成开头不能是数字但可以是下划线或者字母。不能包含空格、连字符减号、、$、%等特殊符号。对大小写敏感Name、name、NAME是三个完全不同的标识符。你可以直接用str.isidentifier()方法来验证一个字符串是不是合法的 Python 标识符这比你自己写正则省事得多print(user_name.isidentifier()) # True print(2user_name.isidentifier()) # False数字开头 print(user-name.isidentifier()) # False连字符不允许 print(_private.isidentifier()) # True下划线开头没问题 print(名字.isidentifier()) # True后面会讲$符号要单独拎出来说。很多从 PHP 或 Shell 转过来的同学习惯了$var这种写法到了 Python 里下意识写出$name 张伟这种代码解释器直接给你SyntaxError: invalid syntax。还有中划线user-name你在 SQL 里可能常写user-name当别名但 Python 里这是减法表达式user - name的变形而不是一个标识符。有一个我见过无数次的错误以数字开头命名。1st_place 冠军这种写法新手经常以为编译器能猜到意图实际上 Python 解释器在读标识符时会先遇到数字1再按词法规则尝试理解后面的内容结果就是解析失败。文件命名也是同理3rd_script.py作为文件名没问题但作为模块名import 3rd_script会直接报语法错误。所以文件名里以数字开头的 Python 模块你是无法用常规方式导入的要么改名要么用importlib的黑科技去加载。2.2 Unicode 标识符中文变量名的能力与风险从 Python 3 开始标识符可以采用 Unicode 字符集中被判定为字母的任意字符。通俗地讲字母不只是英文 26 个字母还包括中文、日文假名、韩文、希腊字母、俄文字母等。所以名字 张三、年龄 18这些写法在语法上完全合法。实测一下名字 李雷 年龄 18 print(f{名字}今年{年龄}岁) # 李雷今年18岁注意这句话我只敢说语法上合法。真在工程代码里写中文变量名我个人的态度是项目内约定大于一切。如果你们团队全是中文母语、项目仅供内部使用、且没有对外接口需要跨语言交互那统一用中文命名一些业务变量有时反而可读性更好。但只要项目要开源、要跟其他代码库协作、或者要用到很多英文工具链和库中文变量名就会变成灾难最常见的两个问题编码环境不一致导致乱码、某些老旧的第三方工具对非 ASCII 标识符支持不完整。所以我给团队的建议一贯是写英文标识符和中文注释别在代码里混用中英文命名。还有一个关于希腊字母的冷知识π 3.14159这种写法语法层面没问题因为π是合法字母字符。有些人拿它耍酷写数学公式但真到了跨平台部署的时候IDE 里看着正常终端里一跑可能因为字体或编码问题变成乱码。我的态度很明确非英语字母尽量别碰除非你是在写教学示例。2.3 标识符与保留字的碰撞测试既然了解了保留字和标识符规则我们自然要判断一下如果一个字符串想当标识符用先检查它是不是保留字再检查它的字符组成是否合法。最稳妥的组合拳是同时用keyword.iskeyword()和str.isidentifier()import keyword def is_valid_variable_name(name): return name.isidentifier() and not keyword.iskeyword(name) test_names [class, user_name, 1st, for, 姓名, True] for name in test_names: print(f{name}: {is_valid_variable_name(name)})输出结果会告诉你class不合法for不合法True不合法user_name合法姓名合法1st不合法。这套判断逻辑可以封装成校验函数用在很多需要让用户自定义变量名的工具脚本里比如你自己写一个配置解析器、写一个小型 DSL都需要在入口处做这种合法性校验。3. 命名规范才是日常代码质量的真正分水岭3.1 PEP 8 命名约定速查表合法是一回事好看、好用、符合社区惯例又是另一回事。你可以写出完全合法但丑到爆炸的代码比如x1 12; X2 hello; x_3 [1,2]解释器一句抱怨都没有。但真正的工程代码要的是可读性。Python 社区把约定俗成的规范写进了 PEP 8Python 官方风格指南其中命名相关的内容我整理成一张速查表对象类型命名风格示例变量名全小写单词间用下划线user_name、max_length函数名全小写单词间用下划线get_user_name()类名CapWords大驼峰UserProfile、HttpRequest常量名全大写单词间用下划线MAX_RETRY_COUNT模块名全小写尽量简短utils.py、db_connect.py私有属性和方法前置下划线_internal_id特殊方法双下划线包围__init__、__str__变量名用全小写加下划线就是大家常说的蛇形命名法snake_case。类名用大驼峰CapWords这是从 C 和 Java 那边传过来的惯例。为什么这样区分因为看到UserProfile你立刻知道它是一个类看到get_user_name你立刻知道它是一个函数看到MAX_RETRY_COUNT你立刻知道这是一个常量。命名规范最重要的价值不是好看而是让你在读代码时不需要跳进定义处就能推断出这个东西的性质。3.2 下划线的四种用法_var、_var、var、var下划线在 Python 标识符里有四种完全不同的语义新手往往分不清这里一次性讲透。单下划线开头_var约定俗成的内部使用标记。它并不阻止外部访问只是个提醒信号让阅读者知道这个属性或方法是内部实现细节不是对外提供稳定接口的。比如你在类的内部维护的一些辅助状态都可以用_开头。Python 的from module import *默认不会导入以_开头的名字这也算是一种半强制的隔离机制。双下划线开头__varPython 解释器会做名字改写name mangling。在类内部如果定义了一个实例变量self.__gender外部代码直接访问obj.__gender会报AttributeError但通过obj._ClassName__gender还是访问得到。这不是安全机制是防止在继承体系里子类不小心覆盖父类私有属性的一种约定。举个例子class Person: def __init__(self): self.__age 18 # 实际上被改写成了 _Person__age class Student(Person): def __init__(self): super().__init__() self.__age 20 # 实际上被改写成了 _Student__age不会覆盖父类字段 s Student() print(s.__dict__) # {_Person__age: 18, _Student__age: 20}双下划线开头且双下划线结尾__var__这是 Python 的特殊方法命名空间也叫 magic method。__init__、__str__、__len__、__call__这些都属于这一类。它们的名字是由语言规范钦定的你自定义的变量名不要用这种格式否则容易跟未来版本加入的新特殊方法冲突。这个圈地里还有一个镜像规则凡是 Python 官方文档里列出的特殊方法你最好不要用它做自己的函数名而你自己创建的类方法也不要用__var__这种格式容易让人误以为它是语言内置的特殊钩子。单下划线结尾var_用来跟保留字或内置函数名做区分。比如你写一个工具函数需要参数名是class所表示的语义但class是保留字于是写成class_。同理type_、id_、filter_这种写法在真实项目里非常常见。还有最普通的单下划线_作为临时变量名for _ in range(10)这种写法表示我不关心循环变量的值纯粹为了循环十次。3.3 团队协作里的命名统一命名规范不是个人审美问题而是团队协作问题。我参与过的项目里因为命名风格不统一导致的 review 争吵数都数不清。有人写userName驼峰有人写user_name蛇形同一个文件里混着来代码可读性直线下降。解决方式只有一条用工具强制执行而不是靠口头提醒。对 Python 项目来说最少要接入flake8或者ruff这类静态检查工具配置好命名规则检查项让它直接在 CI 阶段把不符合规范的代码拦下来。ruff现在更流行一些它速度快、配置简单对 PEP 8 命名的检查覆盖也很全。比 IDE 自动格式化更重要的是一份团队内部的命名约定文档明确回答这些经常争论的问题私有属性用单下划线还是双下划线、模块名用单数还是复数、常量什么时候算常量、DTO 类要不要加DTO后缀、布尔变量用is_开头还是has_开头。这些事情没有标准答案但必须团队内定一个统一答案。4. 那些年我踩过的与保留字相关的坑4.1 用 class 当变量名的经典报错文章开头说的class Python进阶班是我真实遇到过的场景。报错信息长这样 class Python进阶班 File stdin, line 1 class Python进阶班 ^ SyntaxError: invalid syntax很多人看不懂这个报错的含义以为是自己赋值语句写错了其实是class被解释器当作类定义关键字去解析后面跟着一个当然不可能合法。这个坑在数据库字段映射场景里特别常见。比如你从数据库查了一列名字叫class学生的班级在 Python 里处理时写成row[class]访问字典没问题但你一用cursor.description的列名去构造变量手一快就写成class row[class]立刻翻车。正确的做法是改成class_name或者klass用class_也完全可以。我见过更隐蔽的变体for file in files: pass # 这里 file 不是保留字没问题 # 但下面这个 global 10global是保留字在某个配置解析脚本里有人用global config.get(global)这种写法被 review 揪出来了。任何语义为全局配置的变量建议直接叫global_config干净利落。4.2 内置函数名遮蔽type、id、list、dict这一节其实是标识符知识里被讲得最少、但实际危害最大的部分。type、id、list、dict、set、str、int、float、min、max、sum、len、print这一串单词没有一个在保留字清单里你拿来当变量名语法上完全合法。但它们在 Python 的builtins内置命名空间里都是函数或类型一旦你在一段代码里用它们做变量后面所有使用同名内置功能的代码都会在作用域解析时命中你的变量而不是内置函数。最典型的场景是type# 一个数据解析函数 def parse_data(data): type data.get(type) # 你拿到了数据里的类型字段 # 然后你想打印每条数据的类型…… print(type(item)) # TypeError: str object is not callable报错的那一瞬间你才反应过来type已经被你赋值为字符串了再调用type(item)等于在调用一个字符串对象。解决方案很简单数据里那个字段叫type你就用type_或者item_type别跟内置函数抢名字。list和dict的遮蔽更凶险因为它们不报错只是悄悄改变行为list [1, 2, 3] # 把内置 list 类型遮蔽了 my_list list((4, 5)) # 这里不是调 list() 构造函数而是试图调用 [1,2,3] 这个列表对象实际跑一下会发现 Python 会抛TypeError: list object is not callable。虽然还是会报错但如果某段代码里遮蔽list之后又用list()构造列表报错信息会让人一头雾水。这个问题的诡异之处在于你当时可能压根没意识到list是你的变量。所以在团队里我定的规矩很简单不用任何内置函数或内置类型的名字做变量名一个都不行。你把这份名单打印出来贴显示器旁边自检的习惯养成了能避开一片雷区。import builtins # 看看所有内置名字这些词最好都不要用作标识符 print(dir(builtins))4.3 从大小写不敏感语言迁过来的隐形坑从 Java、C#、SQL 这类大小写不敏感或部分不敏感的语言转 Python 的同学很容易在标识符大小写上踩坑。最典型的是把User、user当成同一个东西User {id: 1, name: 张三} def get_user(user_id): return user_db.query(user_id) # 调用时写错大小写 u get_user(User[id]) # 这里 User 和 user 是不同对象还有一个我在 SQL 和 Python 混写场景里踩过的坑SQL 查询的列名大小写不敏感于是你习惯了WHERE Name 张三和WHERE name 张三完全等价。但把同样的列名带进 Python 字典或者对象属性后record[Name]和record[name]是两个完全不同的键。判断逻辑写成if record[name] expected_name:时很可能查出来是None因为数据源里的键是Name不是name。另外要特别注意 Python 对类名和实例名的处理。class Person定义好后实例化用Person()但实例变量一般叫person大小写不同造成新手用print(Person.name)试图访问实例属性的事故屡见不鲜。这些问题的根子都是同一个Python 标识符区分大小写看起来差不多在代码世界里等于完全不同。5. 判断工具与静态检查让规则为你服务5.1 用 keyword 模块和 str.isidentifier 做双保险日常开发里判断一个候选名字能不能用最方便的是keyword.iskeyword()加str.isidentifier()的组合。前者查保留字后者查字符组成合法性。我把这段封装成一个通用函数放在项目工具库里凡是涉及用户输入变量名、配置文件键名转换、代码生成器的场景都会复用import keyword import builtins def check_name_validity(name): 检查一个名称是否能够作为 Python 标识符使用返回 (是否可用, 原因)。 if not name.isidentifier(): return False, 包含非法字符或数字开头 if keyword.iskeyword(name): return False, f{name} 是 Python 保留字 if name in dir(builtins): return False, f{name} 会遮蔽 Python 内置名称 return True, ok注意第三个检查是我额外加的不让你用内置名称。在实际项目里如果只是快速写完扔掉的脚本用type、id当变量名确实无所谓但只要代码超过 100 行或者会被其他人维护遮蔽内置名的风险就排山倒海而来。我见过一个线上数据清洗脚本里有人把sum当变量用结果下游计算总和的地方全用了total()这样的自定义函数才绕过去。这种代码不是在解决问题是在给未来埋雷。str.isidentifier()这个方法有个值得一提的边界行为它只判断语法合法性不判断是否是保留字。所以class.isidentifier()返回True因为class确实是合法字符组成的标识符形态但它不能用这一步必须交给keyword.iskeyword()去拦。另外isidentifier()还会返回True给一些看起来奇怪的 Unicode 组合比如某些不可见字符的某些表示这种情况在真实场景里极少出现但你要知道你写的校验函数并不是百分之百针对人类可读名字的。5.2 正则表达式校验的适用场景如果你想完全自己控制规则不想依赖str.isidentifier()对 Unicode 的宽容行为可以用正则表达式限定只允许 ASCII 字母数字下划线import re VALID_IDENTIFIER_PATTERN re.compile(r^[A-Za-z_][A-Za-z0-9_]*$) def is_ascii_identifier(name): return bool(VALID_IDENTIFIER_PATTERN.match(name))这个正则的逻辑很直观第一个字符必须是英文字母或下划线后续字符可以是英文字母、数字或下划线。用正则的好处是确定性强不分语言、版本、平台规则完全攥在自己手里。我一般在这种场景要求只能英文标识符代码生成器、跨语言的协议字段名、给用户配置表做列名校验。比如你写一个工具自动从 Excel 表头生成 Python 字段名Excel 表头五花八门必须用这个正则先过滤一遍不合法的列名直接报错提醒用户改表头而不是生成出根本不能运行的代码。5.3 在 IDE 和 CI 阶段拦截命名问题命名问题最理想的拦截时机不是在运行时报SyntaxError的时候而是在你写出代码的那一刻。现代的 IDE 在这方面已经做得非常好了。PyCharm 和 VSCode 的 Python 插件都会对保留字做语法高亮和错误标记你一旦把class当变量名编辑器立刻画红波浪线。这个基础能力之外我建议你额外做两件事。第一把命名是否符合 PEP 8接入代码检查工具。老牌的flake8自带N802、N803等命名检查项配合pep8-naming插件能拦截函数名用了驼峰、类名用了蛇形这类风格问题。更现代的选择是ruff一条命令扫描全目录命名规则、语法错误、死代码、未使用的导入一把梭全部检查完。我现在的项目不管大小pre-commit钩子里一定挂ruff check代价极小收益极大。第二用pylint的命名检查兜底。pylint比ruff更严格、固执它甚至会因为你的变量名太短比如x而给出警告。有些团队嫌它话多但恰恰是这种唠唠叨叨能把团队代码的整体命名水平拉上去。我个人推荐的做法是本地开发时 IDE 提示提交前pre-commit跑ruffCI 里再跑一次pylint三层关卡把命名问题挡在上线之前。pip install ruff ruff check your_project/跑完你会看到类似E741 ambiguous variable name l这类平时根本不会注意的警告这其实是 Python 官方推荐的代码检查器如今是ruff生态在提醒你单字母l和数字1长得太像别用。6. 关于保留字变化那些会变的圈地边界很多人以为保留字是永远不变的其实不是。Python 官方在版本迭代中持续调整着保留字的组合。Python 2 时代有exec和print这样的语句式关键字Python 3 里它们变成了普通函数不再是保留字。Python 3.5 加入了async和await。Python 3.10 又加入了match、case这样的软保留字。所以每当你升级 Python 主版本时至少确认一下keyword.kwlist是否发生了变化以此来评估升级对代码库的冲击。在 Python 3.12、3.13 里kwlist仍然是 35 个硬保留字。但 Python 官方已经在讨论把type从builtins里的内置函数变成真正的类型语句关键字类似type Point:这种语法在 3.12 里已经有了type语句的试验性支持。一旦这类讨论正式落定未来版本的type可能就不再只是内置函数遮蔽问题而是真正的保留字限制。关注这些变化最简单的办法就是跟踪whatsnew文档升级版本后跑一遍python -c import keyword; print(keyword.kwlist)确认一下清单。从更宏观的角度看保留字和标识符的边界是 Python 语言设计者反复权衡的产物。他们既要保证语法解析无歧义又要保证新特性不破坏存量生态软保留字的出现就是这种权衡的直接证据。理解了这一点你再看那些为什么 Python 不把这个做成关键字的社区讨论就能明白背后往往不是技术可行性的问题而是兼容性与演进策略的取舍。我在带团队和带新人的过程里反复强调一个观点语法规则是底线你写一个脚本要能跑必须在这些规则内行事命名规范是修养你写的代码要能被人读、被人维护必须在惯例中遣词。前者让你从SyntaxError里解脱出来后者让你的代码十年之后还有人愿意看。把这两件事想透彻了Python 的编程之路才算真正迈过了第一道坎。