2

See full gist here

Consider the case where we have a simple metaclass that generates the __init__ method for a class

class TestType(type):

    def __new__(cls, cname, bases, attrs):
        # Dynamically create the __init__ function
        def init(self, message):
            self.message = message

        # Assign the created function as the __init__ method.
        attrs['__init__'] = init

        # Create the class.
        return super().__new__(cls, cname, bases, attrs)


class Test(metaclass=TestType):

    def get_message(self):
        return self.message

Now this is all good and well to use

test = Test('hello')
assert test.get_message() == 'hello'

But we have problems when subclassing, because if you want to subclass the __init__ method what of course happens is the subclassed method just gets overwritten.

class SubTest(Test):

    def __init__(self, first, second):
        self.first = first
        self.second = second
        super().__init__(first + ' ' second)

subtest = SubTest('hello', 'there')

This will obviously give the

TypeError: init() takes 2 positional arguments but 3 were given

The only way I can think to solve this is to create an intermediate class in the __new__ method of the metaclass and make this the base for the class we are creating. But I can't get this to work, I tried something like this

class TestType(type):

    def __new__(cls, cname, bases, attrs):
        # Dynamically create the __init__ function
        def init(self, message):
            self.message = message

        # If the __init__ method is being subclassed
        if '__init__' in attrs:
            # Store the subclass __init__
            sub_init = attrs.pop('__init__')

            # Assign the created function as the __init__ method.
            attrs['__init__'] = init

            # Create an intermediate class to become the base.
            interm_base = type(cname + 'Intermediate', bases, attrs)

            # Add the intermediate class as our base.
            bases = (interm_base,)

            # Assign the subclass __init__ as the __init__ method. 
            attrs['__init__'] = sub_init

        else:
            # Assign the created function as the __init__ method.
            attrs['__init__'] = init

        # Create the class.
        return super().__new__(cls, cname, bases, attrs)

But this gives me recursion error

RecursionError: maximum recursion depth exceeded while calling a Python object
6
  • I can't reproduce that RecursionError. Commented Oct 9, 2018 at 6:35
  • Hmmm, thats weird, I get it on Python 3.7, Python 3.6, and Python 3.4 (Well python 3.4 is a RuntimeError). I am running macOS. Commented Oct 9, 2018 at 6:53
  • See full gist here gist.github.com/rossmacarthur/9b178e9a0b5450c652159ccea4f158ab Commented Oct 9, 2018 at 6:56
  • Oh, I'd accidentally overwritten the broken TestType class with the original TestType definition... Commented Oct 9, 2018 at 6:57
  • It's unclear to me what the expected behavior of your metaclass is when 1) the class already has an __init__ method and 2) one of the class's parents is already an instance of TestType. Do you want to insert the default init between SubTest.__init__ and Test.__init__? Commented Oct 9, 2018 at 7:02

1 Answer 1

2

The infinite recursion is caused by the fact that the type constructor can return an instance of your metaclass. In this line here:

interm_base = type(cname + 'Intermediate', bases, attrs)

If any of the base classes in bases is an instance of TestType, then the subclass will also be an instance of TestType. That is why Test can be created with no problems, but SubTest causes infinite recursion.

The fix is simple: Create the intermediate class without an __init__ attribute. That way if '__init__' in attrs: will be False, and the endless recursion is avoided.

class TestType(type):
    def __new__(cls, cname, bases, attrs):
        # Dynamically create the __init__ function
        def init(self, message):
            self.message = message

        # If the __init__ method is being subclassed
        if '__init__' in attrs:
            # Create an intermediate class to become the base.
            interm_base = type(cname + 'Intermediate', bases, {})

            # Add the intermediate class as our base.
            bases = (interm_base,)
        else:
            # Assign the created function as the __init__ method.
            attrs['__init__'] = init

        # Create the class.
        return super().__new__(cls, cname, bases, attrs)
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks! I forgot that the __new__ would be called multiple times when we subclass.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.