关于python:嵌套对象上的getattr和setattr?

getattr and setattr on nested objects?

这可能是一个简单的问题,所以希望有人能很容易地指出我的错误,或者这是可能的。

我有一个对象有多个对象作为属性。我希望能够像这样动态地设置这些对象的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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__

输出是:

'pet':,'residence':,'pet.name':'sparky','residence.type':'apartment'

如您所见,与其在人的宠物对象上设置name属性,不如创建一个新的属性"pet.name"。

我不能将person.pet指定为setattr,因为不同的子对象将由相同的方法设置,即如果/当找到相关键时,解析一些文本并填充对象属性。

是否有一种简单/内置的方式来完成这一点?

或者我可能需要编写一个递归函数来解析字符串并多次调用getattr,直到找到必要的子对象,然后对找到的对象调用setattr?

谢谢您!


您可以使用functools.reduce

1
2
3
4
5
6
7
8
9
10
11
12
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('.'))

rgetattrrsetattrgetattrsetattr的替代品。它还可以处理点状的attr字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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('.'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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


对于一个父母和一个孩子:

1
2
3
4
5
6
7
8
9
10
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__

这比这个特定用例的其他答案要简单。


我根据ubntu的答案magicattr制作了一个简单的版本,通过解析和遍历ast,它还可以处理attr、list和dict。

例如,对于此类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person:
    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])

你可以做到这一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 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

它也不允许您/某人调用函数或分配值,因为它不使用eval或允许分配/调用节点。

1
2
3
4
5
6
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)')

UNUTBU的答案(https://stackoverflow.com/a/31174427/2683842)有一个"错误"。在getattr()失败并被default取代后,继续在default上呼叫getattr

例:我认为rgetattr(object(),"nothing.imag", 1)等于1,但返回0

  • getattr(object(), 'nothing', 1)==1.
  • getattr(1, 'imag', 1)==0(因为1是实的,没有复杂的成分)。

解决方案

我修改了rgettr,在第一个缺少的属性时返回default

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import functools

DELIMITER ="."

def rgetattr(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


好吧,所以当我打字的时候,我有一个如何做的主意,它似乎工作得很好。我想到的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def set_attribute(obj, path_string, new_value):
    parts = path_string.split('.')
    final_attribute_index = len(parts)-1
    current_attribute = obj
    i = 0
    for part in parts:
        new_attr = getattr(current_attribute, part, None)
        if current_attribute is None:
            print 'Error %s not found in %s' % (part, current_attribute)
            break
        if i == final_attribute_index:
            setattr(current_attribute, part, new_value)
        current_attribute = new_attr
        i+=1


def get_attribute(obj, path_string):
    parts = path_string.split('.')
    final_attribute_index = len(parts)-1
    current_attribute = obj
    i = 0
    for part in parts:
        new_attr = getattr(current_attribute, part, None)
        if current_attribute is None:
            print 'Error %s not found in %s' % (part, current_attribute)
            return None
        if i == final_attribute_index:
            return getattr(current_attribute, part)
        current_attribute = new_attr
        i += 1

我想这解决了我的问题,但我仍然好奇是否有更好的方法来做到这一点?

我觉得这在oop和python中是很常见的,所以我很惊讶gatattr和setattr在本地不支持这一点。