Automatic delegation for classes extending python abc classes

1.1k views Asked by At

If some class extends abc class (Abstract Base Class) then I can't instantiate it unless I define all abstract methods. But often when implementing Decorator pattern, I want to define only a few abstract methods, and others - just delegate to decorated object. How to do this?

For example, I want to make the following code work:

from abc import ABCMeta, abstractmethod


class IElement(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def click(self):
        return

    @abstractmethod
    def hover(self):
        return

    # ... more IElement's abstractmethods...


class StandardElement(IElement):

    def click(self):
        return "click"

    def hover(self):
        return "hover"

    # ... more implemented IElement's methods...


class MyElement(IElement):
    def __init__(self, standard_element):
        self._standard_element = standard_element
        delegate(IElement, standard_element)

    def click(self):
        return "my click"


assert MyElement(StandardElement()).click() == 'my click'
assert MyElement(StandardElement()).hover() == 'click'

instead of

from abc import ABCMeta, abstractmethod


class IElement(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def click(self):
        return

    @abstractmethod
    def hover(self):
        return

    # ... more IElement's abstractmethods...


class StandardElement(IElement):

    def click(self):
        return "click"

    def hover(self):
        return "hover"

    # ... more implemented IElement's methods...


class MyElement(IElement):
    def __init__(self, standard_element):
        self._standard_element = standard_element

    def click(self):
        return "my click"

    def hover(self):
        return self._standard_element.hover()

    # ... more manually delegated IElement's methods to self._standard_element object, aggregated in initialiser...


assert MyElement(StandardElement()).click() == 'my click'
assert MyElement(StandardElement()).hover() == 'click'

For this I need to implement the delegate method from example above. How to implement it? Some other approaches to provide automatic delegation for classes extending abc classes may also be considered.

P.S. Please do not propose me Inheritance (class MyElement(StandardElement)) as a solution here... The code provided above is just an example. In my real case MyElement is pretty different thing comparing to StandardElement. Still, I have to make MyElement compatible with StandardElement, because sometimes someone is supposed to use MyElement instead of StandardElement. I really need to implement "has a" relationship here, not "is a".

3

There are 3 answers

5
Fomalhaut On

You get this error because the class MyElement is abstract (you haven't overridden the method hover). Abstract class is a class that has at least one abstract method. In order to resolve the error you should add this method in this way for example:

class MyElement(IElement):
    def __init__(self, standard_element):
        self._standard_element = standard_element

    def click(self):
       return "my click"

    def hover(self):
        return self._standard_element.hover()

If you want to delegate many methods using __getattr__ it seems to be impossible via abc.abstractmethod, because __getattr__ dynamically searches for the necessary methods that are impossible to inspect without the direct running of __getattr__.

Therefore I would recommend you to specify all methods as abstract (via @abstractmethod) that surely will be overridden in each inherited class and implement the delegated method using NotImplementedError. An example:

class IElement(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def necessary_to_implement_explicitely(self):
        pass

    def click(self):
        raise NotImplementedError()

    def hover(self):
        raise NotImplementedError()
9
Blckknght On

There is no automatic way to do the delegation you want, by default. Delegation isn't really an explicit part of abstract classes, so that shouldn't really be a surprise. You can however write your own delegating metaclass that adds the missing methods for you:

def _make_delegator_method(name):
    def delegator(self, *args, **kwargs):
        return getattr(self._delegate, name)(*args, **kwargs)
    return delegator

class DelegatingMeta(ABCMeta):
    def __new__(meta, name, bases, dct):
        abstract_method_names = frozenset.union(*(base.__abstractmethods__
                                                  for base in bases))
        for name in abstract_method_names:
            if name not in dct:
                dct[name] = _make_delegator_method(name)

        return super(DelegatingMeta, meta).__new__(meta, name, bases, dct)

The delegator methods are made in a separate function because we need a namespace where name doesn't change after the function's been created. In Python 3, you could do things all in the __new__ method by giving delegator a keyword-only argument with a default value, but there are no keyword only arguments in Python 2, so that won't work for you.

Here's how you'd use it:

class MyElement(IElement):
    __metaclass__ = DelegatingMeta

    def __init__(self, standard_element):
        self._delegate = standard_element

    def click(self):
        return "my click"

Assigning to self._delegate sets the object the methods created by the metaclass will use. If you wanted to, you could make that a method of some kind, but this seemed to be the simplest approach.

0
monomonedula On

Based on Blckknght's answer I've created a python package, extended with functionality of multiple delegates option and custom delegate attribute name.

Check it out here https://github.com/monomonedula/abc-delegation

Installation: pip install abc-delegation