Pickle and decorated classes (PicklingError: not the same object)

Pickle and decorated classes (PicklingError: not the same object)

下面的最小示例使用了一个虚拟装饰器,它只是在构造装饰器 class 的对象时打印一些消息。

import pickle


def decorate(message):
    def call_decorator(func):
        def wrapper(*args, **kwargs):
            print(message)
            return func(*args, **kwargs)

        return wrapper

    return call_decorator


@decorate('hi')
class Foo:
    pass


foo = Foo()
dump = pickle.dumps(foo) # Fails already here.
foo = pickle.loads(dump)

然而,使用它会使 pickle 引发以下异常:

_pickle.PicklingError: Can't pickle <class '__main__.Foo'>: it's not the same object as __main__.Foo

我能做些什么来解决这个问题吗?

Pickle 要求可以通过导入加载实例的 __class__ 属性。

Pickling 实例仅存储实例数据,class 的 __qualname____module__ 属性用于稍后通过导入 [=71] 重新创建实例=] 并为 class.

创建一个新实例

Pickle 验证 class 实际上可以首先导入。 __module____qualname__ 对用于找到正确的模块,然后访问该模块上由 __qualname__ 命名的对象,如果 __class__ 对象和在上找到的对象模块不匹配,出现您看到的错误。

这里,foo.__class__指向一个class对象,__qualname__设置为'Foo'__module__设置为'__main__',但是sys.modules['__main__'].Foo 不指向 class,它指向一个函数,wrapper 嵌套函数你的装饰器 returned.

有两种可能的解决方案:

  • 不要 return 一个函数,return 原来的 class,也许可以检测 class 对象来完成包装器的工作做。如果您正在对 class 构造函数的参数进行操作,请在修饰的 class.

    上添加或包装 __new____init__ 方法

    考虑到 unpickling 通常在 class 上调用 __new__ 来创建一个新的空实例,然后再恢复实例状态(除非 pickling 已经 customised)。

  • 将 class 存储在新位置。更改 __qualname__ 以及 class 的 __module__ 属性,使其指向可以通过 pickle 找到原始 class 的位置。在 unpickling 上,将再次创建正确类型的实例,就像最初的 Foo() 调用一样。

另一种选择是为生产的 class 定制酸洗。您可以提供指向包装函数或自定义 reduce 函数的 class new __reduce_ex__ and new __reduce__ 方法。这可能会变得复杂,因为 class 可能已经自定义了酸洗,而 object.__reduce_ex__ 提供了默认值, return 值可能因酸洗而不同版本。

如果您不想更改 class,您还可以使用 copyreg.pickle() function 为 class 注册自定义 __reduce__ 处理程序。

无论哪种方式,reducer 的 return 值仍应避免引用 class,而应引用新的构造函数,使用可以导入的名称。如果您直接将装饰器与 new_name = decorator()(classobj) 一起使用,这可能会出现问题。 Pickle 本身也不会处理这种情况(因为 classobj.__name__ 不会匹配 newname).

使用 dill 而不是 pickle 不会引发错误。

import dill


def decorate(message):
    def call_decorator(func):
        def wrapper(*args, **kwargs):
            print(message)
            return func(*args, **kwargs)

        return wrapper

    return call_decorator


@decorate('hi')
class Foo:
    pass


foo = Foo()
dump = dill.dumps(foo) # Fails already here.
foo = dill.loads(dump)

输出 -> hi