Skip to content

Descriptors & Properties

Overview

Descriptors control how attributes are accessed: - Properties: @property for computed attributes - Descriptors: Low-level attribute access control - Custom getters/setters: Logic when accessing attributes - Validation: Enforce constraints on attribute values - Implications for ML: PyTorch parameters, TensorFlow variables, JAX pytrees


Properties with @property

Basic Properties

class Circle:
    """Circle with computed properties."""

    def __init__(self, radius):
        self._radius = radius  # Private attribute

    @property
    def radius(self):
        """Get radius."""
        return self._radius

    @radius.setter
    def radius(self, value):
        """Set radius with validation."""
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value

    @property
    def diameter(self):
        """Computed property."""
        return 2 * self._radius

    @property
    def area(self):
        """Computed property."""
        import math
        return math.pi * self._radius ** 2

# Use it
circle = Circle(5)
print(circle.radius)     # 5 (uses getter)
print(circle.diameter)   # 10 (computed)
print(circle.area)       # 78.54... (computed)

circle.radius = 10       # Uses setter
print(circle.diameter)   # 20

# circle.radius = -5    # ValueError!

Computed Properties in ML

import numpy as np

class NeuralNetworkLayer:
    """Layer with parameter properties."""

    def __init__(self, input_size, output_size):
        self._weights = np.random.randn(input_size, output_size)
        self._bias = np.zeros(output_size)

    @property
    def weights(self):
        """Get weights."""
        return self._weights

    @weights.setter
    def weights(self, value):
        """Set weights with validation."""
        if value.shape != self._weights.shape:
            raise ValueError("Shape mismatch")
        self._weights = value

    @property
    def num_parameters(self):
        """Computed property: total parameters."""
        return self._weights.size + self._bias.size

    @property
    def weight_norm(self):
        """Computed property: L2 norm of weights."""
        return np.linalg.norm(self._weights)

# Use it
layer = NeuralNetworkLayer(10, 5)
print(f"Parameters: {layer.num_parameters}")  # 55
print(f"Weight norm: {layer.weight_norm}")    # Some value

Descriptors Protocol

Understanding get, set, delete

class Descriptor:
    """Base descriptor class."""

    def __get__(self, obj, objtype=None):
        """Called when attribute is accessed."""
        print(f"Getting {self}")
        return "descriptor value"

    def __set__(self, obj, value):
        """Called when attribute is assigned."""
        print(f"Setting to {value}")

    def __delete__(self, obj):
        """Called when attribute is deleted."""
        print(f"Deleting")

class Example:
    descriptor = Descriptor()
    regular_attribute = "normal"

# Using it
obj = Example()
print(obj.descriptor)   # Calls __get__
obj.descriptor = 10     # Calls __set__
del obj.descriptor      # Calls __delete__

Type-Checking Descriptor

class TypedProperty:
    """Descriptor that enforces type."""

    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type
        self.private_name = f"_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private_name, None)

    def __set__(self, obj, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f"{self.name} must be {self.expected_type.__name__}")
        setattr(obj, self.private_name, value)

    def __delete__(self, obj):
        delattr(obj, self.private_name)

class Person:
    name = TypedProperty("name", str)
    age = TypedProperty("age", int)

    def __init__(self, name, age):
        self.name = name
        self.age = age

# Use it
person = Person("Alice", 30)
print(person.name, person.age)  # Alice 30

person.age = 31  # OK
# person.age = "thirty"  # TypeError!

Lazy Properties

Computing on Demand

class LazyProperty:
    """Property computed only when first accessed."""

    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self

        # Compute and cache
        value = self.func(obj)

        # Store directly on instance (bypass descriptor next time)
        setattr(obj, self.name, value)

        return value

class DataProcessor:
    def __init__(self, data_path):
        self.data_path = data_path

    @LazyProperty
    def data(self):
        """Load data only when accessed."""
        print(f"Loading data from {self.data_path}...")
        import time
        time.sleep(1)  # Simulate slow loading
        return [1, 2, 3, 4, 5]

# Use it
processor = DataProcessor("path/to/data.csv")
print("Created processor")

# First access: computes
print(processor.data)  # "Loading data..." then returns

# Second access: uses cached value (no print)
print(processor.data)  # Just returns cached value

Practical ML Examples

PyTorch Parameter-like Behavior

class Parameter:
    """Mimics PyTorch parameter behavior."""

    def __init__(self, data, requires_grad=True):
        self._data = data
        self.requires_grad = requires_grad
        self.grad = None

    @property
    def data(self):
        return self._data

    @data.setter
    def data(self, value):
        self._data = value

    @property
    def shape(self):
        return self._data.shape

    @property
    def dtype(self):
        return self._data.dtype

class SimpleModel:
    def __init__(self):
        self.weight = Parameter([[1.0, 2.0], [3.0, 4.0]])
        self.bias = Parameter([0.1, 0.2])

model = SimpleModel()
print(model.weight.shape)      # (2, 2)
print(model.weight.requires_grad)  # True

TensorFlow Variable-like Behavior

class Variable:
    """Mimics TensorFlow variable."""

    def __init__(self, initial_value, trainable=True):
        self._value = initial_value
        self.trainable = trainable

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        self._value = new_value

    @property
    def dtype(self):
        return self._value.dtype

    def assign(self, value):
        """Assign new value."""
        self._value = value
        return self

var = Variable([1.0, 2.0, 3.0])
print(var.value)        # [1, 2, 3]
var.assign([4.0, 5.0, 6.0])
print(var.value)        # [4, 5, 6]