Search code examples
pythoninjectcallable-object

Injecting a callable object into a class as a method


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?


Solution

  • 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)>