2

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 ? :)

10
  • Python classes can't see enclosing classes. Putting sub into another class gives you nothing. Commented Sep 14, 2020 at 8:52
  • It's extremely unclear why you think extra has anything to do with base.sub. Please explain your assumptions better, and post an actual MCVE. Commented Sep 14, 2020 at 8:54
  • I'm sorry :) I thought it was very clear. At run time, given these classes, polymorphism means that self in base.sub.__init__ MAY be of type base.sub or extra.sub. The code above is actually runnable, and if run self will be type extra.sub in the base.sub constructor. Getting type extra from extra.sub is what this comes down to. Commented Sep 14, 2020 at 10:06
  • I think to suggest extra does not have anything to do with base.sub is a bit too strong, extra is related to base by inheritance, and base is related to base.sub by namespace. One would have to get into definition of "related" and start a discussion on relationship theory to take this much further though, but that won't help or interest me. The problem at hand is following the namespace relationship from extra.sub to extra, it's clearly semantically possible, and often done even in various contexts. Writing from .. import x we traverse the module namespace back for example. Commented Sep 14, 2020 at 10:08
  • I'm referring to practical relationships in the language, not some abstract concept. Nested class bodies are not aware of any enclosing scope except global. A class itself is just an object. You can have multiple references to it if you want. I'm not disagreeing that your example runs. I'm just working off a different set of assumptions, so I don't understand why you think the code you show should work. I can explain in much more detail why python is designed for it not to though. And maybe find a workaround. Commented Sep 14, 2020 at 11:50

2 Answers 2

1

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.

Sign up to request clarification or add additional context in comments.

4 Comments

"No answer" and "not possible" is a perfectly reasonable and legit answer. I'm accepting this more interesting answer in the absence of anyone as yet who disagrees with that, and because I couldn't see it as possible in a sane way either. As you point out here, there are other paths with meta classes and factories one may walk; alternative design stratagems may ultimately be the correct way to find one's self on a path that doesn't give rise to the question, but sometimes it's not desirable or practical to spend time and make more extensive changes to a code base to solve a tiny problem.
@Michael. I agree that this answer is more in-depth, but you have to admit that the other one is much more practical in that it correctly covers 90% of cases, and isn't a huge pain to implement.
Yes, the other answer is a valid answer, some may even say more valid in fact, but it's also a slightly improved version of the hackery I suggested in the question comments and I can't bring myself to have anything like either version in any commit log with my name next to it though, and it's maybe an answer we shouldn't admit is one as well :) I chose this answer to accept not because it was more in depth, but because (leaving aside horrors) "no general way to do this" seems to me to be more the answer we should aspire to accept as the real truth.
@Michael. I basically agree with you on this, which is fairly inevitable given that we share the same information now. I would posit that there is nothing wrong with explicitly saying "I want this to be my enclosing class", whether in a metaclass or by manual assignment.
0

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.