0

I'm toying with creating a class that dynamically has a base that is the class of an object passed to it on instantiation. E.g. pass it an int instance, and it subclasses int, etc. I'm trying to do this by employing a metaclass. This seems to break super. __class__ (a closure created if super exists) and the no-arguement form of super do not acknowledge the new base class.

With __class__ / super() broken, I'm not sure how to do what super() would have done. super(self.__class__, self) "works", but would break subclassing this class.

Why does __class__ not match self.__class__ in this case? I know they aren't equivalent concepts, but in here, I'd want them to have the same value. See the factory function alternative below. print(isinstance(self, Class)) also returns false. My guess is that because of the way super and __class__ are created when the code is parsed, it is initially set without considering the change in the metaclass, and the adoption of a new class created with type doesn't change the __class__ already set - though I'd have guessed it would. After all, you're supplying the new class, aren't you?

Can __class__ / super() be "fixed"? If it can't, can super still be somehow used cleanly?

I tried creating a class to do what I wanted super to do, with __getattribute__, but that's also pretty dicey.

See this runnable example with illustrative prints:

class Base:
    pass

class Meta(type):
    def __call__(mcls, obj, *args, **kwargs):
        bases = (obj.__class__,)

        __dict__ = dict(mcls.__dict__)
        if "__dict__" in __dict__:
            del __dict__['__dict__']
        if "__weakref__" in __dict__:
            del __dict__['__weakref__']

        cls = type(mcls.__name__, bases , __dict__)
        instance = cls.__new__(cls, *args, **kwargs)
        instance.__init__(*args, **kwargs)
        return instance

class Class(metaclass=Meta):
    def __init__(self, *args, **kwargs):
        print(__class__.__bases__)
        print(self.__class__.__bases__)
        print(isinstance(self, Class))
        print(isinstance(Class, Meta))
        print(isinstance(self, __class__))
        print(isinstance(self, self.__class__))

        super().__init__(*args, **kwargs)
In [1]: Class(Base())
(<class 'object'>,)
(<class '__main__.Base'>,)
False
True
False
True
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-55-6a5013764b7e> in <module>
----> 1 c = Class(Base())

<ipython-input-54-f1865febd768> in __call__(mcls, obj, *args, **kwargs)
     19         instance = cls.__new__(cls, *args, **kwargs)
     20         # Sets obj in instance for mutables
---> 21         instance.__init__(*args, **kwargs)
     22         return instance
     23

<ipython-input-54-f1865febd768> in __init__(self, *args, **kwargs)
     30         print(isinstance(self, self.__class__))
     31
---> 32         super().__init__(*args, **kwargs)
     33

TypeError: super(type, obj): obj must be an instance or subtype of type

Factory function

I'm aware that the above could be done simply with a factory function that looks like this:

class Base:
    def __init__(self, *args, **kwargs):
        print("Base init")

def get_instance(obj):
    class Class(type(obj)):
        def __init__(self, *args, **kwargs):
            print(__class__.__bases__)
            print(self.__class__.__bases__)
            super().__init__(*args, **kwargs)
    return Class()

which yields

In [1]: get_instance(Base())
Base init
(<class '__main__.Base'>,)
(<class '__main__.Base'>,)
Base init

In this example, super() finds the Base class because that's what it was defined with, and intuitively finds the Base's init dunder method. Rephrasing my question:

If I instead use a metaclass to dynamically subclass something, how can I get super() to find the dynamic base, just as it would if I had used a factory function? If it cannot (cleanly), are there reasonable alternatives?

Another phrasing

Finally, another way I think this question can be restated is:

How can normal subclassing be effectively emulated with a Metaclass?

Why?

Firstly, this behavior surprised me, and I'd like to understand it. In that regard this can be viewed as an academic question.

Beyond that, the question of how to dynamically sublcass isn't new, and there's clearly non-zero interest. But, the methods I've seen all seem lacking. super / __class__ seems problematic for metaclasses, and if that and everything else worked wonderfully (big if), there's still a concern about metaclass conflicts one might have to worry about in the general case. Using factory functions might seem simple, but they make it difficult to subclass the class you're creating. It's not straight forward to subclass the Class above in the factory function example. Using a metaclass, if you could get it to work, would still allow plainer access and subclassing.

nixjdm
  • 121
  • 1
  • 11
  • "I'm toying with creating a class that dynamically has a base that is the class of an object passed to it on instantiation." This is fundamentally not the right approach to this. – juanpa.arrivillaga Mar 11 '22 at 23:17
  • `__class__` in a method (specifically, a function defined within a class body) and `obj.__class__` **are two completely different things that are specifically meant not meant to match (although they may)**. `obj.__class__` is simply the class of that instance, i.e. `type(obj) is obj.__class__`, the special variable `__class__` is a specially defined variable that references the statically defined *class object* that *a class definition statement* is creating. Usually, you *don't want* `super(self.__class__, self)` – juanpa.arrivillaga Mar 11 '22 at 23:22
  • So consider a method `foo()` that exists in a base class, that isn't overwritten in subclass. i.e. `Sub` inherits `Base.foo`. So when you create `sub = Sub()` then call `sub.foo()`, if you had `print(__class__, self.__class__)` in `Base.foo`, you would see `sub.foo()` print out `Base Sub` – juanpa.arrivillaga Mar 11 '22 at 23:25
  • So in your code specifically, `super().__init__(*args, **kwargs)` will **always** be equivalent to `super(Class, self).__init__(*args, **kwargs)` *that's the whole point*. – juanpa.arrivillaga Mar 11 '22 at 23:27
  • 1
    But fundamentally, again, this is just working **against the whole machinery of class based inheritance** to do what should probably just be solved with a factory function and no dynamic inheritance. – juanpa.arrivillaga Mar 11 '22 at 23:31
  • Well, yes, that's another reason `super(self.__class__, self)` is a bad idea. I tried to head that off by providing another reason not to do that, because that's commonly cited. I thought bringing that up would help clarify my intent, but maybe not. To your other comments, I edited my post to try to clarify further. – nixjdm Mar 12 '22 at 00:09
  • Look, I don't know what to tell you. Again, you are working against the whole class machinery. The *reasonable option* is to stop trying to use a metaclass to do this and just use the factory function. But "dynamic inheritance" is a huge red-flag -- inheritance is *not meant to be dynamic in this sense*. You are using the wrong tool to do something that is probably the wrong approach to begin with. – juanpa.arrivillaga Mar 12 '22 at 00:27
  • Now, you could always create *some hack*, like your metaclass could add an attribute `cls.__init__._special_attribute = cls` and then in the `__init__` you would just write `super(self.__init__.__func__._special_attribute, self).__init__(*args, **kwargs)` – juanpa.arrivillaga Mar 12 '22 at 00:33
  • But again, this is all just jumping through hoops to break the way things are *supposed* to work for no obvious benefit. Note, in your proposed solutions, `instance = Class()` then `isinstance(instance, Class)` **would be false** and `isinstance(Class, Meta)` would also be false. So you are subverting the class machinery, to hackily try to reproduce it. You are creating your own problem and coming up with a "solution" that just implements what classes already do in a worse way, when you should just be using them the way they were meant to be used – juanpa.arrivillaga Mar 12 '22 at 00:35
  • And *basically* you want something like the prototype-based inheritance (for example, as seen in Javascript), but you are trying to use the *class based machinery* to achieve this. But you might as well just implement prototype-based inheritance from scratch. – juanpa.arrivillaga Mar 12 '22 at 00:40

1 Answers1

1

The problem here is that your dynamic class does not inherit from itself (Class) at all - the implicit __class__ variable inside your __init__ points to the "hardcoded" "Class" version - but the self received when __init__ is called is an instance of the dynamic class, which does not have Class as its superclass. Thus, the arguments filled implicitly to super(): __class__ and self will mismatch (self is not an instance of the class defined in __class__ or of a subclass of it).

The reliable way to fix this is to allow proper inheritance, and forget copying the class __dict__ attributes around: let the inheritance machinery take care of calling the methods in the appropriate order.

By simply making the dynamic class inherit from your static class and the dynamic-base, all methods are in place, and self will be a proper instance from the baked-in __class__ from __init__.__class__ still points to the static Class, but the conditions for super to be called are fulfilled - and super does the right thing, by looking for the supermethods starting from the self parameter - the newly created dynamic subclass which inherits both from your static Class and the new Base, and calls the methods on Base as they are part of the __mro__ of the new class, in the correct place.

Ok - sounds complicated - but with some print statements added we can see the thing working:


class Base:
    def __init__(self):
        print("at base __init__")

class Meta(type):
    def __call__(cls, obj, *args, **kwargs):
        dynamic_ancestor = type(obj)
        bases = (cls, dynamic_ancestor,)

        new_cls = type(f"{cls.__name__}__{dynamic_ancestor.__name__}", bases , {})
        instance = new_cls.__new__(new_cls, *args, **kwargs)
        instance.__init__(*args, **kwargs)
        return instance

class Class(metaclass=Meta):
    def __init__(self, *args, **kwargs):
        print(__class__.__bases__)
        print(self.__class__.__bases__)
        print(isinstance(self, Class))
        print(isinstance(Class, Meta))
        print(isinstance(self, __class__))
        print(isinstance(self, self.__class__))
        print(self.__class__.__mro__, __class__.__mro__)
        super().__init__(*args, **kwargs)

Class(Base())

Outputs:

at base __init__
(<class 'object'>,)
(<class '__main__.Class'>, <class '__main__.Base'>)
True
True
True
True
(<class '__main__.Class__Base'>, <class '__main__.Class'>, <class '__main__.Base'>, <class 'object'>) (<class '__main__.Class'>, <class 'object'>)
at base __init__
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • Thank you! It makes a lot of sense that explicitly naming `Class` as a base would fix `super()`. I still have two questions though, because I don't understand the inner workings. 1) In my original, I expected `self` to still be an instance of `Class` because that's ultimately the class we're assigning the instance too, in the metaclass. Why isn't it? I guess it just takes the result of the `type` call verbatim, but I didn't expect that, given we're in the context of the metaclass that's being used explicitly. – nixjdm Mar 14 '22 at 18:32
  • 2) Why does `__class__.__bases__` still not reflect `Base` - and also now `Class`? Though that seems to not matter practically. Thanks again! – nixjdm Mar 14 '22 at 18:33
  • 1) In your original code, `self` is an instance of your newly created `cls` class which only base is "obj.__class__` and there are no mentions in the hierarchy to `Class` – jsbueno Mar 14 '22 at 18:40
  • 1
    2) `__class__`, unlike `self.__class__` is "baked in" when the class statement which body contains the method where `__class__` appears is resolved. It takes place after the call to the metaclass `__new__` method (and I don't recall is before or after the metaclass `__init__` is called. I think after). Once "Class" is defined, the value for `__class__` inside `Class.__init__` is bound to `Class`itself and immutable - only creating `Class` a new, with a new reference to `__init__` creating a new `__init__` closure for the original one, could change it. – jsbueno Mar 14 '22 at 18:46
  • What makes my example work is that the `__mro__` from a derived class is defined to include other superclasses in the super() call hierarchy - even when walking the hierarchy up from a superclass that would have no intermediates to `object`, like is the case: it is the way multiple inheritance works in Python. – jsbueno Mar 14 '22 at 18:48
  • 1
    About 1) Right, I think I get that that is the case now. I originally thought that since the metaclass isn't being used in a vacuum, the interpreter might use the context it can see, and make the instance an instance of `Class` without explicitly saying so. But maybe it's a deliberate choice to not do that for me. This way does seem to give more explicitly control since it's really using exactly the class produced by the `type` call, and not adding anything behind the scenes. So it seems more powerful this way, even though my intuition was wrong. – nixjdm Mar 14 '22 at 18:55
  • About 2) Makes sense. Then, if I really wanted to alter a `__class__` from the metaclass, it would have to be in the metaclass's `__new__` when creating an instance of `Class` - but it's `__call__` that can receive the object to get the dynamic base from, not `__new__`. So `__class__` seems stuck where we have it. That's fine though. :) – nixjdm Mar 14 '22 at 19:03