Skip to content

Python Descriptors

This guide covers Python descriptors, their types, and how to use them effectively.

Table of Contents

Introduction

What are Descriptors?

Descriptors are objects that define how attributes are accessed, set, or deleted. They provide a way to customize attribute access and implement reusable behavior.

Key Concepts

  • Attribute access control
  • Reusable behavior
  • Method binding
  • Property-like functionality

Descriptor Protocol

Basic Descriptor

class Descriptor:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        print(f"Getting {self.__class__.__name__}")
        return instance._value

    def __set__(self, instance, value):
        print(f"Setting {self.__class__.__name__}")
        instance._value = value

    def __delete__(self, instance):
        print(f"Deleting {self.__class__.__name__}")
        del instance._value

Descriptor with Name

class NamedDescriptor:
    def __init__(self, name=None):
        self.name = name

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        instance.__dict__[self.name] = value

Descriptor Types

Non-data Descriptor

1
2
3
4
5
class NonDataDescriptor:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return "Non-data descriptor value"

Data Descriptor

1
2
3
4
5
6
7
8
class DataDescriptor:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        instance.__dict__[self.name] = value

Lazy Property

class LazyProperty:
    def __init__(self, function):
        self.function = function
        self.name = function.__name__

    def __get__(self, instance, owner):
        if instance is None:
            return self
        value = self.function(instance)
        setattr(instance, self.name, value)
        return value

Common Patterns

Validated Property

class ValidatedProperty:
    def __init__(self, name, validator):
        self.name = name
        self.validator = validator

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not self.validator(value):
            raise ValueError(f"Invalid value: {value}")
        instance.__dict__[self.name] = value

Cached Property

class CachedProperty:
    def __init__(self, function):
        self.function = function
        self.name = function.__name__

    def __get__(self, instance, owner):
        if instance is None:
            return self
        if self.name not in instance.__dict__:
            instance.__dict__[self.name] = self.function(instance)
        return instance.__dict__[self.name]

Method Binding

from types import MethodType

class Method:
    def __init__(self, name):
        self.name = name

    def __call__(self, instance, *args, **kwargs):
        print(f"{self.name}: {instance} called with {args} and {kwargs}")

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return MethodType(self, instance)

Best Practices

  1. Descriptor Design
  2. Keep descriptors focused and single-purpose
  3. Use clear and descriptive names
  4. Document behavior clearly

  5. Performance

  6. Avoid unnecessary attribute access
  7. Use appropriate caching strategies
  8. Consider memory usage

  9. Error Handling

  10. Provide clear error messages
  11. Handle edge cases
  12. Validate input appropriately

  13. Code Organization

  14. Group related descriptors
  15. Maintain consistent style
  16. Use type hints

  17. Data Model

  18. Properties
  19. Metaclasses
  20. Class Decorators