How can I silently make a nested class aware of it's outer class (by putting all the required code in parent or metaclass)?

Viewed 48

I have made a symbol class that helps me to make symbolic constants by using metaclasses

>>> class SymbolClass(type):
    def __repr__(self): return self.__name__

>>> class Symbol(metaclass=SymbolClass): pass

>>> class Spam(Symbol) : pass

>>> class Egg(Symbol) : pass

>>> class Banana(Symbol) : pass

>>> mydict = {Egg:"Where is the bacon", Banana:(2, 3, 4)}

>>> mydict
{Egg: 'Where is the bacon', Banana: (2, 3, 4)}

However, If I want to use it for enumerations by nesting such symbols in an outer class, I will need a way to make the symbol inner classes aware of their outer class. With the current implementation, here is what happens :

>>> class Animal:
        class Dog(Symbol): pass
        class Cat(Symbol): pass

>>> print(repr(Animal.Dog))
Dog

It would be OK to get this reply from printing str(Animal.Dog), but when printing repr(Animal.Dog), I'd like to get Animal.Dog.

So, How could I make the nested Animal.Dog and Animal.Cat classes, aware of their outer Animal class, but without complicating their declaration. I want to have all the needed code, hidden in the super classes (Symbol or Animal's superclass), or their metaclasses.

2 Answers

The __qualname__ in contrast with name includes the information about enclosing classes or functions:


In [13]: class Symbol(type):
    ...:     def __repr__(cls):
    ...:         return cls.__qualname__
    ...: 
    ...: 

In [14]: class Dog(metaclass=Symbol): pass

In [15]: Dog
Out[15]: Dog

In [16]: class Animal:
    ...:     class Dog(metaclass=Symbol): pass
    ...: 

In [17]: Animal.Dog
Out[17]: Animal.Dog

Of course, if you really want to use Enums, Python's enum.Enumcan cover a wide number of use cases. Sometimes I just find it overkill, and use some simpler ad-hoc things, but once you are resorting to a custom metaclass, that is no longer "simpler", and you likely will have some corner cases ahead that enum has already solved.

Really getting information about the enclosing class

Now, regardless of Enums, if one wants to use a metaclass and have information about the enclosing class body - as in the title of this question, the thing is trickier. For inside a class body definition, the variables in the class body itself are the local variables, and the global variables point to the module globals: there is no equivalent of nonlocal as when the enclosing scope is a function.

If an object that is set as a class attribute needs information about its owner class, Python implements the powerful descriptor procotol, used by the language built-in property. All you need is to have a __get__ method defined in your object's class, and take note of the owners attributes: no metaclasses needed:

In [56]: class Symbol: # not a metaclass
    ...:     def __get__(self, instance, owner):
    ...:         self.owner = owner
    ...:         return self
    ...:     def __set_name__(self, owner, name):
    ...:         self.name = name
    ...:     def __repr__(self):
    ...:         return f"{self.owner.__name__}.{self.name}"
    ...: 
    ...: 

In [57]: class Animal:
    ...:     Dog = Symbol()
    ...: 

In [58]: Animal.Dog
Out[58]: Animal.Dog

Now, if one wants information on the enclosing scope at a nested class creation time, it is feasible, but one needs to introspect the stack in the metaclass' __new__ and find if locals in the enclosing scope is the body of another class. A strong hint of it is if it will have __module__ and __qualname__ keys defined. Then you have access to the enclosing class namespace, but...the enclosing class itself does not exist yet:

In [40]: import inspect

In [41]: class M(type):
    ...:     def __new__(mcls, name, bases, ns, **kw):
    ...:         enclosing_frame = inspect.stack()[1].frame
    ...:         print (enclosing_frame.f_locals)
    ...:         return super().__new__(mcls, name, bases, ns, **kw)
    ...: 

In [42]: class A:
    ...:     class B(metaclass=M): pass
    ...: 
{'__module__': '__main__', '__qualname__': 'A'}

So, if all that is wanted is the enclosing class __name__, one can just use __qualname__ and be done with it. But if all one wanted was the name, __qualname__ itself is already set on the nested class namespace.

If one wants a valid reference to the __class__ itself, it is not possible when the nested class itself is created, because the outer class does not exist at this point: it will only be created at the end of the definition of its own body, of course.

Fortunately, the descriptor mechanism will work if the attribute is a class, and the __set_name__ special method is defined in its metaclass:

In [51]: class Symbol(type):
    ...:     def __set_name__(cls, owner, name):
    ...:         assert name == cls.__name__
    ...:         cls.owner = owner
             # it turns out __get__ is not needed for  __set_name__ to work
    ...:     #def __get__(cls, instance, owner):
    ...:         # needed so the attribute is recognized as a descriptor (? - actually: to be checked)
    ...:     #    return cls
    ...:     def __repr__(cls):
    ...:         return f"{cls.owner.__name__}.{cls.__name__}"
    ...: 
    ...: 

In [52]: class Animal:
    ...:     class Dog(metaclass=Symbol): pass
    ...: 

In [53]: Animal.Dog
Out[53]: Animal.Dog

Again: this last part of the question is just here for people wanting to get a reference to the outer class for other purposes than getting the name. Now, check that the descriptor protocol, with the __get__ method can provide such information without the need for a metaclass at all.

You need to have Animal inherit from Enum:

from enum import Enum

class Animal(Enum):
    class Dog(Symbol): pass
    class Cat(Symbol): pass

>>> print(repr(Animal.Dog))
<Animal.Dog: Dog>

>>> print(Animal.Dog)
Animal.Dog

If you want different str()s and repr()s you need to define them:

class Animal(Enum):
    #
    def __repr__(self):
        return "%s.%s" % (self.__class__.__name__, self._name_)
    #
    def __str__(self):
        return self._name_
    #
    class Dog(Symbol): pass
    class Cat(Symbol): pass
Related