Unpickling 和带有默认值的 args 的问题

Problem with Unpickling and args with default values

假设我有一个 class:

class Character():

    def __init__(self):
        self.race = "Ork"

我创建了一个实例并将其 pickle。

c = Character()

import pickle
with open(r'C:\tmp\state.bin', 'w+b') as f:
    pickle.dump(c, f)

当我尝试解开它时,一切正常。 但是如果我想给 Character 添加另一个属性怎么办? 我去这个:

class Character():

    def __init__(self):
        self.race = "Ork"
        self.health = 100

假设我想解开没有 health 属性的旧版本。如果我只是从文件中提取数据,对象将没有 health 属性。为了以正确的方式进行,按照 "Effective Python" 书中的内容,我需要引入具有默认值的参数并使 copyreg 发挥作用。

所以,我这样做:

class Character

    def __init__(self, race = "Ork", health = 100):
        self.race = race
        self.health = health

import copyreg 

def pickle_character(state):
    kwargs = state.__dict__
    return unpickle_character, (kwargs, )

def unpickle_character(kwargs):
    return Character(**kwargs)

copyreg.pickle(Character, pickle_character)

现在 unpickling 应该可以正常工作了:

with open(r'C:\tmp\state.bin', 'rb') as f:
    c = pickle.load(f)

这段代码工作正常,但是,我仍然没有在 c 对象中看到我们新的 health 属性。

问题很简单,为什么会这样?根据 "Effective Python".

一切都应该正常工作

unpickling 的标准行为直接分配属性 - 它不使用 __init____new__。因此,您的默认参数不适用。

When a class instance is unpickled, its __init__() method is usually not invoked. 1

调用 __init__ 可能会产生副作用,并且可能需要比属性更多、更少或其他参数。这使它成为不安全的默认值。实际上,pickle 使用 object.__new__(cls) 创建实例,然后更新它的 __dict__.

如果需要,您必须明确告诉 pickle 使用 __init__


使用 copyreg 时,必须将 constructor parameter 传递给它。请注意,这确实与您的 unpickle_character.

具有不同的签名

否则,您的 pickling 函数 (pickle_character) 静态定义了用于 unpickle 的函数。由于没有为 Character class 注册构造函数并且旧 pickle 不包含它,加载旧 pickle 不会调用您的构造函数。

def pickle_character(state):
    kwargs = state.__dict__
    return unpickle_character, (kwargs, )
    #      ^ unpickler stored for *newly pickled instance*!
# no constuctor stored for *Character class* v
copyreg.pickle(Character, pickle_character)

在 class 上定义 __setstate__ 更容易。这直接接收状态,甚至来自较旧的泡菜。

class Character:
    def __init__(self, race, health):
        self.race = race
        self.health = health

    # load state with defaults for missing attributes
    def __setstate__(self, state):
        self.race = state.get('race', 'Ork')
        self. health = state.get('health', 100)

如果您知道 __init__ 是安全且向后兼容的,您也可以使用它从 pickled 状态进行初始化。

class Character:
    # defaults for every initialisation
    def __init__(self, race='Ork', health=100):
        self.race = race
        self.health = health

    def __setstate__(self, state):
        # re-use __init__ for initialisation
        self.__init__(**state)