6

I have a fixed set of three sensors that I want to model as an enum. Each of these sensors is parametrised by a few different attributes. I therefore want to model the sensors themselves as a dataclass.

My naive attempt looks something like this:

@dataclass
class SensorLocation:
    address: int
    pins: int
    other_details: ...

class Sensors(SensorLocation, Enum):
    TOP_SENSOR = SensorLocation(address=0x10, pins=0xf,  other_details=...)
    BOTTOM_SENSOR = SensorLocation(address=0x10, pins=0xf0,  other_details=...)
    SIDE_SENSOR = SensorLocation(address=0x15, pins=0xf,  other_details=...)

My expectation is that this should essentially create an enum, where the instances of that enum behave like instances of SensorLocation. This makes the types a bit clearer and puts methods where I'd expect them to be.

However, this fails while creating the enum, with the error:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/path/to/python/3.7.10/lib/python3.7/enum.py", line 232, in __new__
    enum_member.__init__(*args)
  File "<string>", line 3, in __init__
  File "/path/to/python/3.7.10/lib/python3.7/types.py", line 175, in __set__
    raise AttributeError("can't set attribute")
AttributeError: can't set attribute

What I can do is remove the SensorLocation subclassing in the enum declaration, but this means that when using MyPy or similar tools, I lose some ability to type hint the correct values. It also makes accessing the actual values more complicated, but the main purpose of this enum is to provide access to those values.

Is there a way around this error that I'm missing, or another solution that I can't see right now?

martineau
  • 119,623
  • 25
  • 170
  • 301
Johz
  • 161
  • 1
  • 4
  • 3
    It doesn't really make sense for `Sensors` to inherit from `SensorLocation`, what exactly is it that you want to accomplish by doing that? Can you elaborate? "ut this means that when using MyPy or similar tools, I lose some ability to type hint the correct values" – juanpa.arrivillaga Jan 31 '22 at 10:30
  • You might be able to create an Enum whose values were frozen dataclass instances. – martineau Jan 31 '22 at 10:45

3 Answers3

2

In python, dataclass values can be anything so you don't necessarily need to inherit from SensorLocation in Sensors.

SensorLocation could optionally be a subclass inside of Sensors if that's the only place it is used.

The @property decorator can make the dataclass attributes more easily callable but that's a bit of extra work and somewhat defeats the purpose of the dataclass. It's a nice option to have though.

from dataclasses import dataclass
from enum import Enum


@dataclass
class SensorLocation:
    address: int
    pins: int
    other_details: dict


class Sensors(Enum):
    TOP_SENSOR = SensorLocation(address=0x10, pins=0xf, other_details={})
    BOTTOM_SENSOR = SensorLocation(address=0x10, pins=0xf0, other_details={})
    SIDE_SENSOR = SensorLocation(address=0x15, pins=0xf, other_details={})

    @property
    def address(self):
        return self.value.address

print(f'Top sensor address: 0x{Sensors.TOP_SENSOR.address:x}')

Or without the @property decorator, simply call the enum member's value() method. It's slightly more verbose to call it, but keeps the enum class lightweight.

from dataclasses import dataclass
from enum import Enum


@dataclass
class SensorLocation:
    address: int
    pins: int
    other_details: dict

class Sensors(Enum):
    TOP_SENSOR = SensorLocation(address=0x10, pins=0xf, other_details={})
    BOTTOM_SENSOR = SensorLocation(address=0x10, pins=0xf0, other_details={})
    SIDE_SENSOR = SensorLocation(address=0x15, pins=0xf, other_details={})

print(f'Top sensor address: 0x{Sensors.TOP_SENSOR.value.address:x}')

For the type hinting problem, if you need a method to refer back to the type of its owning class Sensors, this syntax works with the class name in single quotes: def something(self) -> 'Sensors':

jljohn00
  • 41
  • 3
2

As far as I understand, Enum metaclass is (way too) smart and handles construction of instances on its own by using each declared member value as an argument for mixin class __new__/__init__ call. I.e. this works (on Python 3.11 at least):

from dataclasses import dataclass
from enum import Enum


@dataclass
class SensorLocation:
    address: int
    pins: int
    other_details: None


class Sensors(SensorLocation, Enum):
    TOP_SENSOR = (0x10, 0xf, None)
    BOTTOM_SENSOR = (0x10, 0xf0, None)
    SIDE_SENSOR = (0x15, 0xf, None)


assert isinstance(Sensors.SIDE_SENSOR, SensorLocation)
zeronineseven
  • 129
  • 2
  • 6
1

I think you can try using a custom __init__ function for your Enum:

from dataclasses import dataclass
from enum import Enum


@dataclass
class SensorLocation:
    address: int
    pins: int
    other_details: dict


class Sensors(SensorLocation, Enum):
    TOP_SENSOR = SensorLocation(address=0x10, pins=0xf, other_details={})
    BOTTOM_SENSOR = SensorLocation(address=0x10, pins=0xf0, other_details={})
    SIDE_SENSOR = SensorLocation(address=0x15, pins=0xf, other_details={})

    def __init__(self, data: SensorLocation):
        for key in data.__annotations__.keys():
            value = getattr(data, key)
            setattr(self, key, value)


print(f'Top sensor address: 0x{Sensors.TOP_SENSOR.address:x}')

Output:

Top sensor address: 0x10
rohlfs
  • 11
  • 3