Unpickling объекты python с измененным пути модуля

Я пытаюсь интегрировать проект 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

но теперь un pickling классы терпит неудачу со следующим сообщением

    with open(fname) as infile: self.clzA = cPickle.load(infile)
ImportError: No module named packageA.moduleA

Итак, почему cPickle явно не видит модуль defs. Нужно ли добавить корень 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

Ответы

Ответ 1

Вам нужно создать псевдоним для импорта рассола для работы; следующее в __init__.py файл пакета WrapperPackage:

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.moduleA и packageA.moduleB в своих старых местах.

Возможно, вам захочется снова записать файл pickle, в то время будет использоваться новое расположение модуля. Дополнительные псевдонимы, созданные выше, должны гарантировать, что соответствующие модули будут иметь новое название местоположения для cPickle, которое будет отображаться при повторном написании классов.

Ответ 2

В дополнение к @MartinPieters ответ другой способ сделать это - определить метод find_global класса cPickle.Unpickler или расширить класс 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'

Более подробное объяснение процесса для pickle и cPickle можно найти здесь.

Ответ 3

Одним из возможных решений является прямое редактирование файла pickle (если у вас есть доступ). Я столкнулся с той же проблемой измененного пути к модулю, и я сохранил файлы как pickle.HIGHEST_PROTOCOL, поэтому он должен быть двоичным в теории, но путь к модулю находился в верхней части файла pickle в виде обычного текста. Поэтому я просто заменил find на всех экземплярах старого пути модуля новым и voila, они загрузились правильно.

Я уверен, что это решение не для всех, особенно если у вас очень сложный маринованный объект, но это быстрое и грязное исправление данных, которое сработало для меня!

Ответ 4

Это мой основной шаблон для гибкой раскраски - через однозначную и быструю карту перехода - так как обычно есть несколько известных классов, кроме примитивных типов данных, релевантных для травления. Это также защищает от нежелательных или вредоносных данных, которые в конце концов могут выполнять произвольный код на Python (!) На простой pickle.load() (с ошибкой или без ошибок при запуске 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()