Injecting a callable object into a class as a method

Question:

It is possible to inject a function into a class like this:

class MainClass:
    ...


def simple_injected_func(self: MainClass, arg: str) -> None:
    print(f"simple_injected_func({arg})")


MainClass.simple_injected_func = simple_injected_func
main_object = MainClass()

main_object.simple_injected_func("arg")
# outputs: simple_injected_func(arg)

Furthermore it is possible to make an object callable like this

class SimpleCallableClass:
    def __call__(self, arg: str) -> None:
        print(f"SimpleCallableClass()({arg})")


simple_callable_object = SimpleCallableClass()
simple_callable_object("arg")
# outputs: SimpleCallableClass()(arg)

I now want to combine these two things and inject a callable class/object as a function into another class while keeping access to object variables and methods of both the CallableClass as well as the MainClass. (Internally I want to use this to effectively implement method inheritance and inject those methods into a class from another file)

from inspect import signature

class CallableClass:
    def __call__(self_, self: MainClass, arg: str) -> None:
        print(f"CallableClass()({arg})")


callable_object = CallableClass()

MainClass.callable_object = callable_object

main_object = MainClass()

print(signature(simple_injected_func))
# outputs: (self: __main__.MainClass, arg: str) -> None
print(signature(callable_object))
# outputs: (self: __main__.MainClass, arg: str) -> None

print(signature(main_object.simple_injected_func))
# outputs: (arg: str) -> None
print(signature(main_object.callable_object))
# outputs: (self: __main__.MainClass, arg: str) -> None

main_object.simple_injected_func("my arg")
# outputs: simple_injected_func(my arg)
main_object.callable_object("my arg")
# Traceback (most recent call last):
#   main_object.callable_object("my arg")
# TypeError: CallableClass.__call__() missing 1 required positional argument: 'arg'

Why does the second self not get correctly stripped in case of the callable object? Is there some way of achieving this?

Asked By: Hans

||

Answers:

When methods of an instance are accessed, Python performs "binding", i.e. it creates a bound method. See here:

>>> class Class:
...     def method(self, x):
...         return x
... 
>>> 
>>> instance = Class()
>>> Class.method
<function Class.method at 0x7fa688037158>
>>> instance.method
<bound method Class.method of <__main__.Class object at 0x7fa688036278>>

The binding is done because methods are implemented as descriptors.

You can also implement your callable as a descriptor if you want to have that behaviour.

In short, you would have to implement a class with at least a __get__ method. That __get__ method will be called when either Class.method or instance.method is evaluated. It should return the callable (which should be a different one depending on whether there is an instance or not).

BTW, to actually bind a method to an instance, it is simplest to use functors.partial:

bound_method = functors.partial(method, instance)

All summed up:

class Callable:
    def __call__(self, instance, arg):
        print(f"Callable()(arg)")


class Descriptor:
    def __init__(self, callable):
        self._callable = callable

    def __get__(self, instance, owner):
        if instance is None:
            return self._callable
        else:
            return functools.partial(self._callable, instance)


class Class:
    pass


Class.method = Descriptor(Callable())

And then:

>>> signature(Class.method)
<Signature (instance, arg)>
>>> signature(Class().method)
<Signature (arg)>
Answered By: zvone
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.