# Python装饰器的原理是什么?
面试官问:"Python装饰器的原理是什么?"
很多人会答"装饰器就是用@修饰函数",但这只是语法表象。真正要讲清楚的是:装饰器是一个接收函数作为参数、返回新函数的高阶函数,通过闭包保存原函数并在调用前后添加额外逻辑,@符号只是语法糖。
# 简要回答
装饰器(Decorator) 的本质是函数包装器。它接收一个函数,返回一个新函数,新函数在调用原函数前后可以执行额外操作。
@语法糖等价于函数重新绑定:
@decorator
def func():
pass
# 等价于
def func():
pass
func = decorator(func)
2
3
4
5
6
7
8
装饰器的核心机制:
- 装饰器函数接收被装饰函数作为参数。
- 装饰器内部定义包装函数,在包装函数中调用原函数。
- 装饰器返回包装函数。
- 原函数名被重新绑定到包装函数。
装饰器依赖闭包保存原函数引用,让包装函数可以访问和调用原函数。
# 详细回答
# 最简单的装饰器
不使用@符号,手动包装函数:
def log_decorator(func):
def wrapper():
print("函数开始执行")
func()
print("函数执行完成")
return wrapper
def say_hello():
print("Hello")
say_hello = log_decorator(say_hello)
say_hello()
# 输出:
# 函数开始执行
# Hello
# 函数执行完成
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
log_decorator接收say_hello,返回wrapper。wrapper是闭包,保存了对原say_hello的引用。最后say_hello名字被重新绑定到wrapper。
# @语法糖的等价转换
使用@符号等价于上面的手动包装:
def log_decorator(func):
def wrapper():
print("函数开始执行")
func()
print("函数执行完成")
return wrapper
@log_decorator
def say_hello():
print("Hello")
say_hello() # 实际调用的是 wrapper
2
3
4
5
6
7
8
9
10
11
12
@log_decorator在函数定义完成时立即执行say_hello = log_decorator(say_hello)。

# 处理有参数的被装饰函数
装饰器的包装函数需要接收任意参数并传递给原函数:
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__}")
result = func(*args, **kwargs)
print(f"返回 {result}")
return result
return wrapper
@log_decorator
def add(a, b):
return a + b
print(add(3, 5))
# 输出:
# 调用 add
# 返回 8
# 8
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
wrapper使用*args和**kwargs接收所有参数,并透传给func。
# 带参数的装饰器
装饰器本身需要参数时,需要再嵌套一层:
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hello():
print("Hello")
say_hello()
# 输出:
# Hello
# Hello
# Hello
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@repeat(3)等价于:
say_hello = repeat(3)(say_hello)
repeat(3)先返回真正的装饰器函数decorator,然后decorator(say_hello)返回wrapper。
# 保留原函数元信息
装饰后,函数名和文档字符串会丢失:
def log_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@log_decorator
def add(a, b):
"""计算两数之和"""
return a + b
print(add.__name__) # wrapper
print(add.__doc__) # None
2
3
4
5
6
7
8
9
10
11
12
使用functools.wraps保留元信息:
from functools import wraps
def log_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@log_decorator
def add(a, b):
"""计算两数之和"""
return a + b
print(add.__name__) # add
print(add.__doc__) # 计算两数之和
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@wraps(func)会把原函数的__name__、__doc__等属性复制到wrapper上。
# 装饰器链
多个装饰器从下到上依次应用:
def bold(func):
def wrapper():
return f"<b>{func()}</b>"
return wrapper
def italic(func):
def wrapper():
return f"<i>{func()}</i>"
return wrapper
@bold
@italic
def say_hello():
return "Hello"
print(say_hello()) # <b><i>Hello</i></b>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
等价于:
say_hello = bold(italic(say_hello))
先应用italic,再应用bold。调用时先执行bold的包装,再执行italic的包装。
# 类装饰器
类也可以作为装饰器,需要实现__call__方法:
class LogDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("函数开始执行")
result = self.func(*args, **kwargs)
print("函数执行完成")
return result
@LogDecorator
def say_hello():
print("Hello")
say_hello()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@LogDecorator等价于say_hello = LogDecorator(say_hello)。调用say_hello()时触发__call__方法。
# 知识拓展
Q1: 装饰器和闭包有什么关系?
装饰器的包装函数通常是闭包。闭包保存了对原函数的引用,让包装函数可以在调用时访问原函数。没有闭包,装饰器无法在包装函数中调用原函数。
可以查看什么是闭包?闭包中的变量为什么会出现延迟绑定?了解闭包机制。
Q2: 装饰器的典型应用场景有哪些?
- 日志记录:在函数调用前后打印日志。
- 性能统计:计算函数执行时间。
- 权限检查:在Web框架中检查用户权限。
- 缓存结果:
functools.lru_cache缓存函数返回值。 - 参数校验:检查函数参数类型或范围。
- 重试机制:失败后自动重试。
Q3: 可以装饰类方法吗?
可以。但要注意包装函数需要接收self参数:
def log_decorator(func):
def wrapper(self, *args, **kwargs):
print(f"调用 {func.__name__}")
return func(self, *args, **kwargs)
return wrapper
class Calculator:
@log_decorator
def add(self, a, b):
return a + b
2
3
4
5
6
7
8
9
10
或者使用*args接收所有参数,self会作为第一个参数传入。
Q4: 装饰器会影响性能吗?
会。每次调用被装饰函数时,都会经过包装函数的额外调用层。但影响通常很小,除非在热点路径中使用复杂装饰器。装饰器带来的代码复用和可读性提升通常值得这点开销。
Q5: 可以在运行时动态添加或移除装饰器吗?
装饰器在函数定义时就已经应用,无法直接移除。但可以通过条件判断控制装饰器行为:
DEBUG = True
def log_decorator(func):
def wrapper(*args, **kwargs):
if DEBUG:
print("执行日志")
return func(*args, **kwargs)
return wrapper
2
3
4
5
6
7
8
或者保存原函数引用,运行时选择调用装饰版本还是原版本。
评论
验证登录状态...