Calimocho
Calimocho

Reputation: 378

How to achieve encapsulation in python dataclasses with dependencies between variables

I have recently learned about the benefits of dataclasses in reducing boilerplate code when writing classes that primarily act as data containers.

I would like to be able to use dataclasses but would like to maintain the level of encapsulation that I have gotten used to with regular classes using private/protected variables and/or properties.

I would like to be able to encapsulate two fields so that they are only ever updated together according to some expected rules.

as a minimum example:

class EncapsulatedClass:
    def __init__(self, field1, field2):
        validate_args(field1, field2)
        self._field1 = field1
        self._field2 = field2

    def update_fields(self, arg1, arg2):
        if arg1:
            self._field1 += 1
        if arg2:
            self._field2 += 1
        if arg1 and arg2:
            self._field1 = 0

The reason I would like this behaviour is that it guarantees (up to user modification of protected args) that field1 and field2 have some expected relationship, this can be validated on initialisation and then never needs to be validated again.

However, with a standard class, I need to implement __eq__ __repr__ and __init__ which I would rather avoid to reduce boilerplate.

Is there a way to achieve this kind of behaviour using dataclasses with minimal boilerplate?

Upvotes: 2

Views: 1596

Answers (1)

chepner
chepner

Reputation: 530960

In general, no. But depending on exactly what you are trying to do, there may be various specific workarounds. For example, here I might make a property that operates on a tuple.

class EncapsulatedClass:
    def __init__(self, field1, field2):
        self.field = (field1, field2)

    @property
    def field(self):
        return (self._field1, self._field2)

    @field.setter
    def field(self, value):
        v1, v2 = value  # Here, just letting a possible exception propogate
        if v1:
            self._field1 += 1
        if v2:
            self._field2 += 1
        if v1 and v2:
            self._field1 = 0

    # Now it's a wrapper around the setter
    def update_fields(self, field1, field2):
        self.field = (field1, field2)

You can continue using a property in a dataclass. __post_init__ can be defined to initialize the property without having to re-impelement everything that would use the underlying attributes.

@dataclass
class EncapsulatedClass:
    _field1: int
    _field2: int

    def __post_init__(self):
        self.field = (self._field1, self._field2)
        
    @property
    def field(self):
        return (self._field1, self._field2)

    @field.setter
    def field(self, value):
        v1, v2 = value  # Here, just letting a possible exception propogate
        if v1:
            self._field1 += 1
        if v2:
            self._field2 += 1
        if v1 and v2:
            self._field1 = 0

    def update_fields(self, field1, field2):
        self.field = (field1, field2)

Upvotes: 1

Related Questions