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

浙公网安备 33010602011771号