user642770
user642770

Reputation: 415

Python descriptor how are class attributes getting assigned to instance member

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

Answers (1)

Dennis Sparrow
Dennis Sparrow

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

Related Questions