web-dev-qa-db-fra.com

getattr et setattr sur des objets imbriqués?

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!

23
TPB

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
43
unutbu

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.

5
ChaimG

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

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.

1
TPB

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

Solution

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
0
jimbo1qaz