4

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".

yashaka
  • 826
  • 1
  • 9
  • 20

3 Answers3

4

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.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • Mmm... That looks like what I need:) You are right, I can wrap your solution in some kind of `delegate` method from my original example. Thank you! I will try to use it. – yashaka Dec 24 '16 at 02:59
  • You really do need to use a metaclass for this, you can't call a `delegate` method in your `__init__` because `__init__` only runs after the instance has been created, and you can't instantiate an abstract class. Using a metaclass you can add the delegator methods before you create the class, which allows the class to be instantiated. Now, setting up the attribute of the object that the delegator methods use, sure that can be in a `delegate()` function or method. But there's probably no need (just setting an attribute is easier). – Blckknght Dec 24 '16 at 03:16
  • Oh, and it's occurred to me since I wrote this that the handling of multiple inheritance is probably incorrect. It should work as written for single inheritance, and for multiple inheritance where the different bases never interact with respect to the abstract methods. It will get it wrong in situations where one base has a still-abstract method that is implemented concretely in another base. Fixing the issue is not impossible, but more effort than I have time for at the moment. – Blckknght Dec 24 '16 at 03:18
  • Thank you a lot for your explanation of "have to use metaclass". I have also came to this conclusion, after I had thought more on the topic... And yeah, for multiple inheritance there should be some additional adjustments made... I will try to to figure out how to do this... Because seems like I also need a multiple inheritance from abstract base classes in my case... – yashaka Dec 24 '16 at 03:25
  • It happened to not work with @abstractproperty decorated abstract base class members... Do you have ideas how to fix this quickly? – yashaka Dec 25 '16 at 04:36
  • my current version is: https://www.refheap.com/124383. It generates a method delegating to _delegate's property. Now I need to figure out how to generate property instead of method. If you also have any ideas of better implementation than my, please share. Thanks! – yashaka Dec 25 '16 at 05:06
  • my next try completely does not work: https://www.refheap.com/124384 it fails with: TypeError: Can't instantiate abstract class .. with abstract methods – yashaka Dec 25 '16 at 05:53
  • I think the solution is in between your two versions. Your first version doen't work because you need to wrap the `delegator` function in a `property` (and refer to the right attribute). The second approach could work (is closer to the approach I was thinking would be necessary to get multiple inheritance working for real), but you need to manually correct the `cls.__abstractmethods__` set after you've added the methods/properties to the constructed class. – Blckknght Dec 25 '16 at 06:56
  • Yeah, I was also thinking of "need to edit __abstractmethods__ somewhere", thank you for the hint to edit them on cls! Somehow it was not obvious for me :) – yashaka Dec 25 '16 at 08:10
0

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()
Fomalhaut
  • 8,590
  • 8
  • 51
  • 95
  • No:) Even with such "classic" delegation implementation: https://www.refheap.com/124379. And this is obvious. It does not work because using abc disallows to instantiate classes with not all abstractmethods overriden – yashaka Dec 24 '16 at 02:21
  • Okay, now I got what you mean. I'll edit my answer in a few minutes. – Fomalhaut Dec 24 '16 at 02:27
  • Unfortunately, that is not the solution I need:) I have an interface - IElement. It can't be modified. It is a classic interface with all abstract methods. Then I have StandardElement that implements this interface. Then I have MyElement, that also should implement IElement. But, only some of IElement's abstract methods will have implementation different from StandardElement. And I don't want to manually delegate implmentation of all "standard" methods to StandardElement object... I want to provide this delegation automatically. – yashaka Dec 24 '16 at 02:47
  • I have explained why it is impossible using `abc`. The exception `NotImplementedError` is designed for the purposes like yours. That's why I suggested to use it. – Fomalhaut Dec 24 '16 at 02:50
  • I have explained that it is obvious that it is impossible using JUST abc. But I thought I may be possible with some additional helper methods, like `delegate` method I used in my original example... :) The `delegate` method was supposed to "hack" the __abstractmethods__ class attribute, which actually holds the information of "remained abstractmethods to be implemented"... – yashaka Dec 24 '16 at 02:53
0

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

monomonedula
  • 606
  • 4
  • 17