6

I want to dynamically query which objects from a class I would like to retrieve. getattr seems like what I want, and it performs fine for top-level objects in the class. However, I'd like to also specify sub-elements.

class MyObj(object):
    def __init__(self):
        self.d = {'a':1, 'b':2}
        self.c = 3

myobj = MyObj()
val = getattr(myobj, "c")
print val # Correctly prints 3
val = getattr(myobj, "d['a']") # Seemingly incorrectly formatted query
print val # Throws an AttributeError

How can I get the object's dictionary elements via a string?

user394430
  • 2,805
  • 2
  • 28
  • 27
  • `getattr(myobj, "d")['a']` would do it but I guess that is not what you want. – Felix Kling Feb 01 '12 at 18:21
  • No, it is not what I want. I want the technique to be generic so that I can ask for an attribute of an object or subattribute/subobject/subitem of the object with a dynamically constructed string. I can do this with "eval", but I'd rather not. – user394430 Feb 02 '12 at 02:09
  • You're gonna have to create some complex function that parses the string, which will essentially be a slimmed down version of "eval". Why do you need this btw? Sounds like there should be a better solution... – Luiz C. Feb 02 '12 at 20:13

4 Answers4

9

The reason you're getting an error is that getattr(myobj, "d['a']") looks for an attribute named d['a'] on the object, and there isn't one. Your attribute is named d and it's a dictionary. Once you have a reference to the dictionary, then you can access items in it.

mydict = getattr(myobj, "d")
val    = mydict["a"]

Or as others have shown, you can combine this in one step (I showed it as two to better illustrate what is actually happening):

val = getattr(myobj, "d")["a"]

Your question implies that you think that items of a dictionary in an object are "sub-elements" of the object. An item in a dictionary, however, is a different thing from an attribute of an object. (getattr() wouldn't work with something like o.a either, though; it just gets one attribute of one object. If that's an object too and you want to get one of its attributes, that's another getattr().)

You can pretty easily write a function that walks an attribute path (given in a string) and attempts to resolve each name either as a dictionary key or an attribute:

def resolve(obj, attrspec):
    for attr in attrspec.split("."):
        try:
            obj = obj[attr]
        except (TypeError, KeyError):
            obj = getattr(obj, attr)
    return obj

The basic idea here is that you take a path and for each component of the path, try to find either an item in a dictionary-like container or an attribute on an object. When you get to the end of the path, return what you've got. Your example would be resolve(myobj, "d.a")

kindall
  • 178,883
  • 35
  • 278
  • 309
  • Thanks for the comment. You understand the issue exactly, but I want the technique to be generic so that I can ask for an attribute of an object or subattribute/subobject/subitem of the object with a dynamically constructed string. I can do this with "eval", but I'd rather not. Any other ideas? – user394430 Feb 02 '12 at 02:11
3

You simply use square brackets to get the dictionary's element:

val = getattr(myobj, "d")["a"]

That'll set val to 1.

NPE
  • 486,780
  • 108
  • 951
  • 1,012
  • Thanks, but this is not what I want. (Copied from above) I want the technique to be generic so that I can ask for an attribute of an object or subattribute/subobject/subitem of the object with a dynamically constructed string. I can do this with "eval", but I'd rather not. – user394430 Feb 02 '12 at 02:09
2

If you need the dictionary item to be dynamic as well, you'll need to call get on the result of getattr:

value = getattr(myobj, 'd').get('a')
Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
1

Thanks to Kindall's answer, I found the following works well for dict keys that are stings.

class Obj2(object):
    def __init__(self):
        self.d = {'a':'A', 'b':'B', 'c': {'three': 3, 'twothree': (2,3)}}
        self.c = 4

class MyObj(object):
    def __init__(self):
        self.d = {'a':1, 'b':2, 'c': {'two': 2, 'onetwo': (1,2)}}
        self.c = 3
        self.obj2 = Obj2()

    def resolve(self, obj, attrspec):
        attrssplit = attrspec.split(".")
        attr = attrssplit[0]
        try:
            obj = obj[attr]
        except (TypeError, KeyError):
            obj = getattr(obj, attr)
        if len(attrssplit) > 1:
            attrspec = attrspec.partition(".")[2] # right part of the string.
            return self.resolve(obj, attrspec) # Recurse
        return obj

    def __getattr__(self, name):
        return self.resolve(self, name)

# Test  
myobj = MyObj()
print getattr(myobj, "c")
print getattr(myobj, "d.a")
print getattr(myobj, "d.c.two")
print getattr(myobj, "obj2.d.a")
print getattr(myobj, "obj2.d.c.twothree")
user394430
  • 2,805
  • 2
  • 28
  • 27