Define abstract read-write property to force getter and setter implementation

Viewed 318

Docs

Python docs (version 3.8):

The above example defines a read-only property; you can also define a read-write abstract property by appropriately marking one or more of the underlying methods as abstract:

class C(ABC):
    @property
    def x(self):
        ...

    @x.setter
    @abstractmethod
    def x(self, val):
        ...

If only some components are abstract, only those components need to be updated to create a concrete property in a subclass:

My Code

from abc import ABC, abstractmethod
class C(ABC):
    @property
    def x(self):
        ...

    @x.setter
    @abstractmethod
    def x(self, val):
        ...

class D(C):
    @property
    def x(self):
        pass

d = D()

Expected behaviour: Fails to instantiate because there are undefined abstract methods

Actual behaviour: Instantiates without error

Question

Why doesn't this fail to instantiate? How do I write this so it does fail to instantiate if no setter is implemented in the derived class. The docs seems to indicate that this should be a covered use-case for abstractmethod. The docs also provide an example where both the getter and the setter are abstract methods which is what I'm aiming for.

Reference: https://docs.python.org/3.8/library/abc.html#abc.abstractproperty

1 Answers

Fundamentally the issue is that the getter and the setter are just part of the same single class attribute. Within in the @property x you've got a fget, fset, and fdel which make up the getter, setter, and deleter (not necessarily all set). Those could be abstract and prevent the init, or just not exist.

In your class D you make a new @property which entirely overrides the parent @property so there is no longer any abstract setter to prevent initing the class.

from abc import ABC, abstractmethod
class C(ABC):
    @property
    def x(self):
        pass

    @x.setter
    @abstractmethod
    def x(self, val):
        pass

class D(C):
    @property
    def x(self):
        pass

def examine_property(p):
    print('get', p.fget, getattr(p.fget, '__isabstractmethod__', False) if p.fget is not None else None)
    print('set', p.fset, getattr(p.fset, '__isabstractmethod__', False) if p.fset is not None else None)
    print('del', p.fdel, getattr(p.fdel, '__isabstractmethod__', False) if p.fdel is not None else None)

print("C.x:")
examine_property(C.x)
print("D.x:")
examine_property(D.x)

Output:

C.x:
get <function C.x at 0x101ac4550> False
set <function C.x at 0x101ac45e0> True
del None None
D.x:
get <function D.x at 0x101ac4670> False
set None None
del None None

So, if you want to override the getter, you need to be very specific about it using @C.x.getter:

class D(C):
    @C.x.getter
    def x(self):
        print("non-abstract getter")

Output:

C.x:
get <function C.x at 0x1022fb550> False
set <function C.x at 0x1022fb5e0> True
del None None
D.x:
get <function D.x at 0x1022fb670> False
set <function C.x at 0x1022fb5e0> True
del None None

This way you don't override the whole property, just the specific function of it.

I wouldn't go mixing making new properties and overriding parents, I saw this finicky behaviour in my testing:

class D(C):
    @property
    def x(self):
        pass
    @C.x.setter
    def x(self, val):
        pass
print("D.x:")
examine_property(D.x)

Output:

D.x:
get <function C.x at 0x10cdd74c0> False
set <function D.x at 0x10cdd7670> False
del None None
# Our new override property is gone

Reordered class definition:

class D(C):
    @C.x.setter
    def x(self, val):
        pass
    @property
    def x(self):
        pass

Output:

D.x:
get <function D.x at 0x105048670> False
set None None
del None None
# We entirely lost the `setter`
Related