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:
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 receiverdefnew_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 delegateto_method=getattr(inner_self,to_method_name)# bound method (to inner_self)returnto_method(*args,**kargs)returnnew_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 itdefdelegate_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_namedefadd_methods(cls):method_names=[nameforname,funcininspect.getmembers(interface_like,predicate=inspect.isfunction)ifname!="__init__"]formethod_nameinmethod_names:setattr(cls,method_name,_create_method(method_name,to_attr_name))returnclsreturnadd_methods
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:
defdelegate_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_namedefadd_methods(cls):formethod_nameinmethod_names:setattr(cls,method_name,_create_method(method_name,to_attr_name))returnclsreturnadd_methods######################################classFormatter:defshort_format(self,txt:str,prepend:str):returnf"{prepend}{txt}"deflong_format(self,txt:str,wrap:str):returnf"{wrap}{txt}{wrap}"@delegate_methods(["short_format","long_format"],"formatter")classTextHelper:def__init__(self,id_,formatter):self.id=id_self.formatter=formatterdefbeautify(self,txt)->str:returnf"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.