¿Cómo se pueden establecer atributos de clase a partir de argumentos variables (kwargs) en Python?

119

Supongamos que tengo una clase con un constructor (u otra función) que toma un número variable de argumentos y luego los establece como atributos de clase condicionalmente.

Podría configurarlos manualmente, pero parece que los parámetros variables son lo suficientemente comunes en Python que debería haber un lenguaje común para hacer esto. Pero no estoy seguro de cómo hacer esto de forma dinámica.

Tengo un ejemplo usando eval, pero eso no es seguro. Quiero saber la forma correcta de hacer esto, ¿tal vez 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])
fijiaaron
fuente
Parece que estas preguntas son similares: stackoverflow.com/questions/3884612/… stackoverflow.com/questions/356718/… stackoverflow.com/questions/1446555/… así que parece que lo que quiero es tal vez esto-- yo mismo .__ dict__ [clave] = kwargs [clave]
fijiaaron
No es realmente relevante para su pregunta, pero es posible que desee consultar PEP8 para obtener algunas sugerencias sobre el estilo convencional de Python.
Thomas Orozco
Hay una biblioteca fantástica para esto llamada attrs. simplemente pip install attrs, decora tu clase con @attr.s, y establece los argumentos como a = attr.ib(); b = attr.ib()etc. Lee más aquí .
Adam Barnes
¿Me estoy perdiendo de algo? Todavía necesitas hacer self.x = kwargs.get'x '] Te abres a los errores tipográficos de la persona que llama Tienes que crear un cliente con caracteres adicionales. Instance = Class (** {}) Si no saltas a través del aro con la mundaneidad self.x = kwargs.get'x '], ¿no te va a morder más tarde de todos modos? es decir, en lugar de self.x, terminarás con self .__ dict __ ['x'] en la línea derecha? O getattr () O más tipeo que uno mismo.
JGFMK

Respuestas:

148

Puede actualizar el __dict__atributo (que representa los atributos de la clase en forma de diccionario) con los argumentos de la palabra clave:

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

entonces tú puedes:

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

y con algo como:

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

podría filtrar las claves de antemano (use en iteritemslugar de itemssi todavía está usando Python 2.x).

fqxp
fuente
2
Aún mejor si usa self.__dict__.update(locals())para copiar también argumentos posicionales.
Giancarlo Sportelli
2
Creo que necesitará esto hoy en día ... kwargs [0] .items () en lugar de kwargs.iteritems () - (Estoy usando Python 3.6.5 al momento de escribir este artículo)
JGFMK
@JGFMK ¿Por qué en kwargs[0]lugar de solo kwargs? ¿ kwargsIncluso puede tener una clave de número entero? Estoy bastante seguro de que tienen que ser cuerdas.
wjandrea
146

Puedes usar el setattr()método:

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

Existe un getattr()método análogo para recuperar atributos.

larsks
fuente
@larsks gracias, pero ¿alguna idea de cómo podríamos descomprimir solo una clave de diccionario? stackoverflow.com/questions/41792761/…
JinSnow
¿Necesitas usar .getattr()? ¿O puedes acceder a los atributos con Foo.key?
Stevoisiak
@StevenVascellaro, por supuesto, puede usar Foo.attrname. Creo que solo estaba señalando el hecho de que el getattrmétodo existe. También es útil si desea proporcionar un valor predeterminado para cuando el atributo nombrado no está disponible.
larsks
3
¿Cuál es la diferencia con la respuesta aceptada? . ¿Cuáles son sus pros y contras?
Eduardo Pignatelli
15

La mayoría de las respuestas aquí no cubren una buena manera de inicializar todos los atributos permitidos en un solo valor predeterminado. Entonces, para agregar a las respuestas dadas por @fqxp y @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
fuente
Creo que esta es la respuesta más completa debido a la inicialización de False. ¡Buen punto!
Kyrol
9

Propongo una variación de la respuesta de fqxp , que, además de los atributos permitidos , le permite establecer valores predeterminados para los atributos:

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)

Este es el código Python 3.x, para Python 2.x necesita al menos un ajuste, iteritems()en lugar de items().

mmj
fuente
1
Esta es la respuesta más flexible, que resume los otros enfoques en este hilo. Establece los atributos, permite valores predeterminados y agrega solo los nombres de atributos permitidos. Funciona bien con Python 3.x como se muestra aquí.
squarespiral
7

Otra variante más basada en las excelentes respuestas de mmj y fqxp . ¿Y si queremos

  1. Evite codificar una lista de atributos permitidos
  2. Establezca directa y explícitamente valores predeterminados para cada atributo en el constructor
  3. Restrinja los kwargs a atributos predefinidos ya sea
    • rechazar silenciosamente argumentos inválidos o , alternativamente,
    • levantando un error.

Por "directamente", me refiero a evitar un default_attributesdiccionario extraño .

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

No es un gran avance, pero tal vez sea útil para alguien ...

EDITAR: Si nuestra clase usa @propertydecoradores para encapsular atributos "protegidos" con getters y setters, y si queremos poder establecer estas propiedades con nuestro constructor, es posible que deseemos expandir la allowed_keyslista con valores de dir(self), como sigue:

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

El código anterior excluye

  • cualquier variable oculta de dir()(exclusión basada en la presencia de "__"), y
  • cualquier método dir()cuyo nombre no se encuentre al final de un nombre de atributo (protegido o no) de __dict__.keys(), por lo que probablemente solo se mantendrán los métodos decorados con @property.

Es probable que esta edición solo sea válida para Python 3 y versiones posteriores.

Billjoie
fuente
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

Llamé a la clase SymbolDictporque esencialmente es un diccionario que opera usando símbolos en lugar de cadenas. En otras palabras, lo haces en x.foolugar de hacerlo, x['foo']pero bajo las sábanas es realmente lo mismo.

wberry
fuente
2

Las siguientes soluciones vars(self).update(kwargs)o self.__dict__.update(**kwargs)no son robustas, porque el usuario puede ingresar a cualquier diccionario sin mensajes de error. Si necesito verificar que el usuario inserte la siguiente firma ('a1', 'a2', 'a3', 'a4', 'a5') la solución no funciona. Además, el usuario debería poder utilizar el objeto pasando los "parámetros posicionales" o los "parámetros de pares kay-valor".

Así que sugiero la siguiente solución usando una metaclase.

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
fuente
1

Puede que sea una mejor solución, pero lo que me viene a la mente es:

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
fuente
Lo que estoy buscando es establecer atributos condicionalmente basados ​​en la validación. Me di cuenta de que el problema con el uso de kwargs es que no valida (o documenta) qué atributos son aceptables
fijiaaron
Sí, me doy cuenta de que la respuesta de @larsks es mejor. ¡Aprenda algo nuevo todos los días en SO!
Tom
1

este es el más fácil a través de larsks

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

mi ejemplo:

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
fuente
¿Podría alguien explicar qué es kwargs.items ()?
Oleg_Kornilov
kwargses un diccionario de argumentos de palabras clave y items()es un método que devuelve una copia de la lista de (key, value)pares del diccionario .
harryscholes
-1

Sospecho que podría ser mejor en la mayoría de los casos usar args con nombre (para un mejor código de autodocumentación), por lo que podría verse así:

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
fuente
Esta iteración no funciona:for key, value in (a, b, c)
rerx