Sunday, 6 August 2023

Delegation in Python

Kotlin provides support for delegation at the language level. It's clearly explained in the documentation, so nothing to add, just to remind you that the compiler adds the methods of the interface to the class, methods that just invoke the corresponding method in the object we are delegating to. Delegation is a very nice feature, it follows the "favor composition over inheritance" principle, but I think depending on the use case mixins (either through multiple inheritance and interface default methods or through monkeypatching in languages that allow for that) can be a better approach. Well, that would give for a long reflection...

The thing is that I was thinking about how to implement delegation in Python. One option is using __getattr__, so that calls to a method missing in the object can be delegated to the corresponding method in the object we delegate to. The other option is leveraging python's dynamism to create the "delegator methods" and add them to the class. So basically we do at runtime the same that the Kotlin compiler does. As we are going to transform a class, using a decorator (that decorates a class) is a perfect fit. For each method that we want to delegate we create a function that traps in its closure the name of the attribute to delegate to and the name of the method. We add that function to the class, and that's all.

So let's say we have a sort of "interface" (in Python that means an abc or a Protocol) Formattable and a class TextHelper with an attribute formatter of the FormatterImpl type, and we want to add the methods in Formattable to TextHelper delegating them to formatter. We define the following helper function and decorator:


def _create_method(to_method_name: str, to_attr_name: str):
    # returns a closure that traps the method to invoke and the attribute_name of the object that will act as receiver
    def new_method(self, *args, **kargs):
        # self is an instance of the class FROM which we delegate 
        inner_self = getattr(self, to_attr_name)
        # inner_self is the object TO which we delegate
        to_method = getattr(inner_self, to_method_name) # bound method (to inner_self)
        return to_method(*args, **kargs)
    return new_method


# decorator with parameters, so it has to return a function (that will be invoked with the class being decorated)
# we don't create a new class, we add functions to the existing class and return it
def delegate_interface(interface_like, to_attr_name: str):
    # receives an "interface" for which methods we will create "delegator methods" to delegate from them to the corresponding method in the object indicated by to_attr_name
    def add_methods(cls):
        method_names = [name for name, func in inspect.getmembers(interface_like, predicate=inspect.isfunction) if name != "__init__"]
        for method_name in method_names:
            setattr(cls, method_name, _create_method(method_name, to_attr_name))
        return cls
    return add_methods

That we can use like this:



class Formattable(Protocol):
    @abstractmethod
    def short_format(self, txt: str, prepend: str):
        pass

    @abstractmethod
    def long_format(self, txt: str, wrap: str):
        pass

class FormattableImp(Formattable):
    def short_format(self, txt: str, prepend: str):
        return f"{prepend}{txt}"

    def long_format(self, txt: str, wrap: str):
        return f"{wrap}{txt}{wrap}"

@delegate_interface(Formattable, "formatter")
class TextHelper2:
    def __init__(self, id_, formatter):
        self.id = id_
        self.formatter = formatter

    def beautify(self, txt) -> str:
        return f"beautifying {self}"


helper = TextHelper2("aa", FormattableImp())
print(helper.long_format("hi", "||"))
print(helper.short_format("hi", "||"))

#||hi||
#||hi


Given python's dynamic nature in many occasions we don't make the effort of defining "interfaces" and we could want to delegate a list of methods rather than all the methods in an "interface". I have an additional decorator for that, delegate_methods:



def delegate_methods(method_names: list[str], to_attr_name: str):
    # decorator with parameters   
    # receives a list of method names to create and delegate from them to the corresponding method in the object indicated by to_attr_name
    def add_methods(cls):
        for method_name in method_names:
            setattr(cls, method_name, _create_method(method_name, to_attr_name))
        return cls
    return add_methods

######################################

class Formatter:
    def short_format(self, txt: str, prepend: str):
        return f"{prepend}{txt}"

    def long_format(self, txt: str, wrap: str):
        return f"{wrap}{txt}{wrap}"


@delegate_methods(["short_format", "long_format"], "formatter")
class TextHelper:
    def __init__(self, id_, formatter):
        self.id = id_
        self.formatter = formatter

    def beautify(self, txt) -> str:
        return f"beautifying {self}"

helper = TextHelper("aa", Formatter())
print(helper.long_format("hi", "||"))
print(helper.short_format("hi", "||"))    

#||hi||
#||hi


I've uploaded the code to a gist. After implemeting this I looked around to see what others had done, and came up with the "using decorators" implementation here that is almost identical.

No comments:

Post a Comment