Descriptors in Python are objects that define how attributes are accessed and modified in a class

Python properties, essentially getters and setters, are a specific use of descriptors. A descriptor is an object that can define how an attribute is accessed using __get__, __set__, and __delete__ methods. Unlike regular methods, adding a property to a class means adding a property class instance. A detailed explanation can be found here.

Descriptors provide a way to intercept attribute access. Properties, built using descriptors and available in the standard library, are a higher-level implementation of this mechanism.

When an attribute is accessed, and it’s a descriptor found in the object’s class (not directly in the object’s __dict__), the descriptor’s __get__, __set__, or __delete__ method is called. It’s important to note that descriptors reside in the class, not the instance itself. This concept is explained further here.

Interestingly, descriptors underpin many Python features. Take bound methods, for example. Rather than specific runtime code, they leverage the function class, which acts as a descriptor. Defining a method adds this function object to the class’s __dict__. When invoked (e.g., my_instance.method1("aaa")), the lookup finds “method1” in the class, and its __get__ method returns a bound method class instance. The actual call then triggers the __call__ method of this instance.

For a deeper dive into descriptors and their role in attribute lookup, bound methods, and class methods (decorator-enhanced function descriptors), refer to python documentation about descriptors. It offers illuminating Python implementations of these concepts.

The core attribute lookup process is handled by object.getattribute and type.getattribute. When you access an attribute, the runtime searches for __getattribute__ in the object’s class. For a regular instance (e.g., p1 of class Person), it looks in Person.__dict__ and up the inheritance chain, ultimately reaching object.__dict__. Accessing an attribute on the class itself (e.g., Person.my_class_attribute) leads to a search starting from type.__dict__, as type is the default metaclass. If a descriptor is encountered, its __get__ is invoked. For object.__getattribute__, this looks like desc.__get__(a, type(a)), while type.__getattribute__ uses desc.__get__(None, A).

Even internal attributes like __class__ (and __name__, as highlighted here) are implemented as descriptors residing in object.__dict__. You can see this in object.__dict__ [’__class__’]. There’s a good explanation here.

The implementation of properties aligns with the behavior of object.__getattribute__ and type.__getattribute__. This means accessing a property from an instance yields its value, while accessing from the class returns the property object itself.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 `In [70]: class Book:
    ...:     @property
    ...:     def price(self):
    ...:         return 4
    ...:         

In [71]: b1 = Book()

In [72]: b1.price
Out[72]: 4


In [74]: b1.__class__.__dict__["price"]
Out[74]: at 0x1c31f647ce0>

In [75]: b1.__class__.price
Out[75]: at 0x1c31f647ce0>` 

This differs slightly from JavaScript, which also employs descriptors (for all attributes, not just accessors). In JavaScript, a specific method, Object.getOwnPropertyDescriptor, is needed to retrieve the descriptor object instead of the value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 ``class Person{
    constructor(name, lastName){
        this.name = name;
        this.lastName = lastName;
    }

    get fullName(){
        return `${this.name} ${this.lastName}`; 
    }

    sayHi(toX){
        return `Hi ${toX} I am ${this.name}`
    }

}

let p1 = new Person("Francois", "Belmondo")
console.log(p1.fullName); //Francois Belmondo

// the fullName access descriptor is in Person.prototype (just as the sayHi function)
//these 2 execute the "getter", but with the Person.prototype as "this", so gets undefined name and lastName
console.log(Person.prototype.sayHi("aa")); //Hi aa I am undefined
console.log(Person.prototype.fullName); //undefined undefined

let d1 = Object.getOwnPropertyDescriptor(Person.prototype, "fullName"); //the descriptor

let d2 = Object.getOwnPropertyDescriptor(p1, "fullName"); // undefined
let d3 = Object.getOwnPropertyDescriptor(Person, "fullName"); //undefined`` 

Attribute look-up

Object.getOwnPropertyDescriptor

Licensed under CC BY-NC-SA 4.0
Last updated on Jun 25, 2024 03:49 +0100