如何用装饰器与property高效校验RectangularRoom类属性的类型及范围?
RectangularRoom Properties Great question! The repetitive getter/setter code you’re using for width can definitely be streamlined when adding constraints to height and dirt_amount. Here are two clean, maintainable approaches to avoid redundant validation logic:
1. Use @property Decorators with Reusable Validation Functions
Instead of writing separate getter/setter methods for each property, use Python’s modern @property decorator syntax, and extract validation logic into a reusable decorator. This keeps your code DRY (Don’t Repeat Yourself) and easy to read.
Example Implementation:
import math import random # Reusable validation decorator for integer values with range constraints def validate_int(min_val=None, max_val=None): def decorator(setter_func): def wrapper(self, value): # Check if value is an integer if not isinstance(value, int): raise TypeError("Value must be an integer") # Check minimum value constraint if min_val is not None and value < min_val: raise ValueError(f"Value must be greater than or equal to {min_val}") # Check maximum value constraint (if needed) if max_val is not None and value > max_val: raise ValueError(f"Value must be less than or equal to {max_val}") # Pass validated value to the original setter return setter_func(self, value) return wrapper return decorator class RectangularRoom(object): """ A RectangularRoom represents a rectangular region containing clean or dirty tiles. A room has a width and a height and contains (width * height) tiles. Each tile has some fixed amount of dirt. The tile is considered clean only when the amount of dirt on this tile is 0. """ def __init__(self, width, height, dirt_amount): """ Initializes a rectangular room with the specified width, height, and dirt_amount on each tile. width: an integer > 0 height: an integer > 0 dirt_amount: an integer >= 0 """ # Assignments here will trigger the property setters and validate inputs self.width = width self.height = height self.dirt_amount = dirt_amount tiles = [(w,h) for w in range(self.width) for h in range(self.height)] self.room = {tile: dirt_amount for tile in tiles} @property def width(self): return self._width @width.setter @validate_int(min_val=1) def width(self, value): self._width = value @property def height(self): return self._height @height.setter @validate_int(min_val=1) def height(self, value): self._height = value @property def dirt_amount(self): return self._dirt_amount @dirt_amount.setter @validate_int(min_val=0) def dirt_amount(self, value): self._dirt_amount = value def __str__(self): return str(self.room)
Why This Works:
- The
validate_intdecorator encapsulates all common integer validation logic, so you don’t repeat type checks and range checks for each property. - Using
@propertydecorators makes the code more readable and follows Python’s idiomatic style. - Assigning values in
__init__now automatically triggers the setter validation, ensuring your initial inputs are valid (something your original code didn’t do!).
2. Use a Custom Descriptor Class
For even more flexibility and reusability across multiple classes, you can create a descriptor that handles integer validation. Descriptors are a powerful Python feature for managing attribute access.
Example Implementation:
import math import random class IntProperty: def __init__(self, min_val=None, max_val=None): self.min_val = min_val self.max_val = max_val def __get__(self, instance, owner): # Retrieve the value from the instance's __dict__ return instance.__dict__[self.name] def __set__(self, instance, value): # Validate type if not isinstance(value, int): raise TypeError("Value must be an integer") # Validate minimum value if self.min_val is not None and value < self.min_val: raise ValueError(f"Value must be >= {self.min_val}") # Validate maximum value if self.max_val is not None and value > self.max_val: raise ValueError(f"Value must be <= {self.max_val}") # Store the validated value instance.__dict__[self.name] = value def __set_name__(self, owner, name): # Automatically set the attribute name when the descriptor is assigned to a class self.name = name class RectangularRoom(object): """ A RectangularRoom represents a rectangular region containing clean or dirty tiles. A room has a width and a height and contains (width * height) tiles. Each tile has some fixed amount of dirt. The tile is considered clean only when the amount of dirt on this tile is 0. """ # Define properties using the custom descriptor width = IntProperty(min_val=1) height = IntProperty(min_val=1) dirt_amount = IntProperty(min_val=0) def __init__(self, width, height, dirt_amount): """ Initializes a rectangular room with the specified width, height, and dirt_amount on each tile. width: an integer > 0 height: an integer > 0 dirt_amount: an integer >= 0 """ self.width = width self.height = height self.dirt_amount = dirt_amount tiles = [(w,h) for w in range(self.width) for h in range(self.height)] self.room = {tile: dirt_amount for tile in tiles} def __str__(self): return str(self.room)
Why This Works:
- The
IntPropertydescriptor encapsulates all validation logic in one place, making yourRectangularRoomclass extremely clean and focused on its core functionality. - Descriptors are reusable across multiple classes—if you ever need another class with integer properties, you can just import
IntPropertyand use it. - Like the decorator approach, assigning values in
__init__triggers validation, ensuring your initial state is valid.
Which Approach to Choose?
- Use the decorator approach if you only need validation for this specific class, or if you want more flexibility to customize validation for individual properties.
- Use the descriptor approach if you plan to reuse this validation logic across multiple classes, or if you want to keep your class definitions as clean as possible.
内容的提问来源于stack exchange,提问作者crystalizeee

