Python高级语法:装饰器

作者声明:仅仅是我在学习过程中的感悟与体会,发出来供大家参考,如有错误请多指正

1. 基本用法

​ decorator,装饰器,为什么会有这个东西呢?先来看一个简单的例子:

def hello1(words : str):
    print("Welcome!")
    print(f"Hello {words}")
    
def hello2():
    print("Welcome!")
    print("Hello decorator")

​ ok,有上面两个函数,可以看到,每个函数都首先要执行一段相同的逻辑,实际在工作中也会经常出现这种情况,可能二三十个函数都要使用相同的一段逻辑,那么我可不可以想一个办法来让代码更加简洁,不那么繁琐呢?

​ 装饰器的出现解决了这个问题。那么,要如何设计一个装饰器呢,下面是一段解决上述问题的一个简单的装饰器:

def welcome(fn):
    def wrapper(*args, **kwargs):
        print("Welcome!")
        result = fn(*args, **kwargs)
        return result
    return wrapper

​ ok,那么这个函数,实现的效果是:传入一个函数,返回一个函数,这是总体上来看的。

​ 那么内部做了什么呢?函数传进来之后,welcome函数内部自己又定义了一个函数,这个就是待会返回的函数。在这个函数中,首先 print 了一下 Welcome,随后就调用了传入的 fn 函数并保存到 result 中并返回。也就是说!!! wrapper 这个函数做的是,将 welcome 传入的函数原封不动执行一边,另外在它的前面执行了一句 print("Welcome!"),这个过程,就可以理解为将传入的函数 decorate 了一下,多了一行 welcome 的输出,然后我再把 wrapper 返回出去。

​ 那么然后呢,写完了该怎么实现对原函数的装饰效果呢,代码如下:

f1 = welcome(hello1)
f1("python")
# output : 
# Welcome!
# Hello python

f2 = welcome(hello2)
f2()
# output : 
# Welcome!
# Hello

​ 为什么是这样呢,因为 welcome 其实返回的是一个函数,返回的是装饰过后的函数,此时我再给他赋值, welcome 就出现了。

​ 但是,但是,你会发现,这好像依旧复杂,似乎比原来还要多写好多行,也没见省事啊。

​ 没错,所以诞生了另外一种更加简便的方法:

def welcome(fn):
    def wrapper(*args, **kwargs):
        print("Welcome!")
        result = fn(*args, **kwargs)
        return result
    return wrapper


@welcome
def hello1(words : str):
    print(f"Hello {words}")

@welcome
def hello2():
    print("Hello decorator")


hello1("python")
hello2()

# output :
# Welcome!
# Hello python
# Welcome!
# Hello decorator

注意:装饰器必须写在前面,如果上述代码中 welcome() 写在了 hello1 后面就会报错,提示未定义函数。

2. @wraps

​ 了解了装饰器的基本用法之后,现在又出现了一个新的问题,如下:

def welcome(fn):
    def wrapper(*args, **kwargs):
        print("Welcome!")
        result = fn(*args, **kwargs)
        return result
    return wrapper


@welcome
def hello1(words : str):
    print(f"Hello {words}")

@welcome
def hello2():
    print("Hello decorator")


print(hello1.__name__) # wrapper
print(hello2.__name__) # wrapper

​ 可以看到,输出的结果是 wrapper,为什么?很简单,看一下 welcome 的实现逻辑就可以了,很明显,它是直接将 hello1 和 hello2 直接改造换成了 wrapper 函数然后返回,那么当然就会覆盖掉原来的函数信息了。

​ 那我不想让原来函数的信息在使用装饰器之后丢失掉该怎么做呢?可以使用 @wraps,没错,这是一个系统提供的装饰器,作用就是在自定义装饰器的时候,保留使用该装饰器函数的信息。用法如下:

from functools import wraps

def welcome(fn):
    @wraps(fn) # 注意这里要传参,,不能直接用 @wraps
    def wrapper(*args, **kwargs):
        print("Welcome!")
        result = fn(*args, **kwargs)
        return result
    return wrapper


@welcome
def hello1(words : str):
    print(f"Hello {words}")

@welcome
def hello2():
    print("Hello decorator")


print(hello1.__name__) # hello1
print(hello2.__name__) # hello2

​ 此时输出结果就是原本的函数了,这就是 @wraps 的用法。

3. 带参数的装饰器

​ 还是前面的例子,如果我想灵活一点,希望每次的欢迎语可以自定义,那么,就需要传参,这点如何做到呢?其实只需要在原本的装饰器的外面套一层普通函数就可以了,这个普通函数可以接受一个传参,然后这个传参就可以在整个装饰器内部生效,具体实现代码如下:

from functools import wraps

def welcome(name):
    def decorator(fn):
        @wraps(fn) # 注意这里要传参,,不能直接用 @wraps
        def wrapper(*args, **kwargs):
            print(f"Welcome, {name}!") # 这里加上传参
            result = fn(*args, **kwargs)
            return result
        return wrapper
    return decorator

@welcome("release")
def hello1(words : str):
    print(f"Hello {words}")

@welcome("way2top")
def hello2():
    print("Hello decorator")


hello1("python")
hello2()

# Welcome, release!
# Hello python
# Welcome, way2top!
# Hello decorator

​ 在外面套一个简单的函数接收传参就可以了,可以看到输出结果是自定义的。

如果把第 13 行 @welcome("release") 改一下呢,不传入参数了,也就是:我定义了一个需要传参的装饰器,但是我不传参,那么……

结果是不会有任何输出,只有第二个函数 hello2 的输出,这点需要注意。

4. 装饰器应用

​ 用它来测试不同算法之间的执行时间,代码如下:

from functools import wraps
import time


# --- 通用计时器装饰器 ---
def timeit(func):
    """一个装饰器,用于测量并打印函数的执行时间。"""

    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"--- Running {func.__name__} ---")
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        elapsed = (end - start) * 1000  # 转换为毫秒

        # 使用 f-string 进行格式化对齐,方便对比
        print(f"{func.__name__:<25} | Time: {elapsed:>10.3f} ms | Primes found: {len(result):,}")
        return result

    return wrapper


# --- 算法 1: 欧拉筛 (线性筛) ---
# 时间复杂度: O(N) - 理论上最高效的筛法
@timeit
def euler_sieve(n):
    """
    欧拉筛 (线性筛) 算法。
    核心思想:保证每个合数只被其“最小质因数”筛掉一次。
    """
    if n < 2:
        return []

    is_prime = [True] * (n + 1)
    primes = []  # 用来存储已找到的质数
    is_prime[0] = is_prime[1] = False

    for i in range(2, n + 1):
        if is_prime[i]:
            primes.append(i)

        # 遍历已找到的质数列表,用来筛掉合数
        for p in primes:
            # 如果 i*p 超出范围,后续的质数也会超出,直接中断
            if i * p > n:
                break
            is_prime[i * p] = False
            if i % p == 0:
                break
    return primes


# --- 算法 2: 埃氏筛 (Sieve of Eratosthenes) ---
# 时间复杂度: O(N log log N) - 非常高效,但略逊于欧拉筛
@timeit
def eratosthenes_sieve(n):
    """
    标准的埃氏筛算法。
    核心思想:从2开始,每找到一个质数,就把它所有的倍数都标记为合数。
    """
    if n < 2:
        return []

    is_prime = [True] * (n + 1)
    is_prime[0] = is_prime[1] = False

    # 优化1: 外层循环只需要遍历到 sqrt(n)
    for p in range(2, int(n ** 0.5) + 1):
        # 如果 p 仍然是质数
        if is_prime[p]:
            is_prime[p * p: n + 1: p] = [False] * len(range(p * p, n + 1, p))

    # 收集所有质数
    primes = [i for i, is_p in enumerate(is_prime) if is_p]
    return primes


# --- 性能对比测试 ---
if __name__ == "__main__":
    n = 1000000

    print(f"======> Starting prime sieve comparison for n = {n:,} <======\n")

    # 执行埃氏筛
    eratosthenes_sieve(n)

    print("\n" + "=" * 60 + "\n")

    # 执行欧拉筛
    euler_sieve(n)

    print(f"\n======> Comparison finished. <======")

结果如下:

======> Starting prime sieve comparison for n = 1,000,000 <======

--- Running eratosthenes_sieve ---
eratosthenes_sieve        | Time:     46.478 ms | Primes found: 78,498

============================================================

--- Running euler_sieve ---
euler_sieve               | Time:    125.815 ms | Primes found: 78,498

======> Comparison finished. <======

​ 这就是装饰器的实际应用之一,其实在工作中很多时候还会用它来打印日志文件。

​ 这里额外提一嘴,实际上欧式筛时间复杂度是优于埃氏筛的,前者是 O(n),后者是 O(nloglogn),那为什么实际输出结果是埃氏筛时间短于欧式筛呢?

​ 其实是因为使用的是Python,所以,欧拉筛的“理论优势”在 Python 中往往被语言执行效率抵消,而埃氏筛结构简单、列表切片高效,导致它在 Python 里跑得更快,哪怕 $n = 10^8$ 也是如此。

​ 但是同样的逻辑写在 C++ 中就很明显了,欧式筛就是优于埃氏筛的。

4cc86e28-c4d9-430a-b433-585fe56599bc

posted @ 2025-07-02 19:01  Way2top  阅读(7)  评论(0)    收藏  举报