Delegation design pattern with abstract methods in python
Asked Answered
P

3

5

I have the following classes implementing a "Delegation Design Pattern" with an additional DelegatorParent class:

class DelegatorParent():

    def __init__(self):
        self.a = 'whatever'    

class ConcreteDelegatee():

    def myMethod(self):
        return 'myMethod'


class Delegator(DelegatorParent):

    def __init__(self):
        self.delegatee = ConcreteDelegatee()
        DelegatorParent.__init__(self)

    def __getattr__(self, attrname):
        return getattr(self.delegatee, attrname)

a = Delegator()
result = a.myMethod()

Everything looks fine.

Now I would like to put an abstract method in DelegatorParent, to ensure that "myMethod" is always defined.

from abc import ABCMeta, abstractmethod

class DelegatorParent():
    __metaclass__ = ABCMeta

    @abstractmethod
    def myMethod(self):
        pass

    def __init__(self):
        self.a = 'whatever'


class ConcreteDelegatee():

    def myMethod(self):
        return 'myMethod'


class Delegator(DelegatorParent):

    def __init__(self):
        self.delegatee = ConcreteDelegatee()
        DelegatorParent.__init__(self)

    def __getattr__(self, attrname):
        return getattr(self.delegatee, attrname)

    # This method seems unnecessary, but if I erase it an exception is
    # raised because the abstract method's restriction is violated
    def myMethod(self): 
        return self.delegatee.myMethod()


a = Delegator()
result = a.myMethod()

Can you help me find an "elegant" way to remove "myMethod" from "Delegator"... Intuition tells me that it is somehow redundant (considering that a custom getattr method is defined).

And more importantly, notice that with this implementation, if I forget to define myMethod in ConcreteDelegatee the program compiles, but it may crash in runtime if I call Delegator.myMethod(), which is exactly what I wanted to avoid by using abstract methods in DelegatorParent.

Obviously a simple solution would be to move @abstractmethod to the Delegator class, but I want to avoid doing that because in my program DelegatorParent is a very important class (and Delegator is just an auxiliary class).

Pomade answered 10/10, 2017 at 22:54 Comment(2)
Have you tried overriding __getattribute__ rather than __getattr__?Jollity
Yes... didn't workPomade
S
6

You can decide to automatically implement abstract methods delegared to ConcreteDelegatee.

For each abstract method, check if it's name exist in the ConcreteDelegatee class and implement this method as a delegate to this class method.

from abc import ABCMeta, abstractmethod

class DelegatorParent(object):
    __metaclass__ = ABCMeta

    def __init__(self):
        self.a = 'whatever'

    @abstractmethod
    def myMethod(self):
        pass


class Delegatee(object):
    pass


class ConcreteDelegatee(Delegatee):    
    def myMethod(self):
        return 'myMethod'

    def myMethod2(self):
        return 'myMethod2'


class Delegator(DelegatorParent):

    def __new__(cls, *args, **kwargs):
        implemented = set()
        for name in cls.__abstractmethods__:
            if hasattr(ConcreteDelegatee, name):
                def delegated(this, *a, **kw):
                    meth = getattr(this.delegatee, name)
                    return meth(*a, **kw)
                setattr(cls, name, delegated)
                implemented.add(name)
        cls.__abstractmethods__ = frozenset(cls.__abstractmethods__ - implemented)
        obj = super(Delegator, cls).__new__(cls, *args, **kwargs)
        obj.delegatee = ConcreteDelegatee()
        return obj

    def __getattr__(self, attrname):
        # Called only for attributes not defined by this class (or its bases).
        # Retrieve attribute from current behavior delegate class instance.
        return getattr(self.delegatee, attrname)

# All abstract methods are delegared to ConcreteDelegatee
a = Delegator() 

print(a.myMethod()) # correctly prints 'myMethod'

print(a.myMethod2()) #correctly prints 'myMethod2'

This solves the main problem (prevent ConcreteDelegatee from forgetting to define myMethod). Other abstract methods are still checked if you forgot to implement them.

The __new__ method is in charge of the delegation, that frees your __init__ to do it.

Sluff answered 16/10, 2017 at 1:36 Comment(0)
S
0

Since you use ABCMeta, you must defined the abstract methods. One could remove your method from the __abstractmethods__ set, but it is a frozenset. Anyway, it involves listing all abstract methods.

So, instead of playing with __getattr__, you can use a simple descriptor.

For instance:

class Delegated(object):
    def __init__(self, attrname=None):
        self.attrname = attrname

    def __get__(self, instance, owner):
        if instance is None:
            return self
        delegatee = instance.delegatee
        return getattr(delegatee, self.attrname)


class Delegator(DelegatorParent):
    def __init__(self):
        self.delegatee = ConcreteDelegatee()
        DelegatorParent.__init__(self)

    myMethod = Delegated('myMethod')

An advantage here: the developer has the explicit information that "myMethod" is delegated.

If you try:

a = Delegator()
result = a.myMethod()

It works! But if you forget to implement myMethod in Delegator class, you have the classic error:

Traceback (most recent call last):
  File "script.py", line 40, in <module>
    a = Delegator()
TypeError: Can't instantiate abstract class Delegator with abstract methods myMethod

Edit

This implementation can be generalized as follow:

class DelegatorParent():
    __metaclass__ = ABCMeta

    @abstractmethod
    def myMethod1(self):
        pass

    @abstractmethod
    def myMethod2(self):
        pass

    def __init__(self):
        self.a = 'whatever'


class ConcreteDelegatee1():
    def myMethod1(self):
        return 'myMethod1'


class ConcreteDelegatee2():
    def myMethod2(self):
        return 'myMethod2'


class DelegatedTo(object):
    def __init__(self, attrname):
        self.delegatee_name, self.attrname = attrname.split('.')

    def __get__(self, instance, owner):
        if instance is None:
            return self
        delegatee = getattr(instance, self.delegatee_name)
        return getattr(delegatee, self.attrname)


class Delegator(DelegatorParent):
    def __init__(self):
        self.delegatee1 = ConcreteDelegatee1()
        self.delegatee2 = ConcreteDelegatee2()
        DelegatorParent.__init__(self)

    myMethod1 = DelegatedTo('delegatee1.myMethod1')
    myMethod2 = DelegatedTo('delegatee2.myMethod2')


a = Delegator()
result = a.myMethod2()

Here, we can specify the delegatee name and delegatee method.

Sluff answered 14/10, 2017 at 17:3 Comment(1)
Thanks Laurent. The only problem I still have with your code is that if I forget to implement 'myMethod' inside 'ConcreteDelegatee', the code crashes when I call a.myMethod(), and not when I instantiate a = Delegator() which was the reason I wanted to use abstract methods in the first place. Please check my own answer to this question to see a possible (but still not perfect) solution to this issue.Pomade
P
0

Here is my current solution. It solves the main problem (prevent ConcreteDelegatee from forgetting to define myMethod), but I'm still not convinced because I still need to define myMethod inside Delegator, which seems redundant

from abc import ABCMeta, abstractmethod

class DelegatorParent(object):
    __metaclass__ = ABCMeta

    def __init__(self):
        self.a = 'whatever'

    @abstractmethod
    def myMethod(self):
        pass


class Delegatee(object):
    def checkExistence(self, attrname):
        if not callable(getattr(self, attrname, None)):
            error_msg = "Can't instantiate " + str(self.__class__.__name__) + " without abstract method " + attrname
            raise NotImplementedError(error_msg)


class ConcreteDelegatee(Delegatee):    
    def myMethod(self):
        return 'myMethod'

    def myMethod2(self):
        return 'myMethod2'


class Delegator(DelegatorParent):
    def __init__(self):
        self.delegatee = ConcreteDelegatee()
        DelegatorParent.__init__(self)
        for method in DelegatorParent.__abstractmethods__:
            self.delegatee.checkExistence(method)

    def myMethod(self, *args, **kw):
        return self.delegatee.myMethod(*args, **kw)

    def __getattr__(self, attrname):
        # Called only for attributes not defined by this class (or its bases).
        # Retrieve attribute from current behavior delegate class instance.
        return getattr(self.delegatee, attrname)



# if I forget to implement myMethod inside ConcreteDelegatee, 
# the following line will correctly raise an exception saying 
# that 'myMethod' is missing inside 'ConcreteDelegatee'.
a = Delegator() 

print a.myMethod() # correctly prints 'myMethod'

print a.myMethod2() #correctly prints 'myMethod2'
Pomade answered 15/10, 2017 at 22:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.