更改模块目录后 Python 进行 pickling
- 2025-04-16 08:57:00
- admin 原创
- 17
问题描述:
我最近更改了程序的目录布局:之前,我把所有模块都放在“main”文件夹中。现在,我把它们移动到了一个以程序名称命名的目录中,并__init__.py
在那里创建了一个包。
现在我的主目录中有一个单独的 .py 文件,用于启动我的程序,这更加整洁。
无论如何,尝试从我程序的先前版本中加载pickle文件失败了。我收到“ImportError: No module named tools”的错误信息——我猜是因为我的模块之前在主文件夹中,现在在whyteboard.tools中,而不是普通的tools。但是,导入tools模块的代码与它位于同一目录中,所以我怀疑是否有必要指定包。
因此,我的程序目录看起来像这样:
whyteboard-0.39.4
-->whyteboard.py
-->README.txt
-->CHANGELOG.txt
---->whyteboard/
---->whyteboard/__init__.py
---->whyteboard/gui.py
---->whyteboard/tools.py
whyteboard.py 会从 whyteboard/gui.py 中启动一段代码,从而启动 GUI。在目录重新整理之前,这个 pickling 问题肯定不会发生。
解决方案 1:
正如pickle 的文档所说,为了保存和恢复类实例(实际上也是一个函数),您必须遵守某些约束:
pickle 可以透明地保存和恢复类实例,但是类定义必须是可导入的,并且与存储对象时位于同一模块中
whyteboard.tools
不是“与...相同的模块” (尽管它可以被同一个包中的其他模块导入,但最终还是会以...结尾:这绝对至关重要,否则同一个包中的模块与另一个包中的模块导入的相同模块最终会出现多个甚至可能冲突的条目!)。tools
`import toolssys.modules
sys.modules['whyteboard.tools']`
如果您的 pickle 文件采用良好/高级格式(而不是出于兼容性原因而默认使用的旧式 ascii 格式),那么在执行此类更改后迁移它们实际上可能并不sys.modules
像“编辑文件”(二进制文件等等)那么简单,尽管另一个答案建议这样做。我建议您编写一个小的“pickle 迁移脚本”:让它像这样修补……:
import sys
from whyteboard import tools
sys.modules['tools'] = tools
然后将cPickle.load
每个文件del sys.modules['tools']
和cPickle.dump
每个加载的对象返回到文件:临时的额外条目sys.modules
应该让泡菜成功加载,然后再次转储它们应该使用实例类的正确模块名称(删除额外的条目应该确保这一点)。
解决方案 2:
这可以通过使用以下自定义“unpickler”来完成find_class()
:
import io
import pickle
class RenameUnpickler(pickle.Unpickler):
def find_class(self, module, name):
renamed_module = module
if module == "tools":
renamed_module = "whyteboard.tools"
return super(RenameUnpickler, self).find_class(renamed_module, name)
def renamed_load(file_obj):
return RenameUnpickler(file_obj).load()
def renamed_loads(pickled_bytes):
file_obj = io.BytesIO(pickled_bytes)
return renamed_load(file_obj)
那么您需要使用renamed_load()
instead ofpickle.load()
和renamed_loads()
instead of pickle.loads()
。
解决方案 3:
发生在我身上,通过在加载 pickle 之前将模块的新位置添加到 sys.path 来解决它:
import sys
sys.path.append('path/to/whiteboard')
f = open("pickled_file", "rb")
pickle.load(f)
解决方案 4:
pickle
通过引用序列化类,因此即使你更改了类的路径,也无法进行 unpickle 操作,因为找不到该类。如果使用dill
而不是pickle
,则可以通过引用或直接序列化类(直接序列化类本身,而不是其导入路径)。只需在 之后dump
和 之前更改类定义,即可轻松模拟这种情况load
。
Python 2.7.8 (default, Jul 13 2014, 02:29:54)
[GCC 4.2.1 Compatible Apple Clang 4.1 ((tags/Apple/clang-421.11.66))] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dill
>>>
>>> class Foo(object):
... def bar(self):
... return 5
...
>>> f = Foo()
>>>
>>> _f = dill.dumps(f)
>>>
>>> class Foo(object):
... def bar(self, x):
... return x
...
>>> g = Foo()
>>> f_ = dill.loads(_f)
>>> f_.bar()
5
>>> g.bar(4)
4
解决方案 5:
这是 pickle 的正常行为,unpickled 对象需要有其定义模块可导入。
您应该能够通过编辑腌制文件来更改模块路径(即从tools
到),因为它们通常是简单的文本文件。whyteboard.tools
解决方案 6:
对于像我这样需要更新大量 pickle 转储的人来说,这里有一个实现@Alex Martelli 的优秀建议的函数:
import sys
from types import ModuleType
import pickle
# import torch
def update_module_path_in_pickled_object(
pickle_path: str, old_module_path: str, new_module: ModuleType
) -> None:
"""Update a python module's dotted path in a pickle dump if the
corresponding file was renamed.
Implements the advice in https://stackoverflow.com/a/2121918.
Args:
pickle_path (str): Path to the pickled object.
old_module_path (str): The old.dotted.path.to.renamed.module.
new_module (ModuleType): from new.location import module.
"""
sys.modules[old_module_path] = new_module
dic = pickle.load(open(pickle_path, "rb"))
# dic = torch.load(pickle_path, map_location="cpu")
del sys.modules[old_module_path]
pickle.dump(dic, open(pickle_path, "wb"))
# torch.save(dic, pickle_path)
就我而言,这些转储是 PyTorch 模型检查点。因此注释掉了torch.load/save()
。
例子
from new.location import new_module
for pickle_path in ('foo.pkl', 'bar.pkl'):
update_module_path_in_pickled_object(
pickle_path, "old.module.dotted.path", new_module
)
解决方案 7:
当你尝试加载包含类引用的 pickle 文件时,必须遵循保存 pickle 时的结构。如果你想在其他地方使用 pickle,则必须指明这个类或其他对象的位置;所以,按照下面的方法操作可以节省时间:
import sys
sys.path.append('path/to/folder containing the python module')
解决方案 8:
我知道这已经有一段时间了,但这为我解决了这个问题:
本质上,使用完整的导入路径(例如concurrent.run_concurrent
),而不仅仅是模块名称(例如run_concurrent
)
共享代码:
import importlib
module_path="concurrent.run_concurrent"
...
module = importlib.util.module_from_spec(spec)
原文(不好):
module_name = module_path.split(".")[-1]
spec = importlib.util.spec_from_file_location(module_name, filepath)
...
sys.modules[module_name] = module
替换为以下内容(删除所有对 的引用module_name
):
# Remove "module_name"
# Use "module_path" instead of "module_name"
spec = importlib.util.spec_from_file_location(module_path, filepath)
...
# Use "module_path" instead of "module_name"
sys.modules[module_path] = module
解决方案 9:
根据这个答案实现,下面的版本使用字典来支持保存泡菜后的多个模块重命名:
import pickle
class UnpicklerRM(pickle.Unpickler):
modNameMap = {
"savedModelName" : "newMadelName",
#...
}
def find_class(self, moduleName:str, objName:str):
if moduleName in self.modNameMap:
moduleName = self.modNameMap[moduleName]
return super().find_class(moduleName, objName)
#read pickle using module name changed after saving
with open('fname.pickle', 'rb') as f:
data = UnpicklerRM(f).load()
#read pickle using module name when saving
with open('fname.pickle', 'rb') as f:
data = pickle.load(f)
扫码咨询,免费领取项目管理大礼包!