Python 的海象运算符 := 是什么?和普通赋值有什么区别?
简化版
海象运算符 :=(正式名「赋值表达式」,Python 3.8+ 引入,PEP 572)的作用是「在一个表达式内部顺手把值绑定到变量」——它既完成赋值,本身又有值,所以能写在 if、while、推导式这些「只能放表达式的地方」。普通赋值 x = 1 是语句(statement),没有值、不能嵌进别的表达式;x := 1 是表达式(expression),有值,可以嵌。三个经典用途:① while 读取循环——while (chunk := f.read(8192)): 一行搞定「读一次、判断、再读」的重复;② if 里复用匹配结果——if (m := re.match(p, s)): 直接用 m.group(),不用先赋值再判断;③ 推导式里避免重复计算——[y for x in data if (y := f(x)) > 0] 让 f(x) 只算一次而不是算两次。注意:为了防止和 == 混淆,语法上做了限制——顶层语句里不能单独写 x := 1(必须加括号或本身在表达式里)、不能给属性或下标赋值(obj.a := 1、d['k'] := 1 都非法);另外它在推导式里绑定的变量会泄漏到外层作用域(这是有意设计)。名字来源::= 侧过来像海象的眼睛和獠牙。核心记忆::= 让「赋值」变成有值的表达式,主要用来消除「先算一遍再判断、判断完再算一遍」的重复。
详细版
= 和 := 的区别:
| 维度 | =(赋值语句) | :=(赋值表达式) |
|---|---|---|
| 本质 | 语句,没有值 | 表达式,值就是被赋的值 |
| 能否嵌入 | 不能(if x = f(): 是语法错误) | 能(if (x := f()):) |
| 支持目标 | 变量、属性、下标、解包 | 只能是简单变量名 |
| 增强赋值 | x += 1 可以 | x :+= 1 不存在 |
| 链式 | a = b = 1 可以 | 不支持链式 |
| 版本 | 一直有 | 3.8+ |
import re
# ① while 读取:消除"读两次"的重复
with open("big.log", "rb") as f:
while chunk := f.read(8192): # 读 + 判断一步到位
process(chunk)
# 旧写法要写两遍 f.read:
# chunk = f.read(8192)
# while chunk:
# process(chunk)
# chunk = f.read(8192) # ← 忘了这行就是死循环
# ② if 里复用匹配结果
line = "score=95"
if m := re.match(r"score=(\d+)", line): # 匹配 + 判断 + 保留结果
print(int(m.group(1))) # 95
# 旧写法:m = re.match(...) 然后 if m:(两行)
# ③ 推导式里避免重复计算(f 只调用一次)
data = [1, -2, 3]
def f(x): return x * 10
print([y for x in data if (y := f(x)) > 0]) # [10, 30]
# 不用 := 就得写 [f(x) for x in data if f(x) > 0] ← f 被调用两次
# ④ 顺手记录中间值(比如求和的同时记住最后一项)
values = [1, 2, 3]
total = sum(last := v for v in values)
print(total, last) # 6 3 ← last 泄漏到了外层(有意设计)
# ⑤ 语法限制
# x := 1 # ✗ SyntaxError:顶层不能裸写
(x := 1) # ✓ 加括号就行
# obj.attr := 1 # ✗ 只能绑定简单变量名
# d['k'] := 1 # ✗ 同上
# ⑥ 优先级坑::= 的优先级最低,几乎总要加括号
n = 10
# if n := n + 1 > 10: ← 实际是 n := (n + 1 > 10),n 变成了 True!
if (n := n + 1) > 10: # ✓ 括号明确
print(n) # 11
⚠️ 两个必须记住的点:①
:=的优先级极低,低于几乎所有运算符,所以n := n + 1 > 10会被解析成n := ((n + 1) > 10)——变量n被赋成了布尔值True,而不是 11,这类 bug 隐蔽且难查。实践准则是只要:=出现在更大的表达式里,就把它整个用括号包起来:(n := n + 1) > 10。② 推导式里的:=绑定会「泄漏」到外层作用域,这和推导式的循环变量([x for x in ...]里的x用完即消失)行为相反,是 PEP 572 有意为之的设计——目的正是让你能在推导式外面拿到「最后一次计算的中间值」(如上面的last)。它遵循的规则是::=绑定到包含推导式的那个作用域(在函数里就是函数的局部变量,在模块层就是全局变量)。有一个例外:在类体内的推导式里用:=会抛SyntaxError;另外也不能用:=去绑定推导式自己的迭代变量([(x := y) for x in data]里若目标名与循环变量同名会报错)。
完整版教学
一、为什么需要它:三种「重复」的代码
Python 3.8 之前,"想在判断的同时保住结果"只能靠重复。三种典型重复:
重复类型 ①:调用两次(浪费性能,且函数有副作用时行为不同)
✗ [f(x) for x in data if f(x) > 0] f 被调用 2n 次
✓ [y for x in data if (y := f(x)) > 0] f 只调用 n 次
算例:f 是一次 30ms 的网络请求、data 有 100 项
旧写法 200 次 × 30ms = 6.0 秒
新写法 100 次 × 30ms = 3.0 秒 ← 直接省一半
重复类型 ②:写两遍读取(容易漏掉一处 → 死循环)
✗ chunk = f.read(n)
while chunk:
process(chunk)
chunk = f.read(n) ← 这行漏了就死循环,是新手经典 bug
✓ while chunk := f.read(n):
process(chunk)
重复类型 ③:多写一行临时变量(把判断和赋值拆成两句)
✗ m = re.match(p, s)
if m:
use(m)
✓ if m := re.match(p, s):
use(m)
三种重复的共同点:
"我需要一个值来做判断,判断完还要用这个值"
→ 而 if / while / 推导式的条件位置只接受"表达式",赋值语句进不去
→ := 就是为了让"赋值"具备表达式身份而生的
海象运算符解决的是一个很具体的语法缺口:Python 里 if、while、推导式的条件位置只能放表达式,而普通赋值 x = f() 是语句,于是「拿到值 → 判断 → 还要用这个值」这件极常见的事只能靠重复来表达——要么把函数调两遍(性能翻倍,函数有副作用时甚至语义都变了),要么把读取写两遍(漏一处就死循环),要么多加一行临时变量(把本来一体的逻辑拆成两句)。上面算例里,f 是 30ms 的网络请求、data 有 100 项时,旧写法要多花整整 3 秒。:= 补上的就是「让赋值具备表达式身份」这块拼图,它不提供任何新能力,只消除重复。
二、语法与限制:为什么处处受限
基本形式: 名字 := 表达式 (整体的值 = 被赋的值)
限制一:顶层语句里不能裸写
x := 1 ✗ SyntaxError
(x := 1) ✓ 加括号合法(但没人这么写,直接 x = 1 就行)
原因:防止把 == 误写成 := 之后代码"还能跑"
(C 语言里 if (x = 1) 这种笔误制造了几十年的 bug)
限制二:目标只能是简单变量名
x := 1 ✓
obj.attr := 1 ✗ SyntaxError
d['k'] := 1 ✗ SyntaxError
a, b := 1, 2 ✗(不支持解包)
原因:保持"赋值表达式"足够简单、可读;复杂目标请用普通赋值
限制三:没有增强形式
x += 1 ✓(语句) x :+= 1 ✗(不存在这种写法)
限制四:优先级最低(★最容易踩★)
n := n + 1 > 10 实际解析为 n := ((n + 1) > 10)
→ n 被赋成 True/False,而不是数字!
✓ 铁律:出现在更大表达式里就整体加括号 (n := n + 1) > 10
限制五:不能绑定推导式自己的循环变量
[(x := 1) for x in data] ✗ 目标名与迭代变量冲突 → SyntaxError
限制六:类体内的推导式里不能用
class C:
vals = [y for x in range(3) if (y := x)] ✗ SyntaxError
原因:类作用域和推导式作用域的交互本身就有历史包袱
限制七:函数调用的关键字参数位置要小心
f(x = 1) 传关键字参数 x
f(x := 1) ★传位置参数 1,顺便把 1 绑给变量 x★ ← 含义完全不同
这一串限制不是随意加的,核心动机是防止 := 变成 = 的笔误温床。C 系语言里 if (x = 1) 这种把 == 写成 = 的笔误合法可运行,制造了几十年的隐蔽 bug;Python 的对策是:普通赋值仍然只能是语句(if x = 1: 直接语法错误),而 := 又被限制得只能用在「确实需要表达式赋值」的地方——顶层裸写非法、不能给属性和下标赋值、没有 :+=。最需要肌肉记忆的是限制四(优先级最低):n := n + 1 > 10 会把布尔值赋给 n,值和类型全错却不报错。还有一个冷门但致命的坑是限制七:f(x = 1) 是传关键字参数,f(x := 1) 是传位置参数并顺手把 1 赋给局部变量 x——一个字符之差,语义完全不同。
三、作用域:为什么推导式里的绑定会「泄漏」
先回忆推导式的作用域规则(Python 3 起):
[x for x in range(3)]
print(x) # ✗ NameError —— 循环变量 x 用完即消失
原因:推导式有自己独立的作用域(相当于一个隐式的函数)
但 := 的绑定不遵守这条:
[y := x*2 for x in range(3)]
print(y) # ✓ 4 —— 泄漏到了外层!
规则(PEP 572 明确规定):
:= 绑定到"包含这个推导式的作用域",而不是推导式自己的作用域
在函数里 → 成为该函数的局部变量
在模块层 → 成为全局变量
在类体的推导式里 → ★直接 SyntaxError★(禁止)
这是有意设计,为了支持这类写法:
# 想知道"第一个满足条件的元素",同时又要判断存在性
if any((found := item).ok for item in items):
use(found) # ← 用到了泄漏出来的 found
# 求和的同时记住最后一项
total = sum(last := v for v in data)
print(total, last)
# 边过滤边记录命中数
if (n := len(matches := [x for x in data if ok(x)])) > 0:
print(f"命中 {n} 条:{matches}")
★ 生成器表达式里要小心"惰性":
gen = (y := x for x in range(3)) # 还没执行
# 此时访问 y → NameError(因为一次都还没跑)
list(gen) # 跑完之后 y 才存在(值为 2)
→ 泄漏出来的变量"什么时候有值"取决于迭代进行到哪一步
★ 想显式声明作用域:
在函数里对全局变量用 := 前,仍然需要 global/nonlocal 声明
def f():
global g
[g := x for x in range(3)] # 有 global 声明才写全局
推导式的循环变量用完即消失(这是 Python 3 修掉的历史坑),但 := 的绑定反而是有意「泄漏」的——它绑定到包含推导式的那个作用域。初看矛盾,理解意图就顺了:循环变量是推导式的内部实现细节,泄漏出去只会污染命名空间;而 := 是你主动写下的、明确想留住的中间值,泄漏正是它的目的(否则「求和的同时记住最后一项」这类写法根本没法表达)。使用时要注意两点:一是生成器表达式的惰性——变量要等迭代真正跑到那一步才有值,没迭代就访问会 NameError;二是类体内的推导式禁用 :=(直接 SyntaxError),因为类作用域和推导式作用域的交互本身就有历史包袱,PEP 干脆一刀切禁掉。
四、五个真正值得用的场景
场景 1:分块读取 / 流式消费(最经典)
while chunk := f.read(8192): 文件
while line := input(): 交互输入
while batch := cur.fetchmany(1000): 数据库游标
while (msg := q.get()) is not None: 队列哨兵 ← 注意用 is not None,不是真值判断
场景 2:正则匹配后立刻使用
if m := PATTERN.search(text):
name, age = m.group("name"), m.group("age")
elif m := FALLBACK.search(text): ← elif 里同样能用,链式匹配特别顺
...
场景 3:推导式里复用昂贵的中间结果
results = [r for item in items if (r := expensive(item)) is not None]
→ expensive 只调一次,且过滤和取值用的是同一个结果
场景 4:避免重复的字典/属性查找
if (user := cache.get(uid)) is not None:
return user
→ 旧写法要么查两次(if uid in cache: return cache[uid]),要么多一行
场景 5:把复杂条件拆成有名字的片段(可读性反而变好)
if (total := price * qty) > 1000 and (disc := rate(total)) > 0:
print(f"金额 {total},折扣 {disc}")
→ total、disc 都在条件里算出来,下面直接用
★ 不该用的地方(会变难读):
✗ 一行里塞多个 := : if (a := f()) and (b := g(a)) and (c := h(b)): ...
✗ 只为了省一行而用: 普通两行赋值 + 判断本来就很清晰
✗ 在 lambda 里搞副作用: sorted(data, key=lambda x: (t := x.a) * t) ← 炫技
准则:":= 是为了消除重复,不是为了压行"
值得用 := 的场景有一个共同特征:同一个值既要用于判断、又要用于后续处理,而不用它就必须重复。「分块读取」是最经典的(旧写法要把 f.read() 写两遍,漏一处就死循环);「正则匹配后立刻用」次之(elif m := ... 这种链式匹配用旧写法会变成难看的嵌套);「推导式里复用昂贵中间结果」收益最实在(直接把调用次数减半);「避免重复的字典查找」把 if k in d: return d[k] 的两次哈希查找降为一次。反过来,为了压行而用就是滥用——一行里塞好几个 :=、在 lambda 里制造副作用,都会让代码更难读。判断标准很朴素:用了之后重复消失了就是好用法,只是行数变少了就是坏用法。
五、和其他写法的对比与取舍
同一个需求(取字典里的值,没有就返回默认)的几种写法:
① if uid in cache: return cache[uid] 两次哈希查找
② v = cache.get(uid) 两行
if v is not None: return v
③ if (v := cache.get(uid)) is not None: 一行、一次查找 ← := 版
return v
④ return cache.get(uid, default) ★最简单的时候根本不需要 :=★
→ 教训:先看有没有更直接的 API,别一有 3.8 就到处塞 :=
同一个需求(过滤 + 转换)的对比:
① [f(x) for x in xs if f(x) > 0] f 调用 2n 次
② [y for y in map(f, xs) if y > 0] ✓ 也只调 n 次,且没有泄漏
③ [y for x in xs if (y := f(x)) > 0] ✓ n 次,但 y 会泄漏到外层
④ [y for y in (f(x) for x in xs) if y > 0] ✓ n 次,嵌套生成器,稍绕
→ ② 和 ③ 都对;一层转换用 map 更干净,多个中间变量时 := 更清晰
版本兼容性:
:= 需要 Python 3.8+。要兼容 3.7 及以下(还有大量遗留服务)就不能用;
语法错误发生在★编译期★,意味着"只要文件里有 := ,整个模块在 3.7 上都 import 不了",
不是运行到那一行才报错 → 库作者尤其要注意 setup.py 里的 python_requires
风格与团队约定:
PEP 572 当年争议极大(Guido 因此卸任 BDFL)
争议点:可读性(是消除重复,还是鼓励把太多东西塞进一行?)
实践共识:用在上面那 5 个典型场景,其余场合优先普通赋值
:= 是个收益明确但适用面窄的特性,选型时要先问「有没有更直接的写法」:想取默认值就用 dict.get(k, default),想做一层转换过滤 map 同样能避免重复调用,这些场合根本轮不到 :=。版本兼容上有个容易忽略的细节::= 的语法错误发生在编译期,也就是说只要文件里出现了一个 :=,整个模块在 Python 3.7 上连 import 都会失败(而不是运行到那一行才报错),写库时要在 python_requires 里如实声明。最后,PEP 572 当年争议大到直接导致 Guido 卸任 BDFL,争论焦点正是「消除重复」和「鼓励压行」之间的界线——所以团队里用它时保持克制,收敛到少数几个公认清晰的模式即可。
六、常见写法自查清单
写下每个 := 之后,逐条核对:
□ 有没有加够括号?
if (n := f()) > 10: ✓
if n := f() > 10: ✗ n 变成了布尔值
□ 判空用的是真值还是 is not None?
while chunk := f.read(n): ✓ 空 bytes 结束,正确
while (v := q.get()) is not None: ✓ 队列可能合法地产出 0/""/[]
while v := q.get(): ✗ 收到 0 或 "" 会误判为结束
□ 这个 := 消除了重复吗?还是只是压行?
消除重复 → 留下;只是压行 → 改回两行
□ 泄漏出来的变量名会不会覆盖外层的同名变量?
total = 100
xs = [total := x for x in data] ✗ 外层的 total 被悄悄改掉了
□ 是不是在类体的推导式里?(禁用,SyntaxError)
□ 目标是简单变量名吗?(属性/下标/解包都不行)
□ 目标 Python 版本 ≥ 3.8 吗?(编译期报错,整个模块都导入不了)
□ 一行里是不是塞了超过一个 := ?(超过一个基本都该拆开)
把上面这份清单当成写完 := 之后的 code review 自查表。其中最值得反复强调的两条是括号和 is not None:前者对应「优先级最低」的语言陷阱,后者对应一类真实事故——用 while v := q.get(): 消费队列时,如果队列里合法地出现了 0、""、[] 这些假值,循环会提前结束、数据被静默丢弃;正确写法是显式和哨兵值比较 is not None。第三条「消除了重复吗」是判断该不该用 := 的总闸门:用它是为了让同一个计算只写一次,不是为了让代码少一行。
记忆钩子:「海象运算符
:=(3.8+,PEP 572)= 让赋值变成『有值的表达式』,从而能写进 if / while / 推导式的条件位置。它不提供新能力,只消除三种重复:①函数调两遍([f(x) for x in xs if f(x)>0]→[y for x in xs if (y:=f(x))>0],调用次数直接减半)②读取写两遍(while chunk := f.read(8192):,旧写法漏掉循环末尾那行就死循环)③多一行临时变量(if m := re.match(p,s):)。★三条铁律:一是「优先级最低,出现在更大表达式里就整体加括号」(n := n+1 > 10会把 True 赋给 n);二是「判空优先is not None」(队列里合法的 0/”/[] 会让真值判断提前结束循环);三是「目标只能是简单变量名」(属性、下标、解包全不行,顶层也不能裸写)。作用域上它反常识地泄漏到包含推导式的外层作用域(这是有意设计,为了留住中间值),但★类体内的推导式里用它直接 SyntaxError★。判断该不该用只看一句:消除了重复就留,只是压行就改回两行。」
七、常见误区与追问
- 误区:
x := 1和x = 1只是写法不同,可以互换。 本质不同:x = 1是语句,没有值,不能出现在任何需要表达式的位置(if x = 1:是语法错误);x := 1是表达式,整体的值就是被赋的值,所以能嵌进if/while/推导式的条件里。而且:=的能力比=弱得多:不能给属性赋值(obj.a := 1✗)、不能给下标赋值(d['k'] := 1✗)、不能解包(a, b := 1, 2✗)、没有增强形式(x :+= 1不存在)、不能链式(a = b = 1在:=里没有对应写法)。普通赋值能用的地方就用=,只有在「必须是表达式」的位置才用:=。 - 误区:
if n := n + 1 > 10:会把n+1赋给 n 再比较。 实际解析是n := ((n + 1) > 10)——:=的优先级低于几乎所有运算符,所以n被赋成了布尔值True/False,而不是数字。这类 bug 特别阴险:程序照常运行,但变量的类型和值全错了,往往要到很远的地方才暴露(比如后面拿n去做算术得到True + 1 == 2)。铁律是只要:=出现在更大的表达式里,就把它整体用括号包住:(n := n + 1) > 10。很多 linter(如 ruff、pylint)也会对没加括号的赋值表达式告警。 - 误区:
while v := q.get():是消费队列的标准写法。 这是真值判断,队列里如果合法地出现0、""、[]、0.0这些假值,循环会提前结束,剩下的数据被静默丢弃,且不报任何错。凡是「用哨兵值表示结束」的场景,都必须显式比较:while (v := q.get()) is not None:。同类问题在读文件时不存在(f.read()读完返回空 bytes/空 str,恰好只有结束时才是假值,所以while chunk := f.read(n):是安全的)——区别在于假值是不是只可能出现在结束时,判断前先想清楚这一点。 - 误区:推导式里的
:=绑定和循环变量一样,用完就消失。 正好相反:循环变量([x for x in ...]里的x)作用域限于推导式内部,用完即消失;而:=绑定的变量会「泄漏」到包含推导式的那个作用域(函数里就是函数局部变量,模块层就是全局变量),这是 PEP 572 有意为之,目的是让你能在推导式外面拿到中间值。带来的风险是静默覆盖外层同名变量:外层已有total = 100,推导式里写[total := x for x in data]会把它悄悄改掉。所以:=在推导式里要用独特的变量名。另外有两个例外要记住:类体内的推导式里用:=直接SyntaxError;生成器表达式是惰性的,泄漏出来的变量要等真正迭代到那一步才有值,没迭代就访问会NameError。 - 误区:有了
:=就应该到处用,代码更简洁。:=的价值判据只有一条——它消除了重复吗。消除了「同一个函数被调两遍」「同一段读取被写两遍」就是好用法;只是把两行压成一行、或者一行里塞好几个:=,就是在牺牲可读性换行数。更常见的错误是忽略了更直接的 API:想取默认值直接cache.get(uid, default)比if (v := cache.get(uid)) is not None简单;想做一层转换过滤[y for y in map(f, xs) if y > 0]同样只调一次f且不产生泄漏。PEP 572 当年争议极大(直接导致 Guido 卸任 BDFL),焦点正是「消除重复」与「鼓励压行」的界线,实践中保持克制、收敛到几个公认清晰的模式即可。 - 追问:
f(x = 1)和f(x := 1)有什么区别? 完全不同的两件事。f(x = 1)是传关键字参数:把1传给f的名为x的形参,调用方作用域里不会产生任何变量。f(x := 1)是传位置参数:把表达式x := 1的值(即1)作为第一个位置参数传进去,同时在调用方作用域里创建/覆盖一个局部变量x = 1。一个字符之差,参数传递方式和副作用都变了,是很容易看走眼的坑。真实用途是「传参的同时留住这个值」,例如if validate(data := load()):use(data);但在参数名恰好和某个形参同名时极具迷惑性,建议这种场合还是拆成两行写。 - 追问:
:=在推导式里到底怎么减少计算次数,能省多少? 关键在于旧写法[f(x) for x in xs if f(x) > 0]里f(x)出现了两次:一次在过滤条件里、一次在输出表达式里,Python 不会自动做公共子表达式消除,所以对每个元素都要调两次f,总共2n次。改成[y for x in xs if (y := f(x)) > 0]后,f只在过滤条件里调用一次并把结果绑给y,输出直接用y,总共n次——调用次数减半。如果f是一次 30ms 的网络请求、xs有 100 项,旧写法 6.0 秒、新写法 3.0 秒。更重要的是语义:如果f有副作用(写日志、计数、扣配额、生成随机数),调两次和调一次的结果完全不同,旧写法甚至可能让过滤用的值和最终输出的值不是同一个(比如f返回随机数或读取实时状态),这是隐蔽的正确性 bug,不只是性能问题。 - 追问:为什么 Python 要设计成「顶层不能裸写
x := 1」? 为了防止==被误写成:=之后代码「还能跑」。C 系语言里if (x = 1)是合法的(赋值表达式的值参与判断),把==手滑写成=的笔误制造了几十年隐蔽 bug,很多 C 编译器不得不加告警、程序员不得不发明「常量写左边」的 Yoda 写法来防御。Python 的策略是双保险:普通赋值=保持语句身份(if x = 1:直接语法错误、编译期就挡住),同时把:=限制在「确实需要表达式赋值」的位置——顶层裸写非法(顶层写x = 1就够了,允许x := 1只会造成两种写法并存的混乱)、不能给属性和下标赋值、没有:+=。这样:=出现的每一处都是程序员明确的意图表达,而不是可能的笔误。
八、加强记忆
海象运算符 :=(正式名「赋值表达式」,Python 3.8+,PEP 572)的全部意义是:让赋值具备「表达式」身份,从而能写进 if/while/推导式的条件位置。它不带来任何新能力,只消除三种重复:① 函数调两遍([f(x) for x in xs if f(x)>0] → [y for x in xs if (y := f(x))>0],调用次数由 2n 降到 n;若 f 有副作用,这还是正确性问题而不只是性能问题);② 读取写两遍(while chunk := f.read(8192):,旧写法漏掉循环末尾那次读取就是死循环);③ 多一行临时变量(if m := re.match(p, s):)。三条铁律:一是优先级最低,嵌在更大表达式里必须整体加括号——n := n + 1 > 10 会把布尔值赋给 n;二是判空优先 is not None——队列里合法的 0/""/[] 会让真值判断提前结束循环、静默丢数据(读文件例外,因为空值只可能出现在结束时);三是目标只能是简单变量名——属性、下标、解包、:+=、顶层裸写全部非法(这些限制是为了杜绝 == 误写成 := 的 C 式笔误)。作用域上它反常识:推导式的循环变量用完即消失,而 := 的绑定泄漏到包含推导式的外层作用域(有意设计,为了留住中间值),所以要用独特变量名以免静默覆盖外层同名变量;类体内的推导式里用 := 直接 SyntaxError,生成器表达式里则要等真正迭代到那一步变量才有值。还有一个易看走眼的坑:f(x = 1) 是传关键字参数,f(x := 1) 是传位置参数并顺带创建局部变量 x。最后判断该不该用只看一句:消除了重复就留下,只是压行就改回两行。