Search code examples
pythonpython-3.xgetattrsetattr

Using __getattr__() and __setattr__() to provide multiple ways to access child variables


Esteemed colleagues!

Please consider...

#!/usr/bin/env python3.6

class Child(object):
  def __init__(self, name:str, value = 0):
    self.name = name;
    self.value = value;
  def __repr__(self):
    return '  child "{}" has value {}\n'.format(self.name, self.value)

class Parent(object):
  def __init__(self, name:str):
    self.name = name
    self.children = {}

  def __repr__(self):
    s = 'Parent "{}":\n'.format(self.name)
    for k, v in self.children.items():
      s += v.__repr__()
    return s

  def __getattr__(self, name:str):
    if name == 'children':
      return self.children;
    elif name in self.children.keys():
      return self.children[name].value
    else:
      return super().__getattr__(name)

  def __setattr__(self, prop, val):
    if prop == 'name':
      super().__setattr__(prop, val)
    else:
      super().__setattr__('children[{}]'.format(prop), val)


p = Parent('Tango')
p.children['Alfa'] = Child('Alfa', 55)
p.children['Bravo'] = Child('Bravo', 66)
print(p)
print(p.children['Alfa'])  # Returns '55' (normal)
print(p.Alfa)              # Returns '55' (__getattr__)

print('-----')
p.Alfa = 99    # This is creating a new variable, need __setattr__ ... 
print(p.Alfa)  # Prints new variable. __setattr__ is not called!
print('-----')

print(p.children['Alfa'])  # Still '55'
print(p)                   # Still '55'

The intent of the code is to allow those holding a Parent to access its children in TWO ways: p.children['Alfa'] or p.Alfa.

Using __getattr__() I can accomplish the read-side of this. (Comment out the def __setattr__() in the code above and you can see the read-side work as expected.) Output without setattr() is:

Parent "Tango":
  child "Alfa" has value 55
  child "Bravo" has value 66

  child "Alfa" has value 55

55
-----
99
-----
  child "Alfa" has value 55

Parent "Tango":
  child "Alfa" has value 55
  child "Bravo" has value 66

Of course now I need __setattr__() to accomplish the write-side of this. I want 99 to be assigned to Alfa. As of now, haven't figured out the incantation to avoid a variety of issues.

The code above with setattr() raises RecursionError:

Traceback (most recent call last):
  File "./test.py", line 37, in <module>
    p.children['Alfa'] = Child('Alfa', 55)
  File "./test.py", line 23, in __getattr__
    return self.children;
  File "./test.py", line 23, in __getattr__
    return self.children;
  File "./test.py", line 23, in __getattr__
    return self.children;
  [Previous line repeated 328 more times]
  File "./test.py", line 22, in __getattr__
    if name == 'children':
RecursionError: maximum recursion depth exceeded in comparison

I expect the test code that follows to show that by writing to p.Alfa, that I am actually updating the value of self.children['Alfa']. After the assignment, 99 should appear when I print it, not the original 55.

Note that in the real world, there is a nearly infinite number of possible children, their names, and their contents.

Your help and insight is appreciated!


Solution

  • def __setattr__(self, attr, val):
        if attr == 'name':
            super().__setattr__(self, 'name', val)
            return
    
        self.children[attr] = val
    

    Your code never initializes an attribute named children. It initializes one named children[children]. As a result, when you later try to assign to self.children['Alfa'], the first thing it tries to do is find an attribute named children. When it isn't found, it calls __getattr__, which says that when name == "children", it should return self.children, and the infinite loop begins.

    __getattr__ is only called when an attribute is not found via the normal process. With __setattr__ defined correctly, __getattr__ should never be called for children, since you define it in __init__. The definition can be reduced to

    def __getattr__(self, name:str):
        if name in self.children.keys():
            return self.children[name].value
        else:
            return super().__getattr__(name)