0

Notes On Similar Q&As:


I'm trying to do a simple nesting of multiple Python @dataclass decorated classes within another class, and have later classes refer back to the earlier ones. If I do not nest them at all, they work as expected, being able to include the first class defined into an object in the second class:

from dataclasses import dataclass, field

@dataclass
class A:
  z:int = field(default=0)

@dataclass
class B:
  a:A = field(default=A(z=1)) ### Object that is class A is included in class B

b = B(a=A(z=3))
print(f'b = B(a=A(z=3)); b.a.z={b.a.z}; b={b}')

But if I try to do the same inside of another class (in this case, not a dataclass), the "B" class cannot see the "A" class. In the below code, the definition of a as a type of A fails with a NameError: "name A is not defined". I've tried A and C.A, neither work.

Note that the other functions in the C class are able to see both A and B just fine, just inside dataclass B cannot see dataclass A.

class C:
  @dataclass
  class A:
    z:int = field(default=0)

  @dataclass
  class B:
    a:A = field(default=A(z=1)) ### NameError: name 'A' is not defined

  def __init__(self):
    self.b = C.B(a=C.A(z=3))

  def print_info(self):
    print(f'b = C.B(a=C.A(z=3)); b.a.z={self.b.a.z}; b={b}')

c = C()
c.print_info()

However, if I convert these to normal Python classes, it works in the nested case:

Rechecking, it turns out this is broken in normal classes as well (per comment below).

Strangely, if one nests dataclass A inside dataclass B, with B still inside class C, it does work - B has direct access to A, but nothing else in class C has direct access to A.

Question

Is it possible to define nested dataclasses with having the later ones access the earlier ones at the same level? If so, how?

LightCC
  • 9,804
  • 5
  • 52
  • 92
  • 1
    ["Flat is better than nested." - The Zen of Python](https://www.python.org/dev/peps/pep-0020/) – I don't see a good reason to nest classes at all here. Also, do note that what you're doing in both of these cases is share a single `A` instance between all instances of `B`. That's likely not what you want since these aren't `frozen` dataclasses. – AKX Oct 25 '21 at 05:39
  • 2
    As another aside, if you remove the `@dataclass` decorators from the nested-dataclasses example, you'll find it still raises the NameError, so this isn't really about dataclasses... – AKX Oct 25 '21 at 05:41
  • @AKX In my "considerably more complex" real-world code, it feels like the right thing to nest them to get functional groupings, but it's true that I could leave them at least flat per module. Regarding the NameError - you are right! I messed up the way I had the file laid out, if I cut it down to just that example, it's also broken. – LightCC Oct 25 '21 at 05:50
  • 2
    Modules are great for functional grouping. I don't think there are many, if any, nested classes in the standard library... – AKX Oct 25 '21 at 05:54
  • Based on my mistake on the normal classes, it is a duplicate of https://stackoverflow.com/q/42185472/6501141, voting to reclose it. Your answer does add some clarity that isn't at that Q&A though. Thanks! – LightCC Oct 25 '21 at 06:06
  • Not 100% sure, but I don't think the referenced answer does actually answer this question. I think (honestly not sure even after reading the question 5x though) the question is more simply stated 'how do I have 3 level deep nested dataclasses with default values)?' ...if so, this is a common question/use-case that is easily handled (the OP just made it more complex than it needs to be, and you don't need globals etc) – Richard Oct 25 '21 at 17:25

1 Answers1

1

To my best understanding, this is due to the semantics for class definitions – emphasis mine:

The class’s suite is then executed in a new execution frame, using a newly created local namespace and the original global namespace. (Usually, the suite contains mostly function definitions.) When the class’s suite finishes execution, its execution frame is discarded but its local namespace is saved.

That is, any class definition only ever has the module-global namespace (which does not yet contain C in this case, since its suite hasn't finished executing) and a new empty local namespace.

EDIT

Based on the above, this can be hacked together to work, but I really wouldn't do this...

from dataclasses import dataclass, field


class C:
    @dataclass
    class A:
        z: int = field(default=0)

    globals()["A"] = A  # "and the original global namespace..."

    @dataclass
    class B:
        a: A = field(default=A(z=1))

    def __init__(self):
        self.b = C.B(a=C.A(z=3))

    def print_info(self):
        print(f"{self.b.a.z=}")


c = C()
c.print_info()
AKX
  • 152,115
  • 15
  • 115
  • 172
  • This makes sense now. The nested classes don't actually exist unless they exist through an instance of that class, unless they were to somehow be made static so they could be referenced directly by the class (which may not even be possible like it is with functions/methods). I need to embrace the zen of Python in this case, which means flattening my class definitions, and splitting into more modules if I really need the functional grouping... – LightCC Oct 25 '21 at 06:10
  • "The nested classes don't actually exist unless they exist through an instance of that class" No, they do exist alright, it's just that the namespace in which they exist is local and anonymous until the outer class is "fully defined", as it were. – AKX Oct 25 '21 at 06:22
  • 2
    Anyway, see my edit – by injecting the inner class into the global namespace, this can be made to work, but it's very ugly. – AKX Oct 25 '21 at 06:25
  • I think you can still reference the inner class without having to make it global, but you reference it through C (as that's where it's defined): `inner_instance = C.A(z=0)` – Richard Oct 25 '21 at 16:15
  • @Richard I doubt that since the name C is not yet a thing while the inner body is being evaluated. – AKX Oct 25 '21 at 17:15
  • @AKX I'm not 100% sure exactly what the OP is wanting, but I _think_ it's handled very easily without having to make globals. ...I've got some working code I'd post, but unfortunately the Q is closed now (I don't think it was correct to close it, but separate problem) – Richard Oct 25 '21 at 17:28