Wie können Sie Klassenattribute aus variablen Argumenten (kwargs) in Python festlegen?

119

Angenommen, ich habe eine Klasse mit einem Konstruktor (oder einer anderen Funktion), die eine variable Anzahl von Argumenten verwendet und diese dann bedingt als Klassenattribute festlegt.

Ich könnte sie manuell einstellen, aber es scheint, dass variable Parameter in Python häufig genug sind, dass es dafür eine gemeinsame Sprache geben sollte. Aber ich bin mir nicht sicher, wie ich das dynamisch machen soll.

Ich habe ein Beispiel mit eval, aber das ist kaum sicher. Ich möchte wissen, wie ich das richtig machen kann - vielleicht mit 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])
Fidschiaaron
quelle
Es sieht so aus, als ob diese Fragen ähnlich sind: stackoverflow.com/questions/3884612/… stackoverflow.com/questions/356718/… stackoverflow.com/questions/1446555/… also sieht es so aus, als ob ich vielleicht dies möchte [Schlüssel] = kwargs [Schlüssel]
Fijiaaron
Nicht wirklich relevant für Ihre Frage, aber vielleicht möchten Sie PEP8 auf einige Hinweise zum herkömmlichen Python- Stil überprüfen .
Thomas Orozco
Hierfür gibt es eine fantastische Bibliothek namens attrs. pip install attrsDekorieren Sie einfach Ihre Klasse mit @attr.sund setzen Sie die Argumente wie a = attr.ib(); b = attr.ib()usw. Lesen Sie hier mehr .
Adam Barnes
Vermisse ich hier etwas? Sie müssen immer noch self.x = kwargs.get'x '] tun. Sie öffnen sich für Tippfehler vom Anrufer. Sie müssen einen Client mit zusätzlichen Zeichen erstellen. Instanz = Klasse (** {}) Wenn Sie nicht durch Reifen springen the self.x = kwargs.get'x '] Weltlichkeit, wird es dich später sowieso nicht beißen? dh Anstelle von self.x werden Sie am Ende selbst .__ diktieren __ ['x'] auf der ganzen Linie, oder? Oder getattr () Entweder mehr tippen als selbst.
JGFMK

Antworten:

146

Sie können das __dict__Attribut (das die Klassenattribute in Form eines Wörterbuchs darstellt) mit den Schlüsselwortargumenten aktualisieren :

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

dann kannst du:

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

und mit so etwas wie:

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

Sie können die Schlüssel vorher filtern (verwenden Sie iteritemsanstelle von, itemswenn Sie noch Python 2.x verwenden).

fqxp
quelle
2
Noch besser, wenn Sie self.__dict__.update(locals())auch Positionsargumente kopieren.
Giancarlo Sportelli
2
Ich denke, Sie werden dies heutzutage brauchen .. kwargs [0] .items () anstelle von kwargs.iteritems () - (Ich verwende Python 3.6.5 zum Zeitpunkt des Schreibens)
JGFMK
@JGFMK Warum kwargs[0]statt nur kwargs? Kann kwargssogar ein ganzzahliger Schlüssel haben? Ich bin mir ziemlich sicher, dass es Strings sein müssen.
Wjandrea
145

Sie können die setattr()Methode verwenden:

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

Es gibt eine analoge getattr()Methode zum Abrufen von Attributen.

Larsks
quelle
@larsks danke aber eine Idee, wie wir nur einen Wörterbuchschlüssel auspacken könnten? stackoverflow.com/questions/41792761/…
JinSnow
Müssen Sie verwenden .getattr()? Oder können Sie mit auf die Attribute zugreifen Foo.key?
Stevoisiak
@StevenVascellaro können Sie natürlich einfach verwenden Foo.attrname. Ich glaube, ich habe nur darauf hingewiesen, dass die getattrMethode existiert. Dies ist auch nützlich, wenn Sie einen Standardwert angeben möchten, wenn das angegebene Attribut nicht verfügbar ist.
Larsks
3
Was ist der Unterschied zur akzeptierten Antwort? . Was sind ihre Vor- und Nachteile?
Eduardo Pignatelli
15

Die meisten Antworten hier bieten keine gute Möglichkeit, alle zulässigen Attribute auf nur einen Standardwert zu initialisieren. Um die Antworten von @fqxp und @mmj zu ergänzen :

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)
Yiannis Kontochristopoulos
quelle
Ich denke, dies ist die vollständigste Antwort aufgrund der Inizialisierung auf False. Guter Punkt!
Kyrol
9

Ich schlage vor , eine Änderung der Antwort des fqxp , die, zusätzlich zu den erlaubten Attribute Sie festlegen können Standardwerte für Attribute:

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)

Dies ist Python 3.x-Code. Für Python 2.x benötigen Sie mindestens eine Anpassung iteritems()anstelle von items().

mmj
quelle
1
Dies ist die flexibelste Antwort, die die anderen Ansätze in diesem Thread zusammenfasst. Es legt die Attribute fest, lässt Standardwerte zu und fügt nur zulässige Attributnamen hinzu. Funktioniert gut mit Python 3.x wie hier gezeigt.
Squarespiral
7

Noch eine Variante basierend auf den hervorragenden Antworten von mmj und fqxp . Was ist, wenn wir wollen?

  1. Vermeiden Sie das Hardcodieren einer Liste zulässiger Attribute
  2. Legen Sie direkt und explizit Standardwerte für jedes Attribut im Konstruktor fest
  3. Beschränken Sie kwargs auf vordefinierte Attribute
    • ungültige Argumente stillschweigend zurückweisen oder alternativ
    • einen Fehler auslösen.

Mit "direkt" meine ich das Vermeiden eines fremden default_attributesWörterbuchs.

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

Kein großer Durchbruch, aber vielleicht nützlich für jemanden ...

BEARBEITEN: Wenn unsere Klasse @propertyDekoratoren verwendet, um "geschützte" Attribute mit Get- und Setzern zu kapseln, und wenn wir diese Eigenschaften mit unserem Konstruktor festlegen möchten, möchten wir die allowed_keysListe möglicherweise mit den folgenden Werten erweitern dir(self):

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

Der obige Code schließt aus

  • jede versteckte Variable von dir()(Ausschluss basierend auf dem Vorhandensein von "__") und
  • Jede Methode, dir()deren Name am Ende eines Attributnamens (geschützt oder anderweitig) nicht gefunden wird __dict__.keys(), behält wahrscheinlich nur mit @property dekorierte Methoden bei.

Diese Bearbeitung ist wahrscheinlich nur für Python 3 und höher gültig.

billjoie
quelle
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

Ich habe die Klasse aufgerufen, SymbolDictweil es sich im Wesentlichen um ein Wörterbuch handelt, das Symbole anstelle von Zeichenfolgen verwendet. Mit anderen Worten, Sie tun es x.foostatt, x['foo']aber unter der Decke ist es wirklich dasselbe.

wberry
quelle
2

Die folgenden Lösungen vars(self).update(kwargs)oder self.__dict__.update(**kwargs)sind nicht robust, da der Benutzer jedes Wörterbuch ohne Fehlermeldungen eingeben. Wenn ich überprüfen muss, ob der Benutzer die folgende Signatur einfügt ('a1', 'a2', 'a3', 'a4', 'a5'), funktioniert die Lösung nicht. Darüber hinaus sollte der Benutzer in der Lage sein, das Objekt durch Übergeben der "Positionsparameter" oder der "Kay-Wert-Paar-Parameter" zu verwenden.

Daher schlage ich die folgende Lösung unter Verwendung einer Metaklasse vor.

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))
antonjs
quelle
1

Sie könnten eine bessere Lösung sein, aber mir fällt Folgendes ein:

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}
Tom
quelle
Was ich suche, ist das bedingte Festlegen von Attributen basierend auf der Validierung. Ich erkannte, dass das Problem bei der Verwendung von kwargs darin besteht, dass nicht überprüft (oder dokumentiert) wird, welche Attribute akzeptabel sind
Fijiaaron
Ja, mir ist klar, dass die Antwort von @larsks besser ist. Lerne jeden Tag etwas Neues bei SO!
Tom
1

Dieser ist der einfachste über Larsks

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

mein Beispiel:

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
Oleg_Kornilov
quelle
könnte jdm erklären was kwargs.items () ist?
Oleg_Kornilov
kwargsist ein Wörterbuch mit Schlüsselwortargumenten und items()eine Methode, die eine Kopie der Paarliste des Wörterbuchs zurückgibt (key, value).
Harryscholes
-1

Ich vermute, dass es in den meisten Fällen besser ist, benannte Argumente zu verwenden (für besseren selbstdokumentierenden Code), so dass es ungefähr so ​​aussehen könnte:

class Foo:
    def setAll(a=None, b=None, c=None):
        for key, value in (a, b, c):
            if (value != None):
                settattr(self, key, value)
Fidschiaaron
quelle
Diese Iteration funktioniert nicht:for key, value in (a, b, c)
Rerx