主题
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默认值