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

Eng*_*ock 1 python serialization pickle

假设我有一堂课:

class Character():

    def __init__(self):
        self.race = "Ork"
Run Code Online (Sandbox Code Playgroud)

我创建了一个实例并对其进行了腌制。

c = Character()

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

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

class Character():

    def __init__(self):
        self.race = "Ork"
        self.health = 100
Run Code Online (Sandbox Code Playgroud)

假设我想取消我们没有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)
Run Code Online (Sandbox Code Playgroud)

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

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

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

问题很简单,为什么会发生?根据“Effective Python”,一切都应该正常工作。

Mis*_*agi 5

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

当一个类实例被 unpickle 时,它​​的__init__()方法通常不会被调用。1

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

如果您愿意,您必须明确告诉pickle使用__init__


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

否则,您的酸洗函数 ( pickle_character) 静态定义用于解压的函数。由于没有为Character该类注册构造函数并且旧的 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)
Run Code Online (Sandbox Code Playgroud)

__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)
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

  • @MisterMiyagi“如果你愿意,你必须明确告诉pickle使用__init__。” 我怎样才能做到这一点?你能修复我的代码以依赖这种解压方式吗?非常感谢您的帮助! (2认同)