Reputation: 415
I have this descriptor which is an example from "Fluent Python" book. I'm failing to get my head around how class attributes are getting changed into instance attributes in the __init__
of LineItem
class. Please see my question in the comment in that function.
class Quantity:
def __init__(self, managed_attribute_name):
print(f'managing attribute {managed_attribute_name}')
self._managed_attribute_name = managed_attribute_name
def __get__(self, instance, owner):
print(f'{self._managed_attribute_name} get')
return instance.__dict__[self._managed_attribute_name]
def __set__(self, instance, value):
print(f'{self._managed_attribute_name} set {value}')
if value > 0:
instance.__dict__[self._managed_attribute_name] = value
class LineItem:
# Managed Attributes
weight = Quantity('weight')
price = Quantity('price')
def __init__(self, wt, pr):
# What are these 2 lines doing?
# How do 'weight' and 'price' become instance members here
# and still maintaining their type Quantity
self.weight = wt
self.price = pr
li1 = LineItem(3, 4)
print(li1.weight)
li2 = LineItem(5, 6)
print(li2.price)
Upvotes: 0
Views: 582
Reputation: 978
You are correct that this is not how functions in general or constructors in particular generally work. Normally, the effect of self.weight = wt
is that the name self.weight
is simply associated with the object wt
, no matter what type of object wt
may happen to be.
What's different here is the magic of descriptors. The Quantity class is a descriptor because it defines the methods __get__
and __set__
. When you define a Quantity object at the class level and later refer to an instance attribute with the same name, as in your example, Python applies the overridden access methods of the descriptor to the instance attribute. There isn't any visible mechanism that makes this happen. The definition of the descriptor at class level is in effect a declaration telling Python, "when I define an instance attribute with this name, use this descriptor's access methods on it."
Further invisible descriptor magic provides the instance
parameters to the Quantity methods. If you try to print out either LineItem.weight
, for example, or its type, the attempt will blow up because the instance
parameter will (appropriately) be None.
To quote from the Descriptor HowTo Guide, "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']
, then type(a).__dict__['x']
, and continuing through the base classes of type(a) excluding metaclasses. 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." The lookup in type(a).__dict__['x']
is how the descriptor behavior travels from the class to the instance.
The weight and price attributes of an instance in the example are not Quantity objects. They are int
objects accessed through Quantity's methods.
I fear this whole answer may be equivalent to "That's just how it works," but I hope it's at least slightly more illuminating than that.
Upvotes: 1