GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • 卡码笔记-最强八股文
  • 计算机基础
  • Python
  • C++
  • Java
  • Go
  • ChatGPT充值
  • Claude充值
  • 国内使用Claude
  • 中转站推荐
  • 🔥大模型🔥
  • 大模型面经
  • Java面经
  • C++面经

This view

  • Original page
  • New address

GeezerVison is showing notes.kamacoder.com — Python装饰器的原理是什么?函数装饰器、类装饰器、装饰器链与常见应用

# Python装饰器的原理是什么?

面试官问:"Python装饰器的原理是什么?"

很多人会答"装饰器就是用@修饰函数",但这只是语法表象。真正要讲清楚的是:装饰器是一个接收函数作为参数、返回新函数的高阶函数,通过闭包保存原函数并在调用前后添加额外逻辑,@符号只是语法糖。

# 简要回答

装饰器(Decorator) 的本质是函数包装器。它接收一个函数,返回一个新函数,新函数在调用原函数前后可以执行额外操作。

@语法糖等价于函数重新绑定:

@decorator
def func():
    pass

# 等价于
def func():
    pass
func = decorator(func)
1
2
3
4
5
6
7
8

装饰器的核心机制:

  1. 装饰器函数接收被装饰函数作为参数。
  2. 装饰器内部定义包装函数,在包装函数中调用原函数。
  3. 装饰器返回包装函数。
  4. 原函数名被重新绑定到包装函数。

装饰器依赖闭包保存原函数引用,让包装函数可以访问和调用原函数。

# 详细回答

# 最简单的装饰器

不使用@符号,手动包装函数:

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
# 函数执行完成
1
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
1
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
1
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
1
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)
1

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
1
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__)   # 计算两数之和
1
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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

等价于:

say_hello = bold(italic(say_hello))
1

先应用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()
1
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
1
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
1
2
3
4
5
6
7
8

或者保存原函数引用,运行时选择调用装饰版本还是原版本。

← 生成器是什么?yield为什么能节省内存? Python上下文管理器是什么?with如何自动释放资源? →

评论

验证登录状态...

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.