2015-07-02 12 views
18

questo è probabilmente un problema semplice, quindi speriamo sia facile per qualcuno segnalare il mio errore o se è possibile.getattr e setattr su oggetti annidati?

Ho un oggetto che ha più oggetti come proprietà. Voglio essere in grado di impostare dinamicamente le proprietà di questi oggetti in questo modo:

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__ 

L'output è:

{ 'pet': < principale .pet oggetto in 0x10c5ec050>, 'residenza' : < principale oggetto .Residence a 0x10c5ec0d0>, 'pet.name': 'Sparky', 'residence.type': 'Apartment'}

Come si può vedere, piuttosto che avere l'attributo name impostato sul oggetto domestico della persona, un nuovo attributo "pet.name" è crea ted.

Non riesco a specificare person.pet su setattr perché diversi oggetti figlio verranno impostati con lo stesso metodo, che sta analizzando del testo e compilando gli attributi dell'oggetto se/quando viene trovata una chiave pertinente.

Esiste un modo facile/integrato per raggiungere questo obiettivo?

O forse ho bisogno di scrivere una funzione ricorsiva per analizzare la stringa e chiamare getattr più volte finché non viene trovato l'oggetto figlio necessario e quindi chiamare setattr su quell'oggetto trovato?

Grazie!

risposta

28

Potreste usare functools.reduce:

import functools 

def rsetattr(obj, attr, val): 
    pre, _, post = attr.rpartition('.') 
    return setattr(rgetattr(obj, pre) if pre else obj, post, val) 

sentinel = object() 
def rgetattr(obj, attr, default=sentinel): 
    if default is sentinel: 
     _getattr = getattr 
    else: 
     def _getattr(obj, name): 
      return getattr(obj, name, default) 
    return functools.reduce(_getattr, [obj]+attr.split('.')) 

rgetattr e rsetattr sono drop-in sostituzioni per getattr e setattr, che può anche gestire tratteggiate attr stringhe.


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) 

sentinel = object() 
def rgetattr(obj, attr, default=sentinel): 
    if default is sentinel: 
     _getattr = getattr 
    else: 
     def _getattr(obj, name): 
      return getattr(obj, name, default) 
    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 
+0

Wow era qualcosa che non sapevo. –

+1

Grazie per la risposta! Questo è bello, mi piace come puoi fare in 5 righe quello che mi ha portato 30. Soluzione molto ordinata e concisa! – TPB

+1

Bella risposta! –

1

Ok così mentre scrivevo la domanda avevo un'idea di come fare questo e sembra funzionare bene. Ecco quello che mi si avvicinò con:

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 

Credo che questo risolva la mia domanda, ma io sono ancora curioso di sapere se c'è un modo migliore per fare questo?

Mi sembra che questo debba essere qualcosa di abbastanza comune in OOP e Python, quindi sono sorpreso che gatattr e setattr non supportino questo in modo nativo.

+0

Il nome dell'ultimo attributo, 'final_attribute' può verificarsi più di una volta. Ad esempio, "p.foo.foo" è legale. Quindi la condizione 'part == final_attribute' potrebbe attivarsi troppo presto. – unutbu

+0

Ahh, ottimo punto, ho cambiato il controllo dell'indice, che dovrebbe risolvere il problema.Grazie per aver segnalato questo! – TPB

1

Per un genitore e un figlio:

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__ 

Questo è più semplice rispetto alle altre risposte per questo particolare caso d'uso.

Problemi correlati