跳到正文

04 函数与函数式特性 ​

一句话:Python 的函数是一等对象,参数按对象引用传递;闭包捕获变量而非值、装饰器把函数当参数、生成器用 yield 暂停——三者都建立在「函数可传递 + 名字延迟绑定」上。

适用:CPython 3.11+。

为什么需要 ​

函数是复用单位,但 Python 的函数有三处反直觉:可变默认参数在定义时只创建一次、闭包捕获的是变量当前位置、yield 把一次返回变成可暂停的流。它们分别对应三类生产问题:调用之间串数据、循环里的 lambda 全返回最后一个值、大文件一次性读进内存。装饰器则把计时、重试、鉴权等横切逻辑从业务函数里抽出来。

核心机制 ​

参数:传对象引用,默认值只创建一次 ​

python
def append_item(lst, item):
    lst.append(item)          # 原地修改,影响外部

def rebind(x):
    x = 100                   # 重新绑定局部名字,不影响外部

data = [1, 2]
append_item(data, 3)
print(data)                   # [1, 2, 3]

# 错误:所有调用共享同一个列表
def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item("a"), add_item("b"))   # ['a', 'b'] ['a', 'b']

# 正确:用 None 表示「未提供」
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

不要写 items = items or []:调用方传入空列表时会被替换成新列表,丢失原地追加语义;传入 0、"" 也有同样问题。判 None 才准确。

*args 收集多余位置参数,**kwargs 收集多余关键字参数;* 后的参数只能按关键字传;调用时用 */** 解包。

作用域与闭包:捕获变量,不是值 ​

查找顺序是 Local → Enclosing → Global → Built-in。函数内给名字赋值会让它成为局部变量;改外层用 nonlocal,改模块级用 global。Python 没有块级作用域,if/for 里赋的变量在函数内仍可见。

python
def counter():
    n = 0
    def step():
        nonlocal n
        n += 1
        return n
    return step

funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])   # [2, 2, 2]:捕获变量 i
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])   # [0, 1, 2]:定义时绑定当前值

循环结束后 i 停在最后一个值;用默认参数 i=i 在定义时绑定。

装饰器:接收函数,返回函数 ​

python
import time
from functools import wraps

def timer(func):
    @wraps(func)                       # 保留 __name__、__doc__
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__}: {time.perf_counter() - start:.4f}s")
        return result
    return wrapper

@timer
def work():
    time.sleep(0.1)
    return 42

print(work())                          # 打印耗时,返回 42

@timer 等价于 work = timer(work)。带参数的装饰器多一层:@timer(unit="ms") 先调用 timer(unit="ms") 得到真正的装饰器。写装饰器永远加 @wraps,否则函数名和文档字符串会丢失,依赖签名的框架也可能识别失败。

生成器:用 yield 做惰性流 ​

python
def flatten(items):
    for item in items:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item

print(list(flatten([1, [2, [3]], 4])))  # [1, 2, 3, 4]

生成器每次 yield 暂停并保留局部状态,适合大文件、流式响应和无限序列。它只能遍历一次,也没有 len();需要重复使用就转成 list。map/filter 返回的也是迭代器,打印前要 list()。

最小上手 ​

保存为 functions.py 后运行,预期输出 [0, 1, 4] 和 6:

python
from functools import wraps

def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f"{func.__name__} -> {result}")
        return result
    return wrapper

@trace
def squares(n):
    return [i * i for i in range(n)]

def take(n):
    for i in range(n):
        yield i

print(squares(3))                 # [0, 1, 4]
print(sum(take(4)))               # 6

生产动作 ​

  • 默认值:可变默认参数一律用 None + 函数内新建;不要用 x or []。
  • 装饰器:必须带 @wraps;内部不要吞异常;重试装饰器要限制次数并加退避。
  • 排障三段:
    • 现象:两次调用结果串了 → 命令:打印默认参数的 id() → 判断:是否用了可变默认参数。
    • 现象:循环里的 lambda 全返回最后一个值 → 命令:检查闭包引用的变量 → 判断:是否需要在定义时绑定 i=i。
    • 现象:装饰后函数名变成 wrapper → 命令:print(func.__name__) → 判断:是否漏了 functools.wraps。

边界 ​

  • 生成器不支持随机访问、len() 和重复遍历,需要这些能力就用 list;装饰器会改变调用栈和签名,依赖签名的框架需要 inspect.signature 配合 @wraps。

讲清楚 ​

Python 的函数是对象,参数传的是对象引用:可变参数原地改会影响外部,重新绑定不会。默认参数只在定义时求值一次,所以可变默认值必须用 None 占位;or 不能兜底,因为它会把空列表、0、空字符串一起替换掉。闭包捕获的是变量而不是值,循环里的 lambda 要么用默认参数绑定,要么改用 partial。装饰器就是「函数进、函数出」,@wraps 保留元信息;生成器用 yield 把一次性返回变成可暂停的流,适合大文件和流式数据,但只能消费一次。

适用 CPython 3.11+ | 初稿 2026-08,2026-10 压缩并订正 or 默认值

把一门技术讲透到既懂也会用