Validated Attribute Descriptors
Swipe um das Menü anzuzeigen
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']}
Danke für Ihr Feedback!
Fragen Sie AI
Fragen Sie AI
Fragen Sie alles oder probieren Sie eine der vorgeschlagenen Fragen, um unser Gespräch zu beginnen
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']}
Danke für Ihr Feedback!