5

Say I have a class which contains several functions.

class Person:
    def __init__(self): pass
    def say(self, speech): pass
    def walk(self, destination): pass
    def jump(self): pass

When the user instantiates a Person, I'd like them to be able to call any method of the class. If the requested method does not exist (e.g. Person.dance()), a default function should be called instead.

I imagine that this could be done via a theoretical magic method -

class Person:
    def __init__(self): pass
    def say(self, speech): pass
    def walk(self, destination): pass
    def jump(self): pass
    def sleep(self): print("Zzz")

    def __method__(self, func):
        if func.__name__ not in ['say','walk','jump']:
            return self.sleep
        else
            return func

billy = Person()
billy.dance()
>> "Zzz"

However, I know of no such magic method.

Is there a way to make non-existent methods within a class redirect to another class?

It's important that the end-user doesn't have to do anything - from their perspective, it should just work.

snazzybouche
  • 2,241
  • 3
  • 21
  • 51
  • 1
    From a user perspective of your API this would be incredibly frustrating. Why do you need this? – Mitch Aug 12 '19 at 13:43
  • Let's say there are several hundred more actions that the Person can take, but most of them are so trivially different that a single function can handle them all. For the few that aren't, though, I need to have a separate function. It'd be nice to be able to have the bulk of the actions redirect to `billy.action()` instead of having to define a function for all of them. Of course, if the requested method does not exist even in the exhaustive list of all actions, an error would be thrown as per usual. – snazzybouche Aug 12 '19 at 13:48

3 Answers3

4

The standard way to catch an undefined attribute is to use __getattr__:

# But see the end of the answer for an afterthought
def __getattr__(self, attr):
    return self.sleep

Python does not differentiate between "regular" attributes and methods; a method call starts with an ordinary attribute lookup, whose result just happens to be callable. That is,

billy.run()

is the same as

f = billy.run
f()

This means that __getattr__ will be invoked for any undefined attribute; there is no way to tell at lookup time whether the result is going to be called or not.


However, if all you want is to define "aliases" for a common method, you can do that with a loop after the class statement.

class Person:
    def __init__(self): pass
    def say(self, speech): pass
    def walk(self, destination): pass
    def jump(self): pass
    def do_all(self): pass

for alias in ["something", "something_else", "other"]:
    setattr(Person, alias, Person.do_all)

You can also make hard-coded assignments in the class statement, but that would be unwieldy if there are, as you mention, hundreds of such cases:

class Person:
    def do_all(self): pass

    something = do_all
    something_else = do_all

(I did not experiment with using exec to automate such assignments in a loop; it might be possible, though not recommended.)

You can also embed the list of aliases in the definition of __getattr__, come to think of it:

 def __getattr__(self, attr):
     if attr in ["something", "something_else", "other"]:
         return self.sleep
     else:
         raise AttributeError(f"type object 'Person' has no attribute '{attr}'")
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Thank you for this comprehensive answer! benvc in another answer raised a point about transferring function name/args/kwargs to the default function - how would you implement that without having to nest the function definition within \_\_getattr\_\_? – snazzybouche Aug 12 '19 at 14:16
  • Attribute lookup and function calling are completely separate. Whatever `foo.bar` resolves to will receive whatever arguments were used in the call `foo.bar(...)`. – chepner Aug 12 '19 at 14:25
  • Good to know - my issue here, then, is that the functions umbrella'd by `Person.do_all()` are very similar, but they're not identical. I need to be able to distinguish which "method" is being called in order to elicit very slightly different behaviour – snazzybouche Aug 12 '19 at 14:29
  • Then `do_all` should take an additional argument (the actual name received by `__getattr__`). Eg., `something = lambda *args, **kwargs: do_all("something", *args, **kwargs)`. – chepner Aug 12 '19 at 14:32
  • I never would've thought to do that! It works perfectly. Thank you very much! – snazzybouche Aug 12 '19 at 14:41
2

Your users might find the API behavior confusing. However, if you're sure you need this pattern, you can try something like

# getattr will get the function attribute by a string name version
# None is returned if no function is found
my_func = getattr(self, 'my_func', None)

# callable ensures `my_func` is actually a function and not a generic attribute
# Add your if-else logic here
if callable(my_func):
    my_func(args)
else:
    ...
rvd
  • 558
  • 2
  • 9
2

You could nest your "default" function inside __getattr__ in order to gain access to the called non-existent method's name and arguments.

class Test:
    def __getattr__(self, attr):
        def default(*args, **kwargs):
            return attr, args, kwargs
        return default

test = Test()
print(test.test('test'))
# ('test', ('test',), {})
benvc
  • 14,448
  • 4
  • 33
  • 54
  • Thank you! Do you know of a way to pass name, args and kwargs without having to nest the definition? – snazzybouche Aug 12 '19 at 14:16
  • @snazzybouche - there may be some other better approach, but this was all that came to mind for passing the non-existent method name and arguments to the "default" function. May be helpful for you to read the following regarding nested functions to get some ideas about accessing the local variable from the `__getattr__` function: https://stackoverflow.com/questions/4020419/why-arent-python-nested-functions-called-closures – benvc Aug 12 '19 at 15:32