Search code examples
pythoninheritanceabstract-classpython-decorators

Abstract @property - instantiating a "partially implemented" class?


I read this very nice documentation on abstract class abc.ABC. It has this example (shortened by me for the purpose of this question):

import abc

class Base(abc.ABC):
    @property
    @abc.abstractmethod
    def value(self):
        return 'Should never reach here'

    @value.setter
    @abc.abstractmethod
    def value(self, new_value):
        return

class PartialImplementation(Base):  # setter not defined/overridden
    @property
    def value(self):
        return 'Read-only'

To my biggest surprise, PartialImplementation can be instantiated though it only overrides the getter:

>>> PartialImplementation()
<__main__.PartialImplementation at 0x7fadf4901f60>

Naively, I would have thought that since the interface has two abstract methods both would have to be overridden in any concrete class, which is what is written in the documentation: "Although a concrete class must provide implementations of all abstract methods,...". The resolution must be in that we actually have only one abstract name, value, that needs to be implemented and that does happen in PartialImplementation.

Can someone please explain this to me properly?

Also, why would you want to lift the setter to the interface if you are not required to implement it; current implementation does nothing if at all callable on a PartialImplementation instance.


Solution

  • TL;DR PartialImplementation is not partially implemented; you defined a new property with a concrete getter and a (logically) concrete setter, instead of supplying only a concrete getter.


    Abstract properties are tricky. Your base class has one property, which defines itself as abstract by the presence of the abstract getter and setter, rather than a property that you explicitly defined as abstract.

    ABCMeta determines if a class is abstract by looking for class attributes with a __isabstractmethod__ attribute set to True. Base.value is such an attribute. PartialImplementation.value is not, because its value attribute is a brand new property independent of Base.value, and it consists solely of a concrete getter.

    Instead, you need to create a new property that's based on the inherited property, using the appropriate method supplied by the inherited property.

    This class is still abstract, because @Base.value.getter creates a property with a concrete getter but retaining the original abstract setter.

    class PartialImplementation(Base):
        @Base.value.getter
        def value(self):
            return 'Read-only'
    

    This class is concrete and also supplies a read-only property.

    class PartialImplementation(Base):
        @Base.value.getter
        def value(self):
            return 'Read-only'
    
        @value.setter
        def value(self, v):
            # Following the example of the __set__ method
            # described in https://docs.python.org/3/howto/descriptor.html#properties
            raise AttributeError("property 'value' has no setter")
    

    Note that if we used Base.value.setter instead of value.setter, we would have create a new property that only added a concrete setter to the inherited property (which still only has an abstract getter). We want to add the setter to our new property with a concrete getter.

    (Also note that while value.setter can take None as an argument to "remove" an existing setter, it is not sufficient to override an abstract setter.)


    I highly recommend looking at the pure-Python implemeantion of property in the Descriptor Guide to see how properties work under the hood, and to understand why such care must be taken when trying to manipulate them.

    One thing it lacks, though, is the code that manipulates the __isabstractmethod__ attribute to make @property stackable with @abstractmethod. Essentially, it would set __isabstractmethod__ on itself if any of its initial components were abstract, and each of getter, setter, and deleter set the attribute to false if the last abstract component were replaced with a concrete one.