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':
如您所见,与其在人的宠物对象上设置name属性,不如创建一个新的属性"pet.name"。
我不能将person.pet指定为setattr,因为不同的子对象将由相同的方法设置,即如果/当找到相关键时,解析一些文本并填充对象属性。
是否有一种简单/内置的方式来完成这一点?
或者我可能需要编写一个递归函数来解析字符串并多次调用getattr,直到找到必要的子对象,然后对找到的对象调用setattr?
谢谢您!
您可以使用
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('.')) |
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(object(), 'nothing', 1) ==1.getattr(1, 'imag', 1) ==0(因为1是实的,没有复杂的成分)。
解决方案
我修改了rgettr,在第一个缺少的属性时返回
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在本地不支持这一点。