更改模块路径后反序列化Python对象

28

我正在尝试将同事构建的 Project A 集成到另一个 Python 项目中。现在这位同事在他的代码中没有使用相对导入,而是采用了

from packageA.moduleA import ClassA
from packageA.moduleA import ClassB

结果因此使用cPickle对类进行了串行化。为了整洁,我希望隐藏他的(A项目)构建的程序包在我的项目中。然而这会改变packageA中定义的类的路径。没问题,我将使用以下方式重新定义导入:

and consequently pickled the classes with cPickle. For neatness I'd like to hide the package that his (Project A) built inside my project. This however changes the path of the classes defined in packageA. No problem, I'll just redefine the import using

from ..packageA.moduleA import ClassA
from ..packageA.moduleA import ClassB

但现在使用 unpickling 类时会出现以下错误信息

    with open(fname) as infile: self.clzA = cPickle.load(infile)
ImportError: No module named packageA.moduleA
所以为什么cPickle似乎看不到模块定义。我需要将packageA的根目录添加到系统路径中吗?这是解决问题的正确方式吗? cPickled文件大致如下:
ccopy_reg
_reconstructor
p1
(cpackageA.moduleA
ClassA
p2
c__builtin__
object
p3
NtRp4

旧的项目层次结构是这样的

packageA/
    __init__.py
    moduleA.py
    moduleB.py
packageB/
    __init__.py
    moduleC.py
    moduleD.py

我想把所有东西放进一个WrapperPackage中。

MyPackage/
.. __init__.py
.. myModuleX.py
.. myModuleY.py
WrapperPackage/
.. __init__.py
.. packageA/
   .. __init__.py
   .. moduleA.py
   .. moduleB.py
.. packageB/
   .. __init__.py
   .. moduleC.py
   .. moduleD.py

我在为KRunner编写插件时遇到了这个问题。Plasma使用的脚本引擎使用路径钩子来创建一个虚假的包,我的代码就在其中。不幸的是,我找不到任何解决方法。我唯一能做的就是手动删除它们的路径钩子,清除sys缓存并重新导入所有内容。但如果你有一些被pickle的数据,那么你必须使用相同的类名来unpickle它(这意味着你必须保留from packageA.moduleA import ClassA)。 请注意,一旦unpickled,您可以使用正确的名称重新pickle它们。 - Bakuriu
4个回答

32

为了让pickle导入起作用,您需要创建别名;请将以下内容添加到WrapperPackage软件包的__init__.py文件中:

你需要为pickle导入创建一个别名;请将以下内容添加到WrapperPackage软件包的__init__.py文件中:
from .packageA import * # Ensures that all the modules have been loaded in their new locations *first*.
from . import packageA  # imports WrapperPackage/packageA
import sys
sys.modules['packageA'] = packageA  # creates a packageA entry in sys.modules

也许你需要创建更多的条目:

sys.modules['packageA.moduleA'] = moduleA
# etc.

现在cPickle将再次在它们的旧位置找到packageA.moduleApackageA.moduleB

您可能希望在此之后重新编写pickle文件,此时将使用新的模块位置。上面创建的附加别名应确保涉及的模块具有新的位置名称,以便cPickle在再次编写类时能够捕获。


我需要在 WrapperPackege.__init__.py 中做这个吗? - Matti Lyra
@MattiLyra:你说得完全正确。我根本避免使用相对导入,所以我搞错了语法。已经更正过来了。 - Martijn Pieters
@MattiLyra:你是否通过再次转储已加载的数据来重新创建pickle? - Martijn Pieters
@MartinPieters 是的,obj = pickle.load(fh) -> pickle.dump(obj,fh2) - Matti Lyra
@MattiLyra:啊,我猜你需要先加载别名模块,这样它们就会先“存在”于新位置。 - Martijn Pieters
显示剩余3条评论

6
除了@MartinPieters提供的答案之外,另一种方法是定义类的方法,或者扩展类。
def map_path(mod_name, kls_name):
    if mod_name.startswith('packageA'): # catch all old module names
        mod = __import__('WrapperPackage.%s'%mod_name, fromlist=[mod_name])
        return getattr(mod, kls_name)
    else:
        mod = __import__(mod_name)
        return getattr(mod, kls_name)

import cPickle as pickle
with open('dump.pickle','r') as fh:
    unpickler = pickle.Unpickler(fh)
    unpickler.find_global = map_path
    obj = unpickler.load() # object will now contain the new class path reference

with open('dump-new.pickle','w') as fh:
    pickle.dump(obj, fh) # ClassA will now have a new path in 'dump-new'

关于picklecPickle的更详细解释可以在这里找到。


该网站已经下线,但是archive.org在这里有备份,并且非常值得一读。 - Jeff Allen
那个链接是archive.org的beta版本,非beta版本在这里 - Daniel M.

2
一种可能的解决方案是直接编辑pickle文件(如果您有访问权限)。我遇到了这个改变模块路径的问题,我已经将文件保存为pickle.HIGHEST_PROTOCOL,理论上应该是二进制的,但模块路径却以纯文本形式位于pickle文件的顶部。所以我只需在旧模块路径的所有实例中查找替换为新路径,就可以正确加载它们了。
我相信这种解决方案并不适用于每个人,特别是如果您有一个非常复杂的pickled对象,但这是一个快速而简单的数据修复方法,对我很有效!

0
这是我用于灵活反序列化的基本模式 - 通过一个明确且快速的转换映射 - 因为除了与 pickling 相关的原始数据类型之外,通常只有几个已知的类。这也可保护反序列化免受错误或恶意构造的数据影响,毕竟它们可以在简单的 pickle.load() 上执行任意 python 代码(带或不带容易出错的 sys.modules 操作)。
Python 2 & 3:
from __future__ import print_function
try:    
    import cPickle as pickle, copy_reg as copyreg
except: 
    import pickle, copyreg

class OldZ:
    a = 1
class Z(object):
    a = 2
class Dangerous:
    pass   

_unpickle_map_safe = {
    # all possible and allowed (!) classes & upgrade paths    
    (__name__, 'Z')         : Z,    
    (__name__, 'OldZ')      : Z,
    ('old.package', 'OldZ') : Z,
    ('__main__', 'Z')       : Z,
    ('__main__', 'OldZ')    : Z,
    # basically required
    ('copy_reg', '_reconstructor') : copyreg._reconstructor,    
    ('__builtin__', 'object')      : copyreg._reconstructor,    
    }

def unpickle_find_class(modname, clsname):
    print("DEBUG unpickling: %(modname)s . %(clsname)s" % locals())
    try: 
        return _unpickle_map_safe[(modname, clsname)]
    except KeyError:
        raise pickle.UnpicklingError(
            "%(modname)s . %(clsname)s not allowed" % locals())
if pickle.__name__ == 'cPickle':  # PY2
    def SafeUnpickler(f):
        u = pickle.Unpickler(f)
        u.find_global = unpickle_find_class
        return u
else:  # PY3 & Python2-pickle.py
    class SafeUnpickler(pickle.Unpickler):  
        find_class = staticmethod(unpickle_find_class)

def test(fn='./z.pkl'):
    z = OldZ()
    z.b = 'teststring' + sys.version
    pickle.dump(z, open(fn, 'wb'), 2)
    pickle.dump(Dangerous(), open(fn + 'D', 'wb'), 2)
    # load again
    o = SafeUnpickler(open(fn, 'rb')).load()
    print(pickle, "loaded:", o, o.a, o.b)
    assert o.__class__ is Z
    try: 
        raise SafeUnpickler(open(fn + 'D', 'rb')).load() and AssertionError
    except pickle.UnpicklingError: 
        print('OK: Dangerous not allowed')

if __name__ == '__main__':
    test()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接