Python Wrap Class Method
Asked Answered
S

5

16

I'm trying to create an object with a run method that will be wrapped by a _wrap_run method. I'd like to be able to call the method and it's wrapper by simply typing instance.run() and I'd like to be able to subclass the object so I can override the run() method and have it still execute the wrapper.

More simply put, I want people to be able to subclass A and override run() but still have calls to the run() method execute the wrapper function.

I'm having some difficulty with the mechanics of this. Does anyone have any suggestions regarding this approach?

class A:

    def run(self):
        print "Run A"
        return True

    def _wrap_run(self):
        print "PRE"
        return_value = self.run()
        print "POST"
        return return_value

    run = property(_wrap_run)


a = A()
a.run()
"""
Should Print: 
PRE
Run A
POST
"""


class B(A):

    def run(self):
        print "Run B"
        return True

b = B()
b.run()
"""
Should Print: 
PRE
Run B
POST
"""
Sadiron answered 21/7, 2011 at 18:31 Comment(2)
What is the point of _wrap_run? Is this supposed to be some kind of decorator? What's wrong with using a decorator for this?Downhill
He doesn't want to have to decorate all the derived classes.Trictrac
T
27

Use a Metaclass.

class MetaClass(type):
    @staticmethod
    def wrap(run):
        """Return a wrapped instance method"""
        def outer(self):
            print "PRE",
            return_value = run(self)
            print "POST"
            return return_value
        return outer
    def __new__(cls, name, bases, attrs):
        """If the class has a 'run' method, wrap it"""
        if 'run' in attrs:
            attrs['run'] = cls.wrap(attrs['run'])
        return super(MetaClass, cls).__new__(cls, name, bases, attrs)

class MyClass(object):
    """Use MetaClass to make this class"""
    __metaclass__ = MetaClass
    def run(self): print 'RUN',

myinstance = MyClass()

# Prints PRE RUN POST
myinstance.run()

Now if other people subclass MyClass, they will still get their run() methods wrapped.

Trictrac answered 21/7, 2011 at 18:50 Comment(0)
C
5

Easiest way: make run the wrapper, and a private method be the overrideable one.

class A(object):
    def run(self):
        print "PRE"
        return_value = self._inner_run()
        print "POST"
        return return_value

    def _inner_run(self):
        print "Run A"
        return True

class B(A):
    def _inner_run(self):
        print "Run B"
        return True
Communication answered 21/7, 2011 at 18:36 Comment(3)
Thanks for your comment. I'd like to make it so the person implementing run() in the subclass doesn't have to worry about making extra calls and depend on them to properly add the function call. Plus the underscored wrapper function is doing some maintenance work that I'd like to keep 'hidden' from the child class. Though I don't know if what I am asking will work...Sadiron
@JoeJ: Document the interface properly, and it won't matter if subclass has to override run or something else.Communication
@Joe: it's very common to let user override _run, see FormEncode _to_python for example, instead of runMicrosurgery
D
2

What other folks do

class A:
   def do_run( self ):
       """Must be overridden."""
       raise NotImplementedError
   def run( self, *args, **kw ):
       """Must not be overridden.
       You were warned.
       """
       print "PRE"
       return_value = self.do_run(*args, **kw)
       print "POST"
       return return_value

class B(A):
    def do_run(self):
        print "Run B"
        return True

That's usually sufficient.

If you want to worry about someone "breaking" this, stop now. Don't waste time worrying.

It's Python. We're all adults here. All the malicious sociopaths will break all you code by copying it, changing it, and then breaking it. No matter what you do, they'll just copy your code and modify it to break the clever bits.

Everyone else will read your comment and stick by your rules. If they want to use your module/package/framework, they will cooperate.

Downhill answered 21/7, 2011 at 19:14 Comment(4)
run() needs to call do_run() which needs to accept arguments.Trictrac
Thanks S.Lott. That seems like a strait-forward approach. Ya, I suppose a few well placed comments would be sufficient.Sadiron
@Joe J: They are sufficient. There's nothing else you can do, really. Anything more complex is just a maintenance headache. And no matter how much extra code you write, someone is always going to mess with it. So write the least code you can get away with.Downhill
In your example objects of class A can't be used, only its descendants.Ribonuclease
K
1

Here a while later, but if the method is a normal method, you could just use a regular decorator. Take the example below, which is a simplified SSH connector.

class SSH:

    def __init__(self) -> None:
        self.connected = False

    def connect(self) -> None:
        self.connected = True

    def call_only_if_connected(self) -> None:
        print("foo")

In this example, we only want the method call_only_if_connected to be called, as the name implies, if the SSH object that it is instantiated to has self.connected=True.

We can define a normal wrapper outside the class, and since self, or the instance of the class, is always passed as the first arg (args[0]), we can just make out check there. Consider:

def assert_connected(f):
    @wraps(f)
    def decorator(*args, **kwargs):
        if not args[0].connected:
            raise RuntimeError("attempted to call a method that requires a connection without an established connection")
        return f(*args, **kwargs)
    return decorator

class SSH:

    def __init__(self) -> None:
        self.connected = False

    def connect(self) -> None:
        self.connected = True

    @assert_connected
    def call_only_if_connected(self) -> None:
        print("foo")

Note in the above example that an AttributeError will be thrown if the object does not have a connected property, so it is important to define connected=False as soon as possible (i.e., as soon as you instantiate the object in __init__).

You could handle the improper connection how you'd like; in my example above, I am throwing a RuntimeError.

Kablesh answered 21/3, 2023 at 3:11 Comment(0)
M
0

What you have there is basically a decorator, so why not go ahead and implement _wrap_run as a decorator and apply it when subclassing the function?

Mind answered 21/7, 2011 at 18:37 Comment(5)
I was thinking about making a decorator out of it, but is there a way to do the decorating in the parent class so the person implementing any child classes doesn't have to add the decorator each time?Sadiron
Nope, that's against what python stands forMind
I think it's exactly what Python stands for. How is it different than duck typing, or context managers? I can write 'run()' in a straightforward manner, and the metaclass takes care of the details.Trictrac
@Trictrac well IMO it's not pythonic if someone subclasses your class and it turns out that the function you implemented does not work as expected because of things going on "behind the scenes". Cat Plus Plus's solution, for example, does not do things behind the scenes: the programmer that implements the function knows it will be run as-is, but also that it will be run through another function.Mind
If I write a class that doesn't work in any way, it's not good. Messing up the wrapper function isn't any different from messing up __init__, or __repr__, or any other method that isn't overridden. They are all expected to 'just work' by subclasses.Trictrac

© 2022 - 2024 — McMap. All rights reserved.