14

Is there a way to create a class which instances respond to arbitrary method calls?

I know there is a the special method __getattr__(self, attr) which would be called when someone is trying to access an attribute of an instance. I am searching for something similar that enables me to intercept method calls, too. The desired behavior would look something like this:

class A(object):
    def __methodintercept__(self, method, *args, **kwargs): # is there a special method like this??
        print(str(method))


>>> a = A()
>>> a.foomatic()
foomatic

EDIT

The other suggested questions do not address my case: I do not want to wrap another class or change the metaclass of a second class or similar. I just want to have a class that responds to arbitrary method calls.

Thanks to jonrshape I now know that __getattr__(self, attr) will also be called when a method is called in the same way as it would be when an attribute is accessed. But how do i distinguish in __getattr__ if attr comes from a method call or an attribute access and how to get the parameters of a potential method call?

Miki
  • 40,887
  • 13
  • 123
  • 202
Salo
  • 1,936
  • 15
  • 24
  • 3
    Methods are handled no differently than any other attributes, you still need `__getattr__`/`__getattribute__`. – jonrsharpe Oct 14 '15 at 12:35
  • @jonrsharpe mmh if i implement a class A with `__getattr__` and a print inside and do `a=A(); a.foo` it will print `foo` but if i call `a=A(); a.foo()` it will raise a `TypeError: 'NoneType' object is not callable` – Salo Oct 14 '15 at 12:40
  • 2
    `__getattr__` will still have to *return* something callable, not *call* it, otherwise you get the default `None` back – jonrsharpe Oct 14 '15 at 12:40
  • @jonrsharpe ah, ok thanks, i misunderstood the error. That fixes it, thank you! – Salo Oct 14 '15 at 12:43
  • If you have answered your own question, please post an answer with your working code and mark it as accepted. This will help people in the future who have the same question. – Josh J Oct 14 '15 at 14:53
  • This feels like you should be extending [`unittest.mock.Mock`](https://docs.python.org/3/library/unittest.mock.html#the-mock-class) – Adam Smith Oct 14 '15 at 17:16

5 Answers5

15

This is something I came up with, which will behave exactly as if the method exists.

First let's establish one thing: You cannot distinguish in __getattr__ if attr comes from a function call or an "attribute access", because a class method is an attribute of your class. So someone can access that method even if they don't intend to call it, as in:

class Test:
    def method(self):
        print "Hi, I am method"

>> t = Test()
>> t.method # just access the method "as an attribute"
<bound method Test.method of <__main__.Test instance at 0x10a970c68>>

>> t.method() # actually call the method
Hi, I am method

Therefore, the closest thing I could think of is this behavior:

Create a class A, such that:

  1. When we try to access an attribute / method, which already exists in that class, act normal and just return the requested attribute / method.
  2. When we try to access something that doesn't exist in the class definition, treat it as a class method and have 1 global handler for all such methods.

I will first write the class definition and then show how accessing a method that doesn't exist behaves exactly like accessing one that exists, whether you are just accessing it, or actually calling it.

Class definition:

class A(object):
    def __init__(self):
        self.x = 1 # set some attribute

    def __getattr__(self,attr):
        try:
            return super(A, self).__getattr__(attr)
        except AttributeError:
            return self.__get_global_handler(attr)

    def __get_global_handler(self, name):
        # Do anything that you need to do before simulating the method call
        handler = self.__global_handler
        handler.im_func.func_name = name # Change the method's name
        return handler

    def __global_handler(self, *args, **kwargs):
        # Do something with these arguments
        print "I am an imaginary method with name %s" % self.__global_handler.im_func.func_name
        print "My arguments are: " + str(args)
        print "My keyword arguments are: " + str(kwargs)

    def real_method(self, *args, **kwargs):
        print "I am a method that you actually defined"
        print "My name is %s" % self.real_method.im_func.func_name
        print "My arguments are: " + str(args)
        print "My keyword arguments are: " + str(kwargs)

I added the method real_method just so I have something that actually exists in the class to compare its behavior with that of an 'imaginary method'

Here's the result:

>> a = A() 
>> # First let's try simple access (no method call)
>> a.real_method # The method that is actually defined in the class
<bound method A.real_method of <test.A object at 0x10a9784d0>>

>> a.imaginary_method # Some method that is not defined
<bound method A.imaginary_method of <test.A object at 0x10a9784d0>>

>> # Now let's try to call each of these methods
>> a.real_method(1, 2, x=3, y=4)
I am a method that you actually defined
My name is real_method
My arguments are: (1, 2)
My keyword arguments are: {'y': 4, 'x': 3}

>> a.imaginary_method(1, 2, x=3, y=4)
I am an imaginary method with name imaginary_method
My arguments are: (1, 2)
My keyword arguments are: {'y': 4, 'x': 3}

>> # Now let's try to access the x attribute, just to make sure that 'regular' attribute access works fine as well
>> a.x
1
tomas
  • 963
  • 6
  • 19
  • Thanks for your answer, gave me a deeper understanding of Python. The only thing I found: if i call `a.attribute_that_doesnt_exist` it will respond with nothing – Salo Oct 15 '15 at 06:02
  • 1
    @Salo If you call `a.attribute_that_doesnt_exist` it shouldn't respond with nothing (`None`). It should actually return a "bound method" object. So: `a.method` returns the method. If you add the parentheses `()` (with arguments,optionally) after the method call (`a.method()`) , it will be evaluated. Open a python interpreter (`python`, ideally `ipython`), and type `a.attribute_that_doesnt_exist`. You should get sth like `>` This is python's way of telling you that this is a class method, but you're not calling it. – tomas Oct 15 '15 at 08:37
  • The assignment to `func_name` is cute but dangerous: it changes the name of the actual “global handler” function *forever*—or rather until another lookup changes it again. – Davis Herring Apr 19 '20 at 22:14
  • 1
    This code raises `AttributeError: 'function' object has no attribute 'im_func'`. – André C. Andersen Mar 28 '21 at 07:19
  • `__getattr__` is only called if the method is not defined. No need to try calling super().__getattr__ source: https://python-reference.readthedocs.io/en/latest/docs/dunderattr/getattr.html – Laurent Pinson Nov 02 '21 at 13:20
7

unittest.mock.Mock does this by default.

from unittest.mock import Mock

a = Mock()

a.arbitrary_method()                             # No error
a.arbitrary_method.called                        # True
a.new_method
a.new_method.called                              # False
a.new_method("some", "args")
a.new_method.called                              # True
a.new_method.assert_called_with("some", "args")  # No error
a.new_method_assert_called_with("other", "args") # AssertionError
Adam Smith
  • 52,157
  • 12
  • 73
  • 112
1

This is the solution I was looking for when coming across this question:

class Wrapper:
    def __init__(self):
        self._inner = []  # or whatever type you want to wrap

    def foo(self, x):
        print(x)

    def __getattr__(self, attr):
        if attr in self.__class__.__dict__:
            return getattr(self, attr)
        else:
            return getattr(self._inner, attr)

t = Test()
t.foo('abc')  # prints 'abc'
t.append('x')  # appends 'x' to t._inner

Criticisms very welcome. I wanted to add methods to the Browser class in the Splinter package, but it only exposes a function to return an instance, not the class itself. This approach permitted pseudo-inheritance, which meant I could declaratively decouple DOM code from website-specific code. (A better approach in hindsight might have been to use Selenium directly.)

Chris
  • 5,664
  • 6
  • 44
  • 55
0

Method calls aren't any different from attribute access. __getattr__() or __getattribute__() is the way to respond to arbitrary attribute requests.

You cannot know if the access comes from "just retrieval" or "method call".

It works like this: first, attribute retrieval, then, call on the retrieved object (in Python, call is just another operator: anything can be called and will throw an exception if it isn't callable). One doesn't, and shouldn't, know about the other (well, you can analyze the code up the call stack, but that's totally not the thing to do here).

One of the reasons is - functions are first-class objects in Python, i.e. a function (or, rather, a reference to it) is no different from any other data type: I can get the reference, save it and pass it around. I.e. there's completely no difference between requesting a data field and a method.

Elaborate on what you need this for for us to suggest a better solution.

E.g., if you need the "method" to be able to be called with different signatures, *args and **kwargs is the way to go.

ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152
0

The follow will respond to all undefined method calls:

class Mock:

    def __init__(self, *args, **kwargs):
        pass

    def __getattr__(self, attr):
        def func(*args, **kwargs):
            pass
        return func

Or just use unittest.mock.Mock.

André C. Andersen
  • 8,955
  • 3
  • 53
  • 79