Python: Circular dependency of dataclasses / Forward variable declaration?
Asked Answered
P

2

7

So, I have these two dataclasses in a file:

@dataclass
class A:
    children: List[B]

@dataclass
class B:
    parent: A

, which are possible with the use of the __future__.annotations feature.

Then I have two other files, each with a bunch of objects for each type that are static for my project.

File objects_A:

import objects_B

obj_a1 = A(
    children=[
        objects_B.obj_b1,
        objects_B.obj_b2
    ]
)

File objects_B:

import objects_A

obj_b1 = B(
    parent=objects_A.obj_a1
)

obj_b2 = B(
    parent=objects_A.obj_a1
)

Obviously, there a circular dependency problem between the files, but it wouldn't work even if they were in the same file, as a variable of one type depends on the other to succeed.
Initialising the B objects inside obj_a1 also won't work as there is no concept of self here.

At the moment, I'm setting parent to None (against the type hinting), and then do a loop on obj_a1 to set them up:

for obj_b in obj_a1.children:
    obj_b.parent = obj_a1

Any bright ideas folks?
Don't know if it helps, but these objects are static (they will not change after these declarations) and they have kind of a parent-children relationship (as you surely have noticed).
If possible, I would like to have the variables of each type in different files.

Prohibitive answered 29/4, 2020 at 22:21 Comment(3)
you are mapping 1 object to another in essence, why can't you make some sort of dictionary? e.g: dict = {PARENT_OBJ: [CHILDREN]}Hayse
The idea is to pass an object of type B around the project and be able to access other stuff from parent A without needing to use other structures, like dictionaries.Prohibitive
There is no way to have actual object references that have circular dependencies. You either have indirect references through a mapping that will be realized eventually, as Peter S proposed, or you initialize all As with an empty list for children, and only add the Bs to the relationship when their object file is executed. Do none of these two options work?Receiver
E
5

I know that I'm late but I'll just leave my answer here for others to use.

According to PEP 563, python 3.7 has introduced lazy evaluation of annotations which can be very useful in the case of a circular dependency.

@dataclass
class StudentData:
    school: 'SchoolData'

@dataclass
class SchoolData:
    students: StudentData

As you can see, the SchoolData type annotation is wrapped inside quotations which allows you to reference the SchoolData type before its declaration.

Ethiopic answered 16/2, 2022 at 18:44 Comment(0)
M
1

One way to solve this is to use a base class as an alternative to forward declaration.

@dataclass
class ParentBase:
    children: List[object]


@dataclass
class Child:
    parent: ParentBase


@dataclass
class Parent(ParentBase):
    children: List[Child]



parent = Parent(children=[])
child = Child(parent=parent)
parent.children.append(child)

This will work. I have added the children: List[object] in the ParentBase. This is not necessary for this code to work, but if you add it your IDE can help you when you start accessing child.parent..

This will not stop anyone from just instantiating a Child with a ParentBase. To solve that problem, you could do something like this:

@dataclass
class ParentBase(abc.ABC):
    children: List[object]

    @abc.abstractmethod
    def __post_init__(self):
        pass


@dataclass
class Child:
    parent: ParentBase


@dataclass
class Parent(ParentBase):
    children: List[Child]

    def __post_init__(self):
        pass


parent = Parent(children=[])
child = Child(parent=ParentBase())  # TypeError
child = Child(parent=parent)
parent.children.append(child)

It might be too defensive and too much boilerplate code. It depends on the preference of your team whether you want to make this safer.

Meerschaum answered 18/11, 2021 at 14:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.