flybonzai
flybonzai

Reputation: 3931

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.

Upvotes: 1

Views: 47

Answers (1)

Laurent LAPORTE
Laurent LAPORTE

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'], 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)

Upvotes: 2

Related Questions