可变默认参数、闭包与作用域:三个最容易误判的细节

基础 · 约 6 分钟

Python 里有些行为看起来“反直觉”,其实都能从对象模型与作用域规则推出。下面三个是踩过最多次的。

一、默认参数只求值一次

def add(item, bucket=[]):      # 危险:bucket 在函数定义时创建
    bucket.append(item)
    return bucket

print(add(1))   # [1]
print(add(2))   # [1, 2]  ← 不是 [2]

def add_ok(item, bucket=None):
    bucket = [] if bucket is None else bucket
    bucket.append(item)
    return bucket

默认值在 def 执行时求值并挂在函数对象上(add.__defaults__ 能看到)。凡可变对象做默认值,一律用 None 哨兵。

二、闭包捕获的是变量,不是值

funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])     # [2, 2, 2]

funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])     # [0, 1, 2]

延迟绑定是设计使然:循环变量在同一个作用域里被反复赋值。需要固定值时用默认参数绑定,或改用 functools.partial。

三、类体不是闭包作用域

class A:
    x = 1
    y = [x for _ in range(3)]   # NameError: name 'x' is not defined

class B:
    x = 1
    y = [x for _ in range(3)] if False else None
    z = x + 1                   # 这里可以,类体按顺序执行

推导式有自己的作用域,无法直接读到类体命名空间;类体本身又是顺序执行的命名空间,不是闭包。写类属性时尽量用常量表达式,复杂逻辑放 __init__ 或类方法。

小结