Skip to content

函数、作用域与闭包

参数绑定

python
def request(path, /, method="GET", *, timeout=3, **headers):
    ...
标记含义
/ 之前仅限位置参数
/* 之间位置或关键字参数
* 之后仅限关键字参数
*args收集额外位置参数为 Tuple
**kwargs收集额外关键字参数为 Dict

仅限关键字参数适合超时、重试等容易因位置混淆的选项;仅限位置参数允许库将来修改内部参数名而不破坏调用方。

可变默认参数

默认值在函数定义时求值一次,不是每次调用创建:

python
def add_item(item, bucket=[]):
    bucket.append(item)
    return bucket

add_item(1)  # [1]
add_item(2)  # [1, 2]

正确写法:

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

缓存等少数场景可以故意利用默认对象持久存在,但应显式使用闭包或缓存工具表达意图。

传对象引用

Python 调用时把对象绑定给形参:

python
def mutate(values):
    values.append(3)  # 修改调用方看到的同一对象

def rebind(values):
    values = [3]      # 只改变局部名字绑定

这既不是传统描述中的“按值复制整个对象”,也不是允许函数重绑调用方变量的“按引用传参”。判断影响要看操作是修改对象还是重新绑定名字。

LEGB 与 nonlocal

名字按 Local、Enclosing、Global、Builtins 顺序查找。函数内只要存在赋值,该名字默认被视为局部变量:

python
count = 0

def increment():
    count += 1  # UnboundLocalError

修改模块变量使用 global;修改闭包外层函数变量使用 nonlocal。更容易维护的方式通常是返回新值或把状态封装进对象。

闭包晚绑定

闭包读取的是变量,不是创建函数时的值:

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

需要冻结当前值时使用默认参数:

python
funcs = [lambda i=i: i for i in range(3)]

装饰器

python
@trace
@retry(times=3)
def load():
    ...

等价于:

python
load = trace(retry(times=3)(load))

装饰从下往上应用,调用时从外往内进入。包装器应使用 functools.wraps 保留 __name__、文档和签名元数据。

python
from functools import wraps

def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(func.__name__)
        return func(*args, **kwargs)
    return wrapper

lambda 的边界

lambda 只能包含一个表达式,适合短小的 Key Function 或回调:

python
users.sort(key=lambda user: (user.age, user.name))

复杂逻辑使用 def,以获得名称、类型标注、文档和可调试的中间步骤。把异常处理、副作用和嵌套条件塞进 Lambda 并不会更函数式。