Невозможно вызвать replace() для класса данных с InitVar

Определение следующего подкласса:

from dataclasses import dataclass, replace, field, InitVar

@dataclass
class MyDataClass:
    foo: InitVar[str]
    bar: str
    foo_len: int = field(init=False)
    def __post_init__(self, foo: int):
        self.foo_len = len(foo)

И создаем его экземпляр:

instance = MyDataClass(foo="foo", bar="bar")

Попытка вызвать replace для экземпляра не удалась:

In[5]: replace(instance, bar="baz")

Traceback (most recent call last):
  File "/home/or/.venv/m/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 3343, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-5-5de186c91dc9>", line 1, in <module>
    replace(instance, bar="baz")
  File "/home/or/.venv/m/lib/python3.6/site-packages/dataclasses.py", line 1170, in replace
    changes[f.name] = getattr(obj, f.name)
AttributeError: 'MyDataClass' object has no attribute 'foo'

Насколько я понимаю, InitVar должны существовать только во время инициализации, но из некоторых исследований я вижу, что они помещаются в __dataclass_fields__, и поэтому replace пытается их использовать.

Я использую Python 3.6, поэтому мой пакет dataclasses является бэкпортом, а не встроенным пакетом для Python 3.7+.

Я нашел эту строку в документации по классам данных:

Переменные только для инициализации без значений по умолчанию, если они существуют, должны быть указаны при вызове replace(), чтобы их можно было передать в init() и post_init(). .

что означает, что если бы у меня было исходное значение instance.foo, я теоретически мог бы сделать:

replace(instance, bar="baz", foo="value of foo")

Но у меня нет исходного значения instance.foo.

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


person Or B    schedule 01.03.2021    source источник
comment
нет способа избежать этой ошибки. __post_init__ может запускать произвольный код с вашей InitVar, и нет никакого способа угадать это, учитывая другие атрибуты вашего класса данных. InitVar даже не нужно влиять на состояние вашего класса данных, возможно, он просто вызывает побочный эффект, например, создает файл с его именем. Как бы вы воспроизвели это поведение, не зная его ценности? Концептуально невозможно скопировать-сконструировать или заменить экземпляр класса данных без явной передачи всех InitVars.   -  person Arne    schedule 02.03.2021
comment
Я не хочу воспроизводить это поведение. Мне просто нужна копия существующего объекта с существующими значениями.   -  person Or B    schedule 02.03.2021
comment
Я вижу, кажется, я пропустил ваше самое последнее предложение. Если все, что вам нужно, это копия, проблема вполне решаема.   -  person Arne    schedule 02.03.2021


Ответы (1)


Наличие InitVar в вашем классе данных означает, что вы не можете вызвать конструктор без повторной явной передачи InitVar. Это дисквалифицирует использование как replace(instance, ...), так и MyDataClass(**asdict(instance), ...), если у вас больше нет доступа к InitVar.

Если все, что вам нужно, это получить действительную копию, вы можете использовать стандартную библиотеку copy.copy (или copy.deepcopy для классов данных, которые имеют свои собственные контейнеры или иным образом вложены), который не вызывает конструктор экземпляра:

>>> from copy import copy
>>> instance_a = MyDataClass(foo="foo", bar="bar")
>>> instance_b = copy(instance_a)
>>> instance_b
MyDataClass(bar='bar', foo_len=3)
>>> instance_a is instance_b
False
person Arne    schedule 02.03.2021