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:
JavaScript
x
13
13
1
class Point:
2
def __init__(self, x, y):
3
self._x = x
4
self._y = y
5
6
@property
7
def x(self):
8
return self._x
9
10
@property
11
def y(self):
12
return self._y
13
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:
JavaScript
1
60
60
1
class ReadOnlyError(Exception):
2
"""Attempt made to assign a new value to something that can't be changed."""
3
4
5
# Based on recipe in book "Python Cookbook 3rd Edition" - section 9.21 -
6
# titled "Avoiding Repetitive Property Methods".
7
def readonly_typed_property(name, expected_type):
8
storage_name = '_' + name
9
10
@property
11
def prop(self):
12
return getattr(self, storage_name)
13
14
@prop.setter
15
def prop(self, value):
16
if hasattr(self, storage_name):
17
raise ReadOnlyError('{!r} is read-only!'.format(name))
18
if not isinstance(value, expected_type):
19
raise TypeError('{!r} must be a {!r}'.format(name, expected_type.__name__))
20
setattr(self, storage_name, value)
21
22
return prop
23
24
25
class Point:
26
x = readonly_typed_property('x', int)
27
y = readonly_typed_property('y', int)
28
29
def __init__(self, x, y):
30
self.x = x
31
self.y = y
32
33
34
if __name__ == '__main__':
35
try:
36
p1 = Point(1, 2)
37
except Exception as e:
38
print('ERROR: No exception should have been raised for case 1.')
39
print(e)
40
else:
41
print('As expected, NO exception raised for case 1.')
42
43
print()
44
try:
45
p2 = Point('1', 2)
46
except TypeError as e:
47
print(e)
48
print(f'As expected, {type(e).__name__} exception raised for case 2.')
49
else:
50
print('ERROR: expected TypeError exception not raised for case 2')
51
52
print()
53
try:
54
p1.x = 42
55
except Exception as e:
56
print(e)
57
print(f'As expected, {type(e).__name__} exception raised for case 3.')
58
else:
59
print('ERROR: expected ReadOnlyError exception not raised for case 3')
60