Skip to content
Advertisement

getattr and setattr on nested subobjects / chained properties?

I have an object (Person) that has multiple subobjects (Pet, Residence) as properties. I want to be able to dynamically set the properties of these subobjects like so:

class Person(object):
    def __init__(self):
        self.pet = Pet()
        self.residence = Residence()

class Pet(object):
    def __init__(self,name='Fido',species='Dog'):
        self.name = name
        self.species = species

class Residence(object):
    def __init__(self,type='House',sqft=None):
        self.type = type
        self.sqft=sqft


if __name__=='__main__':
    p=Person()
    setattr(p,'pet.name','Sparky')
    setattr(p,'residence.type','Apartment')
    print p.__dict__

Currently I get the wrong output: {'pet': <__main__.Pet object at 0x10c5ec050>, 'residence': <__main__.Residence object at 0x10c5ec0d0>, 'pet.name': 'Sparky', 'residence.type': 'Apartment'}

As you can see, instead of setting the name attribute on the Pet subobject of the Person, a new attribute pet.name is created on the Person.

  • I cannot specify person.pet to setattr() because different sub-objects will be set by the same method, which parses some text and fills in the object attributes if/when a relevant key is found.

  • Is there a easy/builtin way to accomplish this?

  • Or perhaps I need to write a recursive function to parse the string and call getattr() multiple times until the necessary subobject is found and then call setattr() on that found subobject?

Advertisement

Answer

You could use functools.reduce:

import functools

def rsetattr(obj, attr, val):
    pre, _, post = attr.rpartition('.')
    return setattr(rgetattr(obj, pre) if pre else obj, post, val)

# using wonder's beautiful simplification: https://stackoverflow.com/questions/31174295/getattr-and-setattr-on-nested-objects/31174427?noredirect=1#comment86638618_31174427

def rgetattr(obj, attr, *args):
    def _getattr(obj, attr):
        return getattr(obj, attr, *args)
    return functools.reduce(_getattr, [obj] + attr.split('.'))

rgetattr and rsetattr are drop-in replacements for getattr and setattr, which can also handle dotted attr strings.


import functools

class Person(object):
    def __init__(self):
        self.pet = Pet()
        self.residence = Residence()

class Pet(object):
    def __init__(self,name='Fido',species='Dog'):
        self.name = name
        self.species = species

class Residence(object):
    def __init__(self,type='House',sqft=None):
        self.type = type
        self.sqft=sqft

def rsetattr(obj, attr, val):
    pre, _, post = attr.rpartition('.')
    return setattr(rgetattr(obj, pre) if pre else obj, post, val)

def rgetattr(obj, attr, *args):
    def _getattr(obj, attr):
        return getattr(obj, attr, *args)
    return functools.reduce(_getattr, [obj] + attr.split('.'))

if __name__=='__main__':
    p = Person()
    print(rgetattr(p, 'pet.favorite.color', 'calico'))
    # 'calico'

    try:
        # Without a default argument, `rgetattr`, like `getattr`, raises
        # AttributeError when the dotted attribute is missing
        print(rgetattr(p, 'pet.favorite.color'))
    except AttributeError as err:
        print(err)
        # 'Pet' object has no attribute 'favorite'

    rsetattr(p, 'pet.name', 'Sparky')
    rsetattr(p, 'residence.type', 'Apartment')
    print(p.__dict__)
    print(p.pet.name)
    # Sparky
    print(p.residence.type)
    # Apartment
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement