Reputation: 3931
So take the following code:
class Quantity:
__counter = 0
def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.storage_name = f'_{prefix}#{index}'
cls.__counter += 1
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
if value > 0:
setattr(instance, self.storage_name, value)
else:
raise ValueError('value must be > 0')
class LineItem:
weight = Quantity()
price = Quantity()
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
if __name__ == '__main__':
l = LineItem('cocoa', 15, 2.70)
print(vars(l))
>>> {'description': 'cocoa', '_Quantity#0': 15, '_Quantity#1': 2.7}
How does Python know to not shadow the class attributes price
and weight
with the instance attributes like it would typically do? I'm getting confused trying to understand the order in which Python evaluates all of this.
Upvotes: 1
Views: 47
Reputation: 22942
Quoting the documentation:
The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. For instance, a.x has a lookup chain starting with
a.__dict__['x']
, thentype(a).__dict__['x']
, and continuing through the base classes oftype(a)
excluding metaclasses.However, if the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined and how they were called.
[...]
Instance Binding
If binding to an object instance,
a.x
is transformed into the call:type(a).__dict__['x'].__get__(a, type(a))
.
When you define the class:
class LineItem:
weight = Quantity()
price = Quantity()
Python will fist see the descriptors.
So, when you instanciate your class:
l = LineItem('cocoa', 15, 2.70)
The class is built with the descriptors and the __init__
is called.
self.weight = weight
Will call:
LineItem.weight.__set__(l, weight)
Upvotes: 2