下面的最小示例使用虚拟装饰器,当构造装饰类的对象时,它会打印一些消息。
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实例仅存储实例数据,类的__qualname__
和__module__
属性用于稍后通过再次导入类并为类创建新实例来重新创建实例。
Pickle验证该类实际上可以先导入。 __module__
和__qualname__
对用于查找正确的模块,然后在该模块上访问由__qualname__
命名的对象,如果__class__
对象和模块上找到的对象不匹配,则会看到您看到的错误。
在这里,foo.__class__
指向一个类对象,__qualname__
设置为'Foo'
,__module__
设置为'__main__'
,但sys.modules['__main__'].Foo
不指向类,它指向一个函数,而你的装饰器返回的wrapper
嵌套函数。
有两种可能的解决方案:
__new__
或__init__
方法。
考虑到在恢复实例状态之前,unpickling通常会在类上调用__new__
来创建一个新的空实例(除非pickle已经是customised)。__qualname__
和类的__module__
属性,指向可以通过pickle找到原始类的位置。在unpickling上,将再次创建正确类型的实例,就像原始的Foo()
调用一样。另一种选择是为生产的类定制酸洗。您可以给类指向包装函数或自定义reduce函数的类new __reduce_ex__
和new __reduce__
方法。这可能变得复杂,因为类可能已经有自定义酸洗,而object.__reduce_ex__
提供了默认值,并且返回值可能因pickle版本而不同。
如果您不想更改类,还可以使用copyreg.pickle()
function为类注册自定义__reduce__
处理程序。
无论哪种方式,reducer的返回值仍然应该避免引用该类,并且应该引用新的构造函数,而不是引用它可以导入的名称。如果直接使用装饰器与new_name = decorator()(classobj)
,这可能会有问题。 Pickle本身也不会处理这种情况(因为classobj.__name__
不会匹配newname)
。