Можно ли заставить поле класса данных вызывать его default_factory, если предоставленный аргумент - None?

У меня есть класс данных с изменяемым полем (списком). Я надеюсь достичь того, что это поле никогда не будет None, даже если явно установлено значение None в вызове __init__. В обычном классе это было бы тривиально реализовать:

class A:
    def __init__(self, l: Optional[List[int]] = None):
        if l is None:
            l = []
        self.l = l

Есть ли способ достичь того же результата, используя только dataclasses.field, т.е. без явной реализации метода __init__ (что было бы громоздко, если у класса много атрибутов)? Могу ли я заставить dataclasses.field вызывать его default_factory, если предоставленный аргумент инициализации равен None?


person gmolau    schedule 24.04.2019    source источник
comment
Вы хотите следующего поведения: a = A (None), затем a.l == []? Или a = A (), затем a.l == []?   -  person Hatatister    schedule 25.04.2019
comment
Первое, второе - это то, для чего предназначен default_factory.   -  person gmolau    schedule 25.04.2019


Ответы (2)


Я не думаю, что можно напрямую вызвать default_factory для явных подтвержденных значений None. Но вы можете использовать метод __post_init__ для явной проверки None и предоставления значения default_value, в частности, если вам нужно проверить много атрибутов.

Вы можете использовать функцию fields для автоматического сканирования вашего класса данных на наличие None значений и вызова default_factory для этих атрибутов, если он был предоставлен:

from dataclasses import dataclass, field, fields, MISSING
from typing import List

@dataclass
class A:
    l: List[int] = field(default_factory=list)

    def __post_init__(self):
        for f in fields(self):
            value = getattr(self, f.name)   
            if value is None and not f.default_factory is MISSING:
                setattr(self, f.name, f.default_factory())

s = A([1,2])
print(s.l)  # [1,2]

t = A(None)
print(t.l)  # []
person Hatatister    schedule 24.04.2019
comment
Примерно так, но завод должен вызываться только когда self.__dict__[field.name] is None. Я также был бы осторожен с использованием MISSING напрямую, документы не рекомендуют этого. Можно просто сделать if callable(field.default_factory). - person gmolau; 25.04.2019
comment
Я забыл ничего не проверять в первой версии. Я обновил свой ответ, чтобы проверить значения None. - person Hatatister; 25.04.2019
comment
Я не уверен в ПРОПУЩЕНИИ. В документации также говорится, что это контрольное значение, указывающее, была ли предоставлена ​​default_factory. Поскольку значение missing инкапсулируется определенным классом данных или Mixin, в этом случае его можно использовать. callable (field.default_factory) полагается на тот факт, что callable (MISSING) == False, но поскольку field.default_factory имеет значение MISSING, истинно, если default_factory не был предоставлен, я не уверен, что это лучше, чем прямое сравнение с MISSING - person Hatatister; 25.04.2019
comment
@Hatatister Я бы сказал, что этот кейс прямо здесь на самом деле отличный вариант использования для MISSING. См. Также stackoverflow.com/questions/53589794/ - person Arne; 29.04.2019
comment
Как бы то ни было, то же самое может быть достигнуто без проверки MISSING, а просто try установки параметра и исключения TypeError, и, возможно, он более питоничен. - person Arne; 29.04.2019
comment
Использование try except TypeError также позволит поймать маловероятный случай повышения TypeError в default_factory. Я думаю о случае возникновения ошибки типа в default_factory, вызванной ошибкой в ​​коде. В этом случае вы подавите ошибку, и ее будет очень сложно обнаружить. Так что явная проверка на MISSING может быть лучшим способом. - person Hatatister; 29.04.2019

Вы можете достичь желаемого результата с помощью метода __post_init__, который установит self.l в пустой список, даже если это None:

@dataclass
class A:
    l: Optional[List[int]]

    def __post_init__(self):
        self.l = self.l or []


a = A(None)
print(a.l)  # []
person sanyassh    schedule 24.04.2019
comment
Да, у меня только что возникла эта идея, и в настоящее время я пытаюсь настроить миксин, который будет делать это автоматически. - person gmolau; 25.04.2019
comment
Миксин, который делает это автоматически - звучит интересно. - person sanyassh; 25.04.2019
comment
Почему я не могу просто унаследовать __post_init__, который делает это для каждого класса, где он нужен? - person gmolau; 25.04.2019
comment
Работает нормально. - person gmolau; 25.04.2019