Search code examples
pythonpython-descriptors

Let a passed function object be called as a method


EDIT: I extended the example to show the more complex case that I was talking about before. Thanks for the feedback in the comments.

Some more context:

  • I am mostly interested in this on a theoretical level. I would be happy with an answer like "This is not possible because ...", giving some detailed insights about python internals.
  • For my concrete problem I have a solution, as the library allows to pass a class which then can do what I want. However, if there would be a way to use the simpler interface of just passing the function which gets bound, I would save many lines of code. (For the interested: This question is derived from the sqlalchemy extension associationproxy and the creator function parameter.)
# mylib.py
from typing import Callable

class C:
    def __init__(self, foo: Callable):
        self.foo = foo

    def __get__(self, instance, owner):
        # simplified
        return CConcrete(self.foo)

class CConcrete:
    foo: Callable

    def __init__(self, foo: Callable):
        self.foo = foo

    def bar(self):
        return self.foo()
# main.py
from mylib import C

def my_foo(self):
    return True if self else False

class House:
    window = C(my_foo)

my_house = House()
print(my_house.window.bar())

This code gives the error

my_foo() missing 1 required positional argument: 'self'

How can I get my_foo be called with self without changing the class C itself?

The point is that a class like this exists in a library, so I can't change it.
In fact it's even more complicated, as foo gets passed down and the actual object where bar exists and calls self.foo is not C anymore. So the solution can also not include assigning something to c after creation, except it would also work for the more complex case described.


Solution

  • You can bind the method to the instance manually, since it won't be living in the class __dict__:

    self.foo = foo.__get__(self)
    

    This is assuming you don't want to monkeypatch the entire class. If you do, assign c.foo = my_foo instead of passing to the instance initializer.

    You could also conceivably use inheritance:

    class C(C):
       def __init__(self):
           super().__init__(self.foo)
    
       def foo(self):
           return True