3

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 
6
  • 1
    if name == ... should almost certainly be if name in ..., and if value is int: should almost certainly be if isinstance(value, int):. Not a cause of a recursion error, but this is terrible code all around. Commented Jan 1, 2016 at 7:28
  • 1
    Also, using getattr inside __getattr__ without having set anything on the instance (so it's still not going to be an existing value) should cause recursion errors by itself. Commented Jan 1, 2016 at 7:29
  • @ShadowRanger I've been going between in and == and this version happened to still have the ==, but I've since changed it. Can you repeat your second comment, I keep rereading it but I don't follow what you're saying. Commented Jan 1, 2016 at 7:36
  • 2
    getattr(x, 'abc') goes through the exact same code path as x.abc. If x.abc doesn't exist, the __getattr__ is invoked to try to satisfy the missing attribute (so you know the attribute doesn't exist if you're in __getattr__ in the first place). If the "can't find attribute handler" then turns around and, without modification, asks for the same attribute on itself, the end result is predictable. You can't just ask for it again without having set the value; the attribute still isn't there. Commented Jan 1, 2016 at 7:42
  • @ShadowRanger Okay, so __getattr__ should only be called if I'm trying to access an attribute that doesn't exist? So if I were to do circle.test then __getattr__ would be run to try and find the attribute test? Commented Jan 1, 2016 at 7:55

2 Answers 2

1

Improved solution

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)) 

Solution

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 

Variation

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)) 

When to use __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.

Sign up to request clarification or add additional context in comments.

4 Comments

Why would it be better to avoid using __getattr__ altogether? Wouldn't you need it if you were to do circle.test (an attribute of circle that doesn't exist) to return that that attribute doesn't exit?
Python dos this for you. Try circle.test on my Variation. It raises AttributeError: 'Circle' object has no attribute 'test' . No need to implement it yourself.
so when would it be necessary to implement both __getattr__ and __setattr__? I'm thinking you'd want to implement __setattr__ as a checker almost so that user's can't implement "invalid" values for an attribute, but what about __getattr__?
There is nothing wrong with the idea behind the use of __getattr__() and __setattr__() in the OP. You use them when you want to intercept attribute access and assignment to do any number of things. This is a fairly usual technique to implement a lot of interesting functionality (e.g. ORM). There were just a couple of small bugs in the OP. See my post below. The solution here though seems to use avoidance instead of actually solving the real problem.
1

You may be interested in the couple of problems you had in your code that were causing the problem.

You cannot set the following directly in __init__() because the assignments trigger a call to __setattr__(), which sets only x and y. Because of this, these attributes were never set.

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) 

You are not checking for r in __setattr__(). This caused r to be ignored silently, and then when r was accessed to set area in __init__(), __getattr__() called getattr() called __getattr__() called getattr() and so on (because r was not set), which caused the recursion.

elif name in ['area', 'circumference', 'distance_to_origin']: raise RuntimeError('Cannot set attribute') 

Here is fixed code. Changes have been marked below with mod in comments.

#!/usr/bin/python3 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 # mod : can't set via self.__getattr__ 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 - 0)*(self.x - 0) + (self.y - 0) * (self.y - 0)) - self.r)) def __getattr__(self, name): print("===== get:", 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("===== set:", name, value) if name in ['x', 'y']: if isinstance(value, int): print('we can set x,y') super().__setattr__(name, value) # mod : better else: # value isn't an int raise TypeError('Expected an int') elif name in ['r', 'area', 'circumference', 'distance_to_origin']: # mod : add 'r' 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]) ''' 

3 Comments

While circle.__getattr__('x') shows the text ===== get: x __get if statement, circle.x does not. So __getattr__() is not called for the typical attribute access. So it does not make much sense to have it in the first place.
__getattr__() is necessary to implement access to virtual or computed attributes. These attributes never exist for real, and their value is obtained by some other means (e.g. computation). When you do need to intercept any access, whether the attribute exists or not, you would use __getattribute__() instead. Its good to have either in your toolbox.
I know this. But in this case self.x exits. It is not computed and self.x does not call __getattr__(). The same is true for y, r, area, circumference, and distance_to_origin. Hence, no need to implement __getattr__() the way its here at all. You don't gain anything in addition to Pythons default behavior.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.