Validated Attribute Descriptors
Свайпніть щоб показати меню
The most common real-world use of descriptors is attribute validation – enforcing type, range, or format constraints on every write without repeating the same if checks across dozens of __init__ methods. A single descriptor class can be reused across the entire codebase.
The Problem with Repeated Validation
Without descriptors, validation logic lives in __init__ and __setattr__ – repeated for every class that needs it:
1234567891011121314151617181920# Validation repeated in every class – hard to maintain class Order: def __init__(self, order_id, quantity, unit_price): if not isinstance(quantity, int) or quantity <= 0: raise ValueError(f"quantity must be a positive int, got {quantity!r}") if not isinstance(unit_price, float) or unit_price <= 0: raise ValueError(f"unit_price must be a positive float, got {unit_price!r}") self.order_id = order_id self.quantity = quantity self.unit_price = unit_price class Shipment: def __init__(self, shipment_id, weight, cost): if not isinstance(weight, float) or weight <= 0: raise ValueError(f"weight must be a positive float, got {weight!r}") if not isinstance(cost, float) or cost <= 0: raise ValueError(f"cost must be a positive float, got {cost!r}") self.shipment_id = shipment_id self.weight = weight self.cost = cost
A Reusable Validated Descriptor
Extract the validation into a descriptor that any class can use:
12345678910111213141516171819202122232425262728293031# Reusable validated descriptor – one class, used everywhere class Validated: def __set_name__(self, owner, name): self._name = name def __get__(self, obj, objtype=None): if obj is None: return self return obj.__dict__.get(self._name) def __set__(self, obj, value): self.validate(self._name, value) obj.__dict__[self._name] = value def validate(self, name, value): raise NotImplementedError("Subclasses must implement validate()") class PositiveInt(Validated): def validate(self, name, value): if not isinstance(value, int) or value <= 0: raise ValueError(f"{name} must be a positive integer, got {value!r}") class PositiveFloat(Validated): def validate(self, name, value): if not isinstance(value, (int, float)) or value <= 0: raise ValueError(f"{name} must be a positive number, got {value!r}") class NonEmptyString(Validated): def validate(self, name, value): if not isinstance(value, str) or not value.strip(): raise ValueError(f"{name} must be a non-empty string, got {value!r}")
Now any class can use these descriptors without repeating validation logic:
1234567891011121314151617181920212223242526# Applying the descriptors across multiple classes class Order: order_id = NonEmptyString() quantity = PositiveInt() unit_price = PositiveFloat() def __init__(self, order_id, quantity, unit_price): self.order_id = order_id self.quantity = quantity self.unit_price = unit_price class Shipment: shipment_id = NonEmptyString() weight = PositiveFloat() cost = PositiveFloat() def __init__(self, shipment_id, weight, cost): self.shipment_id = shipment_id self.weight = weight self.cost = cost order = Order("ORD-001", 10, 99.99) print(order.quantity) # 10 print(order.unit_price) # 99.99 order.quantity = -5 # Raises ValueError
Adding a Class-Level Registry
Descriptors can register themselves on the class they are assigned to – useful for introspection, serialization, or automatic validation frameworks:
1234567891011121314151617181920212223242526272829303132333435# Descriptor that registers itself on the owner class class TypeChecked: _registry = {} def __set_name__(self, owner, name): self._name = name TypeChecked._registry.setdefault(owner.__name__, []).append(name) def __get__(self, obj, objtype=None): if obj is None: return self return obj.__dict__.get(self._name) def __set__(self, obj, value): if not isinstance(value, self._expected_type): raise TypeError( f"{self._name} must be {self._expected_type.__name__}, got {type(value).__name__}" ) obj.__dict__[self._name] = value class TypedString(TypeChecked): _expected_type = str class TypedFloat(TypeChecked): _expected_type = float class Portfolio: name = TypedString() value = TypedFloat() def __init__(self, name, value): self.name = name self.value = value print(TypeChecked._registry) # {'Portfolio': ['name', 'value']}
Дякуємо за ваш відгук!
Запитати АІ
Запитати АІ
Запитайте про що завгодно або спробуйте одне із запропонованих запитань, щоб почати наш чат