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
print(processor.data) # "Loading data..." then returns

# Second access
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]

-

  • [01 Classes & Inheritance](/05-py3/02-object-oriented-patterns/(01-classes-inheritance/) - Class fundamentals
  • 02 Decorators - Property decorator implementation
  • 03 Magic Methods - getattr, setattr
  • [05 Custom Bytecode & Metaprogramming](/05-py3/09-bytecode-and-execution/(05-custom-bytecode-metaprogramming/) - Advanced descriptor patterns