Getattr и setattr для вложенных подобъектов/связанных свойств?
У меня есть объект (Person
), который имеет несколько подобъектов (Pet, Residence
) в качестве свойств. Я хочу иметь возможность динамически устанавливать свойства этих подобъектов, например, так:
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': <__main__.Pet object at 0x10c5ec050>, 'residence': <__main__.Residence object at 0x10c5ec0d0>, 'pet.name': 'Sparky', 'residence.type': 'Apartment'}
Как видите, вместо установки атрибута name
в подобъекте Pet
в Person
в Person
создается новый атрибут pet.name
.
Я не могу указать от person.pet
до setattr()
, потому что разные подобъекты будут установлены одним и тем же методом, который анализирует некоторый текст и заполняет атрибуты объекта, если/когда найден соответствующий ключ.
Есть ли простой/встроенный способ сделать это?
Или, возможно, мне нужно написать рекурсивную функцию для анализа строки и вызова getattr()
несколько раз, пока не будет найден нужный подобъект, а затем вызвать setattr()
для этого найденного подобъекта?
Ответы
Ответ 1
Вы можете использовать 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 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
и rsetattr
являются rsetattr
заменами для getattr
и setattr
, которые также могут обрабатывать точечные строки attr
.
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
Ответ 2
Для одного родителя и одного ребенка:
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__
Это проще, чем другие ответы для этого конкретного случая использования.
Ответ 3
Я сделал простую версию, основанную на ответе ubntu, называемом magicattr, который также работает с attrs, списками и диктовками, анализируя и обходя ast.
Например, с этим классом:
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])
Вы можете сделать это
# 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 и не разрешает узлы Assign/Call.
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)')
Ответ 4
Ответ unutbu (fooobar.com/questions/294712/...) содержит "ошибку". После getattr()
и его замены по default
он продолжает вызывать getattr
по default
.
Пример: rgetattr(object(), "nothing.imag", 1)
должен быть равен 1
на мой взгляд, но возвращает 0
:
-
getattr(object(), 'nothing', 1)
== 1. -
getattr(1, 'imag', 1)
== 0 (поскольку 1 является действительным и не имеет сложного компонента).
Решение
Я изменил rgetattr, чтобы он возвращал default
по default
для первого отсутствующего атрибута:
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
Ответ 5
Хорошо, поэтому, набрав вопрос, у меня возникла идея, как это сделать, и, похоже, он работает нормально. Вот что я придумал:
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
Я думаю, это решает мой вопрос, но мне все еще интересно, есть ли лучший способ сделать это?
Я чувствую, что это должно быть что-то довольно распространенное в ООП и питоне, поэтому я удивлен, что gatattr и setattr не поддерживают это изначально.
Ответ 6
Я просто люблю рекурсивные функции
def rgetattr(obj,attr):
_this_func = rgetattr
sp = attr.split('.',1)
if len(sp)==1:
l,r = sp[0],''
else:
l,r = sp
obj = getattr(obj,l)
if r:
obj = _this_func(obj,r)
return obj
Ответ 7
И простой в понимании трехслойный ответ, основанный на ответе jimbo1qaz, сведенный до самого предела:
def rgetattr(obj, path, default):
try:
return functools.reduce(getattr, path.split(), obj)
except AttributeError:
return default
Использование:
>>> class O(object):
... pass
... o = O()
... o.first = O()
... o.first.second = O()
... o.first.second.third = 42
... rgetattr(o, 'first second third', None)
42
Просто помните, что "пробел" не является типичным разделителем для этого варианта использования.