I'm trying to create a circle class using the magic methods __getattr__
and __setattr__
, and I seem to have my __getattr__
working, but when I implement __setattr__
(which should only allow the values for x
and y
to be set if the value is an int, and raise an AttributeError
when the user tries to set the attributes area
, circumference
, and distance
to circle
), my __getattr__
throws the maximum recursion error. When I comment it out, the __getattr__
then works just fine.
from math import pi, hypot, sqrt
'''
Circle class using __getattr__, and __setattr__ (rename circle2)
'''
# __getattr__(self, name): Automatically called when the attribute name
# is accessed and the object has no such attribute.
# __setattr__(self, name, value): Automatically called when an attempt is made to bind the attribute name to value.
class Circle:
def __init__(self, x, y, r):
self.x = x
self.y = y
self.r = r
self.area = pi * self.r * self.r
self.circumference = 2 * pi * self.r
self.distance_to_origin = abs(sqrt((self.x - 0)*(self.x - 0) + (self.y - 0) * (self.y - 0)) - self.r)
def __getattr__(self, name):
if name in ["x", "y", "r", "area", "circumference", "distance_to_origin"]:
print('__get if statement') # check getattr working
return getattr(self, name)
else:
print('Not an attribute')
return None
'''
def __setattr__(self, name, value):
print(name, value)
if name in ['x', 'y']:
if isinstance(value, int):
print('we can set x,y')
self.__dict__[name] = value
else: # value isn't an int
raise TypeError('Expected an int')
elif name in ['area', 'circumference', 'distance_to_origin']:
raise RuntimeError('Cannot set attribute')
'''
if __name__ == '__main__':
circle = Circle(x=3, y=4, r=5)
# print(circle.x)
print(circle.__getattr__('x'))
# print(circle.y)
print(circle.__getattr__('y'))
# print(circle.r)
print(circle.__getattr__('r'))
# print(circle.area)
print(circle.__getattr__('area'))
# print(circle.circumference)
print(circle.__getattr__('circumference'))
# print(circle.distance_to_origin)
print(circle.__getattr__('distance_to_origin'))
# print(circle.test)
'''
tests = [('circle.x = 12.3', "print('Setting circle.x to non-integer fails')"),
('circle.y = 23.4', "print('Setting circle.y to non-integer fails')"),
('circle.area = 23.4', "print('Setting circle.area fails')"),
('circle.circumference = 23.4', "print('Setting circle.circumference fails')"),
('circle.distance_to_origin = 23.4', "print('Setting circle.distance_to_origin fails')"),
('circle.z = 5.6', "print('Setting circle.z fails')"),
('print(circle.z)', "print('Printing circle.z fails')")]
for test in tests:
try:
exec(test[0])
except:
exec(test[1])
'''
With __setattr__
commented out, the testing code:
if __name__ == '__main__':
circle = Circle(x=3, y=4, r=5)
# print(circle.x)
print(circle.__getattr__('x'))
# print(circle.y)
print(circle.__getattr__('y'))
# print(circle.r)
print(circle.__getattr__('r'))
# print(circle.area)
print(circle.__getattr__('area'))
# print(circle.circumference)
print(circle.__getattr__('circumference'))
# print(circle.distance_to_origin)
print(circle.__getattr__('distance_to_origin'))
prints out:
__get if statement
3
__get if statement
4
__get if statement
5
__get if statement
78.53981633974483
__get if statement
31.41592653589793
__get if statement
0.0
Based on the discussion here, this is a shorter and improved version. Achieves the same as the original solution:
from math import pi, hypot, sqrt
class Circle:
def __init__(self, x, y, r):
self.x = x
self.y = y
super().__setattr__('r', r)
super().__setattr__('area', pi * self.r * self.r)
super().__setattr__('circumference', 2 * pi * self.r)
super().__setattr__('distance_to_origin',
abs(sqrt(self.x * self.x + self.y * self.y) - self.r))
def __setattr__(self, name, value):
if name in ['x', 'y']:
if isinstance(value, int):
print('we can set x,y')
super().__setattr__(name, value)
else: # value isn't an int
raise TypeError('Expected an int for: {}'.format(name))
else:
raise AttributeError('Cannot set attribute: {}'.format(name))
Avoiding __getattr__()
all together and using a flag self._intialized
to signal if the __init__()
was already run would work:
from math import pi, hypot, sqrt
'''
Circle class using __getattr__, and __setattr__ (rename circle2)
'''
# __getattr__(self, name): Automatically called when the attribute name
# is accessed and the object has no such attribute.
# __setattr__(self, name, value): Automatically called when an attempt is made to bind the attribute name to value.
class Circle:
def __init__(self, x, y, r):
self._intialized = False
self.x = x
self.y = y
self.r = r
self.area = pi * self.r * self.r
self.circumference = 2 * pi * self.r
self.distance_to_origin = abs(sqrt(self.x * self.x + self.y * self.y) - self.r)
self._intialized = True
def __setattr__(self, name, value):
if name in ['_intialized']:
self.__dict__[name] = value
return
if name in ['x', 'y']:
if isinstance(value, int):
print('we can set x,y')
self.__dict__[name] = value
else: # value isn't an int
raise TypeError('Expected an int for: {}'.format(name))
elif not self._intialized:
self.__dict__[name] = value
elif name in ['area', 'circumference', 'distance_to_origin']:
raise AttributeError('Cannot set attribute: {}'.format(name))
if __name__ == '__main__':
circle = Circle(x=3, y=4, r=5)
print('x:', circle.x)
print('y:', circle.y)
print('r:', circle.r)
print('area:', circle.area)
print('circumference:', circle.circumference)
print('distance_to_origin:', circle.distance_to_origin)
tests = [('circle.x = 12.3', "print('Setting circle.x to non-integer fails')"),
('circle.y = 23.4', "print('Setting circle.y to non-integer fails')"),
('circle.area = 23.4', "print('Setting circle.area fails')"),
('circle.circumference = 23.4', "print('Setting circle.circumference fails')"),
('circle.distance_to_origin = 23.4', "print('Setting circle.distance_to_origin fails')"),
('circle.z = 5.6', "print('Setting circle.z fails')"),
('print(circle.z)', "print('Printing circle.z fails')")]
for test in tests:
try:
exec(test[0])
except:
exec(test[1])
The output looks good:
python get_set_attr.py
we can set x,y
we can set x,y
x: 3
y: 4
r: 5
area: 78.53981633974483
circumference: 31.41592653589793
distance_to_origin: 0.0
Setting circle.x to non-integer fails
Setting circle.y to non-integer fails
Setting circle.area fails
Setting circle.circumference fails
Setting circle.distance_to_origin fails
Printing circle.z fails
This would allow setting an attribute with any other name:
circle.xyz = 100
But it would not be there:
circle.xyz
Traceback (most recent call last):
File "get_set_attr.py", line 62, in <module>
circle.xyz
AttributeError: 'Circle' object has no attribute 'xyz'
This implementation of __setattr__
would avoid this:
def __setattr__(self, name, value):
if name in ['_intialized']:
self.__dict__[name] = value
return
if name in ['x', 'y']:
if isinstance(value, int):
print('we can set x,y')
self.__dict__[name] = value
return
else: # value isn't an int
raise TypeError('Expected an int for: {}'.format(name))
elif not self._intialized:
self.__dict__[name] = value
else:
raise AttributeError('Cannot set attribute: {}'.format(name))
__getattr__()
?When you access an attribute that does not exist, Python raises an AttributeError
:
class A:
pass
a = A()
a.xyz
....
AttributeError: 'A' object has no attribute 'xyz'
Python calls __getattr__()
only if an attribute does not exist.
One use case is a wrapper around another object instead of using inheritance.
For example, we can define a ListWrapper
that uses an list but allows only white-listed attributes:
class ListWrapper:
_allowed_attrs = set(['append', 'extend'])
def __init__(self, value=None):
self._wrapped = list(value) if value is not None else []
def __getattr__(self, name):
if name in self._allowed_attrs:
return getattr(self._wrapped, name)
else:
raise AttributeError('No attribute {}.'.format(name))
def __repr__(self):
return repr(self._wrapped)
We can use it just like list:
>>> my_list = ListWrapper('abc')
>>> my_list
['a', 'b', 'c']
Append elements:
>>> my_list.append('x')
>>> my_list
['a', 'b', 'c', 'x']
But we cannot use any other attribute except the ones defined in _allowed_attrs
:
my_list.index('a')
...
AttributeError: No attribute index.
What the docs says:
object.__getattr__(self, name)
Called when an attribute lookup has not found the attribute in the usual places (i.e. it is not an instance attribute nor is it found in the class tree for self). name is the attribute name. This method should return the (computed) attribute value or raise an
AttributeError
exception.Note that if the attribute is found through the normal mechanism,
__getattr__()
is not called. (This is an intentional asymmetry between__getattr__()
and__setattr__()
.) This is done both for efficiency reasons and because otherwise__getattr__()
would have no way to access other attributes of the instance. Note that at least for instance variables, you can fake total control by not inserting any values in the instance attribute dictionary (but instead inserting them in another object). See the__getattribute__()
method below for a way to actually get total control over attribute access.