6

I'm working with quite a large OOP code-base, and I'd like to inject some tracing/logging. The easiest way to do this would be to introduce a decorator around certain methods on some base classes, but unfortunately decorators aren't inherited.

I did try something like the following:

def trace(fn):
    def wrapper(instance, *args, **kwargs):
        result = fn(instance, *args, **kwargs)
        # trace logic...
        return result
    return wrapper

class BaseClass(object):
    def __init__(self, ...):
        ...
        self.__call__ = trace(self.__call__)  # line added to end of method

...and while the __call__ method is wrapped (which I can see from printing the instance information to the console) the wrapper function isn't executed as expected.

I also looked briefly at using a metaclass based on this answer, but it instantly breaks other parts of the system that use introspection, so I think that's a no-go.

Is there any other way I can force the application of these decorators around the __call__ method of classes that inherit from BaseClass?

Community
  • 1
  • 1
Phillip B Oldham
  • 18,807
  • 20
  • 94
  • 134
  • So you're saying that you want to wrap `__call__` in `BaseClass` and all its children? – freakish Jan 14 '14 at 14:06
  • @freakish: yes, sort of. `__call__` in `BaseClass` is overwritten by all the children (and there are many of them) so applying a decorator around `__call__` at the level of `BaseClass` is the easiest way to add the tracing in one place consistently. – Phillip B Oldham Jan 14 '14 at 14:08
  • Is this same as what you are trying to solve, decorator on base class inherit to sub-class. http://stackoverflow.com/questions/3782040/python-decorators-that-are-part-of-a-base-class-cannot-be-used-to-decorate-membe – James Sapam Jan 14 '14 at 14:09

2 Answers2

4

You can't do that... see things like here: Python method resolution mystery

Basically. most of special functions are looked up at the class level. Monkey-patching it after initialization won't have any effect.

That said, you should be able to do something like this:

class BaseClass(object):
    __call__ = trace(object.__call__)
    def __init__(self, ...):
        ...

Now, it's wrapped at the class level, so it will work.

edit: That's not really an answer. What you want is something like:

class BaseClass(object):
    @decorator
    __call__ = something

class SubClass(BaseClass):
    __call__ = something_else

In this case... it doesn't matter what you put for the BaseClass __call__ method, the SubClass method will override it. It's not about decorators vs. non-decorators, as it's functions (well, really, attributes, since there isn't really much difference in python) that get inherited. Decorators look separate from the function, but they aren't really - in the above, all that the decorator does is change the definition of __call__ from something to decorator(something) (which must still return a callable).

Then, in the subclass, you reassign this function to something completely different.

You could use a class decorator, but those things tend to break introspection as you noticed; they turn a class, into a function that returns a class. Similar problem with factory functions.

Maybe look into the profile module? I know there are some lower-level hooks that it uses to track function calls etc. that you might be able to use as well (though don't know much about them).

Community
  • 1
  • 1
Corley Brigman
  • 11,633
  • 5
  • 33
  • 40
  • Unfortunately my version of `trace` isn't profiling but more tracking data through the system. – Phillip B Oldham Jan 14 '14 at 15:10
  • well, my point is that the profiler solves a similar problem - it has to wrap/trace all function calls so it can store start_time/stop_time for each call. you need to do a similar thing, except instead of time them, you'll log them and/or inspect arguments to trace dataflow. – Corley Brigman Jan 14 '14 at 15:13
4

Why would metaprogramming mess up introspection? Perhaps you are not using it correctly? Try this (assuming Python2.x):

class MyMeta(type):
    def __new__(mcl, name, bases, nmspc):
        if "__call__" in nmspc:
            nmspc["__call__"] = trace(nmspc["__call__"])
        return super(MyMeta, mcl).__new__(mcl, name, bases, nmspc)

class BaseClass(object):
    __metaclass__ = MyMeta

You can then simply inherit from BaseClass and __call__ will get wrapped automatically.

I'm not sure what kind of introspection would break that. Unless BaseClass actually does not inherit from object but from something that implements its own meta?? But you can deal with that case as well by forcing MyMeta to inherit from that parent's meta (instead of type).

freakish
  • 54,167
  • 9
  • 132
  • 169
  • This has worked; I think my use of the `__init__` method was causing the introspection to fail, whereas `__new__` "cuts in" at the right point. – Phillip B Oldham Jan 14 '14 at 15:10
  • 1
    @unpluggd That's weird, I think it can be done with `__init__` as well. I'll dig into it. Although when it comes to metaprogramming I tend to use `__new__` more often. – freakish Jan 14 '14 at 15:47
  • I don't think you need to pass `mcl` as an argument in the superclass's `__new__` call; `super` binds that argument already. – user2357112 Jan 30 '14 at 23:53
  • @user2357112 Actually you have to. I'm not sure what happens under the hood but Python throws an exception otherwise. This probably has something to do with the fact that `__new__` fires before the actual instance is created (in this case "instance" means "class"). – freakish Jan 31 '14 at 07:13