Search code examples
pythonpython-3.xgetattrsetattrpython-descriptors

Using setattr() and getattr() in Python descriptors


While trying to create descriptors a few different ways I noticed some strange behavior that I'm trying to understand. Below are the three different ways I have gone about creating descriptors:

>>> class NumericValueOne():
...     def __init__(self, name):
...         self.name = name
...     def __get__(self, obj, type=None) -> object:
...         return obj.__dict__.get(self.name) or 0
...     def __set__(self, obj, value) -> None:
...         obj.__dict__[self.name] = value
>>> class NumericValueTwo():
...     def __init__(self, name):
...         self.name = name
...         self.internal_name = '_' + self.name
...     def __get__(self, obj, type=None) -> object:
...         return getattr(obj, self.internal_name, 0)
...     def __set__(self, obj, value) -> None:
...         setattr(obj, self.internal_name, value)
>>> class NumericValueThree():
...     def __init__(self, name):
...         self.name = name
...     def __get__(self, obj, type=None) -> object:
...         return getattr(obj, self.name, 0)
...     def __set__(self, obj, value) -> None:
...         setattr(obj, self.name, value)

I then use them in the Foo classes, like below:

>>> class FooOne():
...     number = NumericValueOne("number")

>>> class FooTwo():
...     number = NumericValueTwo("number")

>>> class FooThree():
...     number = NumericValueThree("number")

my_foo_object_one = FooOne()
my_foo_object_two = FooTwo()
my_foo_object_three = FooThree()

my_foo_object_one.number = 3
my_foo_object_two.number = 3
my_foo_object_three.number = 3

While FooOne and FooTwo work as expected when both setting & getting values. FooThree throws the following error:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __set__
  File "<stdin>", line 7, in __set__
  File "<stdin>", line 7, in __set__
  [Previous line repeated 497 more times]
RecursionError: maximum recursion depth exceeded while calling a Python object

It looks like setattr() is calling the __set__() method? But why should it be doing that if setattr() is modifying the obj __dict__? And why does this work if we use internal_name?

Why is that we NEED to use a private variable in order to use the built-in getattr() and setattr() methods correctly? Also, how is this different from just directly modifying the obj __dict__ like in NumericValueOne?


Solution

  • But why should it be doing that if setattr() is modifying the obj __dict__?

    setattr doesn't just modify the __dict__. It sets attributes, exactly like x.y = z would, and for the attribute you're trying to set, "set this attribute" means "call the setter you're already in". Hence, infinite recursion.

    And why does this work if we use internal_name?

    That name doesn't correspond to a property, so it just gets a __dict__ entry.