Python中用deepcopy复制含BeautifulSoup的URL类触发无限递归报错
递归错误的根本原因
首先纠正对deepcopy的认知偏差:你原本认为的“deepcopy只需要把每个self属性的值复制到新实例”,只是对基础内置类型的简化描述,实际deepcopy的运行逻辑是递归遍历目标对象的所有嵌套子对象、子对象的子对象,逐层生成独立副本,同时通过内存ID映射表识别循环引用避免无限递归。
你遇到的递归完全来自类属性里的BeautifulSoup对象:
- BeautifulSoup解析生成的文档树天生存在大量双向循环引用:比如每个HTML节点会保存父节点的引用、父节点又反向持有所有子节点的引用,整棵树是带环的结构。
- BeautifulSoup的类实现没有正确适配Python的deepcopy机制,当deepcopy沿着它的内部引用链遍历的时候,无法通过自带的缓存映射识别这些循环引用,会沿着引用环无限递归,最终撑爆Python的默认递归深度阈值,触发栈溢出错误。
这完全匹配你观察到的所有现象:
- 把
deepcopy换成普通对象引用赋值时,不会走递归遍历的逻辑,错误直接消失 - 只要去掉
set_soup里的BeautifulSoup赋值,类里剩下的都是字符串、整数、列表这类无环的普通属性,deepcopy可以正常完成遍历,不会触发递归 - 单独保留网页读取逻辑、不生成soup对象时,同样没有带环的特殊结构,deepcopy可以正常运行
为什么常规深浅拷贝教程解决不了这个问题
大部分入门教程只演示数字、字符串、列表、字典这类内置基础类型的深浅拷贝差异,不会覆盖第三方库对象的特殊边界场景:当某个第三方对象的内部结构存在未适配deepcopy的循环引用时,即使对象本身可以正常读写,直接对它做全量deepcopy也会触发无限递归。你遇到的场景就是典型的「第三方库对象和内置deepcopy机制不兼容」问题,不是深浅拷贝概念本身的问题。
可行的落地解决方案
核心思路是绕开对BeautifulSoup对象的全量递归深拷贝,同时满足其他属性独立副本的需求:
- 给自定义的URL类实现
__deepcopy__魔术方法,明确告诉deepcopy遇到soup属性时的处理规则,跳过对soup对象的递归遍历:
这种实现下,你对URL类做deepcopy时,def __deepcopy__(self, memo): # 创建不带初始化的新类实例 new_obj = self.__class__.__new__(self.__class__) # 注册当前对象的映射,从根源上避免循环递归 memo[id(self)] = new_obj # 逐属性处理,普通属性走正常deepcopy,soup直接共享引用 for attr_name, attr_value in self.__dict__.items(): if attr_name == 'soup': setattr(new_obj, attr_name, attr_value) else: setattr(new_obj, attr_name, deepcopy(attr_value, memo)) return new_objurl、depth、parsed_list这类普通属性都会生成完全独立的副本,修改副本不会影响原对象;同时soup对象因为本身是无状态的解析结果,多实例共享引用不会带来任何副作用,完全绕开递归问题。 - 如果不需要在类实例里长期保存soup对象,也可以在做deepcopy前临时把soup属性清空,拷贝完成后再恢复引用,从流程上避开deepcopy遍历带环结构的路径。
- 存储到全局
url_dict里的对象,直接去掉soup属性,等真正需要解析页面时再临时拉取内容生成soup,从根源上避免把带环的BeautifulSoup对象纳入deepcopy的遍历范围。
内容的提问来源于stack exchange,提问作者z7r1k3
相关产品推荐
相关产品推荐

