Reputation: 2471
I'm trying to use python properties and inheritance and something isn't behaving intuitively. I want to be able to utilize property getters and setters of the inherited class to avoid having to repeat code for repeated behavior.
In order to boil down my problem I created the following example. I have a Car with behavior for counting passengers and populating the car's seats when given some number of passengers (using the occupants property getter and setter). For a van with 3 rows of seats I should only have to define the behavior of the 3rd seat and defer to the inherited Car class for the first 2 rows...
class Car(object):
def __init__(self):
#Store vehicle passengers in a dictionary by row
# - by default we'll have 1 in the front seat and 1 in the back
self._contents = {'row1': 1,
'row2': 1}
@property
def occupants(self):
"""
Number of occupants in the vehicle
"""
#Get the number of people in row 1
row1 = self._contents['row1']
#Get the number of people in row 2
row2 = self._contents['row2']
return row1 + row2
@occupants.setter
def occupants(self, val):
#Start with an empty car
self._contents['row1'] = 0
self._contents['row2'] = 0
#Check to see whether there are more than 2 people entering the car
if val > 2:
#Put 2 in the front seats
self._contents['row1'] = 2
#Put the rest in the back seat - no matter how many there are!
self._contents['row2'] = val - 2
else:
#Since there are 2 or fewer people, let them sit in the front
self._contents['row1'] = val
class Van(Car):
def __init__(self):
super(Van, self).__init__()
#Van's have an additional 3rd row
self._contents['row3'] = 1
@property
def occupants(self):
#Number of people in first 2 rows
first_2_rows = super(Van, self).occupants
#Number of people in 3rd row
row3 = self._contents['row3']
#Total number of people
return first_2_rows + row3
@occupants.setter
def occupants(self, val):
#Start with an empty van (Car class handles row1 and row2)
self._contents['row3'] = 0
#Check if there are more than 4 people entering the van
if val > 4:
#Put all but 4 folks in the back row
self._contents['row3'] = val - 4
#Load the first 2 rows in the same manner as for a car
#This causes an AttributeError
super(Van, self).occupants = 4
else:
#This causes an AttributeError
super(Van, self).occupants = val
if __name__ == '__main__':
van = Van()
print "Van has {0} people".format(van.occupants)
print "Seating 6 people in the van..."
van.occupants = 6
print "Van has {0} people".format(van.occupants)
The output I get is as follows:
Van has 3 people
Seating 6 people in the van...
Traceback (most recent call last):
File "C:/scratch.py", line 74, in <module>
van.occupants = 6
File "C:/scratch.py", line 65, in occupants
super(Van, self).occupants = 4
AttributeError: 'super' object has no attribute 'occupants'
Process finished with exit code 1
What's especially interesting to me is that the superclass's getter is working fine, but when I try to use the setter I get the attribute error. Am I using super()
incorrectly? Is there a better way to do this?
My actual application involves reading/writing between a text file format and a dictionary-like data structure. Some of the stuff in the text file is parsed out by my base class and some other special parameters are handled by the subclass. In the subclass setter I want to start by letting the base class parse whatever it needs from the text file (populating the data structure), then let the subclass parse out additional values for storage in the inherited data structure.
Some research lead me to this and eventually Issue 505028 where it is declared a "feature" instead of a bug. So with that out of the way, is there a way to make the above logic work using properties and inheritance? Do I have to use Car.occupants.fset(self, 4)
or something? I may answer myself in a minute, but I'm going to go ahead and post this to share it with folks. SOrry if it's a duplicate.
edit: Fixed some additional bugs like emptying all seats before setting occupants and the numeric logic in the Van occupants setter was wrong and incomplete (only became evident once the property error was fixed).
Upvotes: 0
Views: 367
Reputation: 879481
As you've noted, Guido van Rossum says,
... the semantics of super ... only applies to code attributes, not to data attributes.
So the workaround is to call a code attribute, not a data attribute. That means, in this case, that you need to keep a reference to the setter method; let's call it set_occupants
. So instead of
@occupants.setter
def occupants(self, val):
use
def set_occupants(self, val):
...
occupants = property(get_occupants, set_occupants)
and instead of super(...).occupants = 4
, you call the super's method:
super(Van, self).set_occupants(4)
class Car(object):
def __init__(self):
#Store vehicle passengers in a dictionary by row
# - by default we'll have 1 in the front seat and 1 in the back
self._contents = {'row1': 1,
'row2': 1}
def get_occupants(self):
"""
Number of occupants in the vehicle
"""
#Get the number of people in row 1
row1 = self._contents['row1']
#Get the number of people in row 2
row2 = self._contents['row2']
return row1 + row2
def set_occupants(self, val):
#Check to see whether there are more than 2 people entering the car
if val > 2:
#Put 2 in the front seats
self._contents['row1'] = 2
#Put the rest in the back seat - no matter how many there are!
self._contents['row2'] = val - 2
else:
#Since there are 2 or fewer people, let them sit in the front
self._contents['row1'] = val
occupants = property(get_occupants, set_occupants)
class Van(Car):
def __init__(self):
super(Van, self).__init__()
#Van's have an additional 3rd row
self._contents['row3'] = 1
def get_occupants(self):
#Number of people in first 2 rows
first_2_rows = super(Van, self).occupants
#Number of people in 3rd row
row3 = self._contents['row3']
#Total number of people
return first_2_rows + row3
def set_occupants(self, val):
#Check if there are more than 4 people entering the van
if val > 4:
#Put all but 4 folks in the back row
self._contents['row3'] = val - 4
#Load the first 2 rows in the same manner as for a car
super(Van, self).set_occupants(4)
occupants = property(get_occupants, set_occupants)
if __name__ == '__main__':
van = Van()
print "Van has {0} people".format(van.occupants)
print "Seating 6 people in the van..."
van.occupants = 6
print "Van has {0} people".format(van.occupants)
yields
Van has 3 people
Seating 6 people in the van...
Van has 6 people
To continue using the @property decorator and yet still be able to call the setter from super
, and without having to manually add lots of extra attributes by hand, you could use a metaclass to do the work for you. A class decorator would also be possible, but the advantage of a metaclass is that you need only define it once as the metaclass of Car
, and then the metaclass and its behaviour is inherited by all subclasses of Car
, whereas, a class decorator would have to be applied to each subclass manually.
class MetaCar(type):
def __init__(cls, name, bases, clsdict):
super(MetaCar, cls).__init__(name, bases, clsdict)
for name, val in clsdict.items():
if isinstance(val, property):
setattr(cls, 'get_{}'.format(name), val.fget)
setattr(cls, 'set_{}'.format(name), val.fset)
setattr(cls, 'del_{}'.format(name), val.fdel)
class Car(object):
__metaclass__ = MetaCar
def __init__(self):
#Store vehicle passengers in a dictionary by row
# - by default we'll have 1 in the front seat and 1 in the back
self._contents = {'row1': 1,
'row2': 1}
@property
def occupants(self):
"""
Number of occupants in the vehicle
"""
#Get the number of people in row 1
row1 = self._contents['row1']
#Get the number of people in row 2
row2 = self._contents['row2']
return row1 + row2
@occupants.setter
def occupants(self, val):
#Check to see whether there are more than 2 people entering the car
if val > 2:
#Put 2 in the front seats
self._contents['row1'] = 2
#Put the rest in the back seat - no matter how many there are!
self._contents['row2'] = val - 2
else:
#Since there are 2 or fewer people, let them sit in the front
self._contents['row1'] = val
class Van(Car):
def __init__(self):
super(Van, self).__init__()
#Van's have an additional 3rd row
self._contents['row3'] = 1
@property
def occupants(self):
#Number of people in first 2 rows
first_2_rows = super(Van, self).occupants
#Number of people in 3rd row
row3 = self._contents['row3']
#Total number of people
return first_2_rows + row3
@occupants.setter
def occupants(self, val):
#Check if there are more than 4 people entering the van
if val > 4:
#Put all but 4 folks in the back row
self._contents['row3'] = val - 4
#Load the first 2 rows in the same manner as for a car
super(Van, self).set_occupants(4)
if __name__ == '__main__':
van = Van()
print "Van has {0} people".format(van.occupants)
print "Seating 6 people in the van..."
van.occupants = 6
print "Van has {0} people".format(van.occupants)
Upvotes: 1
Reputation: 2471
Looking around the web I found that SomeClass.property.fset(self, value)
can be used to call the setter of a property. In this case SomeClass is Car (the super-class of Van) and self is the current Van instance, which the car occupants setter operates on to populate the first 2 rows of the van (just like for a car).
class Car(object):
def __init__(self):
#Store vehicle passengers in a dictionary by row
# - by default we'll have 1 in the front seat and 1 in the back
self._contents = {'row1': 1,
'row2': 1}
@property
def occupants(self):
"""
Number of occupants in the vehicle
"""
#Get the number of people in row 1
row1 = self._contents['row1']
#Get the number of people in row 2
row2 = self._contents['row2']
return row1 + row2
@occupants.setter
def occupants(self, val):
#Start with an empty car
self._contents['row1'] = 0
self._contents['row2'] = 0
#Check to see whether there are more than 2 people entering the car
if val > 2:
#Put 2 in the front seats
self._contents['row1'] = 2
#Put the rest in the back seat - no matter how many there are!
self._contents['row2'] = val - 2
else:
#Since there are 2 or fewer people, let them sit in the front
self._contents['row1'] = val
class Van(Car):
def __init__(self):
super(Van, self).__init__()
#Van's have an additional 3rd row
self._contents['row3'] = 1
@property
def occupants(self):
#Number of people in first 2 rows
first_2_rows = super(Van, self).occupants
#Number of people in 3rd row
row3 = self._contents['row3']
#Total number of people
return first_2_rows + row3
@occupants.setter
def occupants(self, val):
#Start with an empty van (first 2 rows handled by Car class)
self._contents['row3'] = 0
#Check if there are more than 4 people entering the van
if val > 4:
#Put all but 4 folks in the back row
self._contents['row3'] = val - 4
#Load the first 2 rows in the same manner as for a car
Car.occupants.fset(self, 4)
else:
#Load the first 2 rows in the same manner as for a car
Car.occupants.fset(self, val)
if __name__ == '__main__':
van1 = Van()
van2 = Van()
print "Van has {0} people".format(van1.occupants)
print "Seating 6 people in van1"
van1.occupants = 6
print "Seating 2 people in van2"
van2.occupants = 2
print "van1 has {0} people".format(van1.occupants)
print "van2 has {0} people".format(van2.occupants)
The output is:
Van has 3 people
Seating 6 people in van1
Seating 2 people in van2
van1 has 6 people
van2 has 2 people
Props also to unutbu for lots of work on this and demonstrating that this could also be solved with the property function or a metaclass. I'm not yet sure which is more elegant since each method has pro's and con's.
If answering my own question is bad form in this instance, call me out and I'll gladly do whatever is necessary to follow the community protocol.
Upvotes: 0