Skip to content

函数进阶

闭包、装饰器(带参/类装饰器)、*args/**kwargs、默认参数陷阱。

Updated View as Markdown
For humans

函数进阶

函数在 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 社区的通行解法。

面试追问

  1. 闭包是什么? 内部函数捕获外部变量,外部函数返回后变量仍存活。修改要 nonlocal
  2. 带参装饰器怎么写? 三层嵌套:工厂函数接收参数返回装饰器,装饰器接收函数返回包装函数
  3. ***args 和 kwargs 是什么? 打包多余位置/关键字参数。定义时收集,调用时用 * 和 ** 解包
  4. 可变默认参数为什么是坑? 默认值定义时创建一次,所有调用共享同一对象。用 None 惯用法
  5. 装饰器和闭包什么关系? 装饰器就是闭包的应用:外层函数接收函数,内层包装函数捕获它
Navigation

Type to search…

↑↓ navigate↵ selectEsc close