Skip to content Skip to sidebar Skip to footer

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

Solution 1:

You could use functools.reduce:

import functools

defrsetattr(obj, attr, val):
    pre, _, post = attr.rpartition('.')
    returnsetattr(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_31174427defrgetattr(obj, attr, *args):
    def_getattr(obj, attr):
        returngetattr(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

classPerson(object):
    def__init__(self):
        self.pet = Pet()
        self.residence = Residence()

classPet(object):
    def__init__(self,name='Fido',species='Dog'):
        self.name = name
        self.species = species

classResidence(object):
    def__init__(self,type='House',sqft=None):
        self.type = type
        self.sqft=sqft

defrsetattr(obj, attr, val):
    pre, _, post = attr.rpartition('.')
    returnsetattr(rgetattr(obj, pre) if pre else obj, post, val)

defrgetattr(obj, attr, *args):
    def_getattr(obj, attr):
        returngetattr(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 missingprint(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)
    # Sparkyprint(p.residence.type)
    # Apartment

Solution 2:

For an out of the box solution, you can use operator.attrgetter:

from operator import attrgetter
attrgetter(dotted_path)(obj)

Solution 3:

For one parent and one child:

if __name__=='__main__':
    p = Person()

    parent, child = 'pet.name'.split('.')
    setattr(getattr(p, parent), child, 'Sparky')

    parent, child = 'residence.type'.split('.')
    setattr(getattr(p, parent), child, 'Sparky')

    print p.__dict__

This is simpler than the other answers for this particular use case.

Solution 4:

unutbu's answer (https://stackoverflow.com/a/31174427/2683842) has a "bug". After getattr() fails and is replaced by default, it continues calling getattr on default.

Example: rgetattr(object(), "nothing.imag", 1) should equal 1 in my opinion, but it returns 0:

  • getattr(object(), 'nothing', 1) == 1.
  • getattr(1, 'imag', 1) == 0 (since 1 is real and has no complex component).

Solution

I modified rgetattr to return default at the first missing attribute:

import functools

DELIMITER = "."defrgetattr(obj, path: str, *default):
    """
    :param obj: Object
    :param path: 'attr1.attr2.etc'
    :param default: Optional default value, at any point in the path
    :return: obj.attr1.attr2.etc
    """

    attrs = path.split(DELIMITER)
    try:
        return functools.reduce(getattr, attrs, obj)
    except AttributeError:
        if default:
            return default[0]
        raise

Solution 5:

I made a simple version based on ubntu's answer called magicattr that also works on attrs, lists, and dicts by parsing and walking the ast.

For example, with this class:

classPerson:
    settings = {
        'autosave': True,
        'style': {
            'height': 30,
            'width': 200
        },
        'themes': ['light', 'dark']
    }
    def__init__(self, name, age, friends):
        self.name = name
        self.age = age
        self.friends = friends


bob = Person(name="Bob", age=31, friends=[])
jill = Person(name="Jill", age=29, friends=[bob])
jack = Person(name="Jack", age=28, friends=[bob, jill])

You can do this

# Nothing new
assert magicattr.get(bob, 'age') == 31# Lists
assert magicattr.get(jill, 'friends[0].name') == 'Bob'
assert magicattr.get(jack, 'friends[-1].age') == 29# Dict lookups
assert magicattr.get(jack, 'settings["style"]["width"]') == 200# Combination of lookups
assert magicattr.get(jack, 'settings["themes"][-2]') == 'light'
assert magicattr.get(jack, 'friends[-1].settings["themes"][1]') == 'dark'# Setattr
magicattr.set(bob, 'settings["style"]["width"]', 400)
assert magicattr.get(bob, 'settings["style"]["width"]') == 400# Nested objects
magicattr.set(bob, 'friends', [jack, jill])
assert magicattr.get(jack, 'friends[0].friends[0]') == jack

magicattr.set(jill, 'friends[0].age', 32)
assert bob.age == 32

It also won't let you/someone call functions or assign a value since it doesn't use eval or allow Assign/Call nodes.

with pytest.raises(ValueError) as e:
    magicattr.get(bob, 'friends = [1,1]')

# Nice try, function calls are not allowed
with pytest.raises(ValueError):
    magicattr.get(bob, 'friends.pop(0)')

Post a Comment for "Getattr And Setattr On Nested Subobjects / Chained Properties?"