river_bell
river_bell

Reputation: 25

Calling super in a class inheriting a metaclass throws an error

I am trying to use automatic registration of subclasses using a metaclass,
but the below code throws an error in class Dep_A on the line

super(Dep_A,self).__init__().

*NameError: global name 'Dep_A' is not defined*

Tried the commented line in MetaClass, but with the same error. I know this works when not using a metaclass so how can I initialize the parent class's init method

registry = {}

def register(cls):
    print cls.__name__
    registry[cls.__name__] = cls()
    return cls


class MetaClass(type):
    def __new__(mcs, clsname, bases, attrs):
        # newclass = super(MetaClass, mcs).__new__(mcs, clsname, bases, attrs)
        newclass = type.__new__(mcs,clsname, bases, attrs)
        register(newclass)
        return newclass


class Department(object):
    __metaclass__ = MetaClass

    def __init__(self):
        self.tasks = dict()


class Dep_A(Department):
    def __init__(self):
        super(Dep_A,self).__init__()
        ...

Upvotes: 0

Views: 360

Answers (2)

Martijn Pieters
Martijn Pieters

Reputation: 1122222

You are trying to create an instance of the class before the class statement has completed. The class object has been created, but has not yet been assigned to a global name.

The order of events is:

  • the class Dep_A statement is found, and the class body is executed to form the attributes (creating the __init__ function).
  • the metaclass __new__ method is called with the classname, bases, and the class body namespace ('Dep_A', (Department,) and {'__init__': <function ...>, ...} respectively).
  • the metaclass creates the new class object
  • The metaclass calls registry(), passing in the new class object
  • registry() calls the class object
  • The class __init__ method is called

Normally, Dep_A is assigned when the metaclass __new__ method returns the newly created object. Until that point there is no global name Dep_A assigned, so the super(Dep_A, self).__init__() call fails.

You could set the name from there first:

class MetaClass(type):
    def __new__(mcs, clsname, bases, attrs):
        # newclass = super(MetaClass, mcs).__new__(mcs, clsname, bases, attrs)
        newclass = type.__new__(mcs,clsname, bases, attrs)
        globals()[clsname] = newclass
        register(newclass)
        return newclass

Alternatively, don't try to create an instance in the registry, store a class instead. You can always create a single instance later on.

The following implements a registry that transparently creates an instance later when you need one, and stores just that one instance created:

from collections import Mapping

class Registry(Mapping):
    def __init__(self):
        self._classes = {}
        self._instances = {}

    def __getitem__(self, name):
        if name not in self._instances:
            self._instances[name] = self._classes.pop(name)()
        return self._instances[name]

    def __iter__(self):
        return iter(self._classes.viewkeys() | self._instances.viewkeys())

    def __len__(self):
        return len(self._classes) + len(self._instances)

    def register(self, cls):
        self._classes[cls.__name__] = cls

registry = Registry()

class MetaClass(type):
    def __new__(mcs, clsname, bases, attrs):
        # newclass = super(MetaClass, mcs).__new__(mcs, clsname, bases, attrs)
        newclass = type.__new__(mcs,clsname, bases, attrs)
        registry.register(newclass)
        return newclass

Now classes are stored in a separate mapping and only when you then later look up the class from the registry mapping will an instance be created and cached:

>>> class Department(object):
...     __metaclass__ = MetaClass
...     def __init__(self):
...         self.tasks = dict()
...
>>> class Dep_A(Department):
...     def __init__(self):
...         super(Dep_A,self).__init__()
...
>>> registry.keys()
['Department', 'Dep_A']
>>> registry['Dep_A']
<__main__.Dep_A object at 0x110536ad0>
>>> vars(registry)
{'_instances': {'Dep_A': <__main__.Dep_A object at 0x110536ad0>}, '_classes': {'Department': <class '__main__.Department'>}}

Upvotes: 3

chepner
chepner

Reputation: 531265

Your register function is trying to instantiate Dep_A before the global name is actually defined. (That is, it gets called while the body of the class statement is still being executed, before Dep_A is assigned the result of the call to the metaclass.)

You want to store the class itself instead:

def register(cls):
    registry[cls.__name__] = cls  # Not cls()
    return cls

Upvotes: 1

Related Questions