James Schinner
James Schinner

Reputation: 1579

Using a metaclass to substitute a class definition?

Python 3.6

I'm trying to modify the behavior of a third party library.

I don't want to directly change the source code.

Considering this code below:

class UselessObject(object):
    pass


class PretendClassDef(object):
    """
    A class to highlight my problem
    """

    def do_something(self):

        # Allot of code here

        result = UselessObject()

        return result

I'd like to substitute my own class for UselessObject

I'd like to know if using a metaclass in my module to intercept the creation of UselessObject is a valid idea?

EDIT

This answer posted by Ashwini Chaudhary on the same question, may be of use to others. As well as the below answer.

P.S. I also discovered that 'module' level __metaclass__ does't work in python 3. So my initial question of it 'being a valid idea' is False

Upvotes: 3

Views: 267

Answers (3)

PM 2Ring
PM 2Ring

Reputation: 55489

FWIW, here's some code that illustrates Rawing's idea.

class UselessObject(object):
    def __repr__(self):
        return "I'm useless"

class PretendClassDef(object):
    def do_something(self):
        return UselessObject()

# -------

class CoolObject(object):
    def __repr__(self):
        return "I'm cool"

UselessObject = CoolObject

p = PretendClassDef()
print(p.do_something())

output

I'm cool

We can even use this technique if CoolObject needs to inherit UselessObject. If we change the definition of CoolObject to:

class CoolObject(UselessObject):
    def __repr__(self):
        s = super().__repr__()
        return "I'm cool, but my parent says " + s

we get this output:

I'm cool, but my parent says I'm useless

This works because the name UselessObject has its old definition when the CoolObject class definition is executed.

Upvotes: 3

Brōtsyorfuzthrāx
Brōtsyorfuzthrāx

Reputation: 4749

This is just building on PM 2Ring's and jsbueno's answers with more contexts:

If you happen to be creating a library for others to use as a third-party library (rather than you using the third-party library), and if you need CoolObject to inherit UselessObject to avoid repetition, the following may be useful to avoid an infinite recursion error that you might get in some circumstances:

module1.py

class Parent:
    def __init__(self):
        print("I'm the parent.")

class Actor:
    def __init__(self, parent_class=None):
        if parent_class!=None: #This is in case you don't want it to actually literally be useless 100% of the time.
            global Parent
            Parent=parent_class
        Parent()

module2.py

from module1 import *

class Child(Parent):
    def __init__(self):
        print("I'm the child.")

class LeadActor(Actor): #There's not necessarily a need to subclass Actor, but in the situation I'm thinking, it seems it would be a common thing.
    def __init__(self):
        Actor.__init__(self, parent_class=Child)

a=Actor(parent_class=Child) #prints "I'm the child." instead of "I'm the parent."
l=LeadActor() #prints "I'm the child." instead of "I'm the parent."

Just be careful that the user knows not to set a different value for parent_class with different subclasses of Actor. I mean, if you make multiple kinds of Actors, you'll only want to set parent_class once, unless you want it to change for all of them.

Upvotes: 0

jsbueno
jsbueno

Reputation: 110516

This is not a job for metaclasses.

Rather, Python allows you to do this through a technique called "Monkeypatching", in which you, at run time, substitute one object for another in run time.

In this case, you'd be changing the thirdyparty.UselessObject for your.CoolObject before calling thirdyparty.PretendClassDef.do_something

The way to do that is a simple assignment. So, supposing the example snippet you gave on the question is the trirdyparty module, on the library, your code would look like:

import thirdyparty

class CoolObject:
    # Your class definition here

thirdyparty.UselesObject = Coolobject

Things you have to take care of: that you change the object pointed by UselessObject in the way it is used in your target module.

If for example, your PretendedClassDef and UselessObject are defined in different modules, you have to procees in one way if UselessObject is imported with from .useless import UselessObject (in this case the example above is fine), and import .useless and later uses it as useless.UselessObject - in this second case, you have to patch it on the useless module.

Also, Python's unittest.mock has a nice patch callable that can properly perform a monkeypatching and undo it if by some reason you want the modification to be valid in a limited scope, like inside a function of yours, or inside a with block. That might be the case if you don't want to change the behavior of the thirdyparty module in other sections of your program.

As for metaclasses, they only would be of any use if you would need to change the metaclass of a class you'd be replacing in this way - and them they only could have any use if you'd like to insert behavior in classes that inherit from UselessObject. In that case it would be used to create the local CoolObject and you'd still perform as above, but taking care that you'd perform the monkeypatching before Python would run the class body of any of the derived classes of UselessObject, taking extreme care when doing any imports from the thirdparty library (that would be tricky if these subclasses were defined on the same file)

Upvotes: 2

Related Questions