前言「基本语法」四个字容易让人以为重点在「怎么打冒号、怎么敲缩进」。真正卡住人的其实不是这些记号而是背后的语义为什么改了一个列表另一个变量也跟着变为什么函数里给全局变量赋值会报UnboundLocalError为什么list.sort()返回None这篇详解按「对象模型 → 运算符 → 控制流 → 函数 → 作用域 → 推导式」的顺序展开每一节都尽量把「结果是什么」和「为什么是这个结果」说清楚。凡是实现细节比如小整数缓存而不属于语言保证的我会单独标出来。示例适用于 Python 3.8 及以上。注意 Python 2.7 已于 2020 年 1 月 1 日停止维护本文不含任何 Python 2 语法。一、名字、对象与可变性Python 的赋值是名字绑定name binding不是「把值放进盒子」。a [1, 2]做的是「让名字a指向那个列表对象」b a让b指向同一个对象。# 适用于 Python 3.8a [1, 2]b ab.append(3)print(a) # [1, 2, 3]a 看到的是同一个列表c a[:] # 切片生成新列表c.append(4)print(a, c) # [1, 2, 3] [1, 2, 3, 4]这就解释了可变mutable与不可变immutable的分野类型可变备注intfloatboolstrtuplefrozenset不可变「修改」都是创建新对象listdictsetbytearray可变支持原地修改不可变对象的「修改」一定产生新对象这也是为什么s.upper()必须接返回值——原字符串不会变。# 适用于 Python 3.8s abcs.upper() # 返回新字符串但没有名字指向它随即被回收print(s) # 仍然是 abcs s.upper() # 这样才改了print(s) # ABCt (1, 2)try:t[0] 9except TypeError as e:print(TypeError:, e)二、运算符与真值测试运算符的语义细节# 适用于 Python 3.8print(7 / 2) # 3.5除号永远返回 floatprint(7 // 2) # 3整除向下取整print(-7 // 2) # -4不是 -3print(7 % 3, -7 % 3) # 1 2取余结果符号跟随除数print(True True) # 2bool 是 int 的子类print(isinstance(True, int))bool是int的子类所以True True 2。写类型判断时如果先判int再判bool布尔值会被误判成普通整数# 适用于 Python 3.8def kind(v):if isinstance(v, bool):return boolif isinstance(v, int):return intreturn otherprint(kind(True), kind(1))is与是两个层面# 适用于 Python 3.8a [1, 2]b [1, 2]print(a b) # True值相等print(a is b) # False不同对象print(a is a) # Trueprint(None is None) # TrueNone 是单例is只应用于None、True、False和哨兵对象。CPython 会缓存 -5 到 256 的小整数所以256 is 256可能是True、257 is 257在脚本里通常也是True同一编译单元内的常量合并但这些都是实现细节不能依赖。短路求值与真值测试and/or不返回布尔值而是返回决定结果的那个操作数并且短路# 适用于 Python 3.8name display name or 匿名用户print(display) # 匿名用户config {retries: 0}count config.get(retries) or 3print(count) # 3注意0 被当成假值了最后一行是个经典陷阱想要「取不到才用默认值」应该用get的 default 参数而不是or否则值为0、、False时会被替换掉。假值一共这些None、False、0、0.0、0j、、[]、{}、()、set()以及定义了__bool__返回False或只定义__len__返回 0的对象。三、控制流、函数与作用域控制流与循环的elsefor/while都能带else含义是「循环没有被break打断时执行」# 适用于 Python 3.8def find_even(numbers):for n in numbers:if n % 2 0:print(找到偶数, n)breakelse:print(没有偶数)returnfind_even([1, 3, 5])find_even([1, 2, 3])这段逻辑用else比用一个标志变量干净得多。注意else里写的条件是「没break」跟if/else的else含义不同。循环变量在循环结束后仍然存在Python 没有块级作用域# 适用于 Python 3.8for i in range(3):passprint(i) # 2函数的五种参数Python 的参数形态比大多数语言多PEP 5703.8又加了仅位置参数。按出现顺序形态写法说明仅位置3.8def f(a, b, /)只能用位置传递位置或关键字def f(a, b)两种都行可变位置def f(*args)收集成tuple仅关键字def f(*, k)只能用k值传可变关键字def f(**kwargs)收集成dict# 适用于 Python 3.8def func(a, b, /, c, *args, d, **kwargs):print(a, b, c, args, d, kwargs)func(1, 2, 3, x, y, d4, extra5)func(1, 2, c3, d4)规则/之前的只能用位置传*之后的只能用关键字传*args之后、**kwargs之前的参数自动变成仅关键字。传参时*和**还能用来解包# 适用于 Python 3.8def add(x, y):return x ynums [3, 4]print(add(*nums)) # 7opts {x: 3, y: 4}print(add(**opts)) # 7默认参数只在定义时求值一次这是函数部分最重要的坑# 适用于 Python 3.8def bad(item, bucket[]):bucket.append(item)return bucketprint(bad(1)) # [1]print(bad(2)) # [1, 2]不是 [2]def good(item, bucketNone):if bucket is None:bucket []bucket.append(item)return bucketprint(good(1)) # [1]print(good(2)) # [2]作用域LEGB 与 global / nonlocal名字查找顺序是Local当前函数→Enclosing外层函数→Global模块→Built-in内置。# 适用于 Python 3.8count 0def increase():global countcount 1 # 没有 global 会抛 UnboundLocalErrorincrease()print(count) # 1关键点只要函数体内有对某个名字的赋值这个名字在整个函数内就被当作局部变量——哪怕赋值语句在读取之后。所以下面的写法会报UnboundLocalError而不是读到全局的count# 适用于 Python 3.8total 0def broken():try:total 1 # UnboundLocalErrortotal 被判定为局部变量except UnboundLocalError as e:return fUnboundLocalError: {e}print(broken())嵌套函数要修改外层函数的变量用nonlocal# 适用于 Python 3.8def counter():n 0def step():nonlocal nn 1return nreturn stepc counter()print(c(), c(), c()) # 1 2 3四、推导式与实战推导式与生成器表达式推导式comprehension是 Python 最有特色的语法之一但要区分清楚# 适用于 Python 3.8squares_list [x * x for x in range(5)] # 列表推导式立即求值squares_gen (x * x for x in range(5)) # 生成器表达式惰性squares_set {x * x for x in range(5)} # 集合推导式squares_map {x: x * x for x in range(5)} # 字典推导式print(squares_list, squares_set)print(list(squares_gen))print(sum(x for x in range(10) if x % 2 0)) # 20生成器表达式不是列表推导式。前者是惰性迭代器只能遍历一次且不占用列表的内存后者立即构造出完整列表。下面这个错误很典型# 适用于 Python 3.8gen (x for x in range(3))print(list(gen)) # [0, 1, 2]print(list(gen)) # [] —— 已被耗尽不会重来推导式在 Python 3 里有自己的作用域循环变量不会泄漏到外面# 适用于 Python 3.8x 外部的result [x for x in range(3)]print(result, x) # [0, 1, 2] 外部的实战把语法点串起来# 适用于 Python 3.8按分数分组统计并演示参数、作用域、推导式与循环 else。def group_by_grade(records, /, *, passing60):records 是 (姓名, 分数) 的可迭代对象返回 {等级: [姓名, ...]}。groups {}for name, score in records:grade 及格 if score passing else 不及格groups.setdefault(grade, []).append(name)return groupsdef find_first(records, predicate):for item in records:if predicate(item):return itemelse: # 没找到才走这里raise LookupError(没有满足条件的记录)if __name__ __main__:students [(小明, 92), (小红, 58), (小刚, 77)]groups group_by_grade(students, passing60)for grade in (及格, 不及格):names groups.get(grade, [])print(f{grade}{len(names)} 人{、.join(names) or 无})top find_first(students, lambda r: r[1] 90)print(最高分记录, top)值得注意的几点def group_by_grade(records, /, *, passing60)用了仅位置参数和仅关键字参数groups.setdefault(grade, []).append(name)是分组惯用法setdefault在键不存在时插入默认值并返回它join(names) or 无利用了空字符串是假值for ... else在没 break 时才抛LookupError。常见坑点❌ 以为b a会复制列表 ✅ 那只是让两个名字指向同一个对象要复制用a[:]、a.copy()或copy.deepcopy(a)。❌ 写def f(x, lst[])用可变对象当默认参数 ✅ 默认参数只在定义时求值一次所有调用共享同一个列表改用lstNone再在函数体里初始化。❌ 在函数里给全局变量赋值却忘了global然后被UnboundLocalError卡住 ✅ 函数体里只要有赋值该名字就是局部的要改全局必须写global 名字。❌ 用value or 默认值处理可能为0或的配置项 ✅0、、[]都是假值会被替换该用config.get(key, 默认值)。❌ 把生成器表达式当列表推导式反复遍历 ✅ 生成器只能消费一次第二次list(gen)得到空列表需要复用请构造列表。❌ 断言257 is 257这类小整数/字符串驻留行为是语言保证 ✅ 那是 CPython 实现细节is只用于None/True/False比较值一律用。❌ 认为-7 // 2是-3✅ 整除是向下取整结果是-4-7 % 3是2取余结果符号跟随除数。❌ 用list.sort()的返回值写new data.sort()✅list.sort()原地排序并返回None要新列表用sorted(data)。总结语法点结论赋值绑定名字到对象不是复制值可变性list/dict/set 可变str/tuple/int 不可变isvs身份 vs 值缓存与驻留是实现细节and/or短路并返回操作数不返回布尔值for ... elseelse在没有 break时执行参数形态/仅位置*后仅关键字*args/**kwargs收集默认参数只在定义时求值一次禁用可变对象作用域LEGB有赋值即局部global/nonlocal显式声明推导式有独立作用域生成器表达式惰性且只能消费一次sort()原地排序返回None基本语法的难点从来不在记号本身而在这些「看起来一样、行为差很远」的地方绑定与复制、可变与不可变、局部与全局。把这些语义理清剩下的语法只是查手册的问题。