Python - 如何将不可pickle的对象转换为可pickle的对象?

11

所以,我有一个对象,其中包含许多无法pickle的内容(pygame事件,orderedDicts,clock等),我需要将其保存到磁盘上。

问题是,如果我只能让这个东西存储一个进度字符串(只需要一个整数),然后将它传递给对象的init,它就会重新构建所有这些东西。不幸的是,我正在使用的一个框架(Renpy)pickle该对象并尝试加载它,尽管我可以将它保存为单个整数,但我无法更改它。

所以,我的问题是,如何覆盖方法,以便每当pickle尝试保存对象时,它只保存进度值,并且每当它尝试加载对象时,它从进度值创建一个新实例?

我看过一些关于__repr__方法的讨论,但我不确定如何在我的情况下使用它。

2个回答

14
您要寻找的钩子是__reduce__。它应该返回一个(可调用对象, 参数)元组;这个可调用对象参数将被序列化,在反序列化时,将通过callable(*args)重新创建对象。如果您的类构造函数接受一个int类型的参数,您可以按照以下方式实现__reduce__
class ComplicatedThing:
    def __reduce__(self):
        return (ComplicatedThing, (self.progress_int,))

你可以在元组中添加一些可选的额外内容,这些内容通常对于具有循环依赖关系的对象图非常有用,但在此处您不需要它们。


所以,看起来系统正在使用pickle.loads而不是load。这仍然有效吗?我收到一个TypeError错误:__init __()需要确切的4个参数(给出1个)我的主要对象不需要任何参数,但它的一些子对象需要。我可以让pickle跳过它们吗?无论如何,当初始化主对象时,它们将被重新生成。 - Matthew Fournier
@MatthewFournier:loadsload都无所谓。您的对象可以从单个int重建;您是否有一个执行此操作的函数?如果有,该函数应该是元组中的“callable”。如果您没有这样的函数,那么您可能需要编写一个。 - user2357112
目前,顶层对象的init函数接受一个参数并重新构建它。我认为错误是指它的某些嵌套类(因为错误表明它需要四个参数)。有没有办法让这些对象被pickler忽略?我根本不需要保存它们。我能让它们缩减到None或其他什么东西吗? - Matthew Fournier
@MatthewFournier:很奇怪。我不明白你为什么会收到那个错误。这段代码应该已经避免保存你不需要的对象了。(我的示例代码有一个错误,我忘记在参数中加括号了,但是那样会产生不同的错误。) - user2357112
嗯,看起来我的问题不在这里。我会将这个标记为已解决,并提出一个新的问题,因为它似乎是完全不同的问题。 - Matthew Fournier
显示剩余2条评论

5
使用__reduce__是一种有效的方法,正如Python文档所述:
尽管强大,但在类中直接实现__reduce__()存在错误的可能性。因此,类设计者应尽可能使用高级接口(即__getnewargs_ex__()__getstate__()__setstate__())。
因此,我将解释如何使用更简单的高级接口__getstate____setstate__使对象可进行序列化。
假设我们有一个非常简单的类,其中包含一个无法序列化的属性,比如文件句柄。
class Foo:
    def __init__(self, filename):
        self.filename = filename
        self.f = open(filename) # this attribute cannot be pickled

Foo的实例不可被pickable:

obj = Foo('test.txt')
pickle.dumps(obj)
# TypeError: cannot pickle '_io.TextIOWrapper' object

我们可以通过实现__getstate____setstate__使这个类可序列化和反序列化,使用pickle。
class Foo:
    ... # the class as it was
    def __getstate__(self):
       """Used for serializing instances"""
       
       # start with a copy so we don't accidentally modify the object state
       # or cause other conflicts
       state = self.__dict__.copy()

       # remove unpicklable entries
       del state['f']
       return state

    def __setstate__(self, state):
        """Used for deserializing"""
        # restore the state which was picklable
        self.__dict__.update(state)
        
        # restore unpicklable entries
        f = open(self.filename)
        self.f = f

现在它可以被腌制了:
obj = Foo('text.txt')
pickle.dumps(obj)
# b'\x80\x04\x951\x00\x00\x00\x00\x00\x00\x00\x8c\x08[...]'

将这个想法应用到你问题中的例子,你可以这样做:
class MyComplicatedObject:
    def __getstate__(self):
        state = self.__dict__.copy()
        del state['progress'] # remove the unpicklable progress attribute
        return state
    def __setstate__(self, state):
        self.__dict__.update(state)
        # restore the progress from the progress integer
        self.progress = make_progress(self.progress_int)

另一种方法是配置 pickler 以了解如何对新对象进行pickle (而不是使类/对象本身可被pickle)。例如,通过自定义的 pickler 和 dispatch_table,您可以将类注册到函数 (__reduce__-like) 中,以便pickle可能无法pickle的对象。

在Python 3.8+中,您还可以为对象实现 自定义reductions

如果您试图pickle可能属于第三方库/代码的类,其中子类化(使 object 可pickle) 不切实际,则这些方法特别有用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接