Come puoi impostare gli attributi di classe da argomenti variabili (kwargs) in Python


119

Supponiamo di avere una classe con un costruttore (o un'altra funzione) che accetta un numero variabile di argomenti e quindi li imposta come attributi di classe in modo condizionale.

Potrei impostarli manualmente, ma sembra che i parametri variabili siano abbastanza comuni in Python che dovrebbe esserci un idioma comune per farlo. Ma non sono sicuro di come farlo dinamicamente.

Ho un esempio che usa eval, ma non è sicuro. Voglio sapere il modo corretto per farlo, forse con lambda?

class Foo:
    def setAllManually(self, a=None, b=None, c=None):
        if a!=None: 
            self.a = a
        if b!=None:
            self.b = b
        if c!=None:
            self.c = c
    def setAllWithEval(self, **kwargs):
        for key in **kwargs:
            if kwargs[param] != None
                eval("self." + key + "=" + kwargs[param])

Sembra che queste domande sono simili: stackoverflow.com/questions/3884612/... stackoverflow.com/questions/356718/... stackoverflow.com/questions/1446555/... così sembra che quello che voglio è forse questo-- self .__ dict__ [key] = kwargs [key]
fijiaaron

Non molto rilevante per la tua domanda, ma potresti voler controllare PEP8 per alcuni suggerimenti sullo stile Python convenzionale.
Thomas Orozco

C'è una fantastica libreria per questo chiamata attrs. semplicemente pip install attrs, decora la tua classe @attr.se imposta gli argomenti come a = attr.ib(); b = attr.ib()ecc. Leggi di più qui .
Adam Barnes

Mi manca qualcosa qui? Devi ancora fare self.x = kwargs.get'x '] Ti apri agli errori di battitura dal chiamante Devi creare un client con caratteri extra istanza = Classe (** {}) Se non salti i cerchi con la mondanità self.x = kwargs.get'x '], non ti morderà comunque più tardi? cioè invece di self.x, finirai con self .__ dict __ ['x'] su tutta la linea, giusto? Oppure getattr () O più digitando che self.
JGFMK

Risposte:


148

È possibile aggiornare l' __dict__attributo (che rappresenta gli attributi della classe sotto forma di dizionario) con gli argomenti della parola chiave:

class Bar(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

allora puoi:

>>> bar = Bar(a=1, b=2)
>>> bar.a
1

e con qualcosa come:

allowed_keys = {'a', 'b', 'c'}
self.__dict__.update((k, v) for k, v in kwargs.items() if k in allowed_keys)

potresti filtrare le chiavi in ​​anticipo (usale iteritemsinvece di itemsse stai ancora usando Python 2.x).


2
Ancora meglio se usi self.__dict__.update(locals())per copiare anche argomenti posizionali.
Giancarlo Sportelli

2
Penso che ne avrai bisogno oggigiorno .. kwargs [0] .items () invece di kwargs.iteritems () - (Sto usando Python 3.6.5 al momento della scrittura)
JGFMK

@JGFMK Perché kwargs[0]invece di solo kwargs? Può kwargsanche avere una chiave intera? Sono abbastanza sicuro che debbano essere stringhe.
wjandrea

146

Puoi usare il setattr()metodo:

class Foo:
  def setAllWithKwArgs(self, **kwargs):
    for key, value in kwargs.items():
      setattr(self, key, value)

Esiste un getattr()metodo analogo per il recupero degli attributi.


@larsks grazie, ma hai idea di come possiamo decomprimere solo una chiave del dizionario? stackoverflow.com/questions/41792761/...
JinSnow

Hai bisogno di usare .getattr()? Oppure puoi accedere agli attributi con Foo.key?
Stevoisiak

@StevenVascellaro puoi ovviamente usare Foo.attrname. Penso di aver solo sottolineato il fatto che il getattrmetodo esiste. È anche utile se si desidera fornire un valore predefinito per quando l'attributo denominato non è disponibile.
Larsks

3
Qual è la differenza con la risposta accettata? . Quali sono i loro pro e contro?
Eduardo Pignatelli

15

La maggior parte delle risposte qui non copre un buon modo per inizializzare tutti gli attributi consentiti su un solo valore predefinito. Quindi, per aggiungere alle risposte fornite da @fqxp e @mmj :

class Myclass:

    def __init__(self, **kwargs):
        # all those keys will be initialized as class attributes
        allowed_keys = set(['attr1','attr2','attr3'])
        # initialize all allowed keys to false
        self.__dict__.update((key, False) for key in allowed_keys)
        # and update the given keys by their given values
        self.__dict__.update((key, value) for key, value in kwargs.items() if key in allowed_keys)

Penso che questa sia la risposta più completa dovuta all'inizializzazione di False. Buon punto!
Kyrol

9

Propongo una variazione della risposta di fqxp , che, oltre agli attributi consentiti , consente di impostare valori predefiniti per gli attributi:

class Foo():
    def __init__(self, **kwargs):
        # define default attributes
        default_attr = dict(a=0, b=None, c=True)
        # define (additional) allowed attributes with no default value
        more_allowed_attr = ['d','e','f']
        allowed_attr = list(default_attr.keys()) + more_allowed_attr
        default_attr.update(kwargs)
        self.__dict__.update((k,v) for k,v in default_attr.items() if k in allowed_attr)

Questo è il codice Python 3.x, per Python 2.x è necessaria almeno una regolazione, iteritems()al posto di items().


1
Questa è la risposta più flessibile, che riassume gli altri approcci in questo thread. Imposta gli attributi, consente i valori predefiniti e aggiunge solo i nomi degli attributi consentiti. Funziona bene con python 3.x come mostrato qui.
squarespiral

7

Ancora un'altra variante basata sulle ottime risposte di mmj e fqxp . E se lo volessimo

  1. Evita di codificare un elenco di attributi consentiti
  2. Impostare direttamente ed esplicitamente i valori predefiniti per ogni attributo nel costruttore
  3. Limita i kwarg agli attributi predefiniti da entrambi
    • rifiutando silenziosamente argomenti non validi o , in alternativa,
    • sollevando un errore.

Con "direttamente" intendo evitare un default_attributesdizionario estraneo .

class Bar(object):
    def __init__(self, **kwargs):

        # Predefine attributes with default values
        self.a = 0
        self.b = 0
        self.A = True
        self.B = True

        # get a list of all predefined values directly from __dict__
        allowed_keys = list(self.__dict__.keys())

        # Update __dict__ but only for keys that have been predefined 
        # (silently ignore others)
        self.__dict__.update((key, value) for key, value in kwargs.items() 
                             if key in allowed_keys)

        # To NOT silently ignore rejected keys
        rejected_keys = set(kwargs.keys()) - set(allowed_keys)
        if rejected_keys:
            raise ValueError("Invalid arguments in constructor:{}".format(rejected_keys))

Non una svolta importante, ma forse utile a qualcuno ...

EDIT: Se la nostra classe utilizza @propertydecoratori per incapsulare attributi "protetti" con getter e setter, e se vogliamo essere in grado di impostare queste proprietà con il nostro costruttore, potremmo voler espandere l' allowed_keyselenco con valori da dir(self), come segue:

allowed_keys = [i for i in dir(self) if "__" not in i and any([j.endswith(i) for j in self.__dict__.keys()])]

Il codice sopra esclude

  • qualsiasi variabile nascosta da dir()(esclusione basata sulla presenza di "__") e
  • qualsiasi metodo il dir()cui nome non si trova alla fine del nome di un attributo (protetto o meno) da __dict__.keys(), quindi probabilmente mantiene solo i metodi decorati @property.

Questa modifica è probabilmente valida solo per Python 3 e versioni successive.


2
class SymbolDict(object):
  def __init__(self, **kwargs):
    for key in kwargs:
      setattr(self, key, kwargs[key])

x = SymbolDict(foo=1, bar='3')
assert x.foo == 1

Ho chiamato la classe SymbolDictperché essenzialmente è un dizionario che opera utilizzando simboli invece di stringhe. In altre parole, lo fai x.fooinvece di, x['foo']ma sotto le coperte è davvero la stessa cosa.


2

Le seguenti soluzioni vars(self).update(kwargs)o self.__dict__.update(**kwargs)non sono robuste, perché l'utente può inserire qualsiasi dizionario senza messaggi di errore. Se devo controllare che l'utente inserisca la seguente firma ('a1', 'a2', 'a3', 'a4', 'a5') la soluzione non funziona. Inoltre, l'utente dovrebbe essere in grado di utilizzare l'oggetto passando i "parametri posizionali" o i "parametri delle coppie di valori kay".

Quindi suggerisco la seguente soluzione utilizzando una metaclasse.

from inspect import Parameter, Signature

class StructMeta(type):
    def __new__(cls, name, bases, dict):
        clsobj = super().__new__(cls, name, bases, dict)
        sig = cls.make_signature(clsobj._fields)
        setattr(clsobj, '__signature__', sig)
        return clsobj

def make_signature(names):
    return Signature(
        Parameter(v, Parameter.POSITIONAL_OR_KEYWORD) for v in names
    )

class Structure(metaclass = StructMeta):
    _fields = []
    def __init__(self, *args, **kwargs):
        bond = self.__signature__.bind(*args, **kwargs)
        for name, val in bond.arguments.items():
            setattr(self, name, val)

if __name__ == 'main':

   class A(Structure):
      _fields = ['a1', 'a2']

   if __name__ == '__main__':
      a = A(a1 = 1, a2 = 2)
      print(vars(a))

      a = A(**{a1: 1, a2: 2})
      print(vars(a))

1

La loro potrebbe essere una soluzione migliore, ma quello che mi viene in mente è:

class Test:
    def __init__(self, *args, **kwargs):
        self.args=dict(**kwargs)

    def getkwargs(self):
        print(self.args)

t=Test(a=1, b=2, c="cats")
t.getkwargs()


python Test.py 
{'a': 1, 'c': 'cats', 'b': 2}

Quello che sto cercando è impostare in modo condizionale gli attributi in base alla convalida. Mi sono reso conto che il problema con l'utilizzo di kwargs è che non convalida (o documenta) quali attributi sono accettabili
fijiaaron

Sì, mi rendo conto che la risposta di @larsks è migliore. Impara qualcosa di nuovo ogni giorno in SO!
Tom

1

questo è il più facile tramite larsks

class Foo:
    def setAllWithKwArgs(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

il mio esempio:

class Foo:
    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

door = Foo(size='180x70', color='red chestnut', material='oak')
print(door.size) #180x70

potrebbe smb spiegare cos'è kwargs.items ()?
Oleg_Kornilov

kwargsè un dizionario di argomenti di parole chiave ed items()è un metodo che restituisce una copia dell'elenco di (key, value)coppie del dizionario .
harryscholes

-1

Sospetto che nella maggior parte dei casi potrebbe essere meglio usare argomenti denominati (per un migliore codice di auto-documentazione), quindi potrebbe essere qualcosa del genere:

class Foo:
    def setAll(a=None, b=None, c=None):
        for key, value in (a, b, c):
            if (value != None):
                settattr(self, key, value)

Questa iterazione non funziona:for key, value in (a, b, c)
rerx
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.