
第五章 深浅拷贝解决闭包与装饰器中的可变对象陷阱5.1 可变对象与不可变对象的本质区别在 Python 中所有对象都可以分为两类可变对象和不可变对象。不可变对象对象创建后其值不能被修改。如int、float、str、tuple、bool 可变对象对象创建后其值可以被修改。如list、dict、set、自定义对象5.1.1 赋值操作的本质引用传递在 Python 中赋值操作本质上是将变量名绑定到对象上而不是复制对象。# 不可变对象示例 a 10 b a a 20 print(a) # 输出20 print(b) # 输出10对于不可变对象当我们修改 a 的值时实际上是创建了一个新的对象 20然后将 a 重新绑定到这个新对象上。b 仍然绑定到原来的对象 10所以不受影响。# 可变对象示例 list1 [1, 2, 3] list2 list1 list1.append(4) print(list1) # 输出[1, 2, 3, 4] print(list2) # 输出[1, 2, 3, 4]对于可变对象当我们修改 list1 的内容时实际上是直接修改了对象本身。由于 list1 和 list2 绑定到同一个对象所以 list2 也会受到影响。这就是很多 bug 的根源在闭包和装饰器中这个问题尤为突出。5.2 浅拷贝操作可变类型为了避免多个变量引用同一个可变对象带来的问题我们需要拷贝对象。Python 提供了两种拷贝方式浅拷贝和深拷贝。5.2.1 什么是浅拷贝浅拷贝Shallow Copy会创建一个新的对象但这个新对象中的元素仍然是原对象中元素的引用。简单来说浅拷贝只拷贝 第一层不拷贝嵌套的可变对象。5.2.2 浅拷贝的实现方式Python 中有多种实现浅拷贝的方式使用 copy.copy () 函数 使用切片操作list [:] 使用工厂函数list ()、dict ()、set ()import copy # 原列表包含一个嵌套列表 original [1, 2, [3, 4]] # 三种浅拷贝方式 copy1 copy.copy(original) copy2 original[:] copy3 list(original) # 修改原列表的第一层元素 original[0] 100 print(修改第一层元素后) print(f原列表{original}) # 输出[100, 2, [3, 4]] print(f拷贝1{copy1}) # 输出[1, 2, [3, 4]] print(f拷贝2{copy2}) # 输出[1, 2, [3, 4]] print(f拷贝3{copy3}) # 输出[1, 2, [3, 4]] # 修改原列表的嵌套列表元素 original[2].append(5) print(\n修改嵌套元素后) print(f原列表{original}) # 输出[100, 2, [3, 4, 5]] print(f拷贝1{copy1}) # 输出[1, 2, [3, 4, 5]] print(f拷贝2{copy2}) # 输出[1, 2, [3, 4, 5]] print(f拷贝3{copy3}) # 输出[1, 2, [3, 4, 5]]可以看到修改原列表的第一层元素时拷贝的列表不受影响 修改原列表的嵌套可变元素时拷贝的列表也会受到影响 这就是浅拷贝的局限性它不能处理嵌套的可变对象。5.3 深拷贝操作可变类型5.3.1 什么是深拷贝深拷贝Deep Copy会创建一个完全独立的新对象它会递归地拷贝原对象中的所有元素包括嵌套的可变对象。5.3.2 深拷贝的实现方式使用 copy.deepcopy () 函数实现深拷贝import copy original [1, 2, [3, 4]] # 深拷贝 deep_copy copy.deepcopy(original) # 修改原列表的嵌套列表元素 original[2].append(5) print(f原列表{original}) # 输出[1, 2, [3, 4, 5]] print(f深拷贝{deep_copy}) # 输出[1, 2, [3, 4]]完美现在修改原列表的嵌套元素不会影响深拷贝的列表了。5.3.3 浅拷贝与深拷贝的核心区别表格特性浅拷贝深拷贝拷贝层次只拷贝第一层递归拷贝所有层次嵌套可变对象共享引用创建新对象内存占用较少较多执行速度较快较慢适用场景没有嵌套可变对象有嵌套可变对象5.4 深浅拷贝在闭包与装饰器中的应用现在我们来看一个闭包中引用可变对象的经典陷阱def create_functions(): functions [] for i in range(3): def func(): print(i) functions.append(func) return functions funcs create_functions() # 调用三个函数 funcs[0]() # 输出2 funcs[1]() # 输出2 funcs[2]() # 输出2为什么三个函数都输出 2而不是我们期望的 0、1、2这是因为闭包引用的是变量 i 本身而不是变量 i 在定义时的值。当循环结束后i 的值变成了 2所以当我们调用这三个函数时它们都打印出 2。解决方案 1使用默认参数捕获当前值def create_functions(): functions [] for i in range(3): # 使用默认参数捕获当前的i值 def func(ii): print(i) functions.append(func) return functions funcs create_functions() funcs[0]() # 输出0 funcs[1]() # 输出1 funcs[2]() # 输出2解决方案 2使用闭包立即执行def create_functions(): functions [] for i in range(3): # 定义一个立即执行的闭包 def make_func(x): def func(): print(x) return func functions.append(make_func(i)) return functions funcs create_functions() funcs[0]() # 输出0 funcs[1]() # 输出1 funcs[2]() # 输出2再来看一个闭包中引用可变对象的例子def make_counter(): count [0] # 使用列表作为可变对象 def counter(): count[0] 1 return count[0] return counter counter1 make_counter() print(counter1()) # 输出1 print(counter1()) # 输出2 counter2 make_counter() print(counter2()) # 输出1 print(counter1()) # 输出3这个例子是正确的因为每个 make_counter 调用都会创建一个新的列表 count。但是如果我们不小心在外部共享了这个可变对象就会出问题shared_count [0] def make_counter(): def counter(): shared_count[0] 1 return shared_count[0] return counter counter1 make_counter() counter2 make_counter() print(counter1()) # 输出1 print(counter2()) # 输出2期望是1在装饰器中也会遇到类似的问题。例如一个统计函数调用次数的装饰器def count_calls(func): count 0 functools.wraps(func) def wrapper(*args, **kwargs): nonlocal count count 1 print(f函数 {func.__name__} 已被调用 {count} 次) return func(*args, **kwargs) return wrapper count_calls def func1(): pass count_calls def func2(): pass func1() # 输出函数 func1 已被调用 1 次 func1() # 输出函数 func1 已被调用 2 次 func2() # 输出函数 func2 已被调用 1 次 func1() # 输出函数 func1 已被调用 3 次这个例子是正确的因为每个被装饰的函数都会有自己独立的 count 变量。但是如果我们在装饰器外部定义 count 变量就会导致所有被装饰的函数共享同一个计数器count 0 def count_calls(func): functools.wraps(func) def wrapper(*args, **kwargs): nonlocal count count 1 print(f函数 {func.__name__} 已被调用 {count} 次) return func(*args, **kwargs) return wrapper count_calls def func1(): pass count_calls def func2(): pass func1() # 输出函数 func1 已被调用 1 次 func2() # 输出函数 func2 已被调用 2 次期望是1总结在闭包和装饰器中我们应该尽量避免引用全局可变对象。如果需要保持状态应该将状态变量定义在外部函数内部这样每个闭包实例都会有自己独立的状态。当我们需要在闭包或装饰器中处理可变对象时一定要注意是否需要进行拷贝。如果我们不希望修改原对象或者不希望多个闭包共享同一个对象就应该使用浅拷贝或深拷贝。第六章 实战应用闭包与装饰器的常见使用场景6.1 日志记录装饰器日志记录是装饰器最常用的场景之一。我们可以编写一个通用的日志装饰器自动记录函数的调用信息、参数、返回值和执行时间。import logging import functools import time # 配置日志 logging.basicConfig( levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s ) def log_decorator(levellogging.INFO, log_argsTrue, log_returnTrue, log_timeTrue): 通用日志装饰器 Args: level: 日志级别 log_args: 是否记录参数 log_return: 是否记录返回值 log_time: 是否记录执行时间 def decorator(func): logger logging.getLogger(func.__module__) functools.wraps(func) def wrapper(*args, **kwargs): # 构建日志消息 log_msg f调用函数 {func.__name__} if log_args: log_msg f参数args{args}, kwargs{kwargs} # 记录开始日志 logger.log(level, log_msg) # 执行函数并计时 start_time time.time() try: result func(*args, **kwargs) status 成功 except Exception as e: result None status f失败错误{e} logger.exception(f函数 {func.__name__} 执行异常) raise finally: end_time time.time() execution_time end_time - start_time # 构建结束日志消息 end_msg f函数 {func.__name__} 执行{status} if log_return and status 成功: end_msg f返回值{result} if log_time: end_msg f耗时{execution_time:.4f}秒 logger.log(level, end_msg) return result return wrapper return decorator使用示例log_decorator(levellogging.DEBUG) def add(a, b): return a b log_decorator(log_argsFalse) def sensitive_operation(password): # 敏感操作不记录参数 return 操作成功 log_decorator(log_timeFalse) def fast_function(): return 快速函数 add(3, 5) sensitive_operation(123456) fast_function()6.2 性能统计装饰器我们可以编写一个性能统计装饰器统计函数的调用次数、总执行时间、平均执行时间等指标。def performance_decorator(func): 性能统计装饰器 call_count 0 total_time 0.0 functools.wraps(func) def wrapper(*args, **kwargs): nonlocal call_count, total_time call_count 1 start_time time.time() try: return func(*args, **kwargs) finally: end_time time.time() execution_time end_time - start_time total_time execution_time # 每调用10次打印一次统计信息 if call_count % 10 0: avg_time total_time / call_count print(f性能统计函数 {func.__name__} 已调用 {call_count} 次 f总耗时{total_time:.4f}秒平均耗时{avg_time:.6f}秒) return wrapper使用示例performance_decorator def calculate_sum(n): return sum(range(n)) for i in range(100): calculate_sum(10000)6.3 权限验证装饰器在 Web 开发中权限验证是一个非常常见的需求。我们可以使用装饰器来实现统一的权限验证逻辑。def login_required(func): 要求用户登录的装饰器 functools.wraps(func) def wrapper(*args, **kwargs): # 模拟检查用户是否登录 current_user get_current_user() if not current_user: raise PermissionError(用户未登录请先登录) return func(*args, **kwargs) return wrapper def permission_required(permission): 要求用户拥有特定权限的装饰器 def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): current_user get_current_user() if not current_user: raise PermissionError(用户未登录请先登录) if permission not in current_user.get(permissions, []): raise PermissionError(f用户没有权限{permission}) return func(*args, **kwargs) return wrapper return decorator # 模拟获取当前用户 def get_current_user(): # 在实际应用中这里会从session或token中获取用户信息 return { id: 1, username: admin, permissions: [view, edit, delete] }使用示例login_required def view_profile(): return 个人资料页面 permission_required(edit) def edit_profile(): return 编辑个人资料 permission_required(delete) def delete_user(user_id): return f删除用户 {user_id} print(view_profile()) print(edit_profile()) print(delete_user(123))6.4 缓存装饰器缓存是提高程序性能的重要手段。我们可以编写一个缓存装饰器自动缓存函数的计算结果。def cache_decorator(max_size128): 简单的缓存装饰器 cache {} def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): # 将参数转换为可哈希的键 key (args, frozenset(kwargs.items())) if key in cache: print(f从缓存中获取 {func.__name__}{args} 的结果) return cache[key] # 计算结果并缓存 result func(*args, **kwargs) # 如果缓存超过最大大小删除最早的条目 if len(cache) max_size: cache.pop(next(iter(cache))) cache[key] result return result # 添加清除缓存的方法 wrapper.clear_cache lambda: cache.clear() return wrapper return decorator使用示例cache_decorator(max_size10) def fibonacci(n): 计算斐波那契数列 if n 1: return n return fibonacci(n-1) fibonacci(n-2) print(fibonacci(30)) # 第一次计算较慢 print(fibonacci(30)) # 从缓存中获取很快 print(fibonacci(30)) # 从缓存中获取很快 # 清除缓存 fibonacci.clear_cache() print(fibonacci(30)) # 重新计算当然Python 标准库中已经提供了一个更强大的缓存装饰器 functools.lru_cache在实际开发中推荐使用from functools import lru_cache lru_cache(max_size128) def fibonacci(n): if n 1: return n return fibonacci(n-1) fibonacci(n-2)6.5 重试机制装饰器在处理网络请求、数据库操作等可能会临时失败的操作时重试机制是非常有用的。def retry_decorator(max_retries3, delay1, backoff2, exceptions(Exception,)): 重试装饰器 Args: max_retries: 最大重试次数 delay: 初始重试间隔秒 backoff: 重试间隔的倍增系数 exceptions: 需要重试的异常类型 def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): retries 0 current_delay delay while retries max_retries: try: return func(*args, **kwargs) except exceptions as e: retries 1 if retries max_retries: print(f函数 {func.__name__} 经过 {max_retries} 次重试后仍然失败放弃重试) raise print(f函数 {func.__name__} 执行失败第 {retries} 次重试 f等待 {current_delay} 秒错误{e}) time.sleep(current_delay) current_delay * backoff # 理论上不会执行到这里 return None return wrapper return decorator使用示例import random retry_decorator(max_retries5, delay1, backoff2) def unreliable_network_call(): 模拟一个不稳定的网络请求 if random.random() 0.7: raise ConnectionError(网络连接失败) return 网络请求成功 try: result unreliable_network_call() print(result) except Exception as e: print(f最终失败{e})第七章 总结与常见问题7.1 全文知识点总结闭包核心知识体系定义引用了外部函数变量的内部函数并且被返回在外部调用 形成条件嵌套函数、引用外部变量、返回内部函数 特性保留外部函数的作用域在多次调用之间保持状态 关键字nonlocal 用于修改外部函数变量 本质函数对象 它所引用的环境装饰器核心知识体系本质一个特殊的闭包接收函数作为参数返回增强后的函数 核心思想开放封闭原则无侵入式增强函数功能 写法 传统写法func decorator (func) 语法糖decorator 带参数装饰器三层嵌套函数 通用模板使用 * args 和 **kwargs 处理任意参数使用 functools.wraps 保留元信息 高级用法多个装饰器叠加、带参数的装饰器深浅拷贝核心知识体系可变与不可变对象赋值是引用传递修改可变对象会影响所有引用它的变量 浅拷贝只拷贝第一层不拷贝嵌套可变对象 深拷贝递归拷贝所有层次创建完全独立的对象 应用解决闭包和装饰器中可变对象共享的问题7.2 常见问题与误区global 用于修改全局作用域的变量 nonlocal 用于修改嵌套作用域外部函数的变量 两者都不能用于创建新变量装饰器在函数定义时执行不是在函数调用时执行 多个装饰器的装饰顺序是从下到上调用顺序是从上到下如果对象中没有嵌套可变对象使用浅拷贝即可 如果对象中有嵌套可变对象并且需要完全独立的副本使用深拷贝 深拷贝会消耗更多内存和时间不要滥用如果在装饰器内部调用了被装饰的函数会导致无限递归 解决方法在装饰器内部调用原函数而不是被装饰后的函数闭包引用的是变量本身而不是变量在定义时的值 解决方法使用默认参数捕获当前值或者使用立即执行的闭包