第九章. 高级特性:装饰器、迭代器与生成器 在前面的章节中,我们学习了函数的基础知识,包括闭包和高阶函数。这一章,我们将学习 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)
现在,你想知道这个函数执行了多长时间。你可能会这样写:
1 2 3 4 5 6 7 8 import timedef 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 4 5 6 7 8 9 10 11 12 def greet (): return "Hello" say_hello = greet print (say_hello()) def execute (func ): return func() print (execute(greet))
这是理解装饰器的基础。
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 ))
闭包的关键特点:
内部函数可以访问外部函数的变量 即使外部函数执行完毕,内部函数仍然可以访问这些变量 这是装饰器的核心机制。
9.1.4. 第一步:手动包装函数 现在,让我们回到计时的问题。我们想给 add 函数添加计时功能,但不修改它的代码。
思路 :创建一个新函数,在这个新函数里调用原函数,并在调用前后记录时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import timedef 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)
这样可以工作,但有两个问题:
每个函数都要手动写一个包装函数 调用时要记得用包装后的函数名 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 timedef 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 )
这里发生了什么?
timer_wrapper(add) 返回了一个新函数 wrapper我们把这个新函数赋值给 add,覆盖了原来的 add 现在调用 add(3, 5) 实际上是调用 wrapper(3, 5) wrapper 内部会调用原来的 add 函数,并在前后记录时间关键点 :
wrapper 是一个闭包,它引用了外部函数的 func 变量*args 和 **kwargs 让 wrapper 可以接收任意参数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 timedef 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 )) print (multiply(3 , 5 ))
这样就实现了一个通用的计时器。但每次都要写 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 timedef 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 )) print (multiply(3 , 5 ))
@timer_wrapper 等价于 add = timer_wrapper(add)
这就是装饰器!它的本质是:
一个接收函数作为参数的函数 返回一个新函数(通常是闭包) 新函数在调用原函数前后添加额外功能 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()
执行流程 :
Python 解释器读到 @my_decorator 时,立即执行 greet = my_decorator(greet) 此时会输出"装饰器正在包装 greet" 调用 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 timedef 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 ))
这里发生了什么?
@timer(unit="毫秒") 先执行 timer(unit="毫秒"),返回 decorator然后执行 add = decorator(add) 最终 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()
执行顺序 :
装饰器从下到上执行:先 decorator2,再 decorator1 等价于:greet = decorator1(decorator2(greet)) 调用时从外到内:先执行 decorator1 的 wrapper,再执行 decorator2 的 wrapper 装饰器有一个问题:被装饰后,函数的元数据(如函数名、文档字符串)会丢失。
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__) print (add.__doc__)
解决方案 :使用 functools.wraps
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from functools import wrapsdef 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__) 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)是一个可以记住遍历位置的对象。
迭代器必须实现两个方法:
__iter__():返回迭代器自身__next__():返回下一个元素,如果没有元素了,抛出 StopIteration 异常9.2.3. iter() 和 next():手动迭代 1 2 3 4 5 6 7 8 9 10 11 12 text = "Python" iterator = iter (text) print (next (iterator)) print (next (iterator)) print (next (iterator)) print (next (iterator)) print (next (iterator)) print (next (iterator))
关键点 :
iter() 函数将可迭代对象转换为迭代器next() 函数获取迭代器的下一个元素迭代完成后,再次调用 next() 会抛出 StopIteration 异常 9.2.4. for 循环的工作原理 for 循环的本质是:
调用 iter() 获取迭代器 不断调用 next() 获取元素 捕获 StopIteration 异常,结束循环 1 2 3 4 5 6 7 8 9 10 11 12 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 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)) print (next (gen)) print (next (gen))
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) gen = generate_numbers() print (gen) for num in gen: print (num)
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) squares_gen = (x**2 for x in range (10 )) print (squares_gen) for num in squares_gen: print (num)
9.3.5. 生成器的优势:内存效率 1 2 3 4 5 6 7 8 9 10 11 12 13 import sysnumbers_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)} 字节" )
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) squares = [x**2 for x in range (10 )] print (squares)
带条件的列表推导式 :[表达式 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) words = ["hello" , "world" , "python" ] upper_words = [word.upper() for word in words] print (upper_words)
多重条件
1 2 3 4 5 6 numbers = [x for x in range (30 ) if x % 2 == 0 if x % 3 == 0 ] print (numbers) numbers = [x for x in range (30 ) if x % 2 == 0 and x % 3 == 0 ]
条件表达式
1 2 3 numbers = [x if x % 2 == 0 else 0 for x in range (10 )] print (numbers)
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) words = ["hello" , "world" , "python" ] length_dict = {word: len (word) for word in words} print (length_dict)
带条件的字典推导式
1 2 3 even_squares = {x: x**2 for x in range (10 ) if x % 2 == 0 } print (even_squares)
字典转换
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) 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)
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) numbers = [1 , 2 , 2 , 3 , 3 , 3 , 4 , 4 , 4 , 4 ] unique = {x for x in numbers} print (unique)
带条件的集合推导式
1 2 3 4 text = "hello world" vowels = {char for char in text if char in "aeiou" } print (vowels)
9.4.4. 推导式 vs 循环:何时用哪个? 使用推导式的场景 :
简单的转换或过滤操作 一行代码能清晰表达意图 不需要复杂的逻辑 1 2 3 squares = [x**2 for x in range (10 )] even_numbers = [x for x in range (20 ) if x % 2 == 0 ]
使用循环的场景 :
逻辑复杂,需要多行代码 需要在循环中使用 break 或 continue 需要处理异常 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 timestart = time.time() result = [] for x in range (1000000 ): result.append(x**2 ) print (f"循环耗时:{time.time() - start:.4 f} 秒" )start = time.time() result = [x**2 for x in range (1000000 )] print (f"推导式耗时:{time.time() - start:.4 f} 秒" )
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)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 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)
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) print (count) 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 ))
何时用哪个?
需要多次遍历或索引访问:用列表推导式 只需要遍历一次:用生成器表达式