¿Getattr y setattr en objetos nesteds?

Este es probablemente un problema simple, así que espero que sea fácil para alguien señalar mi error o si esto es posible.

Tengo un objeto que tiene múltiples objetos como propiedades. Quiero poder establecer dinámicamente las propiedades de estos objetos así:

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__ 

La salida es:

{‘pet’: <objeto principal de .Pet a 0x10c5ec050>, ‘residence’: <objeto principal de residencia a 0x10c5ec0d0>, ‘pet.name’: ‘Sparky’, ‘residence.type’: ‘Apartment’}

Como puede ver, en lugar de tener el atributo de nombre establecido en el objeto de mascota de la persona, se crea un nuevo atributo “pet.name”.

No puedo especificar person.pet a setattr porque diferentes objetos secundarios se establecerán con el mismo método, que consiste en analizar un texto y completar los atributos del objeto si / cuando se encuentra una clave relevante.

¿Hay alguna forma fácil / incorporada para lograr esto?

¿O quizás necesito escribir una función recursiva para analizar la cadena y llamar a getattr varias veces hasta que se encuentre el objeto secundario necesario y luego llamar a setattr en ese objeto encontrado?

¡Gracias!

Podrías usar 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'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('.')) 

rgetattr y rsetattr son reemplazos getattr de getattr y setattr , que también pueden manejar cadenas de puntos punteadas.


 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 

Para un padre y un hijo:

 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__ 

Esto es más simple que las otras respuestas para este caso de uso en particular.

Hice una versión simple basada en la respuesta de ubntu llamada magicattr que también funciona en los atributos, listas y dados al analizar y caminar por el ast.

Por ejemplo, con esta clase:

 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]) 

Puedes hacerlo

 # 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 

Tampoco le permitirá a usted / alguien llamar a funciones o asignar un valor, ya que no usa eval o no permite nodos de asignación / llamada.

 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)') 

Ok, mientras escribía la pregunta, tenía una idea de cómo hacer esto y parece funcionar bien. Aquí está lo que se me ocurrió:

 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 

Supongo que esto resuelve mi pregunta, pero sigo sintiendo curiosidad por encontrar una mejor manera de hacerlo.

Siento que esto tiene que ser algo bastante común en OOP y python, así que estoy sorprendido de que gatattr y setattr no lo admitan de forma nativa.

La respuesta de unutbu ( https://stackoverflow.com/a/31174427/2683842 ) tiene un “error”. Después de que getattr() falla y se reemplaza de default , continúa llamando a getattr en default .

Ejemplo: rgetattr(object(), "nothing.imag", 1) debería ser igual a 1 en mi opinión, pero devuelve 0 :

  • getattr(object(), 'nothing', 1) == 1.
  • getattr(1, 'imag', 1) == 0 (ya que 1 es real y no tiene un componente complejo).

Solución

Modifiqué rgetattr para devolver el default en el primer atributo faltante:

 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