Python中的优雅降级序列化

6 投票
3 回答
1455 浏览
提问于 2025-04-15 13:56

(你可以阅读这个问题来了解一些背景信息)

我想要一种优雅的方式来在Python中对对象进行序列化,也就是“腌制”对象。

在对一个对象进行腌制时,我们可以称它为主对象,有时候腌制器会抛出异常,因为它无法腌制主对象的某个子对象。例如,我经常遇到的一个错误是“无法腌制模块对象”。这是因为我在主对象中引用了一个模块。

我知道我可以写点东西来用一个“外观”替换那个模块,这个外观会包含模块的属性,但这样会有自己的问题(1)。

所以我希望有一个腌制函数,它可以自动将模块(以及其他难以腌制的对象)替换为包含它们属性的外观。这样可能不能做到完美的腌制,但在很多情况下是足够的。

有没有类似的东西?有没有人知道该如何处理这个问题?


(1)一个问题是这个模块可能会引用它内部的其他模块。

3 个回答

0

嗯,这样的东西吗?

import sys

attribList = dir(someobject)
for attrib in attribList:
    if(type(attrib) == type(sys)): #is a module
        #put in a facade, either recursively list the module and do the same thing, or just put in something like str('modulename_module')
    else:
        #proceed with normal pickle

显然,这个需要放到一个扩展的pickle类里面,并重新实现一个叫做dump的方法...

0

下面这个方法怎么样?它是一个包装器,你可以用它来把一些模块(可能是任何模块)包装成可以被序列化的形式。这样的话,你就可以创建一个新的类,继承自Pickler对象,检查目标对象是否是一个模块,如果是的话,就把它包装起来。这样做能达到你想要的效果吗?

class PickleableModuleWrapper(object):
    def __init__(self, module):
        # make a copy of the module's namespace in this instance
        self.__dict__ = dict(module.__dict__)
        # remove anything that's going to give us trouble during pickling
        self.remove_unpickleable_attributes()

    def remove_unpickleable_attributes(self):
        for name, value in self.__dict__.items():
            try:
                pickle.dumps(value)
            except Exception:
                del self.__dict__[name]

import pickle
p = pickle.dumps(PickleableModuleWrapper(pickle))
wrapped_mod = pickle.loads(p)
3

你可以决定并实现如何处理那些之前无法被“序列化”的类型,让它们可以被序列化和反序列化。可以参考标准库中的模块 copy_reg(在Python 3.*中改名为 copyreg)。

简单来说,你需要提供一个函数,这个函数接收该类型的实例,并将其简化为一个元组——这个过程与 reduce 特殊方法的协议是一样的(不过,reduce特殊方法不接受参数,因为它是在对象上直接调用的,而你提供的函数只接受对象作为参数)。

通常,你返回的元组包含两个部分:一个可调用对象和一个传递给它的参数元组。这个可调用对象必须被注册为“安全构造函数”,或者有一个属性 __safe_for_unpickling__,其值为真。这些部分会被序列化,而在反序列化时,这个可调用对象会用给定的参数被调用,并且必须返回反序列化后的对象。

举个例子,假设你想通过名称来序列化模块,这样反序列化它们就意味着重新导入它们(也就是说,假设为了简单起见,你不关心动态修改的模块、嵌套包等,只关心普通的顶级模块)。那么:

>>> import sys, pickle, copy_reg
>>> def savemodule(module):
...   return __import__, (module.__name__,)
... 
>>> copy_reg.pickle(type(sys), savemodule)
>>> s = pickle.dumps(sys)
>>> s
"c__builtin__\n__import__\np0\n(S'sys'\np1\ntp2\nRp3\n."
>>> z = pickle.loads(s)
>>> z
<module 'sys' (built-in)>

我使用的是传统的ASCII格式的序列化,这样 s,包含序列化内容的字符串,就容易检查:它指示反序列化时调用内置的导入函数,参数是字符串 sys。而 z 显示,这确实让我们在反序列化时得到了内置的 sys 模块,正是我们想要的结果。

现在,你需要让事情变得比仅仅使用 __import__ 更复杂(你需要处理保存和恢复动态变化,导航嵌套的命名空间等),因此你还需要在调用 copy_reg 保存模块的函数之前,调用 copy_reg.constructor(传入你自己执行这些工作的函数作为参数)。如果是在不同的运行中,也要在反序列化你用该函数创建的序列化内容之前这样做。但我希望这个简单的例子能帮助你理解,这其实并没有什么“本质上”复杂的东西!-)

撰写回答