简体   繁体   中英

Python: How to override a class' method, while preserving decorators and calling the original?

How do i override a Class' method, based on this situation? However, i cannot edit solid.py and run_me.py. While mainting the decoration and able to call the original.

# - solid.py - (no control)
import http

class Solid(object):
    _cp_path = '/pos'

    @http.jsonrequest
    def break_apart(self):
        return "to pieces!"

;

# - me.py -
import solid

def break_apart(self):
    return "to sand! and " + super(solid.Solid, self).break_apart()

solid.Solid.break_apart = break_apart

;

# - run_me.py - (no control)
import me    # yes, me first
import solid

pebble = solid.Solid()
pebble.break_apart() # "to sand! and to pieces!"

EDIT

Thank you for the assistance, sorry for being incomplete. I forgot to add that solid.py has decorators. Doing the monkeypatch works like a charm, however:

  • i lost the original decoration
  • i can't super call the original

    AttributeError: 'super' object has no attribute 'break_apart'

Since Python's functions are first-class, make your me.py this:

import solid

# replacement method
def break_apart(self):
    return "to sand!"

# override Solid.break_apart with the new method
solid.Solid.break_apart = break_apart

Also, since you called pebble.break_apart(), this implies that solid.py should be:

class Solid(object):
    def break_apart(self):
        return "to pieces!"

Notice the addition of the self parameter to break_apart. This is implied when you call pebble.break_apart() (ie self = pebble instance)

First of all, what kind of class method do you actually have?

It looks like a staticmethod but you're not using the staticmethod decorator.

Instance methods look like:

class C:
    def instance_method(self, *args, **kwargs):
        return self.foo

Class methods look like:

    @classmethod
    def class_method(cls, *args, **kwargs):
        return cls.foo

Static methods look like:

    @staticmethod
    def static_method(*args, **kwargs):
        return foo

You can't use inheritance to change this code if you don't have the ability to change run_me.py .

Instead, you can simply monkeypatch the Solid class with a compatible implementation of break_apart like this:

import solid

def break_apart(self):
    return "whatever you want"

solid.Solid.break_apart = break_apart

Of course, since your code is imported first you could actually just replace the entire class.

So just define Concrete as you are already doing and then monkey patch the whole class inside the solid module:

import solid

class Concrete:
    ...

solid.Solid = Concrete

I think there are two different approaches you can take to override a method in a class you can't edit the source code to.

The first is to create a subclass that overrides the method. This is pretty straightforward, but it only effects objects that you create yourself using the constructor of the child class, rather than the original class:

me.py:

import http

import solid

class MySolid(solid.Solid):
    @http.jsonjequest
    def break_apart(self):
        return "to sand! and " + super(solid.Solid, self).break_apart()

I don't actually know what the http.jsonrequest decorator does, so you may need to change the call to the original value to work with it, if it modifies the call signature in some way.

The other approach is to monkey-patch the existing class. This means that you replace the method implementation in the class with your own alternative version. This is useful if the instances of the original class are being created by other code that you also don't control. Note that if you still need access to the original method implementation, you'll need to save a reference to it yourself ( super doesn't handle this situation).

me.py:

import http

import solid

_orig_break_apart = solid.Solid.break_apart

@http.jsonrequest
def break_apart(self):
    return "to sand! and " + _orig_break_apart(self)

solid.Solid.break_apart = break_apart

Here to, you may need to change how you call the original method if the decorator changed the signature.

It's possible that the decorated version of the method differs greatly from the undecorated version (eg it radically changes the call signature or return value). If that's the case, it may not be easy to "reverse" this process to get to the original method implementation so that you can call it from within your new version. In that case, you'll probably need to copy the code from the existing implementation into your overriding version (using either of the override methods above). In the example code, you'd simply return "to sand! and to pieces!" without trying to call the original method. In real code, this would probably be more complicated, but the same idea applies.

Standing on the shoulder of giants here at stackoverflow, this works for me. I know it's a bit complex but I haven't stumbled on something simpler yet. The code works on Python3.

import functools
def log( fn ):
    @functools.wraps( fn )
    def wrapper( *args, **kwargs ):
        print( "log", fn.__name__, args[ 1: ] )
        return fn( *args, **kwargs )
    return wrapper

def prepare_class( clazz ):
    @classmethod
    def on_class_patcher( cls, func_name, context ):
        def patch_by_name( new_func) :
            original_func = getattr( cls, func_name )
            def patched_func( self, *args, **kwargs ):
                return new_func( self, original_func, context, *args, **kwargs )
            setattr( cls, func_name, patched_func )
        return patch_by_name

    setattr( clazz, "patch", on_class_patcher )

# --- Use like this ---
class Demo:
    @log
    def m1( self, x, y ):
        print( "Demo.m1" )
        print( x, y )

    def m2( self ):
        print( "Demo.m2" )

class Foo:
    def m3( self ):
        print( "Foo.m3" )

prepare_class( Demo )
foo = Foo()

@Demo.patch( "m1", context = { "foo": foo } )
def patched_m1( self, original_func, context, *args, **kwargs ):
    print( "Demo.m1_patched" )
    self.m2()
    context[ "foo" ].m3()
    x = args[ 0 ] * 2
    y = args[ 1 ] * 2
    return original_func( self, x, y )

demo = Demo()
demo.m1( 1, 2 )

Result:

Demo.m1_patched
Demo.m2
Foo.m3
log m1 (2, 4)
Demo.m1
2 4

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM