Jak ustawić atrybuty klasy na podstawie argumentów zmiennych (kwargs) w Pythonie

119

Załóżmy, że mam klasę z konstruktorem (lub inną funkcją), która przyjmuje zmienną liczbę argumentów, a następnie warunkowo ustawia je jako atrybuty klasy.

Mógłbym ustawić je ręcznie, ale wydaje się, że parametry zmiennych są na tyle powszechne w Pythonie, że powinien istnieć wspólny idiom do tego. Ale nie jestem pewien, jak to zrobić dynamicznie.

Mam przykład używający eval, ale to raczej nie jest bezpieczne. Chcę wiedzieć, jak to zrobić - może z lambdą?

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])
fijiaaron
źródło
Wygląda na to, że te pytania są podobne: stackoverflow.com/questions/3884612/… stackoverflow.com/questions/356718/… stackoverflow.com/questions/1446555/… więc wygląda na to, że to, czego chcę, to może to ... ja. [key] = kwargs [key]
fijiaaron
Niezbyt istotne dla twojego pytania, ale możesz chcieć sprawdzić PEP8, aby uzyskać kilka wskazówek na temat konwencjonalnego stylu Pythona.
Thomas Orozco,
Istnieje fantastyczna biblioteka o nazwie attrs. po prostu pip install attrsudekoruj swoją klasę @attr.si ustaw argumenty na a = attr.ib(); b = attr.ib()itp. Przeczytaj więcej tutaj .
Adam Barnes
Czy coś mi umyka? Nadal musisz zrobić self.x = kwargs.get'x '] Otwierasz się na literówki wywołującego Musisz stworzyć klienta z dodatkowymi znakami instance = Class (** {}) Jeśli nie przeskakujesz przez obręcze z self.x = kwargs.get'x '] przyziemność, czy i tak cię później nie ugryzie? tj. zamiast self.x, skończysz z self .__ dict __ ['x'] w dół linii, prawda? Lub getattr () Albo więcej pisania niż self.
JGFMK

Odpowiedzi:

148

Możesz zaktualizować __dict__atrybut (który reprezentuje atrybuty klasy w postaci słownika) za pomocą argumentów słów kluczowych:

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

wtedy możesz:

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

i coś takiego:

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

możesz wcześniej przefiltrować klucze (użyj iteritemszamiast, itemsjeśli nadal używasz Pythona 2.x).

fqxp
źródło
2
Jeszcze lepiej, jeśli używasz self.__dict__.update(locals())do kopiowania również argumentów pozycyjnych.
Giancarlo Sportelli
2
Myślę, że będziesz tego potrzebować w dzisiejszych czasach .. kwargs [0] .items () zamiast kwargs.iteritems () - (używam Pythona 3.6.5 w momencie pisania)
JGFMK
@JGFMK Dlaczego kwargs[0]zamiast po prostu kwargs? Czy kwargsmoże mieć nawet klucz całkowity? Jestem prawie pewien, że muszą to być struny.
wjandrea
146

Możesz skorzystać z setattr()metody:

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

Istnieje analogiczna getattr()metoda pobierania atrybutów.

larsks
źródło
@larsks dzięki, ale masz pomysł, jak rozpakować tylko klucz słownika? stackoverflow.com/questions/41792761/…
JinSnow
Czy potrzebujesz użyć .getattr()? Czy możesz uzyskać dostęp do atrybutów za pomocą Foo.key?
Stevoisiak
@StevenVascellaro możesz oczywiście po prostu użyć Foo.attrname. Myślę, że po prostu wskazałem na fakt, że getattrmetoda istnieje. Jest to również przydatne, jeśli chcesz podać domyślną wartość, gdy nazwany atrybut jest niedostępny.
larsks
3
Jaka jest różnica z zaakceptowaną odpowiedzią? . Jakie są ich zalety i wady?
Eduardo Pignatelli
15

Większość odpowiedzi tutaj nie obejmuje dobrego sposobu inicjowania wszystkich dozwolonych atrybutów tylko do jednej wartości domyślnej. Tak więc, aby dodać do odpowiedzi udzielonych przez @fqxp i @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)
Yiannis Kontochristopoulos
źródło
Myślę, że jest to najpełniejsza odpowiedź ze względu na inicjalizację False. Słuszna uwaga!
Kyrol
9

Proponuję odmianę odpowiedzi fqxp , która oprócz dozwolonych atrybutów pozwala ustawić domyślne wartości atrybutów:

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)

To jest kod Python 3.x, dla Pythona 2.x potrzebujesz przynajmniej jednej korekty iteritems()zamiast items().

mmj
źródło
1
Jest to najbardziej elastyczna odpowiedź, podsumowująca inne podejścia w tym wątku. Ustawia atrybuty, dopuszcza wartości domyślne i dodaje tylko dozwolone nazwy atrybutów. Działa dobrze z Pythonem 3.x, jak pokazano tutaj.
squarespiral
7

Kolejny wariant oparty na doskonałych odpowiedziach mmj i fqxp . A jeśli zechcemy

  1. Unikaj zakodowania listy dozwolonych atrybutów
  2. Bezpośrednio i jawnie ustaw wartości domyślne dla każdego atrybutu w konstruktorze
  3. Ogranicz kwargi do predefiniowanych atrybutów przez jedną z nich
    • po cichu odrzucając nieprawidłowe argumenty lub alternatywnie,
    • zgłoszenie błędu.

Przez „bezpośrednio” mam na myśli unikanie zbędnego default_attributessłownika.

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

Nie jest to duży przełom, ale może komuś się przyda ...

EDYCJA: Jeśli nasza klasa używa @propertydekoratorów do hermetyzacji „chronionych” atrybutów za pomocą metod pobierających i ustawiających, a jeśli chcemy mieć możliwość ustawiania tych właściwości za pomocą naszego konstruktora, możemy chcieć rozszerzyć allowed_keyslistę o wartości od dir(self)w następujący sposób:

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

Powyższy kod wyklucza

  • dowolna ukryta zmienna z dir()(wykluczenie na podstawie obecności „__”) i
  • jakakolwiek metoda, dir()której nazwa nie znajduje się na końcu nazwy atrybutu (chronionej lub innej) od __dict__.keys(), w ten sposób prawdopodobnie zachowują tylko metody z dekoracją @właściwość.

Ta edycja jest prawdopodobnie poprawna tylko dla Pythona 3 i nowszych.

billjoie
źródło
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

Nazwałem tę klasę, SymbolDictponieważ w istocie jest to słownik, który używa symboli zamiast łańcuchów. Innymi słowy, robisz x.foozamiast, x['foo']ale pod kołdrą dzieje się tak samo.

jagoda
źródło
2

Następujące rozwiązania vars(self).update(kwargs)lub self.__dict__.update(**kwargs)nie są niezawodne, ponieważ użytkownik może wejść do dowolnego słownika bez komunikatów o błędach. Jeśli muszę sprawdzić, czy użytkownik wstawił następujący podpis („a1”, „a2”, „a3”, „a4”, „a5”) rozwiązanie nie działa. Ponadto użytkownik powinien mieć możliwość korzystania z obiektu, przekazując „parametry pozycyjne” lub „parametry par wartości kay”.

Proponuję więc następujące rozwiązanie przy użyciu metaklasy.

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
źródło
1

To może być lepsze rozwiązanie, ale przychodzi mi na myśl:

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}
Tomek
źródło
To, czego szukam, to warunkowe ustawienie atrybutów na podstawie weryfikacji. Zdałem sobie sprawę, że problem z używaniem kwargs polega na tym, że nie sprawdza (ani nie dokumentuje), które atrybuty są dopuszczalne
fijiaaron
Tak, zdaję sobie sprawę, że odpowiedź @larsks jest lepsza. Dowiedz się czegoś nowego każdego dnia w SO!
Tom
1

ten jest najłatwiejszy przez Larsks

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

mój przykład:

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
źródło
czy ktoś mógłby wyjaśnić, co to jest kwargs.items ()?
Oleg_Kornilov
kwargsjest słownikiem argumentów słów kluczowych i items()jest metodą zwracającą kopię listy (key, value)par ze słownika .
harryscholes
-1

Podejrzewam, że w większości przypadków lepiej byłoby użyć nazwanych argumentów (dla lepszego samodokumentującego kodu), więc może to wyglądać mniej więcej tak:

class Foo:
    def setAll(a=None, b=None, c=None):
        for key, value in (a, b, c):
            if (value != None):
                settattr(self, key, value)
fijiaaron
źródło
Ta iteracja nie działa:for key, value in (a, b, c)
rerx