Search code examples
pythonenumspython-dataclasses

auto() in Dataclass like auto() in Enum (Python)


Is there a way to do the same code below using Dataclass instead of Enum?

from enum import Enum, auto


class State(Enum):
    val_A= auto()
    val_B = auto()
    val_C = auto()

The only solution I found is the following code:

from dataclasses import dataclass

@dataclass(frozen=True)
class State():
    val_A:str = 'val_A'
    val_B:str = 'val_B'
    val_C:str = 'val_C'

thank you for the suggestions.


Solution

  • Descriptors

    One approach could be to use a descriptor class, defined as below:

    class Auto:
        _GLOBAL_STATE = {}
    
        __slots__ = ('_private_name', )
    
        # `owner` is the class or type, whereas instance is an object of `owner`
        def __get__(self, instance, owner):
            try:
                return getattr(instance, self._private_name)
            except AttributeError:
                _state = self.__class__._GLOBAL_STATE
                _dflt = _state[owner] = _state.get(owner, 0) + 1
                return _dflt
    
        def __set_name__(self, owner, name):
            self._private_name = '_' + name
    
        def __set__(self, instance, value):
            # use object.__setattr__() instead of setattr() as dataclasses
            # also does, in case of a "frozen" dataclass
            object.__setattr__(instance, self._private_name, value)
    

    Usage would be as follows:

    from dataclasses import dataclass
    
    
    @dataclass(frozen=True)
    class State:
        val_A: int = Auto()
        val_B: int = Auto()
        val_C: int = Auto()
    
    
    s = State()
    print(s)  # State(val_A=1, val_B=2, val_C=3)
    assert s.val_B == 2
    
    s = State(val_A=5)
    assert s.val_A == 5
    assert s.val_C == 3
    

    Optimal Approach

    The most performant approach I can think of, would be to replace the default values before dataclasses is able to process the class.

    Initially this is O(N) time, as it would require iterating over all the class members (including dataclass fields) at least once. However, the real benefit is that it replaces the default values for auto values, such as val_A: int = 1, before the @dataclass decorator is able to process the class.

    For example, define a metaclass such as one below:

    # sentinel value to detect when to replace a field's default
    auto = object()
    
    
    def check_auto(name, bases, cls_dict):
        default = 1
    
        for name, val in cls_dict.items():
            if val == auto:
                cls_dict[name] = default
                default += 1
    
        cls = type(name, bases, cls_dict)
        return cls
    

    Usage is as below:

    from dataclasses import dataclass
    
    
    @dataclass(frozen=True)
    class State(metaclass=check_auto):
        val_A: int = auto
        val_B: int = auto
        val_C: int = auto
    
    
    s = State()
    print(s)  # State(val_A=1, val_B=2, val_C=3)
    assert s.val_B == 2
    
    s = State(val_A=5)
    assert s.val_A == 5
    assert s.val_C == 3