Reputation: 2502
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?
Upvotes: 0
Views: 594
Reputation: 19362
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)>
Upvotes: 3