Moakamune
Moakamune

Reputation: 51

Most elegant way to define simple abstract attributes

I'm trying to implement an abstract class with attributes and I can't get how to define it simply.

I just want to define the attribute name to constrain child classes to have it but I don't want to copy/paste getters & setters in every classes that inherit my abstract class. Here are solutions I found but not very elegant in my opinion:

class AbstractC(ABC):

   @property
   @abstractmethod
   def a(self):
       pass

class ConcreteC1(AbstractC):
   def __init__(self, name):
       self.a = name

   def a(self):
       pass

class ConcreteC2(AbstractC):
   def __init__(self, name):
       self.a = name

class ConcreteC3(AbstractC):

   def __init__(self, name):
       self.poney = name

ConcreteC1('foobar') # ok
ConcreteC2('foobar') # error !
ConcreteC3('foobar') # error !
class AbstractC(ABC):

   @property
   @abstractmethod
   def a(self):
       pass

class ConcreteC1(AbstractC):
   a = None

   def __init__(self, name):
       self.a = name

class ConcreteC2(AbstractC):
   def __init__(self, name):
       self.a = name

class ConcreteC3(AbstractC):
   def __init__(self, name):
       self.poney = name

ConcreteC1('foobar') # ok
ConcreteC2('foobar') # error !
ConcreteC3('foobar') # error !
class AbstractC(ABC):

   @abstractmethod
   def __init__(self, val):
       self.a = val


class ConcreteC1(AbstractC):
   def __init__(self, name):
       self.a = name

class ConcreteC2(AbstractC): 
   def __init__(self, name):
       self.poney = name

ConcreteC1('foobar') # ok
ConcreteC2('foobar') # no error !

So is there a way to get an elegant, robust and compact abstract class with abstract attribute ? Or am I trying to get something impossible ? I was thinking about something close to that :

class AbstractC(ABC):

   @property
   @abstractmethod
   def a(self):
       pass

class ConcreteC(AbstractC):
   def __init__(self, name):
       self.a = name

If there is no such solution, what is the best one ?

Upvotes: 3

Views: 711

Answers (3)

Moakamune
Moakamune

Reputation: 51

There's still a problem. If i choose the implementation that raise an error, i have to add @property to the method or i can call ConcreteC().a even if a is not set and it will not raise the error:

class AbstractC(ABC):

    def a(self):
        raise NotImplementedError('Implement _a_ method')

class ConcreteC(AbstractC):
    def __init__(self, val):
        super().__init__()
        self.poney = val

In [3]: ConcreteC('foobar').a
Out[3]: <bound method AbstractC.a of <__main__.ConcreteC object at 0x7f2e1c6b0518>>

But if i add @property i get an error :

class AbstractC(ABC):

    @property
    def a(self):
        raise NotImplementedError('Implement _a_ method')

class ConcreteC(AbstractC):
    def __init__(self, val):
        super().__init__()
        self.a = val

In [4]: ConcreteC('foobar')
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-55-587237cb76e5> in <module>
----> 1 ConcreteC('foobar')

~/workspace/draft.py in __init__(self, val)
    151     def __init__(self, val):
    152         super().__init__()
--> 153         self.a = val
    154 
    155 

AttributeError: can't set attribute

EDIT:

Here the solution I chose:

class AbstractC(ABC):

    @property
    def a(self):
        try:
            return self._a
        except AttributeError:
            raise NotImplementedError('Implement _a_ method')

    @a.setter
    def a(self, val):
        self._a = val


class ConcreteC(AbstractC):
    def __init__(self, val):
        self.a = val

This way I can edit 'a' very simply and if it's not definied, an exception is raised on get. I didn't know that to make a setter work, it must has the same name as the property. In the end, what I wanted isn't an abstract attribute, but a concrete one in an abstract class.

In [1]: c = ConcreteC('foobar')

In [2]: c.a
Out[2]: 'foobar'

In [3]: c.a  = 'poney'

In [4]: c.a
Out[4]: 'poney'

Upvotes: 1

Victor Ruiz
Victor Ruiz

Reputation: 1252

Maybe this will help. I made a class which inherits from ABC. It defines the method __init_subclass__ that is invoked after a new subclass is created. It does the next:
For each abstract property declared, search the same method in the subclass. If it exists (its a function object) convert it to a property and replace it in the subclass dictionary.

from abc import ABC, abstractmethod

class Foo(ABC):
    def __init_subclass__(cls):
        super().__init_subclass__()

        ###### This is the new part. I explain it at the end of the answer
        for name, value in attrs.items():
            if name not in cls.__dict__:
                setattr(cls, name, property(lambda *args, **kwargs: value))
        ######

        # Iterate throught all abstract methods on the class
        for name in Foo.__abstractmethods__:
            absmethod = Foo.__dict__[name]
            # Check if the abstract method is a property
            if not isinstance(absmethod, property):
                continue

            # Check if there is a method defined in the subclass with the same name
            if name not in cls.__dict__ or not callable(cls.__dict__[name]):
                continue
            method = cls.__dict__[name]

            # If the method is not already a property, we decorate it automatically...
            if not isinstance(method, property):
                setattr(cls, name, property(method))

    @property
    @abstractmethod
    def a(self):
        return 1

Now define a subclass and test it:


class Bar(Foo):
    def __init__(self):
        pass

    def a(self):
        return 2

    @property
    def b(self):
        return 3

obj = Bar()
print(obj.a)
print(obj.b)

Output will be:

2
3

The next code will raise an error, because not all abstract methods are implemented:

class Qux(Foo):
    pass

EDIT: Now you can also do:

class Bar(Foo, a=1):
    pass

print(Bar().a) # 1

Upvotes: 1

Drey
Drey

Reputation: 3364

You could misuse namedtuples for fancy inheritance

from collections import namedtuple

BaseAttributes = namedtuple('base', ['attr1', 'attr2'])

print(BaseAttributes('one', 2))

class SomethingElse(BaseAttributes):

    def method(self):
        return 3


blubb = SomethingElse('A', 5)
blubb.method()

but imho your last proposal(s) makes sense if you raise NotImplementedError, e.g.:

class AbstractC(ABC):

   def a(self):
       raise NotImplementedError('Implement _a_ method')

class ConcreteC(AbstractC):
   def __init__(self, name, *args, **kwargs):
       super().__init__(*args, **kwargs)
       self.a = name

Upvotes: 2

Related Questions