What is a better way to iterate over dataclass keys and values?
Asked Answered
B

6

46

I have two dataclasses, Route and Factors. Route contains a value and three copies of Factors.

Route does not know how many variables Factors contains. I want to get the name of these variables, and then get the respective value of each one, for each copy of Factors.

Here is what I currently have:

@dataclass
class Factors:
    do: bool  # does it do the route
    hub: int # how many of the locations are hubs

    def __init__(self, do_init):
        self.do = do_init
        self.hub = 0 # will add later 

    def __str__(self):
        return "%s" % self.do


@dataclass
class Route:
    route: tuple
    skyteam: Factors
    star: Factors
    oneworld: Factors

    def __init__(self, route):
        self.route = route.get('route')
        # this could probably be done with one line loop and a variable with names
        self.skyteam = Factors(route.get('skyteam'))
        self.star = Factors(route.get('star'))
        self.oneworld = Factors(route.get('oneworld'))

    def __str__(self):
        table = [[self.route, "SkyTeam", "StarAlliance", "OneWorld"]] # var name is fine
        for var in Factors.__dict__.get('__annotations__').keys():  # for each factor
            factor = [var]
            factor.append(self.skyteam.__dict__.get(var))
            factor.append(self.star.__dict__.get(var))
            factor.append(self.oneworld.__dict__.get(var))
            table.append(factor)
        return tabulate.tabulate(table, tablefmt='plain')

Input is

{'route': ('BOS', 'DXB'), 'skyteam': True, 'star': True, 'oneworld': True}

Current output is

('BOS', 'DXB')  SkyTeam  StarAlliance  OneWorld
do              True     True          True
hub             0        0             0

Maybe I could search Route for each variable that contains a Factors datatype and iterate over those?

Baryta answered 16/3, 2020 at 21:54 Comment(1)
I got here because I also want tabulate to support data classes as rows.Eradis
P
75

You may use dataclass.fields

from dataclasses import dataclass, fields

for field in fields(YourDataclass):
    print(field.name, getattr(YourDataclass, field.name))
Passably answered 30/11, 2021 at 10:52 Comment(2)
I'm always surprised that we have to use getattr and that they didn't plan access via a simple attribute like field.value.Nuriel
I think this is because the Field returned is definition of a field and mixing it up with a value for each instance of a dataclass will make a lot of duplication and mess in logic. Field is field. It would be nice if they could impl something like items and values functions for dataclass, such as counterparts for dict.Weakfish
L
18

Probably the simplest way is to import asdict from dataclasses, and then write something like:

for key, value in asdict(route).items():
    ...

For a small dataclass, the cost of copying the dataclass object to a new dictionary is outweighed by the convenience and readability.

Logistics answered 1/7, 2022 at 14:23 Comment(0)
I
8

I'd also make use of the __dataclass_fields__ which returns a dict of the variable names and their types. For example something like:

for field in mydataclass.__dataclass_fields__:
    value = getattr(mydataclass, field)
    print(field, value)
Irremediable answered 1/6, 2021 at 2:19 Comment(1)
This works, but doesn't appear to be documented. It's the same attribute returned by dataclasses.fields(), though. Probably better to use the latter.Valentino
M
6

I'd leave the builtin __str__s alone and just call the function visualize or something on the Route class, but that's taste. You also shouldn't overload the __init__ of a dataclass unless you absolutely have to, just splat your input dict into the default constructor.

Final nit, try to use getattr/setattr over accessing the __dict__, dataclasses are popular for using __slots__ to store their attributes, which would break your code in a non-trivial way.

So I'd go with something like this, using the tabulate library to handle the rendering:

from dataclasses import dataclass, fields
import tabulate

@dataclass
class Factor:
    do: bool
    hub: int = 0 # this is how to add a default value


@dataclass
class Route:
    route: tuple
    skyteam: Factor
    star: Factor
    oneworld: Factor

    def __post_init__(self):
        # turn Factor parameter dicts into Factor objects
        for field in fields(self):
            if issubclass(field.type, Factor):
                setattr(self, field.name, field.type(getattr(self, field.name)))

    def visualize(self):
        factors = {
            field.name: getattr(self, field.name)
            for field in fields(self)
            if issubclass(field.type, Factor)
        }
        rows = [[self.route, *factors]]  # header
        for field in fields(Factor):
            rows.append([field.name, *[getattr(f, field.name) for f in factors.values()]])
        print(tabulate.tabulate(rows))

Which works out fine for your example:

>>> r = Route(**{'route': ('BOS', 'DXB'), 'skyteam': True, 'star': True, 'oneworld': True})
>>> r.visualize()
--------------  -------  ----  --------
('BOS', 'DXB')  skyteam  star  oneworld
do              True     True  True
hub             0        0     0
--------------  -------  ----  --------

This solution should continue to work both if you add more fields to the Factor class and more factor instances to the Route.

Murtagh answered 17/3, 2020 at 9:57 Comment(2)
could *[f for f in factors] be *factors?Undulation
@wuyudi absolutely can, that line was an artifact from an earlier version. thanks!Murtagh
O
4

To access the fields within the class itself, here is what I tried.

from dataclass import dataclass, fields

@dataclass
class someDataclass:
    def access_fields(self):
        for field in fields(self.__class__):
            print(field) # you get the Field() instances
Oyler answered 13/1, 2022 at 10:58 Comment(0)
F
0

To iterate over the key-value pairs, you can add this method to your dataclass:

def items(self):
    for field in dataclasses.fields(self):
        yield field.name, getattr(self, field.name)

Then loop as usual:

for key, value in obj.items():
    do_stuff(key, value)
Fundament answered 15/11, 2023 at 9:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.