Como evito o “self.x = x; self.y = y; self.z = z ”padrão em __init__?

170

Eu vejo padrões como

def __init__(self, x, y, z):
    ...
    self.x = x
    self.y = y
    self.z = z
    ...

com bastante frequência, muitas vezes com muito mais parâmetros. Existe uma boa maneira de evitar esse tipo de repetitividade tediosa? Em namedtuplevez disso, a classe deve herdar ?

MaxB
fonte
31
Nem toda receptividade é ruim. Lembre-se de que o modelo de classe do Python não inclui uma definição explícita dos atributos da instância; portanto, essas atribuições são os equivalentes de auto-documentação.
chepner
4
@chepner: Bem, não requer definição explícita. Você pode usar __slots__para o efeito ; é levemente atípico (mais detalhado para obter economia de memória), mas eu gosto muito de evitar o risco de auto-vivificar um atributo totalmente novo se eu digitar o nome.
ShadowRanger
2
Qualquer bom editor terá modelos. Você digita ini <shortcut> x, y, z): <shortcut>e está pronto.
Gerenuk 04/02
3
Os nomeados são impressionantes, se você deseja um objeto de valor imutável. Se você deseja uma classe regular e mutável, não pode usá-las.
RemcoGerlich 04/02
4
"Não" é uma boa opção, qualquer opção disponível matará a assinatura do método (e, portanto, potencialmente toda a interface). Além disso, se suas classes tiverem uma quantidade insuportável de campos para inicializar, considere dividi-las.
Kroltan 5/02

Respostas:

87

Edit: Se você tem python 3.7+, apenas use classes de dados

Uma solução decoradora que mantém a assinatura:

import decorator
import inspect
import sys


@decorator.decorator
def simple_init(func, self, *args, **kws):
    """
    @simple_init
    def __init__(self,a,b,...,z)
        dosomething()

    behaves like

    def __init__(self,a,b,...,z)
        self.a = a
        self.b = b
        ...
        self.z = z
        dosomething()
    """

    #init_argumentnames_without_self = ['a','b',...,'z']
    if sys.version_info.major == 2:
        init_argumentnames_without_self = inspect.getargspec(func).args[1:]
    else:
        init_argumentnames_without_self = tuple(inspect.signature(func).parameters.keys())[1:]

    positional_values = args
    keyword_values_in_correct_order = tuple(kws[key] for key in init_argumentnames_without_self if key in kws)
    attribute_values = positional_values + keyword_values_in_correct_order

    for attribute_name,attribute_value in zip(init_argumentnames_without_self,attribute_values):
        setattr(self,attribute_name,attribute_value)

    # call the original __init__
    func(self, *args, **kws)


class Test():
    @simple_init
    def __init__(self,a,b,c,d=4):
        print(self.a,self.b,self.c,self.d)

#prints 1 3 2 4
t = Test(1,c=2,b=3)
#keeps signature
#prints ['self', 'a', 'b', 'c', 'd']
if sys.version_info.major == 2:
    print(inspect.getargspec(Test.__init__).args)
else:
    print(inspect.signature(Test.__init__))
Siphor
fonte
2
boa resposta, mas não funcionará com python2.7: nosignature
MaxB
3
@alexé o decorador "decorator.decorator" quebra automaticamente a função #
Siphor
4
Estou bastante dividida sobre amar ou odiar isso. Eu aprecio preservar a assinatura.
Kyle Strand
14
"... Explícito é melhor que implícito. Simples é melhor que complexo. ..." -Zen of Python
Jack Stout
9
-1 Francamente, isso é horrível. Não tenho idéia do que esse código está fazendo de relance, e é literalmente dez vezes a quantidade de código. Ser inteligente é legal e tudo, mas isso é um mau uso da sua óbvia inteligência.
22416 Ian Newson
108

Isenção de responsabilidade: Parece que várias pessoas estão preocupadas em apresentar esta solução, portanto fornecerei uma isenção de responsabilidade muito clara. Você não deve usar esta solução. Eu o forneço apenas como informação, para que você saiba que o idioma é capaz disso. O restante da resposta está apenas mostrando os recursos do idioma, não endossando o uso dessa maneira.


Não há realmente nada de errado em copiar explicitamente parâmetros em atributos. Se você tiver muitos parâmetros no ctor, às vezes é considerado um cheiro de código e talvez você deva agrupar esses parâmetros em menos objetos. Outras vezes, é necessário e não há nada de errado com isso. Enfim, fazê-lo explicitamente é o caminho a percorrer.

No entanto, como você está perguntando como isso pode ser feito (e não se deve ou não), então uma solução é a seguinte:

class A:
    def __init__(self, **kwargs):
        for key in kwargs:
          setattr(self, key, kwargs[key])

a = A(l=1, d=2)
a.l # will return 1
a.d # will return 2
gruszczy
fonte
16
boa resposta +1 ... embora self.__dict__.update(kwargs)possa ser marginalmente mais Python
Joran Beasley
44
O problema dessa abordagem é que não há registro de quais argumentos A.__init__realmente esperam, nem verificação de erros para nomes de argumentos digitados incorretamente.
MaxB
7
@JoranBeasley Atualizar o dicionário da instância às cegas kwargsdeixa você aberto para o equivalente a um ataque de injeção SQL. Se o seu objeto tiver um método nomeado my_methode você passar um argumento nomeado my_methodpara o construtor, update()o dicionário, você apenas substituirá o método.
Pedro
3
Como outros disseram, a sugestão é um estilo de programação muito ruim. Esconde informações cruciais. Você pode mostrá-lo, mas você deve desencorajar explicitamente o OP de usá-lo.
Gerenuk 04/02
3
@Pedro: Existe alguma diferença semântica entre a sintaxe de Gruzczy e JoranBeasley?
Gerrit #
29

Como outros já mencionaram, a repetição não é ruim, mas, em alguns casos, um duplo nomeado pode ser um ótimo ajuste para esse tipo de problema. Isso evita o uso de locals () ou kwargs, que geralmente são uma má idéia.

from collections import namedtuple
# declare a new object type with three properties; x y z
# the first arg of namedtuple is a typename
# the second arg is comma-separated or space-separated property names
XYZ = namedtuple("XYZ", "x, y, z")

# create an object of type XYZ. properties are in order
abc = XYZ("one", "two", 3)
print abc.x
print abc.y
print abc.z

Encontrei uso limitado para isso, mas você pode herdar um nome nomeado como em qualquer outro objeto (exemplo: continuação):

class MySuperXYZ(XYZ):
    """ I add a helper function which returns the original properties """
    def properties(self):
        return self.x, self.y, self.z

abc2 = MySuperXYZ(4, "five", "six")
print abc2.x
print abc2.y
print abc2.z
print abc2.properties()
Um pequeno script de shell
fonte
5
Essas são tuplas; portanto, seu propertiesmétodo pode ser escrito como justo return tuple(self), o que é mais sustentável se no futuro mais campos forem adicionados à definição de nome nomeado.
21316 PaulMcG
1
Além disso, sua cadeia de declaração com nome duplo não requer vírgulas entre os nomes de campo, XYZ = namedtuple("XYZ", "x y z")funciona da mesma forma.
PaulMcG 4/16
Obrigado @PaulMcGuire. Eu estava tentando pensar em um complemento realmente simples para mostrar a herança e o tipo de espaço nela. Você está 100% certo e é uma ótima abreviação com outros objetos herdados! Eu mencionar os nomes de campo pode ser vírgula ou espaço - Eu prefiro CSV do hábito
Um pequeno Shell Script
1
Costumo usar namedtuples para esse propósito exato, especialmente em código matemático, em que uma função pode ser altamente parametrizada e ter vários coeficientes que só fazem sentido juntos.
detly
O problema namedtupleé que eles são somente leitura. Você não pode fazer abc.x += 1ou algo assim.
Hamstergene
29

explícito é melhor do que implícito ... então você pode torná-lo mais conciso:

def __init__(self,a,b,c):
    for k,v in locals().items():
        if k != "self":
             setattr(self,k,v)

A melhor pergunta é você deveria?

... dito isso, se você quiser uma tupla nomeada, eu recomendaria o uso de uma tupla nomeada (lembre-se de que as tuplas têm certas condições associadas a elas) ... talvez você queira um decreto ordenado ou apenas um decreto ...

Joran Beasley
fonte
Em seguida, o objeto terá coleta de lixo cíclica uma vez que tem em si como um atributo
John La Rooy
3
@bernie (ou é bemie?), às vezes r ke ning é difícil
gato
4
Para testes um pouco mais eficientes, if k != "self":pode ser alterado para if v is not self:teste de identidade barato, em vez de comparação de cadeias. Suponho que tecnicamente __init__poderia ser chamado uma segunda vez após a construção e passado selfcomo argumento subsequente, mas realmente não quero pensar que tipo de monstro faria isso. :-)
ShadowRanger 04/02
Isso poderia ser feito em uma função que leva o valor de retorno locals: set_fields_from_locals(locals()). Então não são mais do que as soluções baseadas em decoradores mais mágicas.
Lii 17/02
20

Para expandir a gruszczyresposta, usei um padrão como:

class X:
    x = None
    y = None
    z = None
    def __init__(self, **kwargs):
        for (k, v) in kwargs.items():
            if hasattr(self, k):
                setattr(self, k, v)
            else:
                raise TypeError('Unknown keyword argument: {:s}'.format(k))

Eu gosto desse método porque:

  • evita repetição
  • é resistente a erros de digitação ao construir um objeto
  • funciona bem com subclassificação (pode apenas super().__init(...))
  • permite a documentação dos atributos em nível de classe (onde eles pertencem) em vez de em X.__init__

Antes do Python 3.6, isso não dá controle sobre a ordem em que os atributos são definidos, o que pode ser um problema se alguns atributos forem propriedades com configuradores que acessam outros atributos.

Provavelmente poderia ser melhorado um pouco, mas sou o único usuário do meu próprio código, por isso não estou preocupado com nenhuma forma de limpeza de entrada. Talvez um AttributeErrorseria mais apropriado.

gerrit
fonte
10

Você também pode fazer:

locs = locals()
for arg in inspect.getargspec(self.__init__)[0][1:]:
    setattr(self, arg, locs[arg])

Obviamente, você teria que importar o inspectmódulo.

zondo
fonte
8

Esta é uma solução sem importações adicionais.

Função auxiliar

Uma pequena função auxiliar torna mais conveniente e reutilizável:

def auto_init(local_name_space):
    """Set instance attributes from arguments.
    """
    self = local_name_space.pop('self')
    for name, value in local_name_space.items():
        setattr(self, name, value)

Inscrição

Você precisa chamá-lo com locals():

class A:
    def __init__(self, x, y, z):
        auto_init(locals())

Teste

a = A(1, 2, 3)
print(a.__dict__)

Resultado:

{'y': 2, 'z': 3, 'x': 1}

Sem mudar locals()

Se você não gosta de mudar, locals()use esta versão:

def auto_init(local_name_space):
    """Set instance attributes from arguments.
    """
    for name, value in local_name_space.items():
        if name != 'self': 
            setattr(local_name_space['self'], name, value)
Mike Müller
fonte
docs.python.org/2/library/functions.html#locals locals() não deve ser modificado (isso pode afetar o intérprete, no seu caso, removendo selfdo escopo da função de chamada)
MaxB
@MaxB A partir dos documentos que você cita: ... as alterações podem não afetar os valores das variáveis ​​locais e livres usadas pelo intérprete. selfainda está disponível em __init__.
Mike Müller
Certo, o leitor espera que isso afete as variáveis ​​locais, mas pode ou não , dependendo de várias circunstâncias. O ponto é que é UB.
MaxB 11/11
Citação: "O conteúdo deste dicionário não deve ser modificado"
MaxB 11/12
@ MaxB Adicionei uma versão que não altera os locais ().
Mike Müller
7

Uma biblioteca interessante que lida com isso (e evita muitas outras alterações) é attrs . Seu exemplo, por exemplo, pode ser reduzido a isso (suponha que a classe seja chamada MyClass):

import attr

@attr.s
class MyClass:
    x = attr.ib()
    y = attr.ib()
    z = attr.ib()

Você nem precisa mais de um __init__método, a menos que faça outras coisas também. Aqui está uma boa introdução de Glyph Lefkowitz .

RafG
fonte
Em que grau a funcionalidade de attrredundância é feita dataclasses?
gerrit
1
@gerrit Isso é discutido na documentação do pacote attrs . Tbh, as diferenças não parecem mais tão grandes.
Ivo Merchiers 28/03/19
5

Meus 0,02 $. É muito próximo da resposta de Joran Beasley, mas mais elegante:

def __init__(self, a, b, c, d, e, f):
    vars(self).update((k, v) for k, v in locals().items() if v is not self)

Além disso, a resposta de Mike Müller (a melhor para o meu gosto) pode ser reduzida com esta técnica:

def auto_init(ns):
    self = ns.pop('self')
    vars(self).update(ns)

E a justa ligação auto_init(locals())do seu__init__

bgusach
fonte
1
docs.python.org/2/library/functions.html#locals locals() não deve ser modificado (comportamento indefinido)
MaxB
4

É uma maneira natural de fazer coisas em Python. Não tente inventar algo mais inteligente, pois isso levará a códigos excessivamente inteligentes que ninguém na sua equipe entenderá. Se você quer ser um jogador da equipe e continue escrevendo dessa maneira.

Peter Krumins
fonte
4

Python 3.7 em diante

No Python 3.7, você pode (ab) usar o dataclassdecorador, disponível no dataclassesmódulo. A partir da documentação:

Este módulo fornece um decorador e funções para adicionar automaticamente métodos especiais gerados, como __init__()e __repr__()às classes definidas pelo usuário. Foi originalmente descrito em PEP 557.

As variáveis ​​de membro a serem usadas nesses métodos gerados são definidas usando anotações do tipo PEP 526. Por exemplo, este código:

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

Irá adicionar, entre outras coisas, uma __init__()que se parece com:

def __init__(self, name: str, unit_price: float, quantity_on_hand: int=0):
      self.name = name
      self.unit_price = unit_price
      self.quantity_on_hand = quantity_on_hand

Observe que esse método é adicionado automaticamente à classe: não é especificado diretamente na definição de InventoryItem mostrada acima.

Se a sua turma for grande e complexa, pode ser inapropriado usar a dataclass. Estou escrevendo isso no dia do lançamento do Python 3.7.0, para que os padrões de uso ainda não estejam bem estabelecidos.

gerrit
fonte