How to create abstract properties in python abstract classes?
Asked Answered
W

7

259

In the following code, I create a base abstract class Base. I want all the classes that inherit from Base to provide the name property, so I made this property an @abstractmethod.

Then I created a subclass of Base, called Base_1, which is meant to supply some functionality, but still remain abstract. There is no name property in Base_1, but nevertheless python instatinates an object of that class without an error. How does one create abstract properties?

from abc import ABCMeta, abstractmethod

class Base(object):
# class Base(metaclass = ABCMeta): <- Python 3
    __metaclass__ = ABCMeta
    def __init__(self, str_dir_config):
        self.str_dir_config = str_dir_config
    
    @abstractmethod
    def _do_stuff(self, signals):
        pass
    
    @property    
    @abstractmethod
    def name(self):
        """This property will be supplied by the inheriting classes
        individually.
        """
        pass
    

class Base1(Base):
    __metaclass__ = ABCMeta
    """This class does not provide the name property and should
    raise an error.
    """
    def __init__(self, str_dir_config):
        super(Base1, self).__init__(str_dir_config)
        # super().__init__(str_dir_config) <- Python 3
    
    def _do_stuff(self, signals):
        print "Base_1 does stuff"
        # print("Base_1 does stuff") <- Python 3

class C(Base1):
    @property
    def name(self):
        return "class C"
    

if __name__ == "__main__":
    b1 = Base1("abc")
Wisteria answered 11/5, 2011 at 6:49 Comment(1)
Gotcha: If you forget to use decorator @property in class C, name will revert to a method.Orthorhombic
T
326

Since Python 3.3 a bug was fixed meaning the property() decorator is now correctly identified as abstract when applied to an abstract method.

Note: Order matters, you have to use @property above @abstractmethod

Python 3.3+: (python docs):

from abc import ABC, abstractmethod

class C(ABC):
    @property
    @abstractmethod
    def my_abstract_property(self):
        ...

Python 2: (python docs)

from abc import ABCMeta, abstractproperty

class C:
    __metaclass__ = ABCMeta

    @abstractproperty
    def my_abstract_property(self):
        ...
Tarr answered 9/2, 2018 at 16:23 Comment(8)
@Tarr How to make it compatible for python 2 and as well?Vaccine
@Tarr actually I meant for both but nevermind I posted a answer based on your solutionVaccine
i don't think python checks that the implementation actually has @property decorator, it just checks that a method with the name my_abstract_property is created.Fade
@James, does this work with functools.cached_property?Spae
I don't get it. OP is asking about having a property of a base class named name, which has to be implemented by all child classes, but the answers here are implementing abstract functions. Even the abc docs show only functions being abstract. Isn't there a way to make class properties/variables abstract in a way that child classes have to implement them? I need it to have an id variable which all child classes should implement.Baize
@Baize perhaps you want an attribute and not a property?Billbillabong
Chaining classmethod descriptors is now deprecated, see docs.python.org/3/whatsnew/3.11.html It can no longer be used to wrap other descriptors such as property. The core design of this feature was flawed and caused a number of downstream problems. To “pass-through” a classmethod, consider using the __wrapped__ attribute that was added in Python 3.10Bidding
How do I require that the attribute has to be a specific type, e.g. str?Erikaerikson
T
58

Until Python 3.3, you cannot nest @abstractmethod and @property.

Use @abstractproperty to create abstract properties (docs).

from abc import ABCMeta, abstractmethod, abstractproperty

class Base(object):
    # ...
    @abstractproperty
    def name(self):
        pass

The code now raises the correct exception:

Traceback (most recent call last):
  File "foo.py", line 36, in 
    b1 = Base_1('abc')  
TypeError: Can't instantiate abstract class Base_1 with abstract methods name
Threesome answered 11/5, 2011 at 7:3 Comment(5)
actually this answer is wrong for younger pythons: since 3.3, @abstractproperty is deprecated in favor of a combination like OP’s.Pindling
From the 3.3 docs: docs.python.org/3/library/abc.html#abc.abstractpropertyThreesome
@abstractproperty in the Python 2 docsTurnbuckle
so until 3.3, just raise NotImplementedErrorBabbling
can we inherit from object and still use the ABC annotations? Will it work as shown in the example?Bogor
V
10

Based on James answer above

def compatibleabstractproperty(func):

    if sys.version_info > (3, 3):             
        return property(abstractmethod(func))
    else:
        return abstractproperty(func)

and use it as a decorator

@compatibleabstractproperty
def env(self):
    raise NotImplementedError()
Vaccine answered 12/7, 2019 at 11:53 Comment(1)
eeesh! what a mess!Emalia
F
8

For example, you can define the abstract getter, setter and deleter with @abstractmethod and @property, @name.setter or @name.deleter in Person abstract class as shown below. *@abstractmethod must be the innermost decorator otherwise error occurs:

from abc import ABC, abstractmethod

class Person(ABC):

    @property
    @abstractmethod # The innermost decorator
    def name(self): # Abstract getter
        pass

    @name.setter
    @abstractmethod # The innermost decorator
    def name(self, name): # Abstract setter
        pass

    @name.deleter
    @abstractmethod # The innermost decorator
    def name(self): # Abstract deleter
        pass

Then, you can extend Person abstract class with Student class, override the abstract getter, setter and deleter in Student class, instantiate Student class and call the getter, setter and deleter as shown below:

class Student(Person):

    def __init__(self, name):
        self._name = name
    
    @property
    def name(self): # Overrides abstract getter
        return self._name
    
    @name.setter
    def name(self, name): # Overrides abstract setter
        self._name = name
    
    @name.deleter
    def name(self): # Overrides abstract deleter 
        del self._name

obj = Student("John") # Instantiates "Student" class
print(obj.name) # Getter
obj.name = "Tom" # Setter
print(obj.name) # Getter
del obj.name # Deleter
print(hasattr(obj, "name"))

Output:

John
Tom
False

Actually, even if you don't override the abstract setter and deleter in Student class and instantiate Student class as shown below:

class Student(Person): # Extends "Person" class
    
    def __init__(self, name):
        self._name = name
    
    @property
    def name(self): # Overrides only abstract getter
        return self._name

    # @name.setter
    # def name(self, name): # Overrides abstract setter
    #     self._name = name
    
    # @name.deleter
    # def name(self): # Overrides abstract deleter 
    #     del self._name

obj = Student("John") # Instantiates "Student" class
# ...

No error occurs as shown below:

John
Tom
False

But, if you don't override the abstract getter, setter and deleter in Student class and instantiate Student class as shown below:

class Student(Person): # Extends "Person" class
    
    def __init__(self, name):
        self._name = name
    
    # @property
    # def name(self): # Overrides only abstract getter
    #     return self._name

    # @name.setter
    # def name(self, name): # Overrides abstract setter
    #     self._name = name
    
    # @name.deleter
    # def name(self): # Overrides abstract deleter 
    #     del self._name

obj = Student("John") # Instantiates "Student" class
# ...

The error below occurs:

TypeError: Can't instantiate abstract class Student with abstract methods name

And, if you don't override the abstract getter in Student class and instantiate Student class as shown below:

class Student(Person): # Extends "Person" class
    
    def __init__(self, name):
        self._name = name
    
    # @property
    # def name(self): # Overrides only abstract getter
    #     return self._name

    @name.setter
    def name(self, name): # Overrides abstract setter
        self._name = name
    
    @name.deleter
    def name(self): # Overrides abstract deleter 
        del self._name

obj = Student("John") # Instantiates "Student" class
# ...

The error below occurs:

NameError: name 'name' is not defined

And, if @abstractmethod is not the innermost decorator as shown below:

from abc import ABC, abstractmethod

class Person(ABC):

    @abstractmethod # Not the innermost decorator
    @property
    def name(self): # Abstract getter
        pass

    @name.setter
    @abstractmethod # The innermost decorator
    def name(self, name): # Abstract setter
        pass

    @name.deleter
    @abstractmethod # The innermost decorator
    def name(self): # Abstract deleter
        pass

The error below occurs:

AttributeError: attribute 'isabstractmethod' of 'property' objects is not writable

Firman answered 22/11, 2022 at 14:26 Comment(1)
How to annotate this? If I use --> str on the property, it does not complain if I make it an int in a lower class.Markus
C
6

Using the @property decorator in the abstract class (as recommended in the answer by James) works if you want the required instance level attributes to use the property decorator as well.

If you don't want to use the property decorator, you can use super(). I ended up using something like the __post_init__() from dataclasses and it gets the desired functionality for instance level attributes:

import abc
from typing import List

class Abstract(abc.ABC):
    """An ABC with required attributes.

    Attributes:
        attr0
        attr1 
    """

    @abc.abstractmethod
    def __init__(self):
        """Forces you to implement __init__ in 'Concrete'. 
        Make sure to call __post_init__() from inside 'Concrete'."""

    def __post_init__(self):
        self._has_required_attributes()
        # You can also type check here if you want.

    def _has_required_attributes(self):
        req_attrs: List[str] = ['attr0', 'attr1']
        for attr in req_attrs:
            if not hasattr(self, attr):
                raise AttributeError(f"Missing attribute: '{attr}'")

class Concrete(Abstract):

    def __init__(self, attr0, attr1):
        self.attr0 = attr0
        self.attr1 = attr1
        self.attr2 = "some value" # not required
        super().__post_init__() # Enforces the attribute requirement.
Civilize answered 15/8, 2021 at 18:37 Comment(0)
M
6

In python 3.6+, you can also anotate a variable without providing a default. I find this to be a more concise way to make it abstract.

class Base():
    name: str
    
    def print_name(self):
        print(self.name)  # will raise an Attribute error at runtime if `name` isn't defined in subclass

class Base_1(Base):
    name = "base one"

it may also be used to force you to initialize the variable in the __new__ or __init__ methods

As another example, the following code will fail when you try to initialize the Base_1 class

    class Base():
        name: str

        def __init__(self):
            self.print_name()

    class Base_1(Base):
        _nemo = "base one"
    
    b = Base_1() 

AttributeError: 'Base_1' object has no attribute 'name'

Moresque answered 7/3, 2022 at 6:33 Comment(4)
Didn't work for me. Python 3.9.6. Perhaps you missed something?Baize
well, something that might not be clear from my response is that you'll only get the error when you try to access the missing attribute. so you could easily check that in the __init__ method for example. I'll update to my answer to demonstrate that. (by the way, I tested it with Python 3.8.6)Moresque
Maybe this answer is useful, but it's not what the OP question is asking about. You are defining an attribute, not a property.Sporting
This solution might even be dangerous. If you define it this way, you can’t expect that people actually implement it. And if you want to use it later on it might crash, while the linter thinks that this attribute exists. So it has no advantage defining it in the Base class, only disadvantages.Straka
E
0

Another possible solution is to use metaclasses.

A minimal example can look like this:

class BaseMetaClass(type):
    def __new__(mcls, class_name, bases, attrs):
        required_attrs = ('foo', 'bar')
        for attr in required_attrs:
            if not attr in attrs:
                raise RunTimeError(f"You need to set {attr} in {class_name}")
        return super().__new__(mcls, class_name, bases, attrs)


class Base(metaclass=BaseMeta):
    foo: str
    bar: int

One advantage of this approach is that the check will happen at definition time (not instantiation).

Also, setting class attributes in child classes is a bit easier than declaring properties (as long as they are simple values known in advance) and your final classes will look more concise

Electrophorus answered 27/11, 2022 at 10:42 Comment(1)
I like this direction, but I want it to automatically require all attributes that Base is going to get. Any subclass like SubclassBase(Base) should then be forced to have these attributes.Markus

© 2022 - 2024 — McMap. All rights reserved.