Як можна встановити атрибути класу з змінних аргументів (kwargs) у python


119

Припустимо, у мене є клас із конструктором (або іншою функцією), який приймає змінну кількість аргументів і потім умовно встановлює їх як атрибути класу.

Я міг би їх встановити вручну, але, схоже, що змінні параметри досить поширені в python, що для цього має бути загальна ідіома. Але я не впевнений, як це зробити динамічно.

У мене є приклад використання eval, але це навряд чи безпечно. Я хочу знати, як це зробити - можливо, з лямбда?

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

Схоже, ці питання схожі: stackoverflow.com/questions/3884612/… stackoverflow.com/questions/356718/… stackoverflow.com/questions/1446555/…, тож виглядає те, що я хочу, можливо, це - власне .__ dict__ [ключ] = kwargs [ключ]
fijiaaron

Не дуже важливо для вашого запитання, але ви, можливо, захочете перевірити PEP8 на кілька підказок щодо звичайної стилізації Python.
Томас Ороско

Існує фантастична бібліотека для цього під назвою attrs. просто pip install attrsприкрасьте свій клас @attr.sі встановіть аргументи як a = attr.ib(); b = attr.ib()тощо. Детальніше тут .
Адам Барнс

Я щось тут пропускаю? Вам все одно потрібно зробити self.x = kwargs.get'x '] Ви відкриваєте себе для помилок від виклику. Вам потрібно створити клієнта з додатковим екземпляром символів = Class (** {}). Якщо ви не стрибаєте через обручі з the self.x = kwargs.get'x '] приземленість, хіба це не піде вас кусати пізніше? тобто замість self.x, ви закінчите себе .__ dict __ ['x'] внизу прямої лінії? Або getattr () Або більше типізуючи, ніж себе.
JGFMK

Відповіді:


148

Ви можете оновити __dict__атрибут (який представляє атрибути класу у вигляді словника) аргументами ключових слів:

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

тоді ви можете:

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

і щось подібне:

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

ви могли заздалегідь відфільтрувати ключі (використовуйте iteritemsзамість того, itemsякщо ви все ще використовуєте Python 2.x).


2
Ще краще, якщо ви використовуєте self.__dict__.update(locals())для копіювання також позиційні аргументи.
Джанкарло Спортеллі

2
Я думаю, це вам сьогодні знадобиться .. kwargs [0] .items () замість kwargs.iteritems () - (я використовую Python 3.6.5 під час написання)
JGFMK

@JGFMK Чому kwargs[0]замість просто kwargs? Може kwargsнавіть мати цілий ключ? Я майже впевнений, що вони повинні бути струнними.
wjandrea

146

Можна скористатися setattr()методом:

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

Існує аналогічний getattr()метод отримання атрибутів.


@larsks дякую, але будь-яка ідея, як ми могли розпакувати лише ключ словника? stackoverflow.com/questions/41792761 / ...
JinSnow

Чи потрібно використовувати .getattr()? Або ви можете отримати доступ до атрибутів за допомогою Foo.key?
Стевойсяк

@StevenVascellaro можна, звичайно, просто використовувати Foo.attrname. Я думаю, що я просто вказував на те, що getattrметод існує. Це також корисно, якщо ви хочете надати значення за замовчуванням, коли названий атрибут недоступний.
larsks

3
Чим відрізняється прийнята відповідь? . Які їх плюси і мінуси?
Едуардо Піньятеллі

15

Більшість відповідей тут не охоплює хороший спосіб ініціалізації всіх дозволених атрибутів лише до одного значення за замовчуванням. Отже, щоб додати відповіді, надані @fqxp та @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)

Я думаю, що це найбільш повна відповідь через ініціалізацію до False. Гарна думка!
Кіроль

9

Я пропоную варіант відповіді fqxp , який, крім дозволених атрибутів , дозволяє встановити значення за замовчуванням для атрибутів:

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)

Це код Python 3.x, а для Python 2.x вам потрібно принаймні одне коригування iteritems()замість items().


1
Це найбільш гнучка відповідь, узагальнення інших підходів у цій темі. Він встановлює атрибути, допускає значення за замовчуванням та додає лише дозволені імена атрибутів. Добре працює з python 3.x, як показано тут.
squarespiral

7

Ще один варіант, заснований на чудових відповідях mmj та fqxp . Що робити, якщо ми хочемо

  1. Уникайте жорсткого кодування списку дозволених атрибутів
  2. Безпосередньо та явно встановлюйте значення за замовчуванням для кожного атрибута в конструкторі
  3. Обмежте kwargs заздалегідь визначеними атрибутами будь-яким
    • мовчки відкидаючи недійсні аргументи або , як альтернатива,
    • виникнення помилки.

Під «безпосередньо» я маю на увазі уникання стороннього default_attributesсловника.

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

Не великий прорив, але, можливо, комусь корисний ...

EDIT: Якщо наш клас використовує @propertyдекоратори, щоб інкапсулювати "захищені" атрибути за допомогою getters та setters, і якщо ми хочемо мати можливість встановити ці властивості за допомогою нашого конструктора, ми можемо захотіти розширити allowed_keysсписок зі значеннями з 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()])]

Вищевказаний код виключає

  • будь-яку приховану змінну з dir()(виключення на основі наявності "__"), і
  • будь-який метод dir(), ім'я якого не знайдено в кінці імені атрибута (захищено чи іншим чином) від __dict__.keys(), таким чином, ймовірно, зберігається лише @property оформлені методи.

Це редагування, ймовірно, дійсне лише для Python 3 та вище.


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

Я назвав клас, SymbolDictтому що він по суті є словником, який працює з символами замість рядків. Іншими словами, ви робите x.fooзамість цього, x['foo']але під прикриттями відбувається дійсно те саме.


2

Наступні рішення vars(self).update(kwargs)абоself.__dict__.update(**kwargs) не є надійними, оскільки користувач може вводити будь-який словник без повідомлень про помилки. Якщо мені потрібно перевірити, чи користувач вставив наступний підпис ('a1', 'a2', 'a3', 'a4', 'a5'), рішення не працює. Крім того, користувач повинен мати можливість використовувати об'єкт, передаючи "параметри позиції" або "параметри парних значень".

Тому я пропоную наступне рішення, використовуючи метаклас.

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

Вони можуть бути кращим рішенням, але те, що мені спадає на думку:

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}

Що я шукаю - це умовно встановити атрибути на основі валідації. Я зрозумів, що проблема використання kwargs полягає в тому, що він не підтверджує (або документує), які атрибути є прийнятними
fijiaaron

Так, я розумію, що відповідь @larsks краще. Дізнайтеся щось нове щодня в SO!
Том

1

цей найпростіший за допомогою ларків

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

мій приклад:

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

могли б пояснити, що таке kwargs.items ()?
Олег_Корнілов

kwargsє словником аргументів ключових слів і items()є методом, який повертає копію списку (key, value)пар словника .
harryscholes

-1

Я підозрюю, що в більшості випадків може бути краще використовувати названі аргументи (для кращого коду самодокументування), щоб це могло виглядати приблизно так:

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

Ця ітерація не працює:for key, value in (a, b, c)
rerx
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.