对动态参数化子类进行序列化

17 投票
5 回答
6450 浏览
提问于 2025-04-16 09:43

我有一个系统,通常会存储被“腌制”的类类型。

我想以同样的方式保存动态参数化的类,但我遇到了问题,因为当我尝试对一个在全局找不到的类进行“腌制”时,会出现一个叫做PicklingError的错误(也就是这个类没有在简单的代码中定义)。

我的问题可以用下面的示例代码来说明:

class Base(object):
 def m(self):
  return self.__class__.PARAM

def make_parameterized(param_value):
 class AutoSubClass(Base):
  PARAM = param_value
 return AutoSubClass

cls = make_parameterized(input("param value?"))

当我尝试对这个类进行“腌制”时,我收到了以下错误:

# pickle.PicklingError: Can't pickle <class '__main__.AutoSubClass'>: it's not found as __main__.AutoSubClass
import pickle
print pickle.dumps(cls)

我希望能找到一种方法,将Base声明为一个ParameterizableBaseClass,这个类应该定义所需的参数(上面示例中的PARAM)。然后,一个动态参数化的子类(上面的cls)应该可以通过保存“ParameterizableBaseClass”类型和不同的参数值(上面的动态param_value)来进行“腌制”。

我相信在很多情况下,这个问题是可以完全避免的……如果我真的(真的)必须的话,我也可以在我的代码中避免这个问题。我曾经尝试过__metaclass__copyreg,甚至__builtin__.issubclass(别问我为什么),但一直没能解决这个问题。

我觉得如果不问一下怎么能相对简单地实现这个,我就不算是真正理解Python的精神了。

5 个回答

2

我想现在说这个可能有点晚了,但我觉得对于复杂的事情,我还是尽量不使用pickle这个模块,因为它有很多问题,比如这个,还有其他很多。

不过,既然pickle需要这个类在全局范围内,那就让它这样吧:

import cPickle

class Base(object):
    def m(self):
        return self.__class__.PARAM

    @classmethod
    def make_parameterized(cls,param):
        clsname = "AutoSubClass.%s" % param
        # create a class, assign it as a global under the same name
        typ = globals()[clsname] = type(clsname, (cls,), dict(PARAM=param))
        return typ

cls = Base.make_parameterized('asd')

import pickle
s = pickle.dumps(cls)

cls = pickle.loads(s)
print cls, cls.PARAM
# <class '__main__.AutoSubClass.asd'> asd

不过,你可能把事情想得太复杂了。

14

我知道这个问题很老旧,但我觉得有必要分享一种比现在普遍接受的解决方案更好的方法来处理带参数的类(就是把带参数的类设为全局的那种)。

通过使用 __reduce__ 方法,我们可以提供一个可调用的对象,这样就能返回一个未初始化的我们想要的类的实例。

class Base(object):
    def m(self):
        return self.__class__.PARAM

    def __reduce__(self):
        return (_InitializeParameterized(), (self.PARAM, ), self.__dict__)


def make_parameterized(param_value):
    class AutoSub(Base):
        PARAM = param_value
    return AutoSub


class _InitializeParameterized(object):
    """
    When called with the param value as the only argument, returns an 
    un-initialized instance of the parameterized class. Subsequent __setstate__
    will be called by pickle.
    """
    def __call__(self, param_value):
        # make a simple object which has no complex __init__ (this one will do)
        obj = _InitializeParameterized()
        obj.__class__ = make_parameterized(param_value)
        return obj

if __name__ == "__main__":

    from pickle import dumps, loads

    a = make_parameterized("a")()
    b = make_parameterized("b")()

    print a.PARAM, b.PARAM, type(a) is type(b)
    a_p = dumps(a)
    b_p = dumps(b)

    del a, b
    a = loads(a_p)
    b = loads(b_p)

    print a.PARAM, b.PARAM, type(a) is type(b)

值得多读几遍 __reduce__ 的文档,这样可以更清楚地理解这里到底发生了什么。

希望有人觉得这有用。

5

是的,这是可能的 -

当你想要自定义对象的序列化(Pickle)和反序列化(Unpickle)行为时,只需要在类里面设置两个方法:__getstate____setstate__

不过在这种情况下,事情会有点复杂:正如你观察到的,必须在全局命名空间中存在一个类,这个类是当前正在被序列化的对象的类:它必须是同一个类,名字也要一样。其实,这个类可以在序列化的时候创建。

在反序列化的时候,必须存在一个同名的类,但它不一定是同一个对象,只要行为上看起来一样就可以了。在反序列化的过程中,当调用 __setstate__ 方法时,它可以重新创建原始对象的带参数的类,并通过设置对象的 __class__ 属性来让它的类变成那个类。

设置对象的 __class__ 属性可能看起来有点不妥,但这就是Python面向对象的工作方式,而且这是官方文档中有说明的,甚至在不同的实现中也能正常工作。(我在Python 2.6和Pypy中都测试过这个代码片段)

class Base(object):
    def m(self):
        return self.__class__.PARAM
    def __getstate__(self):
        global AutoSub
        AutoSub = self.__class__
        return (self.__dict__,self.__class__.PARAM)
    def __setstate__(self, state):
        self.__class__ = make_parameterized(state[1])
        self.__dict__.update(state[0])

def make_parameterized(param_value):
    class AutoSub(Base):
        PARAM = param_value
    return AutoSub

class AutoSub(Base):
    pass


if __name__ == "__main__":

    from pickle import dumps, loads

    a = make_parameterized("a")()
    b = make_parameterized("b")()

    print a.PARAM, b.PARAM, type(a) is type(b)
    a_p = dumps(a)
    b_p = dumps(b)

    del a, b
    a = loads(a_p)
    b = loads(b_p)

    print a.PARAM, b.PARAM, type(a) is type(b)

撰写回答