Comment définir des attributs de classe à partir d'arguments variables (kwargs) en python

119

Supposons que j'ai une classe avec un constructeur (ou une autre fonction) qui prend un nombre variable d'arguments, puis les définit comme attributs de classe de manière conditionnelle.

Je pourrais les définir manuellement, mais il semble que les paramètres variables sont suffisamment courants en python pour qu'il devrait y avoir un idiome commun pour le faire. Mais je ne sais pas comment procéder de manière dynamique.

J'ai un exemple utilisant eval, mais ce n'est guère sûr. Je veux savoir la bonne façon de faire cela - peut-être avec 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])
fijiaaron
la source
Il semble que ces questions sont similaires: stackoverflow.com/questions/3884612/… stackoverflow.com/questions/356718/... stackoverflow.com/questions/1446555/… donc ça ressemble à ce que je veux, c'est peut-être ceci - soi .__ dict__ [key] = kwargs [key]
fijiaaron
Pas vraiment pertinent pour votre question, mais vous voudrez peut-être vérifier PEP8 pour quelques conseils sur le style Python conventionnel.
Thomas Orozco
Il existe une bibliothèque fantastique pour cela appelée attrs. simplement pip install attrs, décorez votre classe avec @attr.s, et définissez les arguments comme a = attr.ib(); b = attr.ib()etc. Pour en savoir plus, cliquez ici .
Adam Barnes
Est-ce que j'ai râté quelque chose? Vous devez toujours faire self.x = kwargs.get'x '] Vous vous ouvrez aux fautes de frappe de l'appelant Vous devez créer un client avec des caractères supplémentaires instance = Class (** {}) Si vous ne sautez pas à travers les cerceaux avec la banalité self.x = kwargs.get'x '], ne va-t-elle pas vous mordre plus tard de toute façon? c'est-à-dire au lieu de self.x, vous vous retrouverez avec self .__ dict __ ['x'] sur toute la ligne, n'est-ce pas? Ou getattr () Soit plus de frappe que self.
JGFMK

Réponses:

148

Vous pouvez mettre à jour l' __dict__attribut (qui représente les attributs de classe sous la forme d'un dictionnaire) avec les arguments de mot-clé:

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

Ensuite vous pouvez:

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

et avec quelque chose comme:

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

vous pouvez filtrer les clés au préalable (utilisez iteritemsplutôt que itemssi vous utilisez toujours Python 2.x).

fqxp
la source
2
Encore mieux si vous utilisez self.__dict__.update(locals())pour copier également des arguments de position.
Giancarlo Sportelli
2
Je pense que vous en aurez besoin de nos jours .. kwargs [0] .items () au lieu de kwargs.iteritems () - (j'utilise Python 3.6.5 au moment de la rédaction)
JGFMK
@JGFMK Pourquoi kwargs[0]au lieu de juste kwargs? Peut kwargsmême avoir une clé entière? Je suis presque sûr qu'ils doivent être des cordes.
wjandrea le
146

Vous pouvez utiliser la setattr()méthode:

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

Il existe une getattr()méthode analogue pour récupérer les attributs.

larsks
la source
@larsks merci, mais une idée de la façon dont nous pourrions décompresser uniquement une clé de dictionnaire? stackoverflow.com/questions/41792761/…
JinSnow
Avez-vous besoin d'utiliser .getattr()? Ou pouvez-vous accéder aux attributs avec Foo.key?
Stevoisiak
@StevenVascellaro, vous pouvez bien sûr simplement utiliser Foo.attrname. Je pense que je voulais simplement souligner le fait que la getattrméthode existe. Il est également utile si vous souhaitez fournir une valeur par défaut lorsque l'attribut nommé n'est pas disponible.
larsks
3
Quelle est la différence avec la réponse acceptée? . Quels sont leurs avantages et leurs inconvénients?
Eduardo Pignatelli le
15

La plupart des réponses ici ne couvrent pas un bon moyen d'initialiser tous les attributs autorisés à une seule valeur par défaut. Donc, pour ajouter aux réponses données par @fqxp et @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
la source
Je pense que c'est la réponse la plus complète en raison de l'inizialisation de False. Bon point!
Kyrol
9

Je propose une variante de la réponse de fqxp , qui, en plus des attributs autorisés , vous permet de définir des valeurs par défaut pour les attributs:

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)

Il s'agit du code Python 3.x, pour Python 2.x vous avez besoin d'au moins un ajustement, iteritems()à la place de items().

mmj
la source
1
C'est la réponse la plus flexible, résumant les autres approches de ce fil. Il définit les attributs, autorise les valeurs par défaut et ajoute uniquement les noms d'attributs autorisés. Fonctionne très bien avec python 3.x comme indiqué ici.
squarespiral
7

Encore une autre variante basée sur les excellentes réponses de mmj et fqxp . Et si on veut

  1. Évitez de coder en dur une liste d'attributs autorisés
  2. Définir directement et explicitement les valeurs par défaut pour chaque attribut dans le constructeur
  3. Restreignez les kwargs aux attributs prédéfinis soit
    • rejetant silencieusement les arguments invalides ou , alternativement,
    • soulevant une erreur.

Par «directement», je veux dire éviter un default_attributesdictionnaire superflu .

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

Pas une avancée majeure, mais peut-être utile à quelqu'un ...

EDIT: Si notre classe utilise des @propertydécorateurs pour encapsuler des attributs "protégés" avec des getters et des setters, et si nous voulons pouvoir définir ces propriétés avec notre constructeur, nous pouvons vouloir étendre la allowed_keysliste avec les valeurs de dir(self), comme suit:

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

Le code ci-dessus exclut

  • toute variable masquée de dir()(exclusion basée sur la présence de "__"), et
  • toute méthode dir()dont le nom n'est pas trouvé à la fin d'un nom d'attribut (protégé ou non) de __dict__.keys(), conservant ainsi probablement uniquement les méthodes décorées @property.

Cette modification n'est probablement valable que pour Python 3 et supérieur.

Billjoie
la source
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

J'ai appelé la classe SymbolDictparce qu'il s'agit essentiellement d'un dictionnaire qui fonctionne en utilisant des symboles au lieu de chaînes. En d'autres termes, vous faites x.fooau lieu de x['foo']mais sous les couvertures, c'est vraiment la même chose qui se passe.

wberry
la source
2

Les solutions suivantes vars(self).update(kwargs)ou self.__dict__.update(**kwargs)ne sont pas robustes, car l'utilisateur peut saisir n'importe quel dictionnaire sans message d'erreur. Si je dois vérifier que l'utilisateur insère la signature suivante («a1», «a2», «a3», «a4», «a5»), la solution ne fonctionne pas. De plus, l'utilisateur doit pouvoir utiliser l'objet en passant les "paramètres positionnels" ou les "paramètres de paires valeur kay".

Je suggère donc la solution suivante en utilisant une métaclasse.

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
la source
1

Leur solution est peut-être meilleure, mais ce qui me vient à l'esprit est:

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}
À M
la source
Ce que je recherche, c'est de définir des attributs conditionnellement basés sur la validation. J'ai réalisé que le problème avec l'utilisation de kwargs est qu'il ne valide pas (ou ne documente pas) quels attributs sont acceptables
fijiaaron
Ouais, je me rends compte que la réponse de @larsks est meilleure. Apprenez quelque chose de nouveau tous les jours chez SO!
Tom
1

celui-ci est le plus simple via larsks

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

mon exemple:

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
la source
smb pourrait-il expliquer ce qu'est kwargs.items ()?
Oleg_Kornilov
kwargsest un dictionnaire d'arguments de mots clés et items()est une méthode qui renvoie une copie de la liste de (key, value)paires du dictionnaire .
harryscholes
-1

Je soupçonne qu'il pourrait être préférable dans la plupart des cas d'utiliser des arguments nommés (pour un meilleur code d'auto-documentation) afin que cela puisse ressembler à ceci:

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
la source
Cette itération ne fonctionne pas:for key, value in (a, b, c)
rerx