Pieter
Pieter

Reputation: 13

Creating property with abstract getter doesn't work as expected

I'm trying to create a property on a abstract class. Instead of stacking @property on top of @abstractmethod on top of the getter, I'm trying to create the property in the abstract base class manually with the getter as input, which I've decorated with @abstractmethod.

With the former approach you need to decorate the getter with @property again in the subclass. Whereas the latter approach is slightly nicer for me since now the subclass only needs to define a getter method and accessing the property should work. However, it doesn't seem to work as expected.

import abc

class Foo(abc.ABC):
    @abc.abstractmethod
    def _get_bar(self) -> bool:
        ...

    # not preferred.
    # @property
    # @abc.abstractmethod
    # def BAR(self) -> bool:
    #     ...

    # works!
    # @property
    # def BAR(self) -> bool:
    #     return self._get_bar()

    # doesn't work!
    BAR = property(_get_bar)

class FooBar(Foo):
    # not preferred.
    # @property
    # def BAR(self) -> bool:
    #     ...

    def _get_bar(self) -> bool:
        return True

print(FooBar().BAR)  # TypeError: Can't instantiate abstract class FooBar with abstract methods BAR

I'm basing this off what is written in the docs about @abc.abstractmethod, the last bit of the code snippet. Or at least I think I am. But it looks like I'm doing something wrong or not understanding properties that well.

Upvotes: 1

Views: 91

Answers (1)

jsbueno
jsbueno

Reputation: 110506

The difference is that this code

BAR = property(_get_bar)

is executed only once when the body of your base class (Foo) is executed - the _getbar in this expression refers to the method defined in this class (Foo), which is abstract and empty.

While the form

@property
def BAR(self) -> bool:
    return self._get_bar()

encapsulates the reference to _get_bar inside a method (BAR), which will receive the instance as a parameter, and then retrieve the method from whatever subclass from Foo the instance (self) happens to be.

The way to fix that is to wrap the property argument in the function call form as a lambda, so that the method is retrieved from the appropriate class on call time:

BAR = property(lambda self: self._get_bar())

Upvotes: 1

Related Questions