flybonzai flybonzai - 28 days ago 8
Python Question

How does Python resolve when an attribute is a descriptor?

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.

Answer Source

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'], then type(a).__dict__['x'], and continuing through the base classes of type(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)