Is it possible to retain a reference to the parent object when creating an instance of a nested class through the outer object, without explicitly passing the parent object as an argument?

class OuterClass:
    class InnerClass:
        def get_parent_link(self):
            pass

parent_obj = OuterClass()
child_obj = parent_obj.InnerClass()
child_obj.get_parent_link()  # Here, I want to access the reference to parent_obj

Please help with a solution that avoids explicitly passing the parent object when creating an instance of the inner class, or something like that, such as child_obj = parent_obj.InnerClass(parent_obj).

I don't know how to do this, maybe you need to use some complex decorators or metaclasses

2

There are 2 best solutions below

1
On BEST ANSWER

To do this, you have to go through a lot of rigamarole that almost certainly isn't worth it. But you can make the metaclass that injects an __init__ that also is a descriptor that partially applies that object when accessed on an instance:

import functools

class InjectParent(type):
    def __new__(cls, name, bases, ns):
        user_init = ns.get("__init__")

        def __init__(self, parent=None,  *args, **kwargs):
            self.parent = parent
            if user_init:
                user_init(*args, **kwargs)

        return super().__new__(cls, name, bases, {**ns, "__init__":__init__})

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return functools.partial(self, obj)

class Outer:
    class Inner(metaclass=InjectParent):
        pass

parent = Outer()
child = parent.Inner()
orphan = Outer.Inner()

assert child.parent is parent
assert orphan.parent is None

While this is a fun exercise, I highly suggest you don't do this in production code. Explicit is better than implicit.

Note, parent.Inner no longer refers to the actual class (although, Outer.Inner does), so isinstance(child, parent.Inner) will fail.

Also, it doesn't handle inheritance of __init__. It only handles the case of an __init__ being defined in the class directly.

0
On

There is not much sense in creating nested classes, although it is possible. What you probably want is just an associated child object, as an attribute to instances of the OuterClass object.

A nested InnerClass, as asked, cannot know, and can't retrieve things from the outer class.

However, using the descriptor mechanisms, even its ready made implementation, the @property decorator, you can have associated objects of the innerclass built for free, and with a "natural knowledge" about its parent.

This is the pattern I believe can be useful for you:

class InnerClass:
    def __init__(self, parent):
        self.parent = parent
    def get_parent_link(self):
        return self.parent
    
class OuterClass:

    @property
    def child_obj(self):
        # this if clause bellow will restrict one child instance of "inner"
        # per "outter" instance. Just `return InnerClass(parent=self)`
        # to create a new instance each time .child_obj is retrieved
        # if desired
        if not hasattr(self, "_inner"):
            self._inner = InnerClass(parent=self)
        return InnerClass(parent = self)
    
parent_obj = OuterClass()
child_obj = parent_obj.child_obj # like this - no parenthesis needed
child_obj.get_parent_link()  

However, what you are asking for is possible using metaclasses and the descriptor protocol. It is just not useful - but for entertainment purposes, you can write:

from copy import copy

class Meta(type):
    def __get__(cls, instance, owner=None):
        new_cls = copy(cls)
        new_cls._parent = instance
        return new_cls
        

class OuterClass:
    class InnerClass(metaclass=Meta):
        def get_parent_link(self):
            return self._parent

parent_obj = OuterClass()
child_obj = parent_obj.InnerClass()
child_obj.get_parent_link()

Please, note that this is for demonstration purposes only - it will work exactly as you described, but this design will leak memory due to a new copy of inner_class being created each time.

It can be fixed with a wrapper function holding the reference to the parent inside the metaclass __get__, though.