c'est probablement un problème simple, alors espérons qu'il sera facile pour quelqu'un de signaler mon erreur ou même si cela est possible.
J'ai un objet qui a plusieurs objets en tant que propriétés. Je veux pouvoir définir dynamiquement les propriétés de ces objets comme suit:
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 sortie est:
{'animal': < main . Objet animalier à 0x10c5ec050>, 'résidence': < principal . Objet résidentiel à 0x10c5ec0d0>, 'animal.name': 'Sparky', 'residence.type' : 'Appartement'}
Comme vous pouvez le constater, plutôt que de définir l'attribut name sur l'objet animal de la personne, un nouvel attribut "pet.name" est créé.
Je ne peux pas spécifier personne.pet à setattr car différents objets enfants seront définis par la même méthode, qui consiste à analyser du texte et à renseigner les attributs de l'objet si/quand une clé pertinente est trouvée.
Existe-t-il un moyen facile/intégré de réaliser cela?
Ou peut-être ai-je besoin d'écrire une fonction récursive pour analyser la chaîne et appeler getattr plusieurs fois jusqu'à ce que l'objet enfant nécessaire soit trouvé, puis d'appeler setattr sur cet objet trouvé?
Je vous remercie!
Vous pouvez utiliser 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
et rsetattr
sont des remplacements instantanés pour getattr
et setattr
, qui peuvent également gérer des chaînes en pointillé 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
Pour un parent et un enfant:
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__
C'est plus simple que les autres réponses pour ce cas d'utilisation particulier.
J'ai créé une version simple basée sur la réponse d'ubntu appelée magicattr qui fonctionne également sur les attributs, les listes et les dictionnaires en analysant et en guidant les astuces.
Par exemple, avec cette classe:
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])
Tu peux le faire
# 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
Cela ne vous laissera pas non plus, vous ou quelqu'un, appeler des fonctions ou attribuer une valeur car il n'utilise ni les nœuds eval, ni les nœuds 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)')
Ok, donc en tapant la question, j'avais une idée de la façon de procéder et cela semble bien fonctionner. Voici ce que je suis venu avec:
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
Je suppose que cela résout ma question, mais je suis toujours curieux de savoir s’il existe une meilleure façon de procéder.
Je pense que cela doit être quelque chose d'assez courant dans OOP et en python. Je suis donc surpris que gatattr et setattr ne le supportent pas nativement.
la réponse de unutbu ( https://stackoverflow.com/a/31174427/2683842 ) a un "bogue". Une fois que getattr()
a échoué et est remplacé par default
, il continue à appeler getattr
sur default
.
Exemple: rgetattr(object(), "nothing.imag", 1)
devrait être égal à 1
à mon avis, mais il renvoie 0
:
getattr(object(), 'nothing', 1)
== 1.getattr(1, 'imag', 1)
== 0 (puisque 1 est réel et n’a aucun composant complexe).J'ai modifié rgetattr pour retourner default
au premier attribut manquant:
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