Проблема с расслоением и аргументами со значениями по умолчанию - PullRequest
0 голосов
/ 27 марта 2019

Допустим, у меня есть класс:

class Character():

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

Я создаю экземпляр и мариную его.

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. Чтобы сделать это правильно, следуя тому, что написано в книге «Эффективный 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)

Теперь расслоение должно работать нормально:

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

Этот код работает нормально, однако я все еще не вижу в объекте c наш новый атрибут health.

Вопрос прост, почему это происходит? Все должно работать нормально в соответствии с «Эффективным Питоном».

1 Ответ

3 голосов
/ 27 марта 2019

Стандартное поведение при откреплении напрямую назначает атрибуты - оно не использует __init__ или __new__. Поэтому аргументы по умолчанию не применяются.

Когда экземпляр класса не выбран, его метод __init__() обычно не вызывается. 1

Вызов __init__ может иметь побочные эффекты и может принимать дополнительные, меньшие или другие параметры, чем атрибуты. Это делает его небезопасным по умолчанию. По сути, pickle использует object.__new__(cls) для создания экземпляра, а затем обновляет его __dict__.

Вы должны явно указать pickle, чтобы использовать __init__, если хотите.


При использовании copyreg вы должны передать ему constructor параметр . Обратите внимание, что эта подпись отличается от вашей unpickle_character.

В противном случае ваша функция травления (pickle_character) статически определяет функцию, используемую для расслоения. Поскольку для класса Character не зарегистрирован конструктор, а старый метод 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)

Легче определить __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__ является безопасным и обратно совместимым, вы можете также использовать его для инициализации из протравленного состояния.

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)
...