0

I use an external pacakge which I bring in via pip.

This package has a structure as follows:

class OuterThing:
  field: Innerthing
  def outer_method1(self...

class InnerThing():
   def inner_method1(self,...
   def inner_method2(self,...
   def inner_method3(self,...

I instantiate only OuterThing objects (which then internally instantiate InnerThing objects).

I want the inner thing objects to have all the normal fields and methods, just inner_method1 I need to customise a bit for my use.

What is the shortest way (i.e. the way with the least code) to do this?

I do not want to copy the whole package into my source tree if possible, just "inject" the changes at runtime (but possibly before all instantiations) to the specified method in InnerThing.

martineau
  • 119,623
  • 25
  • 170
  • 301
halloleo
  • 9,216
  • 13
  • 64
  • 122
  • See question [_What is a monkey patch?_](https://stackoverflow.com/questions/5626193/what-is-a-monkey-patch) – martineau Mar 29 '22 at 01:32

2 Answers2

1

Python allows you to Monkey Patch code simply by assigning a different function pointer to the function you're trying to replace. This can be done as long as you can grab a reference to the instance of InnerThing at runtime.

In your case, it seems like OuterThing does have a reference to InnerThing so you can do something like this:

def your_implementation_of_inner_method1(self, ...):
   # Do stuff

outerThing = OuterThing()
outerThing.field.inner_method1 = your_implementation_of_inner_method1

If you want to dig deeper as to why this is possible, I recommend having a look at the Python documentation for classes. The tl;dr is that methods are actually objects and are stored as fields in instances of the class.

Rayan Hatout
  • 640
  • 5
  • 22
  • Beware: OP's code declares `field` to be an instance of `InnerThing` and not a reference to the class... – Serge Ballesta Mar 28 '22 at 08:03
  • @SergeBallesta This can only work if you have a reference to an *instance* as far as I'm aware, monkey-patching uninitialised classes can only be done with some AST manipulation wizzardry – Rayan Hatout Mar 28 '22 at 08:05
  • 1
    No. You can patch a method directly on the class object once the **class** has been defined and independently of any of its instances. – Serge Ballesta Mar 28 '22 at 08:06
  • @SergeBallesta TIL, I always thought methods weren't bound until initialised – Rayan Hatout Mar 28 '22 at 08:07
  • The methods are indeed initialized at the end of the definition of the class. – Serge Ballesta Mar 28 '22 at 08:08
  • *Binding* is a different thing. An unbound method is just the function that is defined in the class. A *bound method* is more or less the pair instance + method. If `field` is an instance of `InnerClass`, `field.inner_method1` is a bound method: after `bm = field.inner_method1`, you can actually call `bm(...)` with the exact same behaviour as `field1.inner_method1(...)` (or `InnerThing.inner_method1(field, ...)` which is exactly the same thing) – Serge Ballesta Mar 28 '22 at 08:14
1

In Python, a method is just an attribute of the class object that happens to be a function having self as its first parameter. That means that you can easily replace it by your own function, provided you keep the same signature. You can even call the original method from your own one:

# after having imported InnerThing
_orig_inner_method1 = InnerThing.inner_method1

def _patched_inner_method1(self, ...):
    # your own code
    ...
    _orig_inner_method1(self, ...)   # eventually call the original method
    ...

InnerThing.inner_method1 = _patched_inner_method1

From that point, any InnerThing object created will use your patched method.

Benjin
  • 3,448
  • 1
  • 10
  • 20
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • Wow, super cool that it works on *classes*! Will try it tomorrow. – halloleo Mar 28 '22 at 12:37
  • Just tested it - and this works as long as the instantiation happens *in the file* where you do the importing of `InnerThing` and the patching via `InnerThing.inner_method1 = _patched_inner_method1`. – halloleo Mar 30 '22 at 05:42