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.
|
|
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.
|
|