🎀 20. 装饰器

本章概述

学习 Python 装饰器,在不修改原函数代码的情况下增强函数功能。 预计学习时间:60 分钟


20.1 什么是装饰器

装饰器是一种设计模式,用来在不修改原函数代码的情况下,给函数增加新功能。

装饰器的特点

  • 不修改原函数的代码
  • 不改变原函数的调用方式
  • 可以给多个函数增加相同的功能
  • 代码复用

20.2 函数是一等公民

在 Python 中,函数就是普通对象,可以:

  • 赋值给变量
  • 作为参数传递
  • 作为返回值
  • 定义在另一个函数里面

函数赋值给变量

def say_hello():
    print("Hello")
 
f = say_hello  # 函数赋值给变量
f()            # 调用变量,相当于调用函数

函数作为参数

def add(a, b):
    return a + b
 
def calc(func, x, y):
    return func(x, y)
 
result = calc(add, 3, 5)  # 函数作为参数
print(result)  # 8

函数作为返回值

def outer():
    def inner():
        print("我是内部函数")
    return inner  # 返回函数
 
f = outer()
f()  # 调用返回的函数

闭包

内部函数引用外部函数的变量,外部函数返回内部函数:

def make_adder(n):
    def adder(x):
        return x + n  # 引用了外部的n
    return adder
 
add5 = make_adder(5)
print(add5(3))   # 8
print(add5(10))  # 15

装饰器的基础

理解了「函数是一等公民」和「闭包」,装饰器就好理解了。


20.3 简单装饰器

手写装饰器

def my_decorator(func):
    def wrapper():
        print("函数执行前")
        func()  # 调用原函数
        print("函数执行后")
    return wrapper
 
def say_hello():
    print("Hello!")
 
# 手动装饰
say_hello = my_decorator(say_hello)
 
say_hello()
# 输出:
# 函数执行前
# Hello!
# 函数执行后

@ 语法糖

Python 提供了 @ 语法,更简洁:

def my_decorator(func):
    def wrapper():
        print("函数执行前")
        func()
        print("函数执行后")
    return wrapper
 
@my_decorator  # 等价于 say_hello = my_decorator(say_hello)
def say_hello():
    print("Hello!")
 
say_hello()

20.4 装饰带参数的函数

固定参数

def my_decorator(func):
    def wrapper(a, b):  # 和原函数参数一样
        print(f"参数:{a}, {b}")
        result = func(a, b)
        print(f"结果:{result}")
        return result
    return wrapper
 
@my_decorator
def add(a, b):
    return a + b
 
print(add(3, 5))

通用参数(推荐)

*args**kwargs 接收任意参数,可以装饰任何函数:

def my_decorator(func):
    def wrapper(*args, **kwargs):  # 接收任意参数
        print("函数执行前")
        result = func(*args, **kwargs)  # 传递参数
        print("函数执行后")
        return result
    return wrapper
 
@my_decorator
def add(a, b):
    return a + b
 
@my_decorator
def greet(name):
    print(f"你好,{name}")
 
print(add(3, 5))
greet("Tom")

推荐写法

装饰器的 wrapper 函数用 *args, **kwargs,可以装饰任何函数。


20.5 装饰器带参数

装饰器本身也可以接收参数。

示例

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()  # 会打印3次Hello!

执行过程

# @repeat(3) 等价于:
# say_hello = repeat(3)(say_hello)
# 1. repeat(3) 返回 decorator 函数
# 2. decorator(say_hello) 返回 wrapper 函数

20.6 多个装饰器

一个函数可以用多个装饰器,从下往上执行。

示例

def decorator1(func):
    def wrapper(*args, **kwargs):
        print("装饰器1:前")
        result = func(*args, **kwargs)
        print("装饰器1:后")
        return result
    return wrapper
 
def decorator2(func):
    def wrapper(*args, **kwargs):
        print("装饰器2:前")
        result = func(*args, **kwargs)
        print("装饰器2:后")
        return result
    return wrapper
 
@decorator1
@decorator2
def say_hello():
    print("Hello!")
 
say_hello()

输出:

装饰器1:前
装饰器2:前
Hello!
装饰器2:后
装饰器1:后

执行顺序

装饰器的执行顺序:离函数近的先执行 相当于 decorator1(decorator2(say_hello))


20.7 保留原函数信息

装饰器会把原函数替换成 wrapper 函数,原函数的名字、文档等信息会丢失。

问题

def my_decorator(func):
    def wrapper(*args, **kwargs):
        """这是wrapper的文档"""
        return func(*args, **kwargs)
    return wrapper
 
@my_decorator
def add(a, b):
    """这是add函数的文档"""
    return a + b
 
print(add.__name__)  # wrapper(不是add!)
print(add.__doc__)   # 这是wrapper的文档(不是add的!)

解决方案:functools.wraps

import functools
 
def my_decorator(func):
    @functools.wraps(func)  # 保留原函数信息
    def wrapper(*args, **kwargs):
        """这是wrapper的文档"""
        return func(*args, **kwargs)
    return wrapper
 
@my_decorator
def add(a, b):
    """这是add函数的文档"""
    return a + b
 
print(add.__name__)  # add(正确了!)
print(add.__doc__)   # 这是add函数的文档(正确了!)

好习惯

写装饰器时都加上 @functools.wraps(func),保留原函数信息。


20.8 常用装饰器场景

1. 计时装饰器

import time
import functools
 
def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 执行时间:{end - start:.4f}秒")
        return result
    return wrapper
 
@timer
def slow_function():
    time.sleep(1)
 
slow_function()

2. 日志装饰器

import functools
 
def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"调用函数:{func.__name__}")
        print(f"位置参数:{args}")
        print(f"关键字参数:{kwargs}")
        result = func(*args, **kwargs)
        print(f"返回值:{result}")
        return result
    return wrapper
 
@log
def add(a, b):
    return a + b
 
add(3, 5)

3. 缓存装饰器

import functools
 
def cache(func):
    cached = {}
 
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cached:
            cached[args] = func(*args)
        return cached[args]
    return wrapper
 
@cache
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)
 
print(fibonacci(100))  # 很快,因为有缓存

标准库也有

Python 3.2+ 有 functools.lru_cache,比自己写的更好用。

from functools import lru_cache
 
@lru_cache(maxsize=None)
def fibonacci(n):
    ...

4. 权限验证装饰器

import functools
 
def require_login(func):
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("is_login"):
            raise PermissionError("请先登录")
        return func(user, *args, **kwargs)
    return wrapper
 
@require_login
def view_profile(user):
    print(f"查看{user['name']}的资料")

20.9 类装饰器

用类当装饰器,需要实现 __call__ 方法。

示例

import functools
 
class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0
 
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"调用次数:{self.count}")
        return self.func(*args, **kwargs)
 
@CountCalls
def say_hello():
    print("Hello!")
 
say_hello()  # 调用次数:1
say_hello()  # 调用次数:2

20.10 内置装饰器

@property

把方法变成属性,见 面向对象编程 - property

@staticmethod

静态方法,见 面向对象编程 - 静态方法

@classmethod

类方法,见 面向对象编程 - 类方法

@functools.lru_cache

缓存装饰器,见上文 20.8 节。


🔗 相关章节


📝 我的笔记

在这里记录你的理解、疑问和练习代码

# 你的练习代码
 

✅ 本章检查清单

  • 理解函数是一等公民
  • 理解闭包
  • 掌握装饰器的基本用法
  • 会写带参数的装饰器
  • 会用多个装饰器
  • 会用 functools.wraps 保留原函数信息
  • 了解常见的装饰器应用场景
  • 了解类装饰器
  • 知道常用的内置装饰器