Рассол замороженный класс данных, который имеет __slots__ - PullRequest
4 голосов
/ 22 марта 2019

Как я могу засечь экземпляр замороженного класса данных с __slots__?Например, следующий код вызывает исключение в Python 3.7.0:

import pickle
from dataclasses import dataclass

@dataclass(frozen=True)
class A:
  __slots__ = ('a',)
  a: int

b = pickle.dumps(A(5))
pickle.loads(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 3, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'a'

Это работает, если я удаляю либо frozen, либо __slots__.Это просто ошибка?

1 Ответ

4 голосов
/ 22 марта 2019

Проблема возникает из pickle, использующего метод __setattr__ экземпляра при настройке состояния слотов.

Значение по умолчанию __setstate__ определено в load_build в _pickle.c строке 6220 .

Для элементов в состоянии dict экземпляр __dict__ имеет видобновлено напрямую:

 if (PyObject_SetItem(dict, d_key, d_value) < 0)

, тогда как для элементов в слоте состояния слота используется экземпляр __setattr__:

if (PyObject_SetAttr(inst, d_key, d_value) < 0)

Теперь, поскольку экземпляр заморожен, __setattr__ повышает FrozenInstanceError при загрузке.

Чтобы обойти это, вы можете определить свой собственный метод __setstate__, который будет использовать object.__setattr__, а не экземпляр __setattr__.

В документах выдается какое-то предупреждение за это:

Существует небольшая потеря производительности при использовании frozen = True: __init__() не может использовать простое назначениедля инициализации полей, и должны использовать object.__setattr__().

Также может быть полезно определить __getstate__, так как экземпляр __dict__ всегда None в вашем случае.Если вы этого не сделаете, аргумент state __setstate__ будет кортежем (None, {'a': 5}), первое значение будет значением __dict__ экземпляра, а второе - dicttate.

import pickle
from dataclasses import dataclass

@dataclass(frozen=True)
class A:
    __slots__ = ('a',)
    a: int

    def __getstate__(self):
        return dict(
            (slot, getattr(self, slot))
            for slot in self.__slots__
            if hasattr(self, slot)
        )

    def __setstate__(self, state):
        for slot, value in state.items():
            object.__setattr__(self, slot, value) # <- use object.__setattr__


b = pickle.dumps(A(5))
pickle.loads(b)

Лично я бы не назвал это ошибкой, поскольку процесс травления разработан так, чтобы быть гибким, но есть место для улучшения функций.Пересмотр протокола травления может исправить это в будущем.Если только я что-то упустил и, кроме крошечного снижения производительности , использование PyObject_GenericSetattr для всех слотов может быть разумным решением?

...