A very similar question was posted here, but there are no accepted answers, no code examples and I don’t really like the idea of using an external library as suggested by the only one answer provided there.
The following code allows to define read-only instance attributes:
class Point: def __init__(self, x, y): self._x = x self._y = y @property def x(self): return self._x @property def y(self): return self._y
but I would like to validate the user inputs as well. I want to validate if x
and y
have the right type.
What is the most pythonic/elegant way for doing this? If I provide setters, the attributes are no more read-only.
Is performing the input validation within the constructor the only way to go?
Advertisement
Answer
Here’s an elegant and pythonic way to do it which uses a factory function to create the properties:
class ReadOnlyError(Exception): """Attempt made to assign a new value to something that can't be changed.""" # Based on recipe in book "Python Cookbook 3rd Edition" - section 9.21 - # titled "Avoiding Repetitive Property Methods". def readonly_typed_property(name, expected_type): storage_name = '_' + name @property def prop(self): return getattr(self, storage_name) @prop.setter def prop(self, value): if hasattr(self, storage_name): raise ReadOnlyError('{!r} is read-only!'.format(name)) if not isinstance(value, expected_type): raise TypeError('{!r} must be a {!r}'.format(name, expected_type.__name__)) setattr(self, storage_name, value) return prop class Point: x = readonly_typed_property('x', int) y = readonly_typed_property('y', int) def __init__(self, x, y): self.x = x self.y = y if __name__ == '__main__': try: p1 = Point(1, 2) except Exception as e: print('ERROR: No exception should have been raised for case 1.') print(e) else: print('As expected, NO exception raised for case 1.') print() try: p2 = Point('1', 2) except TypeError as e: print(e) print(f'As expected, {type(e).__name__} exception raised for case 2.') else: print('ERROR: expected TypeError exception not raised for case 2') print() try: p1.x = 42 except Exception as e: print(e) print(f'As expected, {type(e).__name__} exception raised for case 3.') else: print('ERROR: expected ReadOnlyError exception not raised for case 3')