更改模块目录后的 Python pickle

问题描述 投票:0回答:9

我最近更改了程序的目录布局:之前,我将所有模块都放在“main”文件夹中。现在,我已将它们移动到以程序命名的目录中,并在那里放置一个

__init__.py
来制作一个包。

现在我的主目录中有一个 .py 文件,用于启动我的程序,这更加整洁。

无论如何,尝试从我的程序的早期版本加载腌制文件失败了。我收到“ImportError:没有名为工具的模块” - 我猜这是因为我的模块以前位于主文件夹中,现在位于 Whyteboard.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。这个酸洗问题在目录重新组织之前肯定不会发生。

python pickle
9个回答
155
投票

正如 pickle 的文档所说,为了保存和恢复类实例(实际上也是一个函数),您必须遵守某些约束:

pickle 可以保存和恢复类 实例透明,但是 类定义必须是可导入的 并住在同一个模块中 对象已存储

whyteboard.tools
不是“与”
tools
相同的模块(即使它可以由同一包中的其他模块通过
import tools
导入,它最终会在
sys.modules
中作为
sys.modules['whyteboard.tools']
:这个绝对重要,否则同一个包中的一个模块与另一个包中的一个模块导入的相同模块最终会出现多个并且可能存在冲突的条目!)。

如果您的pickle文件采用良好/高级格式(而不是旧的ascii格式,后者只是出于兼容性原因而默认),那么在执行此类更改后迁移它们实际上可能像“编辑文件”(这是二进制 &c...!),尽管另一个答案表明了这一点。 相反,我建议您制作一个小“pickle 迁移脚本”:让它像这样修补

sys.modules
...:

import sys
from whyteboard import tools

sys.modules['tools'] = tools

然后

cPickle.load
每个文件,
del sys.modules['tools']
cPickle.dump
每个加载的对象回到文件:
sys.modules
中的临时额外条目应该让泡菜成功加载,然后再次转储它们应该使用正确的模块-实例类的名称(删除额外的条目应该可以确保这一点)。


53
投票

这可以通过使用

find_class()
:

的自定义“unpickler”来完成
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()
代替
pickle.load()
,使用
renamed_loads()
代替
pickle.loads()


32
投票

发生在我身上,通过在加载pickle之前将模块的新位置添加到sys.path来解决它:

import sys
sys.path.append('path/to/whiteboard')
f = open("pickled_file", "rb")
pickle.load(f)

14
投票

pickle
通过引用序列化类,因此如果您更改类的生存位置,它不会解封,因为找不到该类。 如果您使用
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

4
投票

这是pickle的正常行为,未pickle的对象需要使其定义模块可导入

您应该能够通过编辑腌制文件来更改模块路径(即从

tools
whyteboard.tools
),因为它们通常是简单的文本文件。


3
投票

对于像我这样需要更新大量泡菜转储的人,这里有一个实现@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
    )

1
投票

当您尝试加载包含类引用的pickle文件时,您必须遵守保存pickle时相同的结构。如果你想在其他地方使用pickle,你必须告诉这个类或其他对象在哪里;因此,执行以下操作即可挽救这一天:

import sys
sys.path.append('path/to/folder containing the python module')

0
投票

我知道这已经有一段时间了,但这为我解决了这个问题:

本质上,使用完整的导入路径(例如。

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

0
投票

在此answer上实现,下面的版本使用字典来支持保存pickle后的多个模块重命名:

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)
最新问题
© www.soinside.com 2019 - 2025. All rights reserved.