Change python mro at runtime

Viewed 8197

I've found myself in an unusual situation where I need to change the MRO of a class at runtime.

The code:

class A(object):
    def __init__(self):
        print self.__class__
        print "__init__ A"
        self.hello()

    def hello(self):
        print "A hello"

class B(A):
    def __init__(self):
        super(B, self).__init__()
        print "__init__ B"
        self.msg_str = "B"
        self.hello()

    def hello(self):
        print "%s hello" % self.msg_str

a = A()
b = B()

As to be expected, this fails as the __init__ method of A (when called from B) calls B's hello which attempts to access an attribute before it exists.

The issue is that I'm constrained in the changes I can make:

  • B must subclass A
  • A cannot be changed
  • Both A and B require a hello method
  • B cannot initialise other attributes before calling the super __init__

I did solve this conceptually, by changing the MRO at runtime. In brief, during B's __init__, but before calling super __init__, the MRO would be changed so that A would be searched for methods first, thereby calling A's hello instead of B's (and therefore failing).

The issue is that MRO is read only (at class runtime).

Is there another way to implement this ? Or possibly a different solution altogether (that still respects the aforementioned constraints) ?

9 Answers

I've found a way to change object's class or rewrite it's mro.

The easiest way is to build a new class with type function:

def upgrade_class(obj, old_class, new_class):
    if obj.__class__ is old_class:
        obj.__class__ = new_class
    else:
        mro = obj.__class__.mro()

        def replace(cls):
            if cls is old_class:
                return new_class
            else:
                return cls

        bases = tuple(map(replace, mro[1:]))
        old_base_class = obj.__class__
        new_class = type(old_base_class.__name__, bases, dict(old_base_class.__dict__))
        obj.__class__ = new_class

Call self.msg_str = "B" before super(B, self).__init__().

This is not good practice and there's likely better way to solve your use-case. But in general, you can modify the instance __class__ at runtime, which will modify the mro of the instance (not the class).

For example:

class A:
  def f(self):
    print('a')

class B:
  def f(self):
    print('b')

b = B()
b.f()  # b
b.__class__ = A
b.f()  # a
assert type(b) is A

With your use-case, it would look like:

class A:
    def __init__(self):
        self.hello()

    def hello(self):
        print("A hello")

class B(A):
    def __init__(self):
        parent_init = super().__init__
        # While A.__init__ is called, inheritance is removed
        self.__class__ = A
        parent_init()
        self.__class__ = B  # Restore inheritance

        self.msg_str = "B"

    def hello(self):
        print("%s hello" % self.msg_str)

a = A()  # A hello
b = B()  # A hello  (called in A.__init__)
b.hello()  # B hello
Related