函数高级特性

装饰器(Decorator)

原理

装饰器是接收函数、返回新函数的高阶函数,本质是语法糖:

@decorator
def func(): ...
 
# 等价于:
def func(): ...
func = decorator(func)

底层实现:字节码层面

import dis
 
def my_decorator(f):
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    return wrapper
 
@my_decorator
def hello():
    pass
 
# 反编译模块级字节码,可以看到:
# LOAD_GLOBAL   my_decorator   ← 加载装饰器
# MAKE_FUNCTION hello          ← 创建函数对象
# CALL_FUNCTION 1              ← 调用 my_decorator(hello)
# STORE_NAME    hello          ← 将结果赋值回 hello
 
# 所以 @decorator 就是在模块加载时立即执行的一次函数调用

底层实现:装饰器靠闭包”记住”原函数

核心问题my_decorator 返回之后,它的局部变量 func 按理应该销毁,但 wrapper 里还在用它——Python 怎么处理?

def my_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)  # my_decorator 已返回,func 还在?
    return wrapper

答案:闭包(closure)。当内层函数引用了外层函数的变量,Python 会把这个变量**“捕获”并随内层函数一起保存**,不随外层函数销毁。

decorated = my_decorator(print)
 
# wrapper 捕获了 func,可以验证:
print(decorated.__closure__)                    # (<cell at 0x...>,)
print(decorated.__closure__[0].cell_contents)  # <built-in function print>

面试回答:装饰器返回的 wrapper 通过闭包捕获了原函数 func,所以即使 my_decorator 已经返回,调用 wrapper 时仍能访问 func。这也是 nonlocal 能修改外层变量的原因——内外层函数共享同一个捕获的变量。

底层实现:functools.wraps 做了什么?

import functools
 
# functools.wraps 实际上是调用 functools.update_wrapper
# 它把以下属性从 wrapped 复制到 wrapper:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__',
                       '__annotations__', '__doc__')
WRAPPER_UPDATES = ('__dict__',)  # wrapper.__dict__.update(wrapped.__dict__)
 
# 还额外设置:
# wrapper.__wrapped__ = wrapped  ← 指向原始函数,方便 inspect 解包
 
def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
 
@my_decorator
def greet():
    """Say hello."""
    pass
 
print(greet.__wrapped__)   # 原始函数(可用 inspect.unwrap 递归解包)

底层实现:@property 是描述符(Descriptor)

@property 不是普通装饰器,它利用了描述符协议

# property 的等价纯 Python 实现
class MyProperty:
    def __init__(self, fget=None, fset=None, fdel=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
 
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self          # 通过类访问时返回描述符自身
        return self.fget(obj)   # 通过实例访问时调用 getter
 
    def __set__(self, obj, value):
        self.fset(obj, value)
 
    def __delete__(self, obj):
        self.fdel(obj)
 
    def setter(self, fset):
        return MyProperty(self.fget, fset, self.fdel)
 
# 描述符协议:
# 当访问 obj.attr 时,Python 先在 type(obj).__mro__ 中找 attr
# 如果找到的是实现了 __get__ 的对象(描述符),调用 __get__ 而不是直接返回
# 这就是 @property、@staticmethod、@classmethod 的统一实现基础

基本实现

import functools
 
def my_decorator(func):
    @functools.wraps(func)  # 保留原函数的 __name__, __doc__
    def wrapper(*args, **kwargs):
        print("Before")
        result = func(*args, **kwargs)
        print("After")
        return result
    return wrapper
 
@my_decorator
def greet(name):
    """Say hello."""
    print(f"Hello, {name}!")
 
greet("Alice")
print(greet.__name__)  # "greet"(因为用了 @functools.wraps)

带参数的装饰器

def repeat(n):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator
 
@repeat(3)
def say_hi():
    print("Hi!")
 
say_hi()  # 打印 3 次 "Hi!"

类装饰器

class Timer:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.call_count = 0
 
    def __call__(self, *args, **kwargs):
        import time
        self.call_count += 1
        start = time.time()
        result = self.func(*args, **kwargs)
        print(f"Elapsed: {time.time() - start:.3f}s")
        return result
 
@Timer
def slow_func():
    import time
    time.sleep(0.1)
 
slow_func()
print(slow_func.call_count)  # 1

闭包(Closure)

闭包是引用了自由变量的函数,自由变量存在于外层函数的作用域中。

def make_counter():
    count = 0  # 自由变量
    def counter():
        nonlocal count  # 声明修改外层变量
        count += 1
        return count
    return counter
 
c = make_counter()
print(c())  # 1
print(c())  # 2
print(c.__closure__[0].cell_contents)  # 2(查看闭包变量)

常见陷阱:循环中的闭包

# 错误:所有函数共享同一个 i
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])  # [2, 2, 2] ← 全是 2!
 
# 正确:用默认参数捕获当前值
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]

生成器(Generator)

原理

生成器是惰性求值的迭代器,使用 yield 暂停执行并返回值,状态保留在栈帧(frame)中

底层实现:生成器对象的结构

def my_gen():
    yield 1
    yield 2
 
g = my_gen()
 
# 生成器对象内部结构:
print(type(g))            # <class 'generator'>
print(g.gi_frame)         # 保存的栈帧对象(包含局部变量、字节码位置)
print(g.gi_code)          # 代码对象(字节码)
print(g.gi_running)       # 当前是否正在执行(防止递归)
 
# gi_frame.f_lasti:上次执行到的字节码偏移量
# 每次 next() 就从 f_lasti + 1 继续执行

关键:生成器挂起时,整个栈帧(包括局部变量、执行位置)都被保存在堆上,不在调用栈上。这与普通函数完全不同(普通函数返回后栈帧销毁)。

底层实现:yield 的字节码

import dis
 
def simple_gen():
    yield 1
    yield 2
 
dis.dis(simple_gen)
# LOAD_CONST    1
# YIELD_VALUE       ← 弹出栈顶值,传给调用者;保存帧状态;挂起
# RESUME            ← next() 调用时恢复到这里
# LOAD_CONST    2
# YIELD_VALUE
# RESUME
# LOAD_CONST    None
# RETURN_VALUE      ← 触发 StopIteration

生成器的状态机

import inspect
 
def my_gen():
    yield 1
 
g = my_gen()
print(inspect.getgeneratorstate(g))  # GEN_CREATED(刚创建,未执行)
next(g)
print(inspect.getgeneratorstate(g))  # GEN_SUSPENDED(挂起)
try:
    next(g)
except StopIteration:
    pass
print(inspect.getgeneratorstate(g))  # GEN_CLOSED(结束)
 
# 状态:GEN_CREATED → GEN_RUNNING → GEN_SUSPENDED → GEN_CLOSED
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b
 
fib = fibonacci()
print([next(fib) for _ in range(8)])  # [0, 1, 1, 2, 3, 5, 8, 13]

生成器表达式 vs 列表推导式

# 列表推导式:立即计算,全部存入内存
lst = [x * x for x in range(10**7)]   # 占用大量内存
 
# 生成器表达式:惰性计算,节省内存
gen = (x * x for x in range(10**7))   # 几乎不占内存
 
# 对大数据集,优先用生成器
total = sum(x * x for x in range(10**7))

yield from

def flatten(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)  # 委托给子生成器
        else:
            yield item
 
print(list(flatten([1, [2, [3, 4]], 5])))  # [1, 2, 3, 4, 5]

迭代器协议

实现 __iter____next__ 即可成为迭代器:

class Range:
    def __init__(self, start, stop):
        self.current = start
        self.stop = stop
 
    def __iter__(self):
        return self
 
    def __next__(self):
        if self.current >= self.stop:
            raise StopIteration
        val = self.current
        self.current += 1
        return val
 
for i in Range(0, 5):
    print(i)

可迭代对象 vs 迭代器

可迭代对象迭代器
必须实现__iter____iter__ + __next__
例子list, str, dictgenerator, file
可多次迭代❌(耗尽后需重新创建)

*args**kwargs

def func(*args, **kwargs):
    print(args)    # tuple
    print(kwargs)  # dict
 
func(1, 2, 3, name="Alice", age=30)
# (1, 2, 3)
# {'name': 'Alice', 'age': 30}
 
# 解包传参
nums = [1, 2, 3]
info = {"name": "Alice"}
func(*nums, **info)

面试高频题

Q: @property 的作用?

class Circle:
    def __init__(self, radius):
        self._radius = radius
 
    @property
    def radius(self):
        return self._radius
 
    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value
 
    @property
    def area(self):
        import math
        return math.pi * self._radius ** 2
 
c = Circle(5)
print(c.area)    # 78.53...(像属性一样访问,实际是方法)
c.radius = 10    # 触发 setter,有验证逻辑

Q: functools.lru_cache 原理?

from functools import lru_cache
 
@lru_cache(maxsize=128)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)
 
# 内部用 dict 缓存结果,key 是函数参数
# maxsize=None 时用 functools.cache(Python 3.9+)
print(fib.cache_info())  # CacheInfo(hits=..., misses=..., maxsize=128, currsize=...)