Python Delegation

Kotlin offers support for delegation as a language feature. The documentation provides a clear explanation of how the compiler incorporates interface methods into a class, with those methods simply calling the corresponding method of the delegated object. Delegation, a valuable feature promoting composition over inheritance, can be implemented in Python, too. However, depending on the use case, mixins (using multiple inheritance, interface default methods, or monkey patching) might be more suitable, though that’s a broader discussion.

Let’s explore delegation in Python. One approach utilizes the __getattr__ method to delegate calls to missing methods in an object to the corresponding methods of the delegated object. Another leverages Python’s dynamism to dynamically create “delegator methods” within the class at runtime, mirroring the Kotlin compiler’s behavior. Decorators, specifically class decorators, fit well here since we’re modifying a class. For each method requiring delegation, a function is created that captures the delegate attribute name and method name in its closure. This function is then added to the class.

Let’s illustrate this with an example. Suppose we have a “pseudo-interface” Formattable (represented by an abstract base class or Protocol in Python) and a TextHelper class possessing a formatter attribute of type FormatterImpl. We aim to incorporate Formattable methods into TextHelper, delegating them to formatter. Here’s how we define the helper function and decorator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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

We can then use this setup like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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, we might not always define formal interfaces. Instead, we may want to delegate a specific set of methods. The delegate_methods decorator caters to this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

The code is available at a gist. Interestingly, after implementing this, a similar “using decorators” approach was found at here.

Licensed under CC BY-NC-SA 4.0
Last updated on May 17, 2024 13:02 +0100