Search code examples
pythonpython-3.xpython-dataclasses

Passing helper variables to dataclass __init__ without assigning them


I have a root-level config class that I pass throughout my codebase through dependency injection.

Problem is, I have this dataclass that needs some attributes from this config to calculate a value world_coords in __post_init__().

In order to keep my tests clean and to circumvent other test import issues (not discussed here), I want to be able to pass in the config directly to this object instead of reading these values from an import. If I structure the config as an argument, however, it becomes an attribute of the dataclass, which I'm trying to avoid. There isn't really a need for RoadSegmentNode to hold on to a reference of the config once used.

Here's the basic structure of the class:

@dataclass(eq=True, frozen=True)  # make hashable
class RoadSegmentNode:

    tile_index: Tuple[int, int]  # (r, c)
    dir: Direction
    node_type: RoadNodeType
    world_coords: Tuple[int, int] = field(init=False)

    def __post_init__(self):

        # (Use config variables here, e.g. `config.TILE_WIDTH`, to calculate x and y)

        # Hack to get around frozen=True. We don't care that we're mutating
        # an "immutable" object on __init__().
        object.__setattr__(self, "world_coords", (x, y))

Here's the stopgap I went with to keep with the dependency injection model to unblock my tests for now. Note how RoadSegmentNode now has a bunch of new attributes that would only use for initialization. It's a little better than keeping a reference to the config, because at least they're explicit, but it's still a pretty poor design.

@dataclass(eq=True, frozen=True)  # make hashable
class RoadSegmentNode:

    # NOTE: DO NOT ACCESS THESE ATTRIBUTES!
    grid_width: int
    grid_height: int
    tile_width: int
    tile_height: int
    road_width: int

    tile_index: Tuple[int, int]  # (r, c)
    dir: Direction
    node_type: RoadNodeType
    world_coords: Tuple[int, int] = field(init=False)

    def __post_init__(self):

        # (Use attributes here, e.g. `self.tile_width`, to calculate x and y)

        # Hack to get around frozen=True. We don't care that we're mutating
        # an "immutable" object on __init__().
        object.__setattr__(self, "world_coords", (x, y))

How can I pass the config to the dataclass for initialization without making it an attribute of the dataclass? Should I even be considering a dataclass for this use case? I believe the original intention was to keep all instances immutable, but I can't confirm.


Solution

  • You should define config as an init-only variable. This way it'll be passed to __post_init__(), but will disappear after that:

    from dataclasses import dataclass, field, InitVar
    
    @dataclass(eq=True, frozen=True)
    class RoadSegmentNode:
    
        tile_index: Tuple[int, int]
        dir: Direction
        node_type: RoadNodeType
        world_coords: Tuple[int, int] = field(init=False)
        config: InitVar[Config]  # will not appear in instances
    
    
        def __post_init__(self, config):
            x, y = ..., ...  # calculate using config
    
            # looks hacky for sure, but is the right way to work with frozen dataclasses
            object.__setattr__(self, "world_coords", (x, y))