可变默认参数、闭包与作用域:三个最容易误判的细节
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__ 或类方法。
小结
- 可变默认值 → 用 None 哨兵
- 循环里建闭包 → 用默认参数绑定
- 类体里用推导式引用类属性 → 改成类方法或模块级常量