Python 基础篇(九): 第九章. 高级特性:装饰器、迭代器与生成器

第九章. 高级特性:装饰器、迭代器与生成器

在前面的章节中,我们学习了函数的基础知识,包括闭包和高阶函数。这一章,我们将学习 Python 的高级特性:装饰器、迭代器和生成器。这些特性让 Python 代码更加优雅、高效。


9.1. 装饰器:从闭包到装饰器

9.1.1. 为什么需要装饰器?

假设你写了一个函数,用于计算两个数的和:

1
2
3
4
5
def add(a, b):
return a + b

result = add(3, 5)
print(result) # 8

现在,你想知道这个函数执行了多长时间。你可能会这样写:

1
2
3
4
5
6
7
8
import time

def add(a, b):
start_time = time.time()
result = a + b
end_time = time.time()
print(f"函数执行时间:{end_time - start_time}秒")
return result

但这样有个问题:如果你有 10 个函数都需要计时,难道要在每个函数里都写一遍计时代码吗?

装饰器可以解决这个问题:它可以在不修改原函数代码的情况下,给函数添加新功能。


9.1.2. 回顾:函数是一等公民

在第八章中,我们学习了"函数是一等公民",这意味着:

  1. 函数可以赋值给变量
  2. 函数可以作为参数传递
  3. 函数可以作为返回值
1
2
3
4
5
6
7
8
9
10
11
12
def greet():
return "Hello"

# 函数赋值给变量
say_hello = greet
print(say_hello()) # Hello

# 函数作为参数
def execute(func):
return func()

print(execute(greet)) # Hello

这是理解装饰器的基础。


9.1.3. 回顾:闭包的本质

在第八章中,我们学习了闭包:内部函数引用了外部函数的变量。

1
2
3
4
5
6
7
def outer(x):
def inner(y):
return x + y
return inner

add_5 = outer(5)
print(add_5(3)) # 8

闭包的关键特点:

  • 内部函数可以访问外部函数的变量
  • 即使外部函数执行完毕,内部函数仍然可以访问这些变量

这是装饰器的核心机制。


9.1.4. 第一步:手动包装函数

现在,让我们回到计时的问题。我们想给 add 函数添加计时功能,但不修改它的代码。

思路:创建一个新函数,在这个新函数里调用原函数,并在调用前后记录时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import time

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

# 手动包装
def add_with_timer(a, b):
start_time = time.time()
result = add(a, b)
end_time = time.time()
print(f"函数执行时间:{end_time - start_time}秒")
return result

# 使用包装后的函数
result = add_with_timer(3, 5)
print(result) # 8

这样可以工作,但有两个问题:

  1. 每个函数都要手动写一个包装函数
  2. 调用时要记得用包装后的函数名

9.1.5. 第二步:用函数包装函数

我们可以写一个通用的包装函数,它接收一个函数作为参数,返回一个包装后的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time

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

def timer_wrapper(func):
"""给函数添加计时功能"""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"函数执行时间:{end_time - start_time}秒")
return result
return wrapper

# 包装函数
add = timer_wrapper(add)

# 使用
result = add(3, 5)
# 输出:
# 函数执行时间:0.0秒
# 8

这里发生了什么?

  1. timer_wrapper(add) 返回了一个新函数 wrapper
  2. 我们把这个新函数赋值给 add,覆盖了原来的 add
  3. 现在调用 add(3, 5) 实际上是调用 wrapper(3, 5)
  4. wrapper 内部会调用原来的 add 函数,并在前后记录时间

关键点

  • wrapper 是一个闭包,它引用了外部函数的 func 变量
  • *args**kwargswrapper 可以接收任意参数

9.1.6. 第三步:通用的函数包装器

现在,我们可以用同样的方式包装其他函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import time

def timer_wrapper(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} 执行时间:{end_time - start_time}秒")
return result
return wrapper

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

def multiply(a, b):
return a * b

# 包装函数
add = timer_wrapper(add)
multiply = timer_wrapper(multiply)

# 使用
print(add(3, 5)) # add 执行时间:0.0秒 \n 8
print(multiply(3, 5)) # multiply 执行时间:0.0秒 \n 15

这样就实现了一个通用的计时器。但每次都要写 func = timer_wrapper(func) 还是有点麻烦。


9.1.7. 第四步:装饰器语法糖 @

Python 提供了一个语法糖 @,让包装函数的过程更简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time

def timer_wrapper(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} 执行时间:{end_time - start_time}秒")
return result
return wrapper

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

@timer_wrapper
def multiply(a, b):
return a * b

# 使用
print(add(3, 5)) # add 执行时间:0.0秒 \n 8
print(multiply(3, 5)) # multiply 执行时间:0.0秒 \n 15

@timer_wrapper 等价于 add = timer_wrapper(add)

这就是装饰器!它的本质是:

  1. 一个接收函数作为参数的函数
  2. 返回一个新函数(通常是闭包)
  3. 新函数在调用原函数前后添加额外功能

9.1.8. 装饰器的执行时机

重要:装饰器在函数定义时就会执行,而不是在函数调用时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def my_decorator(func):
print(f"装饰器正在包装 {func.__name__}")
def wrapper():
print("wrapper 执行")
return func()
return wrapper

@my_decorator
def greet():
print("Hello")

# 输出:装饰器正在包装 greet

# 调用函数
greet()
# 输出:
# wrapper 执行
# Hello

执行流程

  1. Python 解释器读到 @my_decorator 时,立即执行 greet = my_decorator(greet)
  2. 此时会输出"装饰器正在包装 greet"
  3. 调用 greet() 时,实际上是调用 wrapper()

9.1.9. 带参数的装饰器

有时候,我们希望装饰器本身也能接收参数。比如,我们想指定计时的单位(秒或毫秒)。

思路:再包装一层函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import time

def timer(unit="秒"):
"""带参数的装饰器"""
def decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()

elapsed = end_time - start_time
if unit == "毫秒":
elapsed *= 1000

print(f"{func.__name__} 执行时间:{elapsed}{unit}")
return result
return wrapper
return decorator

@timer(unit="毫秒")
def add(a, b):
return a + b

print(add(3, 5))
# 输出:
# add 执行时间:0.0毫秒
# 8

这里发生了什么?

  1. @timer(unit="毫秒") 先执行 timer(unit="毫秒"),返回 decorator
  2. 然后执行 add = decorator(add)
  3. 最终 add 被替换为 wrapper

三层结构

  • 最外层:接收装饰器参数
  • 中间层:接收被装饰的函数
  • 最内层:接收函数调用的参数

9.1.10. 装饰器叠加

一个函数可以被多个装饰器装饰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 greet():
print("Hello")

greet()
# 输出:
# 装饰器1 开始
# 装饰器2 开始
# Hello
# 装饰器2 结束
# 装饰器1 结束

执行顺序

  • 装饰器从下到上执行:先 decorator2,再 decorator1
  • 等价于:greet = decorator1(decorator2(greet))
  • 调用时从外到内:先执行 decorator1wrapper,再执行 decorator2wrapper

9.1.11. functools.wraps:保留函数元数据

装饰器有一个问题:被装饰后,函数的元数据(如函数名、文档字符串)会丢失。

1
2
3
4
5
6
7
8
9
10
11
12
13
def timer_wrapper(func):
def wrapper(*args, **kwargs):
"""wrapper 的文档字符串"""
return func(*args, **kwargs)
return wrapper

@timer_wrapper
def add(a, b):
"""计算两个数的和"""
return a + b

print(add.__name__) # wrapper(不是 add)
print(add.__doc__) # wrapper 的文档字符串(不是原来的)

解决方案:使用 functools.wraps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from functools import wraps

def timer_wrapper(func):
@wraps(func) # 保留原函数的元数据
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

@timer_wrapper
def add(a, b):
"""计算两个数的和"""
return a + b

print(add.__name__) # add
print(add.__doc__) # 计算两个数的和

最佳实践:在装饰器的 wrapper 函数上始终使用 @wraps(func)


9.2. 迭代器:理解 for 循环的本质

9.2.1. 什么是可迭代对象?

在 Python 中,可以用 for 循环遍历的对象叫做可迭代对象(Iterable)。

1
2
3
4
5
6
7
8
9
# 这些都是可迭代对象
for char in "Python":
print(char)

for num in [1, 2, 3]:
print(num)

for key in {"a": 1, "b": 2}:
print(key)

可迭代对象的特点:实现了 __iter__() 方法,该方法返回一个迭代器。


9.2.2. 什么是迭代器?

迭代器(Iterator)是一个可以记住遍历位置的对象。

迭代器必须实现两个方法:

  1. __iter__():返回迭代器自身
  2. __next__():返回下一个元素,如果没有元素了,抛出 StopIteration 异常

9.2.3. iter() 和 next():手动迭代

1
2
3
4
5
6
7
8
9
10
11
12
# 创建迭代器
text = "Python"
iterator = iter(text) # 调用 text.__iter__()

# 手动迭代
print(next(iterator)) # P
print(next(iterator)) # y
print(next(iterator)) # t
print(next(iterator)) # h
print(next(iterator)) # o
print(next(iterator)) # n
# print(next(iterator)) # StopIteration

关键点

  • iter() 函数将可迭代对象转换为迭代器
  • next() 函数获取迭代器的下一个元素
  • 迭代完成后,再次调用 next() 会抛出 StopIteration 异常

9.2.4. for 循环的工作原理

for 循环的本质是:

  1. 调用 iter() 获取迭代器
  2. 不断调用 next() 获取元素
  3. 捕获 StopIteration 异常,结束循环
1
2
3
4
5
6
7
8
9
10
11
12
# for 循环
for char in "abc":
print(char)

# 等价于
iterator = iter("abc")
while True:
try:
char = next(iterator)
print(char)
except StopIteration:
break

9.3. 生成器:更优雅的迭代器

9.3.1. 为什么需要生成器?

假设你要生成一个包含 100 万个数字的列表:

1
2
3
# 方式 1:列表
numbers = [i for i in range(1000000)]
# 占用大量内存

如果你只需要逐个处理这些数字,没必要一次性生成所有数字。生成器可以按需生成数据,节省内存


9.3.2. yield 关键字:暂停与恢复

生成器使用 yield 关键字,而不是 return

1
2
3
4
5
6
7
8
9
10
11
12
13
def simple_generator():
print("第一次 yield")
yield 1
print("第二次 yield")
yield 2
print("第三次 yield")
yield 3

gen = simple_generator()
print(next(gen)) # 第一次 yield \n 1
print(next(gen)) # 第二次 yield \n 2
print(next(gen)) # 第三次 yield \n 3
# print(next(gen)) # StopIteration

yield 的特点

  • 遇到 yield 时,函数暂停,返回 yield 后面的值
  • 下次调用 next() 时,从上次暂停的地方继续执行
  • 函数执行完毕后,抛出 StopIteration 异常

9.3.3. 生成器函数 vs 普通函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 普通函数
def get_numbers():
return [1, 2, 3]

# 生成器函数
def generate_numbers():
yield 1
yield 2
yield 3

# 使用
numbers = get_numbers()
print(numbers) # [1, 2, 3](列表)

gen = generate_numbers()
print(gen) # <generator object>(生成器对象)

# 遍历生成器
for num in gen:
print(num) # 1, 2, 3

9.3.4. 生成器表达式

生成器表达式类似于列表推导式,但使用圆括号:

1
2
3
4
5
6
7
8
9
10
11
# 列表推导式
squares_list = [x**2 for x in range(10)]
print(squares_list) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# 生成器表达式
squares_gen = (x**2 for x in range(10))
print(squares_gen) # <generator object>

# 遍历生成器
for num in squares_gen:
print(num)

9.3.5. 生成器的优势:内存效率

1
2
3
4
5
6
7
8
9
10
11
12
13
import sys

# 列表:占用大量内存
numbers_list = [i for i in range(1000000)]
print(f"列表大小:{sys.getsizeof(numbers_list)} 字节")

# 生成器:占用很少内存
numbers_gen = (i for i in range(1000000))
print(f"生成器大小:{sys.getsizeof(numbers_gen)} 字节")

# 输出:
# 列表大小:8448728 字节
# 生成器大小:112 字节

9.4. 推导式:简洁的数据生成

在前面的章节中,我们学习了如何使用循环创建列表、字典等数据结构。Python 提供了一种更简洁的语法:推导式。


9.4.1. 列表推导式

列表推导式可以用一行代码创建列表。

基础语法[表达式 for 变量 in 可迭代对象]

1
2
3
4
5
6
7
8
9
# 传统方式
squares = []
for x in range(10):
squares.append(x**2)
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# 列表推导式
squares = [x**2 for x in range(10)]
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

带条件的列表推导式[表达式 for 变量 in 可迭代对象 if 条件]

1
2
3
4
5
6
7
8
# 只保留偶数的平方
even_squares = [x**2 for x in range(10) if x % 2 == 0]
print(even_squares) # [0, 4, 16, 36, 64]

# 字符串处理
words = ["hello", "world", "python"]
upper_words = [word.upper() for word in words]
print(upper_words) # ['HELLO', 'WORLD', 'PYTHON']

多重条件

1
2
3
4
5
6
# 筛选能被2和3整除的数
numbers = [x for x in range(30) if x % 2 == 0 if x % 3 == 0]
print(numbers) # [0, 6, 12, 18, 24]

# 等价于
numbers = [x for x in range(30) if x % 2 == 0 and x % 3 == 0]

条件表达式

1
2
3
# 偶数保留,奇数变为0
numbers = [x if x % 2 == 0 else 0 for x in range(10)]
print(numbers) # [0, 0, 2, 0, 4, 0, 6, 0, 8, 0]

9.4.2. 字典推导式

字典推导式可以快速创建字典。

基础语法{键表达式: 值表达式 for 变量 in 可迭代对象}

1
2
3
4
5
6
7
8
# 创建数字到平方的映射
squares_dict = {x: x**2 for x in range(5)}
print(squares_dict) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 字符串长度映射
words = ["hello", "world", "python"]
length_dict = {word: len(word) for word in words}
print(length_dict) # {'hello': 5, 'world': 5, 'python': 6}

带条件的字典推导式

1
2
3
# 只保留偶数
even_squares = {x: x**2 for x in range(10) if x % 2 == 0}
print(even_squares) # {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

字典转换

1
2
3
4
5
6
7
8
9
# 交换键值
original = {"a": 1, "b": 2, "c": 3}
swapped = {value: key for key, value in original.items()}
print(swapped) # {1: 'a', 2: 'b', 3: 'c'}

# 过滤字典
scores = {"Alice": 85, "Bob": 92, "Charlie": 78, "David": 95}
high_scores = {name: score for name, score in scores.items() if score >= 90}
print(high_scores) # {'Bob': 92, 'David': 95}

9.4.3. 集合推导式

集合推导式用于创建集合,会自动去重。

基础语法{表达式 for 变量 in 可迭代对象}

1
2
3
4
5
6
7
8
# 创建平方数集合
squares_set = {x**2 for x in range(10)}
print(squares_set) # {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}

# 去重
numbers = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
unique = {x for x in numbers}
print(unique) # {1, 2, 3, 4}

带条件的集合推导式

1
2
3
4
# 提取字符串中的元音字母
text = "hello world"
vowels = {char for char in text if char in "aeiou"}
print(vowels) # {'e', 'o'}

9.4.4. 推导式 vs 循环:何时用哪个?

使用推导式的场景

  1. 简单的转换或过滤操作
  2. 一行代码能清晰表达意图
  3. 不需要复杂的逻辑
1
2
3
# ✅ 适合用推导式
squares = [x**2 for x in range(10)]
even_numbers = [x for x in range(20) if x % 2 == 0]

使用循环的场景

  1. 逻辑复杂,需要多行代码
  2. 需要在循环中使用 breakcontinue
  3. 需要处理异常
1
2
3
4
5
6
7
8
9
10
# ❌ 不适合用推导式(逻辑太复杂)
result = []
for x in range(10):
if x % 2 == 0:
try:
result.append(10 / x)
except ZeroDivisionError:
result.append(0)

# ✅ 用循环更清晰

性能对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import time

# 循环
start = time.time()
result = []
for x in range(1000000):
result.append(x**2)
print(f"循环耗时:{time.time() - start:.4f}秒")

# 推导式
start = time.time()
result = [x**2 for x in range(1000000)]
print(f"推导式耗时:{time.time() - start:.4f}秒")

# 推导式通常更快

9.4.5. 嵌套推导式

推导式可以嵌套使用,但要注意可读性。

嵌套列表推导式

1
2
3
4
5
6
7
8
9
# 创建二维列表
matrix = [[i * j for j in range(3)] for i in range(3)]
print(matrix)
# [[0, 0, 0], [0, 1, 2], [0, 2, 4]]

# 展平二维列表
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [num for row in matrix for num in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]

理解嵌套推导式的顺序

1
2
3
4
5
6
7
8
# 推导式
flat = [num for row in matrix for num in row]

# 等价于
flat = []
for row in matrix:
for num in row:
flat.append(num)

带条件的嵌套推导式

1
2
3
4
# 提取二维列表中的偶数
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
even_numbers = [num for row in matrix for num in row if num % 2 == 0]
print(even_numbers) # [2, 4, 6, 8]

9.4.6. 推导式的常见错误与避坑指南

错误 1:过度嵌套导致可读性差

1
2
3
4
5
6
7
8
9
10
11
12
# ❌ 难以理解
result = [[x * y for y in range(3) if y % 2 == 0] for x in range(5) if x > 2]

# ✅ 用循环更清晰
result = []
for x in range(5):
if x > 2:
row = []
for y in range(3):
if y % 2 == 0:
row.append(x * y)
result.append(row)

最佳实践:如果推导式超过一行或嵌套超过两层,考虑使用循环。


错误 2:在推导式中修改外部变量

1
2
3
4
5
6
7
8
9
10
11
12
# ❌ 错误:推导式不应该有副作用
count = 0
numbers = [count := count + 1 for _ in range(5)] # 使用海象运算符
print(numbers) # [1, 2, 3, 4, 5]
print(count) # 5

# ✅ 正确:用循环
count = 0
numbers = []
for _ in range(5):
count += 1
numbers.append(count)

最佳实践:推导式应该是纯函数式的,不应该修改外部状态。


错误 3:推导式中使用复杂表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ 难以理解
result = [x if x % 2 == 0 else x * 2 if x % 3 == 0 else x * 3 for x in range(10)]

# ✅ 用函数提取逻辑
def transform(x):
if x % 2 == 0:
return x
elif x % 3 == 0:
return x * 2
else:
return x * 3

result = [transform(x) for x in range(10)]

错误 4:忘记推导式会立即执行

1
2
3
4
5
# 列表推导式:立即生成所有元素
numbers = [x**2 for x in range(1000000)] # 占用大量内存

# 生成器表达式:按需生成
numbers = (x**2 for x in range(1000000)) # 占用很少内存

何时用哪个?

  • 需要多次遍历或索引访问:用列表推导式
  • 只需要遍历一次:用生成器表达式