This is probably a stupid question. But I encountered this need when writing a tool that needs to slightly modify the API from an existing package. The minimal example is as such
class Foo:
def __init__(self):
print("foo")
class Qux(Foo):
def __init__(self):
print("qux")
class Baz:
def __init__(self):
self.foo = Foo()
def f(self):
# ... a lot of code
Foo()
# ... a lot of code
def g(self):
# ... a lot of code
Foo()
# ... a lot of code
class Bar(Baz):
Baz.Foo = Qux #(my rough idea but it doesn't work)
b = Bar()
b.f()
b.g()
# want to print qux all above
I want to achieve this without overloading all methods from Baz
that used the Foo
constructor to use Qux
instead. Is there any way to do this without touching the code for the superclass?
Also, please kindly enlighten me if there's a name for such situation. I tried to search up solution on Stackoverflow but had no luck. Thanks!
CodePudding user response:
What you need there is called "monkey patching" - an operation possible in dynamic languages such as Javascript, Python and Ruby, in which either a module level variable or an attribute of a class/instance is replaced by other at runtime.
It is popular with testing, as it allows one to replace functions and classes with mocks - but if used with wisdom, can be made to work in production code.
Its usage can range from simple to complicated, depending on how things are arranged there:
If you can change all occurrences and usages of Bar
in the process to use Qux instead of Foo, and never look back - you are in luck for the simplest scenario, with no need for a Baz
subclass, and no need to rollback the monkey-patching.
Let's suppose the usages of Foo
in Bar
code are just as shown in your example: the code assumes that there is a Foo
name bound at module level (the module where Bar
is declared), and just do Foo()
, and not, in contrast, module1.Foo()
- you just have to replace that name before instantiating any Bar
objects.
If Bar
is defined in module2
, for example, you can write at the very beggining of your Python project (usually the __init__.py
file at the project base, or, sometimes an app.py
file for stuff that will run in containers) you do:
import module2
from module3 import Qux
module2.Foo = Qux
# program follows as usual
(...)
from module2 import Bar
def myfun():
x = Bar() # <- when this is run, `Bar` will actually "see" Qux insteaf of Foo
That was the simple thing. Now, there are intermediate scenarios, like Foo
sometimes showing up as Foo
and sometimes as module1.Foo
in the source code, or Foo
bein used in other places than in the Bar
class - there are ways to adapt the idea above to handle that.
using the Baz
class approach
But if you really need a different Baz
class, then we have to fine tune the change - and it won't be a proper "monkey patching" anymore. The original Bar
class would run pointing to Foo
, and Baz
is a new, isolated class - as you ask for it. But it does not come down as simple inheritance -- it will require you to clone the Bar
class into a new Baz
. It can optionally be made to inherit from Bar
if it is needed to pass isinstance
checks, though it will override all methods in the class with clones of those methods - and if there are uses of "super()" call in methods that also call Foo()
it would require yet another level of hacking around.
I won't worry about fixing super
calls right now - if you need these calls to work, please mention in the comments.
So the steps to be taken are:
- duplicate the
Bar
class by calling type with the new name,Bar
as a base, and- iterating over all members of
Bar
and for each method, clone the method. - this clonning however should recreate the function objects, replacing the its
__globals__
attribute with a copy whereFoo
points toQux
.
- iterating over all members of
Actually, "super()" calls will work straightforward - I found that out when trying to write some special code for it to work, it turns out it is the default behavior: one will want to pass straight from the method in the cloned class to the method of the superclass of the original class (a method in Bar that uses super()
will land directly in the superclass to Baz, skipping the original method in Baz, that is)
def replace_inner(base_cls, new_name, replacements):
from types import FunctionType
class New(base_cls):
pass
New.__name__ = new_name
new_globals = None
for name, obj in base_cls.__dict__.items():
if not isinstance(obj, FunctionType):
try:
setattr(New, name, obj)
except AttributeError:
pass
continue
if not new_globals:
new_globals = copy(obj.__globals__)
new_globals.update(replacements)
new_func = FunctionType(obj.__code__, new_globals, obj.__name__, obj.__defaults__, obj.__closure__)
setattr(New, name, new_func)
return New
And this working in the interactive mode (Foo and Qux are defined as in your example)
In [52]: class A:
...: def __init__(self):
...: print("A")
...:
In [53]: class B(A):
...: def __init__(self):
...: self.f = Foo()
...: super().__init__()
...:
In [54]: C = replace_inner(B, "C", {"Foo": Qux})
In [55]: c = C()
Qux
A
In [56]: c.f
Out[56]: <__main__.Qux at 0x7f0f399ac310>