Get python class's namespace parent type

Viewed 145

Is it possible to get the the namespace parent, or encapsulating type, of a class?

class base:
  class sub:
    def __init__(self):
      # self is "__main__.extra.sub"
      # want to create object of type "__main__.extra" from this
      pass

class extra(base):
  class sub(base.sub):
    pass

o = extra.sub()

The problem in base.sub.__init__ is getting extra from the extra.sub.

The only solutions I can think of at the moment involve having all subclasses of base provide some link to their encapsulating class type or turning the type of self in base.sub.__init__ into a string an manipulating it into a new type string. Both a bit ughly.

It's clearly possible to go the other way, type(self()).sub would give you extra.sub from inside base.sub.__init__ for a extra type object, but how do I do .. instead of .sub ? :)

2 Answers

The real answer is that there is no general way to do this. Python classes are normal objects, but they are created a bit differently. A class does not exist until well after its entire body has been executed. Once a class is created, it can be bound to many different names. The only reference it has to where it was created are the __module__ and __qualname__ attributes, but both of these are mutable.

In practice, it is possible to write your example like this:

class Sub:
    def __init__(self):
        pass

class Base:
    Sub = Sub
    Sub.__qualname__ = 'Base.Sub'

class Sub(Sub):
    pass

class Extra(Base):
    Sub = Sub
    Sub.__qualname__ = 'Extra.Sub'

del Sub  # Unlink from global namespace

Barring the capitalization, this behaves exactly as your original example. Hopefully this clarifies which code has access to what, and shows that the most robust way to determine the enclosing scope of a class is to explicitly assign it somewhere. You can do this in any number of ways. The trivial way is just to assign it. Going back to your original notation:

class Base:
    class Sub:
        def __init__(self):
            print(self.enclosing)

Base.Sub.enclosing = Base

class Extra(Base):
    class Sub(Base.Sub):
        pass

Extra.Sub.enclosing = Extra

Notice that since Base does not exist when it body is being executed, the assignment has to happen after the classes are both created. You can bypass this by using a metaclass or a decorator. That will allow you to mess with the namespace before the class object is assigned to a name, making the change more transparent.

class NestedMeta(type):
    def __init__(cls, name, bases, namespace):
        for name, obj in namespace.items():
            if isinstance(obj, type):
                obj.enclosing = cls

class Base(metaclass=NestedMeta):
    class Sub:
        def __init__(self):
            print(self.enclosing)

class Extra(Base):
    class Sub(Base.Sub):
        pass

But this is again somewhat unreliable because not all metaclasses are an instance of type, which takes us back to the first statement in this answer.

In many cases, you can use the __qualname__ and __module__ attributes to get the name of the surrounding class:

import sys

cls = type(o)
getattr(sys.modules[cls.__module__], '.'.join(cls.__qualname__.split('.')[:-1]))

This is a very literal answer to your question. It just shows one way of getting the class in the enclosing scope without addressing the probably design flaws that lead to this being necessary in the first place, or any of the many possible corner cases that this would not cover.

Related