120

What's the best practice to define an abstract instance attribute, but not as a property?

I would like to write something like:

class AbstractFoo(metaclass=ABCMeta):

    @property
    @abstractmethod
    def bar(self):
        pass

class Foo(AbstractFoo):

    def __init__(self):
        self.bar = 3

Instead of:

class Foo(AbstractFoo):

    def __init__(self):
        self._bar = 3

    @property
    def bar(self):
        return self._bar

    @bar.setter
    def setbar(self, bar):
        self._bar = bar

    @bar.deleter
    def delbar(self):
        del self._bar

Properties are handy, but for simple attribute requiring no computation they are an overkill. This is especially important for abstract classes which will be subclassed and implemented by the user (I don't want to force someone to use @property when he just could have written self.foo = foo in the __init__).

Abstract attributes in Python question proposes as only answer to use @property and @abstractmethod: it doesn't answer my question.

The ActiveState recipe for an abstract class attribute via AbstractAttribute may be the right way, but I am not sure. It also only works with class attributes and not instance attributes.

Javier Montón
  • 4,601
  • 3
  • 21
  • 29
Lapinot
  • 1,305
  • 2
  • 9
  • 11
  • Why do you need to force someone to have a specific attribute on their class? – Philip Adler May 23 '14 at 14:29
  • 27
    Isn't it the whole thing of ABC? If you want my concrete example, I want people to write a class for their sensor and the class should have a self.port attribute. – Lapinot May 23 '14 at 14:49
  • Upon reflection, yes, I suppose it is; though I think that this rather flies in the face of ducktyping... – Philip Adler May 23 '14 at 14:58
  • Maybe I am just asking for too many complications, but it would bother me not to use ABC when doing abstract classes (I think I am just going to use normal base class)... – Lapinot May 23 '14 at 15:14
  • anentropics's solution is simple and works well. Why is it not the accepted answer? – Dave Kielpinski May 01 '17 at 22:38
  • @DaveKielpinski this question is old and i got disinterested in this kind of ABC shenanigans (when i want to get code statically typed i write ocaml or haskell, python has a lot more problems with ABC then just the issue that i raised here)... Also, i don't remember why but I stated in the second last sentence that i knew this answer but that it wasn't what i wanted. – Lapinot May 03 '17 at 10:14

8 Answers8

76

A possibly a bit better solution compared to the accepted answer:

from better_abc import ABCMeta, abstract_attribute    # see below

class AbstractFoo(metaclass=ABCMeta):

    @abstract_attribute
    def bar(self):
        pass

class Foo(AbstractFoo):
    def __init__(self):
        self.bar = 3

class BadFoo(AbstractFoo):
    def __init__(self):
        pass

It will behave like this:

Foo()     # ok
BadFoo()  # will raise: NotImplementedError: Can't instantiate abstract class BadFoo
# with abstract attributes: bar

This answer uses same approach as the accepted answer, but integrates well with built-in ABC and does not require boilerplate of check_bar() helpers.

Here is the better_abc.py content:

from abc import ABCMeta as NativeABCMeta

class DummyAttribute:
    pass

def abstract_attribute(obj=None):
    if obj is None:
        obj = DummyAttribute()
    obj.__is_abstract_attribute__ = True
    return obj


class ABCMeta(NativeABCMeta):

    def __call__(cls, *args, **kwargs):
        instance = NativeABCMeta.__call__(cls, *args, **kwargs)
        abstract_attributes = {
            name
            for name in dir(instance)
            if getattr(getattr(instance, name), '__is_abstract_attribute__', False)
        }
        if abstract_attributes:
            raise NotImplementedError(
                "Can't instantiate abstract class {} with"
                " abstract attributes: {}".format(
                    cls.__name__,
                    ', '.join(abstract_attributes)
                )
            )
        return instance

The nice thing is that you can do:

class AbstractFoo(metaclass=ABCMeta):
    bar = abstract_attribute()

and it will work same as above.

Also one can use:

class ABC(ABCMeta):
    pass

to define custom ABC helper. PS. I consider this code to be CC0.

This could be improved by using AST parser to raise earlier (on class declaration) by scanning the __init__ code, but it seems to be an overkill for now (unless someone is willing to implement).

2021: typing support

You can use:

from typing import cast, Any, Callable, TypeVar


R = TypeVar('R')


def abstract_attribute(obj: Callable[[Any], R] = None) -> R:
    _obj = cast(Any, obj)
    if obj is None:
        _obj = DummyAttribute()
    _obj.__is_abstract_attribute__ = True
    return cast(R, _obj)

which will let mypy highlight some typing issues

class AbstractFooTyped(metaclass=ABCMeta):

    @abstract_attribute
    def bar(self) -> int:
        pass


class FooTyped(AbstractFooTyped):
    def __init__(self):
        # skipping assignment (which is required!) to demonstrate
        # that it works independent of when the assignment is made
        pass


f_typed = FooTyped()
_ = f_typed.bar + 'test'   # Mypy: Unsupported operand types for + ("int" and "str")


FooTyped.bar = 'test'    # Mypy: Incompatible types in assignment (expression has type "str", variable has type "int")
FooTyped.bar + 'test'    # Mypy: Unsupported operand types for + ("int" and "str")

and for the shorthand notation, as suggested by @SMiller in the comments:

class AbstractFooTypedShorthand(metaclass=ABCMeta):
    bar: int = abstract_attribute()


AbstractFooTypedShorthand.bar += 'test'   # Mypy: Unsupported operand types for + ("int" and "str")
krassowski
  • 13,598
  • 4
  • 60
  • 92
  • 1
    Pretty neat, this is indeed the kind of solution i was suspecting to exist (i think i'm gonna change the accepted answer). Still with the hindsight, the syntax of declaration is just really weird. On the long run what works in python is duck-typing: ABC and annotations will just make the code look like poor java (syntax and structure). – Lapinot May 18 '18 at 02:12
  • 4
    I wish I could upvote this more. Hopefully this solution will become integrated into the standard library at some point. Thank you. – BCR Feb 21 '19 at 16:37
  • Is there any reason why you created the `DummyAttribute` instead of just setting `obj = object()` in the `abstract_attribute` decorator? – Andy Perez Jul 11 '19 at 17:06
  • 1
    @AndyPerez, it might be easier to debug if you have an object of named class, not a plain object, but I think that you could easily change it to `obj = object()` in your code if you prefer that. – krassowski Jul 11 '19 at 20:41
  • Thanks, @krassowski. Follow on question: I tried implementing this in an ABC, but I'm having problems with it because I am overriding the `__setattr__` to perform a validation on the supplied value before setting the attribute. It keeps throwing an error saying that the attribute is "read-only". Is there a way to get around this? – Andy Perez Jul 12 '19 at 19:52
  • Never mind. I figured it out. The problem was that it didn't like my `__slots__` attribute in the ABC. – Andy Perez Jul 16 '19 at 22:17
  • one question if `bar` would be a constant attribute, is it also fine to define it in this way? Is it okay to write it's name as `BAR` then, like the constant convention says, even though it's a decorated function? – tikej Apr 11 '20 at 21:30
  • 2
    i've gone ahead and packaged this code snippet: `pip install better-abc`. If you'd like to contribute some tests or additional features please open a pull request [here](https://github.com/shonin/better-abc) – rykener Nov 10 '20 at 20:11
  • Just a note, this solution will execute any properties and descriptors that you have specified on your class. So if you've hidden an expensive calculation behind a property, you'll need to try something else. – rwhitt2049 Nov 12 '20 at 03:55
  • What is the fate of this idea? Is it OK with Python 3.8 and 3.9? It seems too good to be true. Are there objections from core Python developers? – pauljohn32 Jun 24 '21 at 15:28
  • 3
    In case anyone else is frustrated that this elegant solution breaks pylint type hinting, you can go further and set `bar = abstract_attribute()` to `bar: int = abstract_attribute()` instead. Probably common sense, but took me a couple hours to realize this. – SMiller Sep 28 '21 at 20:10
  • 2
    That's a good remark @SMiller, thank you! I updated the answer with a draft of what would be needed for typing support (it may not cover all the edge cases, but I hope it's helpful). – krassowski Sep 29 '21 at 12:42
  • While quickly skimming through this answer, I got the wrong impression that this was part of the standard ABC. The sentence `As of 2018...` is somewhat misleading in that regard, a bit of clarity at the start of this post probably wouldn't hurt. – 303 Oct 23 '21 at 21:07
  • @303 there is no `As of 2018 ...` sentence though. There is a sentence expressing my disappointment with not having this feature in standard ABC three years ago. I will remove it though, as it is outdated indeed. – krassowski Oct 23 '21 at 21:10
39

Just because you define it as an abstractproperty on the abstract base class doesn't mean you have to make a property on the subclass.

e.g. you can:

In [1]: from abc import ABCMeta, abstractproperty

In [2]: class X(metaclass=ABCMeta):
   ...:     @abstractproperty
   ...:     def required(self):
   ...:         raise NotImplementedError
   ...:

In [3]: class Y(X):
   ...:     required = True
   ...:

In [4]: Y()
Out[4]: <__main__.Y at 0x10ae0d390>

If you want to initialise the value in __init__ you can do this:

In [5]: class Z(X):
   ...:     required = None
   ...:     def __init__(self, value):
   ...:         self.required = value
   ...:

In [6]: Z(value=3)
Out[6]: <__main__.Z at 0x10ae15a20>

Since Python 3.3 abstractproperty is deprecated. So Python 3 users should use the following instead:

from abc import ABCMeta, abstractmethod

class X(metaclass=ABCMeta):
    @property
    @abstractmethod
    def required(self):
        raise NotImplementedError
Anentropic
  • 32,188
  • 12
  • 99
  • 147
  • it's a shame that the ABC checking doesn't occur at the initialisation of the instance rather than the class definition, that way abstractattribute would be possible. that would allow us to have an abstractattribute_or_property - which is what the op wants – chris Jun 28 '17 at 19:45
  • adding an attr in __init__ doesn't work for me. python3.6 – zhukovgreen Nov 23 '17 at 05:21
  • 1
    @ArtemZhukov the example above is a Python 3.6 session in IPython, it works. You need the `required = None` in class body as well though, as shown above. – Anentropic Nov 23 '17 at 10:06
  • 1
    @chris, see my https://stackoverflow.com/a/50381071/6646912 for an ABC subclass which does the checking at initialisation. – krassowski Oct 30 '18 at 15:11
  • @krassowski that doesn't achieve anything more than the `@abstractproperty` definitions above, but is worse because it doesn't handle attributes defined in class body rather than `__init__` – Anentropic Oct 30 '18 at 16:32
  • IMO, this is the right answer. The one trip-up, as James Irwin pointed out in another answer, is that trying to override the attribute in `init()` instead of at the class level results in `AttributeError: can't set attribute`. The workaround is to do override it with a dummy value (`None`) at the class level and do `self.__dict__['required'] = something_dynamic()` in `__init__()`. – mikenerone Feb 26 '19 at 18:38
  • @CryingCyclops not if you have already defined the attribute at class level (as shown in the last example above), this is part of the definition of an abstract property/attr/method – Anentropic Feb 26 '19 at 18:42
  • @Anentropic Oh, you're right. If you've done the class level override I stated, you don't need the `__dict__` trick. – mikenerone Feb 27 '19 at 23:04
33

If you really want to enforce that a subclass define a given attribute, you can use metaclasses:

 class AbstractFooMeta(type):
 
     def __call__(cls, *args, **kwargs):
         """Called when you call Foo(*args, **kwargs) """
         obj = type.__call__(cls, *args, **kwargs)
         obj.check_bar()
         return obj
     
     
 class AbstractFoo(object):
     __metaclass__ = AbstractFooMeta
     bar = None
 
     def check_bar(self):
         if self.bar is None:
             raise NotImplementedError('Subclasses must define bar')
 
 
 class GoodFoo(AbstractFoo):
     def __init__(self):
         self.bar = 3
 
 
 class BadFoo(AbstractFoo):
     def __init__(self):
         pass

Basically the meta class redefine __call__ to make sure check_bar is called after the init on an instance.

GoodFoo()  # ok
BadFoo ()  # yield NotImplementedError
Seb D.
  • 5,046
  • 1
  • 28
  • 36
9

As Anentropic said, you don't have to implement an abstractproperty as another property.

However, one thing all answers seem to neglect is Python's member slots (the __slots__ class attribute). Users of your ABCs required to implement abstract properties could simply define them within __slots__ if all that's needed is a data attribute.

So with something like,

class AbstractFoo(abc.ABC):
    __slots__ = ()

    bar = abc.abstractproperty()

Users can define sub-classes simply like,

class Foo(AbstractFoo):
    __slots__ = 'bar',  # the only requirement

    # define Foo as desired

    def __init__(self):
        self.bar = ...

Here, Foo.bar behaves like a regular instance attribute, which it is, just implemented differently. This is simple, efficient, and avoids the @property boilerplate that you described.

This works whether or not ABCs define __slots__ at their class' bodies. However, going with __slots__ all the way not only saves memory and provides faster attribute accesses but also gives a meaningful descriptor instead of having intermediates (e.g. bar = None or similar) in sub-classes.1

A few answers suggest doing the "abstract" attribute check after instantiation (i.e. at the meta-class __call__() method) but I find that not only wasteful but also potentially inefficient as the initialization step could be a time-consuming one.

In short, what's required for sub-classes of ABCs is to override the relevant descriptor (be it a property or a method), it doesn't matter how, and documenting to your users that it's possible to use __slots__ as implementation for abstract properties seems to me as the more adequate approach.


1 In any case, at the very least, ABCs should always define an empty __slots__ class attribute because otherwise sub-classes are forced to have __dict__ (dynamic attribute access) and __weakref__ (weak reference support) when instantiated. See the abc or collections.abc modules for examples of this being the case within the standard library.

Edward Elrick
  • 121
  • 1
  • 6
8

The problem isn't what, but when:

from abc import ABCMeta, abstractmethod

class AbstractFoo(metaclass=ABCMeta):
    @abstractmethod
    def bar():
        pass

class Foo(AbstractFoo):
    bar = object()

isinstance(Foo(), AbstractFoo)
#>>> True

It doesn't matter that bar isn't a method! The problem is that __subclasshook__, the method of doing the check, is a classmethod, so only cares whether the class, not the instance, has the attribute.


I suggest you just don't force this, as it's a hard problem. The alternative is forcing them to predefine the attribute, but that just leaves around dummy attributes that just silence errors.

Veedrac
  • 58,273
  • 15
  • 112
  • 169
  • Yes, this is what I was doing since now (but it's weird to define it as an abstract **method**). But it doesn't solve the question of instance attribute. – Lapinot May 23 '14 at 15:11
  • I think [this](http://stackoverflow.com/a/32536493/2943985) is an even better (and more pythonic) solution. – kawing-chiu Jun 15 '16 at 02:48
0

I've searched around for this for awhile but didn't see anything I like. As you probably know if you do:

class AbstractFoo(object):
    @property
    def bar(self):
        raise NotImplementedError(
                "Subclasses of AbstractFoo must set an instance attribute "
                "self._bar in it's __init__ method")

class Foo(AbstractFoo):
    def __init__(self):
        self.bar = "bar"

f = Foo()

You get an AttributeError: can't set attribute which is annoying.

To get around this you can do:

class AbstractFoo(object):

    @property
    def bar(self):
        try:
            return self._bar
        except AttributeError:
            raise NotImplementedError(
                "Subclasses of AbstractFoo must set an instance attribute "
                "self._bar in it's __init__ method")

class OkFoo(AbstractFoo):
    def __init__(self):
        self._bar = 3

class BadFoo(AbstractFoo):
    pass

a = OkFoo()
b = BadFoo()
print a.bar
print b.bar  # raises a NotImplementedError

This avoids the AttributeError: can't set attribute but if you just leave off the abstract property all together:

class AbstractFoo(object):
    pass

class Foo(AbstractFoo):
    pass

f = Foo()
f.bar

You get an AttributeError: 'Foo' object has no attribute 'bar' which is arguably almost as good as the NotImplementedError. So really my solution is just trading one error message from another .. and you have to use self._bar rather than self.bar in the init.

James Irwin
  • 419
  • 5
  • 4
  • First I want to mention that despite naming your class `AbstractFoo`, you didn't actually make your class abstract, and this answer is completely unrelated to the context set out by OP. That said, your workaround for regular properties' "unsettable attribute" problem may be kindof expensive, depending on how often the attribute is accessed. A cheaper one (based on your first code block) is this in your `Foo.__init__()`: `self.__dict__['bar'] = "bar"` – mikenerone Feb 26 '19 at 18:46
0

Following https://docs.python.org/2/library/abc.html you could do something like this in Python 2.7:

from abc import ABCMeta, abstractproperty


class Test(object):
    __metaclass__ = ABCMeta

    @abstractproperty
    def test(self): yield None

    def get_test(self):
        return self.test


class TestChild(Test):

    test = None

    def __init__(self, var):
        self.test = var


a = TestChild('test')
print(a.get_test())
Javier Montón
  • 4,601
  • 3
  • 21
  • 29
0

Here's a simple approach that satisfies "subclasses of AbstractFoo must define bar or an error will be raised":

from abc import ABC

class AbstractFoo(ABC):
    bar: int

    def __init__(bar: int)
        self.bar = bar


class Foo(AbstractFoo):
    def __init__(self):
        super().__init__(bar=3)

The idea is to simply make bar a required input argument to the constructor for AbstractFoo. children of AbstractFoo, such as Foo, must pass bar into AbstractFoo.__init__() or a TypeError will be raised on a missing argument. I think the main issue is that the exceptions and warning will not be oriented around "abstract" classes but are rather standard python exceptions.

Jagerber48
  • 488
  • 4
  • 13