Como você pode definir atributos de classe a partir de argumentos variáveis ​​(kwargs) em python


119

Suponha que eu tenha uma classe com um construtor (ou outra função) que recebe um número variável de argumentos e os define como atributos de classe condicionalmente.

Eu poderia defini-los manualmente, mas parece que os parâmetros variáveis ​​são comuns o suficiente em python que deve haver um idioma comum para fazer isso. Mas não tenho certeza de como fazer isso dinamicamente.

Eu tenho um exemplo usando eval, mas dificilmente é seguro. Quero saber a maneira correta de fazer isso - talvez com 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])

Parece que essas perguntas são semelhantes: stackoverflow.com/questions/3884612/… stackoverflow.com/questions/356718/… stackoverflow.com/questions/1446555/… então parece que o que eu quero é talvez este-- self .__ dict__ [key] = kwargs [key]
fijiaaron

Não é realmente relevante para a sua pergunta, mas você pode querer verificar o PEP8 para algumas dicas sobre o estilo convencional do Python.
Thomas Orozco

Há uma biblioteca fantástica para esse atributo chamado. simplesmente pip install attrs, decore sua classe com @attr.se defina os argumentos como a = attr.ib(); b = attr.ib()etc. Leia mais aqui .
Adam Barnes

Estou faltando alguma coisa aqui? Você ainda precisa fazer self.x = kwargs.get'x '] Você se abre para erros de digitação do chamador Você tem que criar o cliente com chars extras instance = Class (** {}) Se você não pular aros com a mundanidade self.x = kwargs.get'x '], não vai te morder mais tarde de qualquer maneira? ou seja, em vez de self.x, você terminará com self .__ dict __ ['x'] abaixo da linha, certo? Ou getattr () Ou mais digitação do que self.
JGFMK

Respostas:


146

Você pode atualizar o __dict__atributo (que representa os atributos da classe na forma de um dicionário) com os argumentos de palavra-chave:

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

então você pode:

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

e com algo como:

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

você pode filtrar as chaves de antemão (use em iteritemsvez de itemsse ainda estiver usando o Python 2.x).


2
Melhor ainda se você usar self.__dict__.update(locals())para copiar também argumentos posicionais.
Giancarlo Sportelli

2
Acho que você vai precisar disso hoje em dia .. kwargs [0] .items () em vez de kwargs.iteritems () - (Estou usando Python 3.6.5 no momento em que escrevo)
JGFMK

@JGFMK Por que em kwargs[0]vez de apenas kwargs? Pode kwargsaté ter uma chave inteira? Tenho certeza de que devem ser cordas.
wjandrea

145

Você pode usar o setattr()método:

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

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


@larsks obrigado, mas alguma ideia de como poderíamos desempacotar apenas uma chave de dicionário? stackoverflow.com/questions/41792761/…
JinSnow

Você precisa usar .getattr()? Ou você pode acessar os atributos com Foo.key?
Stevoisiak

@StevenVascellaro você pode simplesmente usar Foo.attrname. Acho que estava apenas apontando o fato de que o getattrmétodo existe. Também é útil se você deseja fornecer um valor padrão para quando o atributo nomeado não estiver disponível.
larsks de

3
Qual é a diferença com a resposta aceita? . Quais são seus prós e contras?
Eduardo Pignatelli

15

A maioria das respostas aqui não cobre uma boa maneira de inicializar todos os atributos permitidos com apenas um valor padrão. Então, para adicionar às respostas dadas por @fqxp e @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)

Acho que esta é a resposta mais completa devido à inicialização para False. Bom ponto!
Kyrol

9

Proponho uma variação da resposta de fqxp , que, além dos atributos permitidos , permite definir valores padrão para os 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 é o código Python 3.x, para Python 2.x você precisa de pelo menos um ajuste, iteritems()no lugar de items().


1
Esta é a resposta mais flexível, resumindo as outras abordagens neste tópico. Ele define os atributos, permite valores padrão e adiciona apenas nomes de atributos permitidos. Funciona bem com python 3.x conforme mostrado aqui.
squarepiral

7

Mais uma variante baseada nas excelentes respostas de mmj e fqxp . E se quisermos

  1. Evite codificar uma lista de atributos permitidos
  2. Defina direta e explicitamente os valores padrão para cada atributo no construtor
  3. Restringir kwargs a atributos predefinidos por qualquer um
    • rejeitando silenciosamente argumentos inválidos ou , alternativamente,
    • levantando um erro.

Por "diretamente", quero dizer evitar um default_attributesdicionário estranho .

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

Não é um grande avanço, mas talvez útil para alguém ...

EDITAR: Se nossa classe usa @propertydecoradores para encapsular atributos "protegidos" com getters e setters, e se queremos ser capazes de definir essas propriedades com nosso construtor, podemos expandir a allowed_keyslista com valores de dir(self), como segue:

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

O código acima exclui

  • qualquer variável oculta de dir() (exclusão com base na presença de "__"), e
  • qualquer método dir()cujo nome não seja encontrado no final de um nome de atributo (protegido ou não) de __dict__.keys(), provavelmente mantendo apenas os métodos decorados com @property.

Esta edição provavelmente só é válida para Python 3 e superior.


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

Chamei a classe SymbolDictporque é essencialmente um dicionário que opera usando símbolos em vez de strings. Em outras palavras, você faz em x.foovez de, x['foo']mas por trás das cobertas, é realmente a mesma coisa acontecendo.


2

As seguintes soluções vars(self).update(kwargs)ouself.__dict__.update(**kwargs) não são robustas, pois o usuário pode entrar em qualquer dicionário sem mensagens de erro. Se eu precisar verificar se o usuário insere a seguinte assinatura ('a1', 'a2', 'a3', 'a4', 'a5') a solução não funciona. Além disso, o usuário deve ser capaz de usar o objeto passando os "parâmetros posicionais" ou os "parâmetros dos pares de valor kay".

Portanto, sugiro a seguinte solução usando uma metaclasse.

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

Pode ser uma solução melhor, mas o que me vem à mente é:

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}

O que estou procurando é definir atributos condicionalmente com base na validação. Percebi que o problema de usar kwargs é que ele não valida (ou documenta) quais atributos são aceitáveis
fijiaaron

Sim, eu sei que a resposta de @larsks é melhor. Aprenda algo novo todos os dias no SO!
Tom

1

este é o mais fácil via larsks

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

meu exemplo:

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

poderia smb explicar o que é kwargs.items ()?
Oleg_Kornilov

kwargsé um dicionário de argumentos de palavra-chave e items()é um método que retorna uma cópia da lista de (key, value)pares do dicionário .
Harryscholes

-1

Suspeito que seja melhor na maioria dos casos usar args nomeados (para um melhor código de autodocumentação), então pode ser algo assim:

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

Esta iteração não funciona:for key, value in (a, b, c)
rerx
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.