4

I have a dataclass with a mutable field (a list). What I'm hoping to achieve is that this field will never be None, even when explicitly set to None in the __init__ call. In a normal class this would be trivial to implement:

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

Is there a way to achieve the same result with just the dataclasses.field function, i.e. without explicitly implementing an __init__ method (which would be cumbersome when the class has a lot of attributes)? Can I force dataclasses.field to call its default_factory when the supplied init argument is None?

gmolau
  • 2,815
  • 1
  • 22
  • 45

2 Answers2

5

I don't think, that it is possible to directly force the default_factory to be called on explicit provied None values. But you can use the __post_init__ method to explicitly check for None and provide a default_value, in particular if you have to check many attributes.

You can use the fields function to scan your dataclass' automatically for None values and invoke default_factory for those attributes, if it was provided:

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)  # []
Arne
  • 17,706
  • 5
  • 83
  • 99
Hatatister
  • 962
  • 6
  • 11
  • Roughly so, but the factory should only be called when `self.__dict__[field.name] is None`. I would also be careful with using `MISSING` directly, the docs advise against this. One could simply do `if callable(field.default_factory)`. – gmolau Apr 24 '19 at 22:09
  • I forgot checking for none in the first version. I updated my answer to check for None values. – Hatatister Apr 24 '19 at 22:13
  • I am not shure about MISSING. The docs also say that it is a sentinel value to indicate if a default_factory was provided. Since missing is encapsulated by the defined dataclass or Mixin, this might be ok to use in this case. callable(field.default_factory) relies on the fact, that callable(MISSING) == False but since field.default_factory is MISSING is true in case it default_factory was not provided, I am not shure if it is better than directly comparing against MISSING – Hatatister Apr 24 '19 at 22:24
  • @Hatatister I'd say that this case right here is actually a great use-case for `MISSING`. See also https://stackoverflow.com/questions/53589794/pythonic-way-to-check-if-a-dataclass-field-has-a-default-value – Arne Apr 29 '19 at 06:35
  • For what it's worth, the same can be achieved without checking for `MISSING` and just `try`ing settattr and excepting a `TypeError`, and is arguably more pythonic. – Arne Apr 29 '19 at 06:40
  • Using `try` `except TypeError` would also catch the unlikely case of raising a `TypeError` in `default_factory`. I think of the case of raising a typeerror in `default_factory` which is caused by a bug in the code. In this case you would supress the bug and it would be very hard to detect. So checking explicit for MISSING could be the better way. – Hatatister Apr 29 '19 at 18:15
1

You can achieve desired result with __post_init__ method which will set self.l to empty list even if it is None:

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

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


a = A(None)
print(a.l)  # []
sanyassh
  • 8,100
  • 13
  • 36
  • 70