Python装饰器与语法糖

装饰器是Python中最具代表性的语法特性之一。它让你能在不修改原函数代码的前提下,以声明的方式增强或改变函数/类的行为。而@语法糖则是让这种能力变得优雅易读的关键。

1.基础回顾:函数是一等公民

理解装饰器之前,必须牢记Python中函数就是普通对象:

python 复制代码
def greet(name):
    return f"Hello, {name}"

# 1. 可以赋值给变量
g = greet
print(g("World"))        # Hello, World

# 2. 可以存储在容器中
funcs = [greet, len]
print(funcs[0]("Python")) # Hello, Python

# 3. 可以作为参数传递
def apply(func, arg):
    return func(arg)

print(apply(greet, "Decorator"))  # Hello, Decorator

# 4. 可以在函数内部定义并返回(闭包)
def make_multiplier(n):
    def multiplier(x):
        return x * n
    return multiplier

times3 = make_multiplier(3)
print(times3(10))        # 30

装饰器本质上就是一种接收函数,返回新函数的可调用对象,它借用了闭包和高阶函数的特性。

2.装饰器原理与@语法糖

语法糖@的核心等价式是 func = decorator(func)

先看一个没有语法糖的装饰器

假设我们要打印函数执行的耗时:

python 复制代码
import time

def timer(func):
    """一个简单的计时装饰器"""
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.6f}s")
        return result
    return wrapper

def slow_square(n):
    time.sleep(0.1)
    return n * n

# 手动装饰
slow_square = timer(slow_square)
print(slow_square(5))  
# 输出:
# slow_square took 0.100123s
# 25

timer就是一个典型的装饰器:接收函数func,返回一个增强后的新函数wrapper。

语法糖@的本质

@decorator仅仅是一种语法糖(Syntactic Sugar),即让代码更易读,更美观,而不改变底层执行逻辑。编辑器遇到:

python 复制代码
@timer
def slow_square(n):
    time.sleep(0.1)
    return n * n

完全等价于:

python 复制代码
def slow_square(n):
    time.sleep(0.1)
    return n * n
slow_square = timer(slow_square)

注意:装饰动作发生在函数定义时,而不是调用时。导入模块时,装饰器代码就会立即执行。

3.通用装饰器:支持任意参数

为了装饰任意签名(参数列表)的函数,我们使用 *args,**kwargs 来转发所有参数:

python 复制代码
def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@log_call
def add(a, b):
    return a + b

add(3, 5)
# 输出:
# Calling add with args=(3, 5), kwargs={}
# add returned 8

保留元信息:functools.wraps

直接返回 wrapper 会丢失原函数的名称,文档字符串,参数列表等元信息:

python 复制代码
print(add.__name__)  # wrapper  (我们希望是 add)
print(add.__doc__)   # None

这会给调试和文档生成造成困扰。标准做法是使用 functools.wraps,它会把原函数元信息复制到 wrapper 上:

python 复制代码
from functools import wraps

def log_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_call
def add(a, b):
    """Return the sum of a and b."""
    return a + b

print(add.__name__)   # add
print(add.__doc__)    # Return the sum of a and b.

永远记得给装饰器内部的 wrapper 加上@wraps(func)。

4.带参数的装饰器

有时我们希望装饰器本身接收参数,例如 @repeat(3)让函数执行三次。这需要三层嵌套:

python 复制代码
def repeat(n):
    """装饰器工厂:返回真正的装饰器"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say_hi(name):
    print(f"Hi, {name}!")

say_hi("Alice")
# 输出三遍 Hi, Alice!

执行过程解析:

1.repeat(3)返回 decorator 函数。

2.@decorator 相当于 say_hi = decorator(say_hi)。

3.decorator(say_hi)返回最终的 wrapper。

带参装饰器本质上是装饰器工厂------外层函数负责接收配置参数,内层才是真正的装饰器

5.类装饰器

装饰器不一定是函数,如何可调用对象(实现了 call 的类实例)都可以作为装饰器。

基于类的无参数装饰器

python 复制代码
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} has been called {self.count} times")
        return self.func(*args, **kwargs)

@CountCalls
def greet():
    print("Hello!")

greet()  # greet has been called 1 times
greet()  # greet has been called 2 times

@CountCalls 等价于 greet = CountCalls(greet),创建的实例自身就是装饰后的可调用对象。

带参数的类装饰器

python 复制代码
class Retry:
    def __init__(self, max_attempts, delay=0):
        self.max_attempts = max_attempts
        self.delay = delay

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, self.max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Attempt {attempt} failed: {e}")
                    if attempt == self.max_attempts:
                        raise
                    time.sleep(self.delay)
        return wrapper

@Retry(max_attempts=3, delay=1)
def unstable_network_call():
    # 模拟失败
    raise ConnectionError("No network")
    return "data"

@Retry(max_attempts = 3,delay = 1)相当于先创建实例 Retry(3,1),然后该实例对函数进行调用: unstable_network_call = Retry(3,1)(unstable_network_call)。

6.执行顺序

同一个函数可以被多个装饰器修饰,它们按由下而上的顺序应用:

python 复制代码
@deco1
@deco2
@deco3
def func():
    pass

等价于:

python 复制代码
func = deco1(deco2(deco3(func)))

函数定义时的执行顺序是从最靠近函数的装饰器开始(自下而上)。

但在调用时,每一层 wrapper 的执行顺序恰好相反:最外层装饰器的迁至代码先执行,然后逐层进入,最后执行原函数;返回时原路返回。

从下到上装饰,从内到外执行的顺序

相关推荐
郑州光合科技余经理1 小时前
海外外卖平台源码改造实战——多语言核心代码实现
java·开发语言·前端·后端·mysql·架构·php
CClaris1 小时前
大模型量化从0到1(五):GPTQ 原理详解 + 从零量化一个真实大模型
人工智能·python·算法·机器学习
凯瑟琳.奥古斯特2 小时前
力扣1012数位DP解法详解
开发语言·c++·算法·leetcode·职场和发展
Railshiqian2 小时前
UserPickerActivity 内部逻辑分析
开发语言·python
一tiao咸鱼2 小时前
前端转 agent # 02 - FastAPI 框架入门与原理
前端·python
喜欢打篮球的普通人2 小时前
Trition程序编写:从“Hello CUDA“到“Hello Triton“:向量加法背后的编译黑魔法
开发语言·后端·rust
code_std2 小时前
java WebSocket 使用
java·开发语言·websocket
gihigo19982 小时前
FastSLAM2.0(精度优于1.0)MATLAB 实现
开发语言·matlab
Yolo566Q2 小时前
Noah-MP陆面过程模型建模方法与站点、区域模拟实践技术应用
开发语言·python