pythonpython-dataclasses

Call the generated __init__ from custom constructor in dataclass for defaults


Is it possible to benefit from dataclasses.field, especially for default values, but using a custom constuctor? I know the @dataclass annotation sets default values in the generated __init__, and won't do it anymore if I replace it. So, is it possible to replace the generated __init__, and to still call it inside?

@dataclass
class A:
    l: list[int] = field(default_factory=list)
    i: int = field(default=0)
        
    def __init__(self, a: Optional[int]): # completely different args than instance attributes
        self.call_dataclass_generated_init() # call generated init to set defaults
        if a is not None: # custom settings of attributes
            self.i = 2*a

A workaround would be to define __new__ instead of overriding __init__, but I prefer to avoid that.

So, in short, I want to benefit from dataclass's feature to have default values for attributes, without cumbersome workarounds. Note that raw default values is not an option for me because it sets class attributes:

class B:
    a: int = 0 # this will create B.a class attribute, and vars(B()) will be empty
    l: list[int] = [] # worse, a mutable object will be shared between instances

Solution

  • As I perceive it, the cleaner approach there is to have an alternative classmethod to use as your constructor: this way, the dataclass would work exactly as intended and you could just do:

    from dataclasses import dataclass, field
    from typing import Optional
    
    
    @dataclass
    class A:
        l: list[int] = field(default_factory=list)
        i: int = field(default=0)
         
        @classmethod
        def new(cls, a: Optional[int]=0): # completely different args than instance attributes
            # creates a new instance with default values:
            instance = cls()
            # if one wants to have more control over the instance creation, it is possible to call __new__ and __init__ manually:
            # instance = cls.__new__(cls)
            # instance.__init__()
            if a is not None: # custom settings of attributes
                i = 2*a
                
            return instance
    

    But if you don't want an explicit constructor method, and really need to call just A(), it can be done by creating a decorator, that will be applied after @dataclass - it can then move __init__ to another name. The only thng being that your custom __init__ has to be called another name, otherwise @dataclass won't create the method.

    def custom_init(cls):
        cls._dataclass_generated_init = cls.__init__
        cls.__init__ = cls.__custom_init__
        return cls
    
    @custom_init    
    @dataclass
    class A:
        l: list[int] = field(default_factory=list)
        i: int = field(default=0)
            
        def __custom_init__(self, a: Optional[int]): # completely different args than instance attributes
            self._dataclass_generated_init() # call generated init to set defaults
            if a is not None: # custom settings of attributes
                i = 2*a
                ...
            print("custom init called")