31.6 Slot Descriptors and __slots__

While descriptors provide a powerful mechanism for managing attribute access, Python classes inherently rely on a dictionary (__dict__) to store instance attributes. This offers tremendous flexibility but comes with a memory cost: every instance must allocate a dictionary, which can be significant when creating many small objects. The __slots__ class variable offers a high-performance alternative by fundamentally changing how instances store their data. How slots Replaces dict When you define __slots__ in a class, you are instructing the Python interpreter to create a fixed set of names (slots) for attributes on each instance. Instead of a dynamic dictionary, the interpreter reserves space for a small, fixed-length array (a C-style struct) within each instance to hold the values for these predefined attributes. This transformation happens deep within the object creation machinery.

31.5 Descriptors in the Standard Library: classmethod, staticmethod

While property is the most common descriptor encountered by Python developers, the language’s standard library includes two other critical descriptors that are fundamental to class design: classmethod and staticmethod. These built-in types are implemented as descriptors to provide a clean, consistent API for defining methods that do not operate on a specific instance. The classmethod Descriptor A classmethod is a method that receives the class itself (cls) as its implicit first argument, rather than an instance (self). This makes it ideal for factory methods, alternative constructors, or any method that needs to operate at the class level, potentially accessing or modifying class state.

31.4 Writing a Reusable Validator Descriptor

While Python’s property decorator is excellent for adding managed access to a single attribute in a single class, it lacks reusability. If you need the same validation logic across multiple attributes or multiple classes, copying and pasting the property definition is a maintenance nightmare. This is where writing a descriptor, specifically a reusable validator descriptor, becomes a powerful tool. A descriptor is a class that implements at least one of the __get__, __set__, or __delete__ methods. These methods are the “machinery” that properties are built upon, and by creating your own, you can encapsulate validation logic into a single, reusable object.

31.3 How @property Is Implemented Using Descriptors

While the @property decorator provides an elegant, high-level interface for creating managed attributes, its implementation rests squarely on the more fundamental concept of descriptors. Understanding this machinery is crucial for any Python developer who wishes to move beyond merely using properties to extending and customizing the behavior of object attribute access. The Descriptor Protocol: A Primer At its core, a descriptor is any object that defines at least one of the methods in the descriptor protocol: __get__(), __set__(), or __delete__(). These methods are hooks that are automatically called by Python’s internal attribute access machinery when a descriptor is accessed as an attribute on another object. The __get__ method is called when the attribute is retrieved (instance.descriptor), __set__ when it’s assigned (instance.descriptor = value), and __delete__ when it’s deleted (del instance.descriptor). A descriptor that only defines __get__ is termed a non-data descriptor, while one that defines __set__ or __delete__ is a data descriptor. This distinction is critical, as data descriptors have precedence over an instance’s dictionary, which is why properties can override instance attributes.

31.2 Data Descriptors vs Non-Data Descriptors

In Python, descriptors are a powerful mechanism that underpins properties, methods, and class-level functionality. The distinction between data descriptors and non-data descriptors is fundamental to understanding attribute lookup precedence and is crucial for designing robust and predictable classes. The Defining Difference: __set__ or __delete__ The official Python documentation defines a descriptor as any object that implements at least one of the three special methods: __get__(), __set__(), or __delete__(). This single criterion creates the primary classification:

31.1 The Descriptor Protocol: __get__, __set__, __delete__, __set_name__

Descriptors are a core mechanism in Python’s object system, enabling the powerful property, method, and class method functionality we often take for granted. They provide a protocol for overriding default attribute access behavior (getting, setting, and deleting) on a per-attribute basis. Any object that defines at least one of the methods __get__, __set__, or __delete__ is considered a descriptor. This protocol is the foundational machinery that makes the @property decorator work.

29.6 Properties vs __getattr__: When Each Is Appropriate

In object-oriented Python, the mechanisms for controlling attribute access are powerful but distinct. Two primary tools for this are properties and the __getattr__ method. Understanding their fundamental differences—specifically, the distinction between data access via the descriptor protocol and via the lookup chain—is crucial for employing them correctly. Properties are a declarative tool built on Python’s descriptor protocol. They allow you to define getter, setter, and deleter methods for a specific, predefined attribute, transforming what looks like simple attribute access into method calls. This is ideal when you have a known attribute whose value you need to compute, validate, or otherwise control at the point of access.

29.5 When to Use Each: Design Guidance

Understanding the Core Concepts Before delving into design guidance, it’s crucial to have a precise understanding of the three mechanisms. A class in Python is a blueprint for creating objects. The methods defined within it typically operate on instances of that class. However, Python provides decorators to modify the behavior of these methods, creating @property, @classmethod, and @staticmethod. The @property decorator allows you to define a method that can be accessed like an attribute, enabling getter, setter, and deleter functionality without explicit method calls. A @classmethod receives the class itself (cls) as its first implicit argument, rather than an instance (self). This binds the method to the class, not to any particular instance. A @staticmethod receives no implicit first argument; it is essentially a function bundled into a class’s namespace for organizational purposes. It behaves like a regular function but is called on the class or an instance.

29.4 @staticmethod: Utility Functions Attached to a Class

The @staticmethod decorator in Python defines a method that is bound to a class rather than an instance of that class. Crucially, these methods receive no implicit first argument—neither an instance (self) nor a class (cls). This makes them identical to a regular function defined outside the class, except for one vital distinction: they are encapsulated within the class’s namespace. This encapsulation is the primary reason for their existence; it signals a strong logical connection between the function and the class, even if the function doesn’t need to access any class or instance state.

29.3 @classmethod: Alternate Constructors and Class-Level Operations

The @classmethod decorator in Python transforms a method into a class method. Unlike a standard instance method, which receives the instance (self) as its first argument, a class method receives the class itself (cls) as its first implicit argument. This fundamental shift in perspective unlocks two primary use cases: the creation of alternate constructors and the performance of operations that are bound to the class rather than any particular instance.

29.2 Setters and Deleters with @property

While the @property decorator allows you to define a getter, the full power of the property protocol is unlocked when you also define the corresponding setter and deleter methods. These methods are invoked when an attempt is made to assign a value to the property or delete it using the del statement, respectively. This mechanism transforms a simple attribute access into a controlled method call, enabling validation, computation, and side effects.

29.1 @property: Computed Attributes and Encapsulation

The @property decorator in Python provides an elegant mechanism for creating computed attributes and enforcing encapsulation within classes. It allows a method to be accessed like an attribute, blurring the line between simple data access and method invocation while maintaining a clean, object-oriented interface. This is a cornerstone of the “uniform access principle,” which states that clients should be able to access a class’s services without knowing whether they are implemented via storage or computation.

— joke —

...