收到社区同学的反馈,希望MMClassification支持kfold-cross-valid交叉验证功能,开发同学立马安排起来,计划 24 小时内支持该特性。
然而,开发的时候却遇到了难题:深拷贝生成的 Config 对象没有 dump 方法。 于是打印对象的类型想一探究竟,发现深拷贝生成的对象并不是 Config 类型。那么真相只有一个,深拷贝出了问题。下面是描述问题的示例:
# https://github.com/open-mmlab/mmcv/blob/v1.4.5/mmcv/utils/config.py
>>> from mmcv import Config
>>> from copy import deepcopy
>>> cfg = Config.fromfile("./tests/data/config/a.py")
>>> new_cfg = deepcopy(cfg)
>>> type(cfg) == type(new_cfg)
False
>>> type(cfg), type(new_cfg)
(mmcv.utils.config.Config, mmcv.utils.config.ConfigDict)
可以发现,深拷贝生成的对象 new_cfg 竟然是 mmcv.utils.config.ConfigDict 类型而不是期望的 mmcv.uapplicationtils.config.Config 类gitlab型。
当然最后问题得到了解决,新特性也顺利上线。之前听到不少关于深拷贝问题的反馈,于是在这里分享一下解决深拷贝问题的全过程,希源码编辑器下载望对大家理解深拷贝有帮助。
要解决深拷github永久回家地址贝问题,首先要弄清楚什么是深拷贝以及它与浅https域名拷贝的区别。
浅拷贝 vs 深拷贝
当被拷贝的对象是不Git可变源码是什么意思对象时,例如字符串、无可变元素的元组,浅拷贝和深拷贝没有区别,都是返回被拷贝的对象,即没有发生拷贝。
>>> import copy
>>> a = (1, 2, 3) # 元组的元素均为不可变对象
>>> b = copy.copy(a) # 浅拷贝
>>> c = copy.deepcopy(a) # 深拷贝
>>> id(a), id(b), id(c) # 查看内存地址
(140093083446128, 140093083446128, 140093083446128)
从上面的例子可以看到,a、b 和 c 的地址是一致的,说明没有发生拷贝,三者指向同一个对象。
而当被拷贝的对象是可变对象时,源码编辑器例如字典、列表、有可变元素的元组等,浅拷贝和深拷贝有http://192.168.1.1登录区别。
浅拷贝会创建一个新对象,然后拷贝原对象approach中的引用。不同的是,深拷贝会创建一个新对象,https和http的区别然后递归地将深拷贝原对象中的值。
下面是一个说明浅拷贝和深拷贝都会创建一个新对象的例子。
>>> import copy
>>> a = [1, 2, 3]
>>> b = copy.copy(a)
>>> c = copy.deepcopy(a)
>>> id(a), id(b), id(c)
(140093084981120, 140093585550464, 140093085038592)
从上面的例子可以看到,a、b 和 c 的地址不一致,并不指向同一对象,即浅拷贝和深拷贝都创建了新对象。
但github开放私库如果 a 中有可变对appreciate象,那么对 a 的修改会影响 bhttps认证 的值,但不会影响 c 的值。
下面是被拷源码交易平台贝对象中有可变对象的例子。
>>> import copy
>>> a = [1, 2, [3, 4]]
>>> b = copy.copy(a)
>>> c = copy.deepcopy(a)
>>> id(a), id(b), id(c)
(140093082172288, 140093090759296, 140093081717760)
>>> id(a[2]), id(b[2]), id(c[2])
(140093087982272, 140093087982272, 140093084980288) # 可以看到 a[2]、b[2] 指向同一个对象
>>> a[2].append(5)
>>> a, b, c
([1, 2, [3, 4, 5]], [1, 2, [3, 4, 5]], [1, 2, [3, 4]])
从上面的例子可以APP看到,修改 a 中的可变对象时,使http协议用浅拷贝生成的对象 b 也发生了改变,而使用深拷源码编辑器贝生成的对象 c 没有发生改变。
问题的产生
在了apple解浅拷贝和深拷贝的区别后,我们回到本文的重点,Config 中的深拷贝为什么不能正常拷贝?答案是 Config 没有实现 d源码时代eepcopy 魔术方法。那么,是不是没有实现application deepcopy 的类一定会出现深拷贝类型不一致源码编辑器问题么?
我们先来gitlab看一个例子。
>>> from copy import deepcopy
>>> class HelloWorld:
def __init__(self):
self.attr1 = 'attribute1'
self.attr2 = 'attribute2'
>>> hello_world = HelloWorld()
>>> new_hello_world = deepcopy(hello_world)
>>> type(hello_world), type(new_hello_world)
(__main__.HelloWorld, __main__.HelloWorld)
从上面可以可以看到,深拷贝生成的对象 new_hello_worldhttps和http的区别 和被拷贝 hello_world 是一致的。
不禁陷入了沉思https和http的区别,Config 和 HelloWorld 都没有提供 deepcopy 方法,但为什么前者深拷贝的对象类型不一app小胖子致,而后者的http 500却一致。
为了弄清楚这背后的原因,需要阅approach读一下 copy 模块的源码。
下面是 copyhttp://www.baidu.com 模https认证块中有关深拷贝的源码。
# https://github.com/python/cpython/blob/3.10/Lib/copy.py#L128
# _deepcopy_dispatch 是一个字典,用于记录内置类型对应的深拷贝方法
_deepcopy_dispatch = d = {}
def _deepcopy_atomic(x, memo):
return x
# 对于不可变对象,直接返回被拷贝的对象
d[int] = _deepcopy_atomic
d[float] = _deepcopy_atomic
d[str] = _deepcopy_atomic
# 对于可变对象,首先创建空对象,然后深拷贝对象中的元素
def _deepcopy_list(x, memo, deepcopy=deepcopy):
y = []
memo[id(x)] = y
append = y.append
for a in x:
append(deepcopy(a, memo))
return y
d[list] = _deepcopy_list
def deepcopy(x, memo=None, _nil=[]):
"""Deep copy operation on arbitrary Python objects.
See the module's __doc__ string for more info.
"""
if memo is None:
memo = {}
# 如果对象 x 已被拷贝,则返回拷贝的对象 y
# 避免循环递归拷贝
d = id(x)
y = memo.get(d, _nil)
if y is not _nil:
return y
# 判断 x 的类型,如果是内置类型,调用对应的深拷贝方法
cls = type(x)
copier = _deepcopy_dispatch.get(cls)
if copier is not None:
y = copier(x, memo)
else:
if issubclass(cls, type):
y = _deepcopy_atomic(x, memo)
else:
# 如果能获取对象 x 的 __deepcopy__ 方法,则调用该方法进行深拷贝
copier = getattr(x, "__deepcopy__", None)
if copier is not None:
y = copier(memo)
else:
# https://github.com/python/cpython/blob/3.10/Lib/copyreg.py
reductor = dispatch_table.get(cls)
if reductor:
rv = reductor(x)
else:
# __reduce_ex__ 和 __reduce__ 用于序列化
# 它们会返回字符串或者元组
# https://docs.python.org/3/library/pickle.html#object.__reduce__
reductor = getattr(x, "__reduce_ex__", None)
if reductor is not None:
rv = reductor(4)
else:
reductor = getattr(x, "__reduce__", None)
if reductor:
rv = reductor()
else:
raise Error(
"un(deep)copyable object of type %s" % cls)
if isinstance(rv, str):
y = x
else:
# rv 是元组的情况下,调用 _reconstruct 创建对象
y = _reconstruct(x, memo, *rv)
# If is its own copy, don't memoize.
if y is not x:
memo[d] = y
_keep_alive(x, memo) # Make sure x lives at least as long as d
return y
对于 HelloWorld 对象 hello_world,cohttp://www.baidu.compy.deepcopy(hello_world) 首先调用 reduce_ex 序列http://192.168.1.1登录化对象,然后调用 _reconstructgiti轮胎 创建对象。
而对于 Config 对象 cfg,copy.deegit命令pcopy(cfg) 理应调用 Config 的 deepcopy 方法完成对象的拷贝,但是getattr(x, "__deepcopy__", None)
(上面源码的第 50 行appointment)却找不到 Config 的__deepcopy__
方法,因为 Config 没有实现该方法,于是便调用 Confiappointmentggithub开放私库 的 getattr(self, name) 方法,但该方法返回的却是 _cfg_dict (类型是 ConfigDihttp 404ct)的 deepcogithub中文官网网页py 方法。因此,深拷贝生成的对象new_cfg = copy.deepcopy(cfg)
的类型是 ConfigDict。
# https://github.com/open-mmlab/mmcv/blob/v1.4.4/mmcv/utils/config.py
class Config:
def __getattr__(self, name):
return getattr(self._cfg_dict, name)
问题https认证的解决
为了避免调用 _cfg_dict 的 deepcopgithub永久回家地址y 方法,我们需要给 Config 添加 deepHTTPcopy 方法,这样一来,copier = getapproachattr(x, "__deepcopy__", None)
就会调用 Config 的 deepcopy 完成对象的深拷贝。
# https://github.com/open-mmlab/mmcv/blob/master/mmcv/utils/config.py
class Config:
def __deepcopy__(self, memo):
cls = self.__class__
# 使用 __new__ 创建空对象
other = cls.__new__(cls)
# 将 other 对象添加到 memo 是为了避免循环创建同一个对象
# 更多关于 memo 的介绍可阅读 https://pymotw.com/3/copy/
memo[id(self)] = other
# 对象初始化
for key, value in self.__dict__.items():
super(Config, other).__setattr__(key, copy.deepcopy(value, memo))
return other
开发的同学往 MMCV 提了一个 PR(github.com/open-mmlab/…)解决最终解决了该问题,下源码面是 PR message 中的 Example。\
- 合入该 PR 前(MMCV 版本 <= 1giti轮胎.4源码之家.5)
>>> from mmcv import Config
>>> from copy import deepcopy
>>> cfg = Config.fromfile("./tests/data/config/a.py")
>>> new_cfg = deepcopy(cfg)
>>> type(cfg) == type(new_cfg)
False
>>> type(cfg), type(new_cfg)
(mmcv.utils.config.Config, mmcv.utils.config.ConfigDict)
可以发现,使用copy.deepcohttp://192.168.1.1登录py
拷贝的 Config 对象类型变成了 ConfigDict 类型,这并不符合我们的期望。\
- 合入该 PR 后(MMCV 版本 > 1.4.5)
>>> from mmcv import Config
>>> from copy import deepcopy
>>> cfg = Config.fromfile("./tests/data/config/a.py")
>>> new_cfg = deepcopy(cfg)
>>> type(cfg) == type(new_cfg)
True
>>> type(cfg), type(new_cfg)
(mmcv.utils.config.Config, mmcv.utils.config.Config)
>>> print(cfg._cfg_dict == new_cfg._cfg_dict)
True
>>> print(cfg._cfg_dict is new_cfg._cfg_dict)
False
合入该 PR 后,拷贝的 Config 对象符合期望。
参考文献
- copy — Shallow and deep copy operappointmentations
- what-is-the-difference-between-reduce-and-reduce-ex
- how-to-override-the-copy-deepcopy-operations-for-a-pythongithub-object