20

I'm looking for a pythonic solution on how to store a method which is called on an object right inside the object.

Because in python, if I want to catch for example the abs() method, I will overload this operator like:

Catcher(object):
    def __abs__(self):
        self.function = abs

c = Catcher()
abs(c)  # Now c.function stores 'abs' as it was called on c

If I want to catch a function, which have an other attribute in it, for example pow(), I'm going to use this:

Catcher(object):
    def __pow__(self, value):
        self.function = pow
        self.value = value

c = Catcher()
c ** 2  # Now c.function stores 'pow', and c.value stores '2'

Now, what I'm looking for is a general solution, to catch and store any kind of function called on Catcher, without implementing all overloads, and other cases. And as You can see, I also want to store the values (maybe in a list, if there is more than one of them?) which are the attributes of a method.

Thanks in advance!

Peter Varo
  • 11,726
  • 7
  • 55
  • 77
  • What about functions that don't delegate to a dunder method? –  May 04 '13 at 08:28
  • You might wanna look into class decorators and metaclasses. – Martin Maillard May 04 '13 at 08:37
  • @delnan I guess, those are also OK, because in my case, these functions are looking for something else, a value or a method to call. – Peter Varo May 04 '13 at 08:37
  • @morphyn could you be more specific? AFAIK metaclasses are good for class-object creation, aren't they? – Peter Varo May 04 '13 at 08:42
  • One way is to have a decorator on each method that will register the calls. A class decorator will allow you to automatically apply a decorator on each method. You could also do that with a metaclass but I guess it would be more complicated. – Martin Maillard May 04 '13 at 08:50
  • Can't you override `__getattr__` and check that the attribute being accessed is a method? – Benjamin Hodgson May 04 '13 at 08:56
  • 2
    @poorsod: No, I tried that approach. Dunder hooks are looked up on the *class*, not the instance, so a metaclass is then required. But `__getattr__` doesn't seem to be used in that case. Neither does `__getattribute__`. – Martijn Pieters May 04 '13 at 08:57
  • The answer might be there: http://stackoverflow.com/questions/6954116/rubys-method-missing-in-python – Martin Maillard May 04 '13 at 09:26
  • Related: http://code.activestate.com/recipes/366254-generic-proxy-object-with-beforeafter-method-hooks/ – Piotr Dobrogost Mar 26 '15 at 10:42

2 Answers2

10

A metaclass won't help here; although special methods are looked up on the type of the current object (so the class for instances), __getattribute__ or __getattr__ are not consulted when doing so (probably because they are themselves special methods). So to catch all dunder methods, you are forced to create them all.

You can get a pretty decent list of all operator special methods (__pow__, __gt__, etc.) by enumerating the operator module:

import operator
operator_hooks = [name for name in dir(operator) if name.startswith('__') and name.endswith('__')]

Armed with that list a class decorator could be:

def instrument_operator_hooks(cls):
    def add_hook(name):
        operator_func = getattr(operator, name.strip('_'), None)
        existing = getattr(cls, name, None)

        def op_hook(self, *args, **kw):
            print "Hooking into {}".format(name)
            self._function = operator_func
            self._params = (args, kw)
            if existing is not None:
                return existing(self, *args, **kw)
            raise AttributeError(name)

        try:
            setattr(cls, name, op_hook)
        except (AttributeError, TypeError):
            pass  # skip __name__ and __doc__ and the like

    for hook_name in operator_hooks:
        add_hook(hook_name)
    return cls

Then apply that to your class:

@instrument_operator_hooks
class CatchAll(object):
    pass

Demo:

>>> c = CatchAll()
>>> c ** 2
Hooking into __pow__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 11, in op_hook
AttributeError: __pow__
>>> c._function
<built-in function pow>
>>> c._params
((2,), {})

So, even though our class doesn't define __pow__ explicitly, we still hooked into it.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • As I'm quite new to `@decorators`, I had to read this [article](http://www.artima.com/weblogs/viewpost.jsp?thread=240808), which is amazingly straightforward, and then I just understood, what you did.. and I have to admit, now, that I know what is going on — it's not that kind of a magic anymore :):) I reimplemented your solution in a decorator-class — i guess, it's more easy to follow what is going on in my code. – Peter Varo May 04 '13 at 11:30
  • @PeterVaro: That is quite alright. :-) The focus of my answer was how to generate a list of dunder-method names anyway. :-P – Martijn Pieters May 04 '13 at 12:28
4

This is a way to do it.

import inspect
from functools import wraps
from collections import namedtuple

call = namedtuple('Call', ['fname', 'args', 'kwargs'])
calls = []

def register_calls(f):
    @wraps(f)
    def f_call(*args, **kw):
        calls.append(call(f.__name__, args, kw))
        print calls
        return f(*args, **kw)
    return f_call


def decorate_methods(decorator):
    def class_decorator(cls):
        for name, m in inspect.getmembers(cls, inspect.ismethod):
            setattr(cls, name, decorator(m))
        return cls
    return class_decorator


@decorate_methods(register_calls)
class Test(object):

    def test1(self):
        print 'test1'

    def test2(self):
        print 'test2'

Now all the calls to test1 and test2 will be registers in the calls list.

decorate_methods applies a decorator to each method of the class. register_calls registers the calls to the methods in calls, with the name of the function and the arguments.

Martin Maillard
  • 2,751
  • 19
  • 24
  • But this still requires you to create *all* special methods on the class first. – Martijn Pieters May 04 '13 at 09:08
  • @morphyn yes, Martijn Pieters is right, I've just tested this — maybe I'm not using it properly — but I can't do what I want with this... – Peter Varo May 04 '13 at 09:11
  • Yes, you still need to create the methods. I did not understand what you wanted. You're looking for ruby's `method_missing` then :) You will have to use `__getattr__` then. – Martin Maillard May 04 '13 at 09:25