Search code examples
pythoninner-classespython-dataclasses

Inner classes can't reference eachother, is there a more Pythonic way?


I want to use inner classes (mainly dataclass and Enums) to keep things encapsulated. They hold data and defines that are only relevant to the main class, so I'd like to keep them inside it. I get the sense that this is not the most Pythonic way to do things, but I'm not sure how to make it better.

The real problem is that I need some of those inner classes to contain variables that use types of the other inner classes, and Python doesn't seem to allow that.

This is what I would like to do (this is just a pared down example). This keeps everything as part of DataPacket, so that when you reference the inner classes you use DataPacket to get to it. ie. DataPacket.DataStatus.GOOD, etc, and it's clear where that "define" comes from. However the DataStatus reference in SensorData is not found unless it is moved out of the DataPacket class.

from dataclasses import dataclass
from typing import List
from enum import IntEnum

class DataPacket:

    class DataStatus(IntEnum):
        GOOD = 0
        ERROR = 1
        UNKNOWN = 255

    @dataclass
    class SensorData():
        sensor_name: int = 0
        sensor_type: int = 0
        unit_type: int = 0
        status: DataStatus = DataStatus.UNKNOWN
        value: float = 0

    @dataclass
    class Sensors():
        data: List[SensorData]
        count: int = 0


    def build_packet(self):
        
        sensors = self.Sensors([])
        
        # Read data from device to fill in sensor values

        sensors.count = 1

        data = self.SensorData()
        data.sensor_name = 1
        data.sensor_type = 2
        data.unit_type = 3
        data.status = self.DataStatus.GOOD
        data.value = 100

        sensors.data.append(data)

        return sensors


packet = DataPacket()

sensors = packet.build_packet()

if sensors.data[0].status == DataPacket.DataStatus.GOOD:
    print(sensors.data[0].value)
else:
    print("Sensors data error")

This is how to get it to work, but I don't like this structure:

from dataclasses import dataclass
from typing import List
from enum import IntEnum

class DataStatus(IntEnum):
    GOOD = 0
    ERROR = 1
    UNKNOWN = 255

@dataclass
class SensorData():
    sensor_name: int = 0
    sensor_type: int = 0
    unit_type: int = 0
    status: DataStatus = DataStatus.UNKNOWN
    value: float = 0

@dataclass
class Sensors():
    data: List[SensorData]
    count: int = 0

class DataPacket:  

    def build_packet(self):
        
        sensors = Sensors([])
        
        # Read data from device to fill in sensor values

        sensors.count = 1

        data = SensorData()
        data.sensor_name = 1
        data.sensor_type = 2
        data.unit_type = 3
        data.status = DataStatus.GOOD
        data.value = 100

        sensors.data.append(data)

        return sensors


packet = DataPacket()

sensors = packet.build_packet()

if sensors.data[0].status == DataStatus.GOOD:
    print(sensors.data[0].value)
else:
    print("Sensors data error")

Thanks for your help/suggestions!


Solution

  • The problem you are encountering is because class bodies do not create enclosing scopes. Nesting class definitions isn't a common pattern in Python. You are going to be working against the language to get it to work. Here is a minimal example of your problem:

    >>> class Foo:
    ...     class Bar:
    ...         pass
    ...     class Baz:
    ...         bar = Bar()
    ...
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 4, in Foo
      File "<stdin>", line 5, in Baz
    NameError: name 'Bar' is not defined
    

    Here is a way to get around it, refer to Bar where it is in scope, and assign to the class attribute after the class definition:

    >>> class Foo:
    ...     class Bar:
    ...         pass
    ...     class Baz:
    ...         pass
    ...     Baz.bar = Bar()
    ...
    

    But really, you should just keep the solution you have already, which is perfectly Pythonic. Note, this workaround means you are going to have to abandon the dataclasses.dataclass code generator, since that requires annotations in the class body. Or, I suppose, you could use string annotations:

    >>> import dataclasses
    >>> class Foo:
    ...     class Bar:
    ...         pass
    ...     @dataclasses.dataclass
    ...     class Baz:
    ...         bar: "Bar"
    ...
    

    But if you want a default value, which actually requires the class, it's not going to work.

    Your reasoning for nesting the classes is that "They hold data and defines that are only relevant to the main class, so I'd like to keep them inside it." but the main unit of code organization is the module in Python. Everything being in a module here is perfectly acceptable.