Python equivalent of Ruby's 'method_missing'
Asked Answered
C

4

32

What is Python's equivalent of Ruby's method_missing method? I tried using __getattr__ but this hook applies to fields too. I only want to intercept the method invocations. What is the Python way to do it?

Clemence answered 15/7, 2011 at 8:3 Comment(0)
I
31

There is no difference in Python between properties and methods. A method is just a property, whose type is just instancemethod, that happens to be callable (supports __call__).

If you want to implement this, your __getattr__ method should return a function (a lambda or a regular def, whatever suite your needs) and maybe check something after the call.

Interdependent answered 15/7, 2011 at 8:22 Comment(2)
Thanks. I found this on a little googling.Clemence
For reference, the link in question gets around the ambiguity by defining a list of attribute names that should be considered methods (which sounds like it kinda defeats the purpose, since you could just define each of those methods and delegate to a stub).Unbuild
L
5

Python doesn't distinguish between methods and attributes (a.k.a. "instance variables") the way Ruby does. Methods and other object attributes are looked up in exactly the same way in Python -- not even Python knows the difference at the look-up stage. Until the attribute is found, it's just a string.

So if you're asking for a way to ensure that __getattr__ is only called for methods, I'm afraid you probably won't find an elegant solution. But it's easy enough to simply return a function (or even a brand-new dynamically bound method) from __getattr__.

Lashonlashond answered 15/7, 2011 at 8:18 Comment(6)
For the same reason you would use method_missing in Ruby or doesNotUnderstand in Smalltalk.Clemence
I understand why you would want to use __getattr__. I just don't understand why you "only want to intercept the method invocations".Lashonlashond
My design didn't require intercepting field access, but it doesn't seem to be a big stumbling block. I find the solution provided by @Emil good enough for my requirements.Clemence
Ruby does not distinguish between methods and attributes at all - there is no such thing as an attribute in Ruby.Rising
@steenslag, that sounds to me like a really bizarre claim. When I say "attribute" I mean "internal state". Are you claiming that objects in Ruby have no internal state? Or do you mean that the internal state of objects in Ruby is always private? That's true. I suppose in Ruby-speak an attribute is really an accessor method of an "instance variable". But since we're talking about Python, I'm using Python-speak.Lashonlashond
In Ruby terms, Python's __getattr__ is somewhere between method_missing and overriding Hash#[] to do something special for missing keys.Chrystel
B
3

You could implement a missing_method like feature in the below way:

https://gist.github.com/gterzian/6400170

import unittest
from functools import partial

class MethodMissing:
    def method_missing(self, name, *args, **kwargs):
        '''please implement'''
        raise NotImplementedError('please implement a "method_missing" method')

    def __getattr__(self, name):
        return partial(self.method_missing, name)


class Wrapper(object, MethodMissing):
    def __init__(self, item):
        self.item = item

    def method_missing(self, name, *args, **kwargs):
        if name in dir(self.item):
            method = getattr(self.item, name)
            if callable(method):
                return method(*args, **kwargs)
            else:
                raise AttributeError(' %s has not method named "%s" ' % (self.item, name))


class Item(object):
    def __init__(self, name):
        self.name = name

    def test(self, string):
        return string + ' was passed on'


class EmptyWrapper(object, MethodMissing):
    '''not implementing a missing_method'''
    pass


class TestWrapper(unittest.TestCase):
    def setUp(self):
        self.item = Item('test')
        self.wrapper = Wrapper(self.item)
        self.empty_wrapper = EmptyWrapper()

    def test_proxy_method_call(self):
        string = self.wrapper.test('message')
        self.assertEqual(string, 'message was passed on')

    def test_normal_attribute_not_proxied(self, ):
        with self.assertRaises(AttributeError):
            self.wrapper.name
            self.wrapper.name()

    def test_empty_wrapper_raises_error(self, ):
        with self.assertRaises(NotImplementedError):
            self.empty_wrapper.test('message')


if __name__ == '__main__':
    unittest.main()
Broadbrim answered 31/8, 2013 at 19:36 Comment(0)
G
0

This solution creates a Dummy object for which:

  • every non-existing method will work and return None (even if you pass parameters)
  • you can also provide a dictionary with methodname: defaultvalue for methods that should return a specific default value
class Dummy:
    def __init__(self, methods):
        self.methods = methods

    def __getattr__(self, attr):
        defaultvalue = self.methods[attr] if attr in self.methods else None
        return lambda *args, **kwargs: defaultvalue

    def baz(self):
        return 'custom'

d = Dummy({'foo': 1234})

print(d.foo())                        # 1234
print(d.bar(1, 2, x=123, y=456))      # None
print(d.kjfdhgjf())                   # None
print(d.baz())                        # 'custom'
Gulden answered 15/6, 2022 at 8:46 Comment(4)
defaultvalue = self.methods.get(attr)Westberry
You're right, feel free to edit @KellyBundy!Gulden
Note this isn't a dummy in the traditional sense (see e.g. tanzu.vmware.com/developer/guides/test-doubles/#dummy).Graber
@Graber I used the term "dummy" in the general meaning that it is a class that does nothing but just answers a default value for each call of a method.Gulden

© 2022 - 2024 — McMap. All rights reserved.