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