当前位置: 首页 > news >正文

Python闭包与装饰器从入门到精通之深浅拷贝:解决闭包与装饰器中的可变对象陷阱

第五章 深浅拷贝:解决闭包与装饰器中的可变对象陷阱

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(i=i): 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( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) def log_decorator(level=logging.INFO, log_args=True, log_return=True, log_time=True): """ 通用日志装饰器 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(level=logging.DEBUG) def add(a, b): return a + b @log_decorator(log_args=False) def sensitive_operation(password): # 敏感操作,不记录参数 return "操作成功" @log_decorator(log_time=False) 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_size=128): """简单的缓存装饰器""" 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_size=10) 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_size=128) def fibonacci(n): if n <= 1: return n return fibonacci(n-1) + fibonacci(n-2)

6.5 重试机制装饰器

在处理网络请求、数据库操作等可能会临时失败的操作时,重试机制是非常有用的。

def retry_decorator(max_retries=3, delay=1, backoff=2, 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_retries=5, delay=1, backoff=2) 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 用于修改嵌套作用域(外部函数)的变量 两者都不能用于创建新变量

装饰器在函数定义时执行,不是在函数调用时执行 多个装饰器的装饰顺序是从下到上,调用顺序是从上到下

如果对象中没有嵌套可变对象,使用浅拷贝即可 如果对象中有嵌套可变对象,并且需要完全独立的副本,使用深拷贝 深拷贝会消耗更多内存和时间,不要滥用

如果在装饰器内部调用了被装饰的函数,会导致无限递归 解决方法:在装饰器内部调用原函数,而不是被装饰后的函数

闭包引用的是变量本身,而不是变量在定义时的值 解决方法:使用默认参数捕获当前值,或者使用立即执行的闭包

http://www.jsqmd.com/news/1135446/

相关文章:

  • 3种方案实测:GEO系统如何提升网站AI引荐率
  • 【Java从入门到入土】47:构建工具:Maven与Gradle的战争
  • 如何快速搭建企业级后台管理系统:Layui-Admin完整实战指南
  • 新手友好!基于零代码平台学生用户画像-考勤主题标签构建全流程演示
  • 数据库Day1
  • 消息列表虚拟滚动_message-list
  • YOLOv8手语识别检测系统(项目源码+YOLO数据集+模型权重+UI界面+python+深度学习+环境配置)
  • 聊天智能体开发基础
  • 医美海报别再踩坑了!2026靠谱设计服务商推荐,合规、不贵、有质感
  • QRazyBox:让破损二维码重获新生的专业修复工具
  • jesd204b学习记录
  • 信息安全章节核心知识梳理
  • MCP基础版本
  • 基于Metasploit的内网渗透实战:从外网突破到域控攻陷
  • Claude 聊天记录导出|AI 导出鸭高效实现各类格式转换
  • 第一篇:Java 语言核心特性与运行机制
  • iCloud Photos Downloader:终极免费命令行工具,完整备份苹果照片库
  • 终极指南:如何用PKHeX自动合法性插件快速管理宝可梦数据
  • ASM330LHH与STM32F303VE的6DoF运动跟踪系统设计
  • 超自动化运维如何优化IT成本结构?
  • 01 模型剪枝是什么?为什么深度模型需要剪枝?
  • MC6470与PIC18LF4682在工业自动化中的高精度控制方案
  • Google Sheets API接入指南:服务账号认证与自动化实战
  • 好用还专业!2026年必备AI论文工具榜单,AI工具一键写高质论文
  • LeetCode //C - 1137. N-th Tribonacci Number
  • ClickHouse 填平 Elasticsearch 护城河?日志分析新选择!
  • YOLOv8热成像人员识别检测系统(项目源码+YOLO数据集+模型权重+UI界面+python+深度学习+环境配置)
  • 《超简单:用 Python 让 Excel 飞起来》读书笔记:第7章 案例04 举一反三 添加图例
  • Kimi LeetCode 3495. 使数组元素都变为零的最少操作次数 Java实现
  • WebSocket 安全实战(二):多层防御穿透攻击模型与长连接底层架构缺陷深度解析