Класс данных Python: можете ли вы установить значение по умолчанию для полей?

Я хочу создать базовый класс класса данных, в котором все поля в подклассах автоматически становятся Необязательными и по умолчанию имеют значение Нет (если значение по умолчанию не указано).

Следующий код ... кажется, делает то, что я хочу, но не совсем. Это происходит так же, как если бы я никогда не писал __init_subclass__ (т.е. жалуется на незаполненные параметры) ... возможно, потому что мой код запускается после магии класса данных?

@dataclass(order=True, frozen=True)
class BaseDictKey:
    def __init_subclass__(cls, *args, **kwargs):
        super().__init_subclass__(*args, **kwargs)
        # noinspection PyUnresolvedReferences
        for field in cls.__dataclass_fields__.values():
            field.default = None if field.default is None else field.default
            field.type = typing.Union[field.type, NoneType]

@dataclass(order=True, frozen=True)
class ScoreDictKey(BaseDictKey):
    catalog: str  # magically becomes catalog: Optional[str] = None
    dataset: str = 'foo'  # magically becomes dataset: Optional[str] = 'foo'

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

Возможно ли это в Python 3.7+?


person user3534080    schedule 25.10.2019    source источник
comment
Опишите, пожалуйста, каким образом не совсем так, чтобы нам не приходилось гадать.   -  person martineau    schedule 25.10.2019
comment
Отредактировал оригинал. Вот мое изменение: это происходит так же, как если бы я никогда не писал __init_subclass__ (т.е. жалуется на незаполненные параметры) ... возможно, потому что мой код запускается после того, как происходит волшебство класса данных?   -  person user3534080    schedule 25.10.2019
comment
Что эта линия должна делать? field.default = None if field.default is None else field.default   -  person Pynchia    schedule 25.10.2019
comment
если вы посмотрите на ScoreDictKey.dataset, он должен иметь больше смысла, но в основном: если было предоставлено значение по умолчанию, не перезаписывайте его с помощью None, но если ничего не было предоставлено, установите значение по умолчанию None. Я не делал None if not field.default else field.default, потому что это не сработает для значений по умолчанию falsy-non-None (0, '', [] и т. Д.)   -  person user3534080    schedule 25.10.2019


Ответы (1)


Я нашел способ изменить поле класса __annotations__, чтобы сделать поля необязательными, и установить атрибуты непосредственно в классе для предоставления значения по умолчанию None:

from dataclasses import dataclass

import typing


@dataclass(order=True, frozen=True)
class BaseDictKey:
    def __init_subclass__(cls, *args, **kwargs):
        for field, value in cls.__annotations__.items():
            cls.__annotations__[field] = typing.Union[value, None]
            if not hasattr(cls, field):
                setattr(cls, field, None)
        super().__init_subclass__(*args, **kwargs)


@dataclass(order=True, frozen=True)
class ScoreDictKey(BaseDictKey):
    catalog: str  # magically becomes catalog: Optional[str] = None
    dataset: str = 'foo'  # magically becomes dataset: Optional[str] = 'foo'

    def __init__(self, *args, **kwargs):  # to get rid of PyCharm warning
        pass


c = ScoreDictKey()
print(c.catalog, c.dataset)  # None foo
person sanyassh    schedule 25.10.2019
comment
Спасибо! Это 90% пути! Работает во время выполнения без сбоев. Однако ... PyCharm недостаточно умен, чтобы понять, что было сделано ... он ошибочно выделяет пустую скобку и говорит Parameter 'catalog_str' unfilled. Единственное, что я могу придумать, - это вручную сгенерировать функции init, которые звучит как боль. - person user3534080; 25.10.2019
comment
@ user3534080, чтобы избавиться от предупреждения PyCharm. Я нашел только один способ с текущим решением: добавить пустой def __init__(self, *args, **kwargs): pass в определение подкласса. - person sanyassh; 25.10.2019