【问题标题】:Unpickling python objects with a changed module path使用更改的模块路径对 python 对象进行 unpickling
【发布时间】:2012-11-15 13:29:26
【问题描述】:

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

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

因此使用cPickle 腌制课程。为了整洁,我想隐藏他 (Project A) 在我的项目中构建的包。然而,这改变了packageA 中定义的类的路径。没问题,我将使用重新定义导入

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

但现在取消腌制类失败并显示以下消息

    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 缓存并重新导入所有内容。但是如果你有一些腌制数据,那么你必须用相同的类名取消腌制它(这意味着你必须保留from packageA.moduleA import ClassA)。请注意,取消腌制后,您可以使用正确的名称重新腌制它们。

标签: python import pickle


【解决方案1】:

您需要为 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 中这样做吗?
  • @MattiLyra:你可以在任何地方这样做,但WrapperPackage/__init__.py 文件可能是最好的地方。
  • @MartinPieters 根据 PEP328 import .something 无效,必须是 from .something import moduleimport .something 抛出 SyntaxError? python.org/dev/peps/pep-0328/#guido-s-decision
  • @MattiLyra:你完全正确。我完全避免了相对导入,所以我摸索了语法。已更正。
  • @MartinPieters 棒极了,似乎可以正常工作,虽然我仍然无法让新腌制的文件显示更改后的路径,它仍然是 packageA.moduleA,但我可以毫无问题地加载它。跨度>
【解决方案2】:

除了@MartinPieters 回答之外,另一种方法是定义cPickle.Unpickler 类的find_global 方法,或者扩展pickle.Unpickler 类。

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 的过程的更详细说明可以找到here

【讨论】:

  • 链接的网站离线,但是archive.org有here,值得一读。
  • 该链接是archive.org beta,非beta here
【解决方案3】:

一种可能的解决方案是直接编辑 pickle 文件(如果您有权访问)。我遇到了更改模块路径的同样问题,并且我将文件保存为 pickle.HIGHEST_PROTOCOL 所以理论上它应该是二进制的,但是模块路径以纯文本形式位于 pickle 文件的顶部。因此,我只是对旧模块路径的所有实例进行了查找替换,并使用新的,瞧,它们正确加载了。

我确信这个解决方案并不适合所有人,特别是如果你有一个非常复杂的腌制对象,但它是一种快速而肮脏的数据修复方法,对我有用!

【讨论】:

    【解决方案4】:

    这是我灵活解酸的基本模式——通过明确且快速的转换图——因为除了与酸洗相关的原始数据类型之外,通常只有几个已知的类。这还可以保护 unpickling 免受错误或恶意构造的数据的影响,毕竟这些数据可以在简单的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()
    

    【讨论】:

      猜你喜欢
      • 2014-05-31
      • 1970-01-01
      • 2017-08-10
      • 2013-02-23
      • 2020-12-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多