函数进阶
函数在 Python 里是一等公民:能赋值、能传参、能返回。闭包和装饰器都建立在这个事实上。面试主线:闭包怎么捕获变量、装饰器怎么写全、参数怎么收放。
闭包
闭包 = 内部函数 + 捕获的外部变量(自由变量)。
def make_counter():
count = 0
def increment():
nonlocal count # 声明修改外部变量
count += 1
return count
return increment
c = make_counter()
c() # 1
c() # 2要点:
- 内部函数引用外部函数的局部变量,外部函数返回后,变量仍存活(被闭包持有)
- 修改捕获变量要
nonlocal(否则只是新建同名局部变量,经典 bug) - 闭包应用:工厂函数、装饰器、回调、偏函数
装饰器
装饰器是“接收函数返回函数”的函数(见设计模式篇的完整展开),这里补两个进阶形态:
带参装饰器(装饰器工厂):
def retry(times: int):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
try:
return func(*args, **kwargs)
except Exception:
pass
raise
return wrapper
return decorator
@retry(times=3)
def fetch_data():
...类装饰器:类实现 __call__,可保存状态:
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)
@CountCalls
def hello():
...记忆点:@decorator 就是 func = decorator(func);带参时先执行工厂返回真正的装饰器。@classmethod、@staticmethod、@property 本质都是装饰器。
*args 与 **kwargs
def f(a, b, *args, c=10, **kwargs):
# a, b 位置参数; args 收多余位置参数(元组)
# c keyword-only 参数(带默认值); kwargs 收多余关键字(字典)- 定义侧:
*args打包多余位置参数为元组,**kwargs打包多余关键字为字典 - 调用侧:
f(*lst)解包可迭代对象,f(**d)解包字典 - 参数顺序:位置参数、
*args、keyword-only 参数(带默认值)、**kwargs - 典型用途:装饰器透传参数、函数签名包装
默认参数陷阱
def bad(items=[]): # 危险: 默认值在定义时创建一次, 所有调用共享
items.append(1)
return items
bad() # [1]
bad() # [1, 1] 同一个 list!正确写法:
def good(items=None):
if items is None:
items = []
...原因:默认参数在函数定义时求值一次,可变默认值被所有调用共享。None 惯用法是 Python 社区的通行解法。
面试追问
- 闭包是什么? 内部函数捕获外部变量,外部函数返回后变量仍存活。修改要 nonlocal
- 带参装饰器怎么写? 三层嵌套:工厂函数接收参数返回装饰器,装饰器接收函数返回包装函数
- ***args 和 kwargs 是什么? 打包多余位置/关键字参数。定义时收集,调用时用 * 和 ** 解包
- 可变默认参数为什么是坑? 默认值定义时创建一次,所有调用共享同一对象。用 None 惯用法
- 装饰器和闭包什么关系? 装饰器就是闭包的应用:外层函数接收函数,内层包装函数捕获它