函数高级特性
装饰器(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_CLOSEDdef 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, dict | generator, 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=...)