Проблема с отменой выбора и аргументами со значениями по умолчанию

#python #serialization #pickle

#python #сериализация #удаление

Вопрос:

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

 class Character():

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

Я создаю экземпляр и обрабатываю его.

 c = Character()

import pickle
with open(r'C:tmpstate.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:tmpstate.bin', 'rb') as f:
    c = pickle.load(f)
  

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

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

Ответ №1:

Стандартное поведение для отмены выбора напрямую присваивает атрибуты — оно не использует __init__ or __new__ . Следовательно, ваши аргументы по умолчанию не применяются.

Когда экземпляр класса не выбран, его __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)
  

Это проще определить __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)
  

Комментарии:

1. Так pickle будет ли вместо этого вызываться __new__ ?

2. @C.Nivs Он напрямую создает объект, также обходя класс __new__ .

3. @MisterMiyagi хм… почему подписи разные? Я думал, автор в «Эффективном Python» имел в виду, что мы вводим dict в виде аргументов со значениями по умолчанию, и поэтому мы можем передавать ** kwargs.

4. @MisterMiyagi «Это недопустимо только при использовании в качестве конструктора» что вы имеете в виду под этим? «Обратите внимание, что я не могу воспроизвести вашу ошибку» — вы имеете в виду, что на вашем компьютере мой код отменяет выбор старой версии, и у вас есть объект с недавно добавленным атрибутом на месте?

5. @MisterMiyagi «вы должны явно указать pickle использовать init , если хотите». как я могу это сделать? Можете ли вы исправить мой код, чтобы он полагался на этот способ отмены выбора? Большое спасибо за вашу помощь!