6

I'm trying to create a base class with a number of abstract python properties, in python 3.7.

I tried it one way (see 'start' below) using the @property, @abstractmethod, @property.setter annotations. This worked but it doesn't raise an exception if the subclass doesn't implement a setter. That's the point of using @abstract to me, so that's no good.

So I tried doing it another way (see 'end' below) using two @abstractmethod methods and a 'property()', which is not abstract itself but uses those methods. This approach generates an error when instantiating the subclass:

 # {TypeError}Can't instantiate abstract class FirstStep with abstract methods end 

I'm clearly implementing the abstract methods, so I don't understand what it means. The 'end' property is not marked @abstract, but if I comment it out, it does run (but I don't get my property). I also added that test non-abstract method 'test_elapsed_time' to demonstrate I have the class structure and abstraction right (it works).

Any chance I'm doing something dumb, or is there some special behavior around property() that's causing this?

class ParentTask(Task): def get_first_step(self): # {TypeError}Can't instantiate abstract class FirstStep with abstract methods end return FirstStep(self) class Step(ABC): # __metaclass__ = ABCMeta def __init__(self, task): self.task = task # First approach. Works, but no warnings if don't implement setter in subclass @property @abstractmethod def start(self): pass @start.setter @abstractmethod def start(self, value): pass # Second approach. "This method for 'end' may look slight messier, but raises errors if not implemented. @abstractmethod def get_end(self): pass @abstractmethod def set_end(self, value): pass end = property(get_end, set_end) def test_elapsed_time(self): return self.get_end() - self.start class FirstStep(Step): @property def start(self): return self.task.start_dt # No warnings if this is commented out. @start.setter def start(self, value): self.task.start_dt = value def get_end(self): return self.task.end_dt def set_end(self, value): self.task.end_dt = value 
3
  • I can't reproduce your issue with start on 3.7.4 or 3.8.5. Simplified example with just start, no *end, and an empty definition of FirstStep raises I get TypeError: Can't instantiate abstract class FirstStep with abstract methods start. This has worked since 3.3. Commented Dec 9, 2020 at 22:13
  • Oh, I see. You defined a getter but no setter, and you want both getter and setter to be required. Ignore me. Commented Dec 9, 2020 at 22:16
  • Yes @chepner answer seems to explain that part. I'm still trying to digest the rest. Commented Dec 9, 2020 at 22:17

2 Answers 2

4

I suspect this is a bug in the interaction of abstract methods and properties.

In your base class, the following things happen, in order:

  1. You define an abstract method named start.
  2. You create a new property that uses the abstract method from 1) as its getter. The name start now refers to this property, with the only reference to the original name now held by Self.start.fget.
  3. Python saves a temporary reference to start.setter, because the name start is about to be bound to yet another object.
  4. You create a second abstract method named start
  5. The reference from 3) is given the abstract method from 4) to define a new property to replace the once currently bound to the name start. This property has as its getter the method from 1 and as its setter the method from 4). Now start refers to this property; start.fget refers to the method from 1); start.fset refers to the method from 4).

At this point, you have a property, whose component functions are abstract methods. The property itself was not decorated as abstract, but the definition of property.__isabstractmethod__ marks it as such because all its component methods are abstract. More importantly, you have the following entries in Step.__abstractmethods__:

  1. start, the property
  2. end, the property
  3. set_end, the setter for end
  4. gen_end, the getter for end

Note that the component functions for the start property are missing, because __abstractmethods__ stores names of, not references to, things that need to be overriden. Using property and the resulting property's setter method as decorators repeatedly replace what the name start refers to.

Now, in your child class, you define a new property named start, shadowing the inherited property, which has no setter and a concrete method as its getter. At this point, it doesn't matter if you provide a setter for this property or not, because as far as the abc machinery is concerned, you have provided everything it asked for:

  1. A concrete method for the name start
  2. Concrete methods for the names get_end and set_end
  3. Implicitly a concrete definition for the name end, because all of the underlying functions for the property end have been provided concrete definitions.
Sign up to request clarification or add additional context in comments.

3 Comments

The docs for abstractproperty (deprecated) document a replacement approach with just @abstractmethod+@property. The example given states outright that properties can be partially abstract (e.g. just the setter, not the getter in their example) and that you should need to overwrite only that part of the property (implying if you have multiple abstract components, you must overwrite all abstract components), the fact that it doesn't actually behave that way definitely constitutes a bug.
Apparently, there is a closed bug for it. Looks like the implication I read there isn't really there; you can overwrite individual elements by referencing the parent property, but if you make your own property (rather than borrowing from and overwriting bits of the parent property) it's always going to satisfy the requirements of the ABC, even if you omit important bits.
Accepted, as I think I get the gist of what's going on here based on this explanation. Thanks... and to @ShadowRanger for the bug report find.
0

@chepner answered and explained it well. Based on that, I came up with a way around it that is... well... you decide. Sneaky at best. But it achieves my 3 main goals:

  1. Raises exceptions for unimplemented setters in subclasses
  2. Supports the python property semantics (vs. functions etc)
  3. Avoids boilerplate re-declaring every property in every subclass which still might not have solved #1 anyway.

Just declare the abstract get/set functions in the base class (not the property). Then add a @classmethod initializer to the base class that creates the actual properties using those abstract methods, but at that point, they'll be concrete methods on the subclass.

It's a one liner after the subclass declaration to init the properties. Nothing enforces that call being made, so it's not ironclad. Not a big savings in this example, but I'll have many properties. The end results doesn't look as dirty as I thought it would. Would like to hear comments or warnings of things I'm overlooking.

from abc import abstractmethod, ABC class ParentTask(object): def __init__(self): self.first_step = FirstStep(self) self.second_step = SecondStep(self) print(self.first_step.end) print(self.second_step.end) class Step(ABC): def __init__(self, task): self.task = task @classmethod def init_properties(cls): cls.end = property(cls.get_end, cls.set_end) @abstractmethod def get_end(self): pass @abstractmethod def set_end(self, value): pass class FirstStep(Step): def get_end(self): return 1 def set_end(self, value): self.task.end = value class SecondStep(Step): def get_end(self): return 2 def set_end(self, value): self.task.end = value FirstStep.init_properties() SecondStep.init_properties() ParentTask() 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.