I would like to better understand the relation between container object attributes and contained object attributes.
I would like to understand the output of this example:
I create a class Car and a class Tire.
Tire also has an attribute age that should "point" to the attribute of the instance of car named bmw.
So I would think that by changing the attribute of bmw instance the pirelli attribute age should also be updated since it references to the same variable. As visible from the example it is not the case.
How could I do to actually have it updated? I am planning of using composition multiple times so in the last class it would be very impractical to use
finalInstance.age = previousInstances.tire.car.age.
class Car:
def __init__(self, age):
self.age = age
class Tire:
def __init__(self, car):
self.car = car
self.age = self.car.age
bmw = Car(age=1)
print('Car age is ',bmw.age)
pirelli = Tire(car=bmw)
print('Tire age is ',pirelli.age)
bmw.age = 2
print('Car age is now ',bmw.age)
print('Tire age is now ',pirelli.age)
I would expect this output:
Car age is 1
Tire age is 1
Car age is now 2
Tire age is now 2
But what I get is:
Car age is 1
Tire age is 1
Car age is now 2
Tire age is now 1
Anyone can help me find a way to have self.age = self.car.age of Tire to automatically update?
Thank you a lot.
The assignment operator =
in Python really changes the object the name assigned to points to.
In other words, in your example, the line self.age = self.car.age
points to the number (an object) defined in self.car
when that line is run. When you later run bmw.age = 2
that changes the attribute in the car class - it points to a new object. But bmw.car.age
is not changed.
You have to first understand that well, and that the plain and simple thing to do is just use, even in the Tire
class, the full path to the attribute self.car.age
.
Then if you really think you need the attributes to change dynamically, we have to resort to "reactive" programming - an approach in which one change in an state automatically propagate to other places in the code. Python allows this to be implemented in a number of ways. An example follows.
So, one of the powerful mechanisms Python has to deal with this kind of thing is the "descriptor protocol" - the language specifies that if an object tied as a class attribute - that is, defined in the class
body, not just inside the __init__
or other method - features a __get__
method (or a couple other special named methods), retrieving or trying to rebind (with =
) that attribute in an instance of that class will go through this object in the class. It is called a "descriptor".
Python's property
decorator uses this mechanism to provide the most used case of creating setters and getters for an attribute. But since you will want the same access and getting rules for a lot of similar attributes, it makes more sense to create an specialised descriptor class - that will "know" the source of an attribute, and always go to its source to get or set its value.
The code bellow, along with the "Car, Tire & Screw" classes provide an example of this:
class LinkedAttribute:
def __init__(self, container_path):
self.container_path = container_path
def __set_name__(self, owner, name):
self.name = name
def _get_container(self, instance):
container = instance
for component in self.container_path.split("."):
container = getattr(container, component)
return container
def __get__(self, instance, owner):
if instance is None:
return self
container = self._get_container(instance)
return getattr(container, self.name)
def __set__(self, instance, value):
container = self._get_container(instance)
setattr(container, self.name, value)
class Car:
def __init__(self, age):
self.age = age
class Tire:
age = LinkedAttribute("car")
def __init__(self, car):
self.car = car
self.age = self.car.age
class Screw:
age = LinkedAttribute("tire.car")
def __init__(self, tire):
self.tire = tire
And in the interactive interpreter we have:
In [37]: bmw = Car(2)
...:
In [38]: t1 = Tire(bmw)
...:
In [39]: s1 = Screw(t1)
In [40]: t1.age
Out[40]: 2
In [41]: bmw.age = 5
In [42]: t1.age
Out[42]: 5
In [43]: s1.age
Out[43]: 5
In [44]: s1.age = 10
In [45]: bmw.age
Out[45]: 10
And, of course, if you don't want to allow any of the parent's attributes to be modified from the contained classes, just leave out (or put a guard flag) on the __set__
method.