Inicializar variáveis ​​de instância automaticamente?

90

Eu tenho uma classe Python que se parece com isto:

class Process:
    def __init__(self, PID, PPID, cmd, FDs, reachable, user):

Seguido por:

        self.PID=PID
        self.PPID=PPID
        self.cmd=cmd
        ...

Existe alguma maneira de autoinicializar essas variáveis ​​de instância, como a lista de inicialização do C ++? Isso pouparia muitos códigos redundantes.

Adam Matan
fonte
1
Veja também a discussão sobre a autoassignreceita de ativação e uma autoargsimplementação alternativa em: Qual é a melhor maneira de fazer atribuição automática de atributos em Python, e é uma boa ideia? - Stack Overflow
nealmcb

Respostas:

104

Você pode usar um decorador:

from functools import wraps
import inspect

def initializer(func):
    """
    Automatically assigns the parameters.

    >>> class process:
    ...     @initializer
    ...     def __init__(self, cmd, reachable=False, user='root'):
    ...         pass
    >>> p = process('halt', True)
    >>> p.cmd, p.reachable, p.user
    ('halt', True, 'root')
    """
    names, varargs, keywords, defaults = inspect.getargspec(func)

    @wraps(func)
    def wrapper(self, *args, **kargs):
        for name, arg in list(zip(names[1:], args)) + list(kargs.items()):
            setattr(self, name, arg)

        for name, default in zip(reversed(names), reversed(defaults)):
            if not hasattr(self, name):
                setattr(self, name, default)

        func(self, *args, **kargs)

    return wrapper

Use-o para decorar o __init__método:

class process:
    @initializer
    def __init__(self, PID, PPID, cmd, FDs, reachable, user):
        pass

Resultado:

>>> c = process(1, 2, 3, 4, 5, 6)
>>> c.PID
1
>>> dir(c)
['FDs', 'PID', 'PPID', '__doc__', '__init__', '__module__', 'cmd', 'reachable', 'user'
Nadia Alramli
fonte
5
Isso funciona e responda à pergunta, então votei. Mas mantive a resposta de Ferdidand Beyer: "Explícito é melhor do que implícito"
Lucas Gabriel Sánchez
14
1 Por uma ótima resposta que resolveu meu problema. Mas não deveria ser uma funcionalidade central da linguagem? Você acha que vale a pena escrever um PEP?
Adam Matan,
3
Esta é uma resposta realmente boa - foi direto para minha caixa de ferramentas.
Michael van der Westhuizen,
3
@NadiaAlramli Acho que há um pequeno bug no código. Olhe aqui gist.github.com/pmav99/137dbf4681be9a58de74 (original.py)
pmav99
2
O exemplo atual tem um bug e não funcionará se a assinatura não incluir argumentos padrão. Você precisa incluir uma verificação para garantir que os nomes e padrões não sejam Nenhum. Ex: se nomes e padrões:
36

Se estiver usando o Python 2.6 ou superior, você pode usar Collections.namedtuple :

>>> from collections import namedtuple
>>> Process = namedtuple('Process', 'PID PPID cmd')
>>> proc = Process(1, 2, 3)
>>> proc.PID
1
>>> proc.PPID
2

Isso é apropriado especialmente quando sua classe é, na verdade, apenas um grande saco de valores.

Kiv
fonte
2
"Isso é apropriado, especialmente quando sua classe é realmente apenas um grande saco de valores." Nesse cenário, você também pode fazer isso: https://docs.python.org/3.3/tutorial/classes.html#odds-and-ends
Big Sharpie
35

Para Python 3.7+, você pode usar uma classe de dados , que é uma maneira muito pítônica e de fácil manutenção de fazer o que você quiser.

Ele permite que você defina campos para sua classe, que são suas variáveis ​​de instância inicializadas automaticamente.

Seria algo assim:

@dataclass
class Process:
    PID: int
    PPID: int
    cmd: str
    ...

O __init__método já estará em sua aula.

Observe que aqui a sugestão de tipo é necessária , é por isso que usei inte strno exemplo. Se você não souber o tipo do seu campo, pode usar Qualquer dotyping módulo .

A Data Class tem muitas vantagens em comparação com as soluções propostas:

  • É explícito : todos os campos são visíveis, o que respeita o Zen do Python e o torna legível e sustentável. Compare-o com o uso de**kwargs .
  • Pode haver métodos . Como qualquer outra classe.
  • Permite ir além do automático __init__usando o __post_init__método.

EDIT: Razões para evitar o uso de NamedTuples

Alguns sugerem o uso de namedtuplepara este caso, mas as duplas nomeadas têm alguns comportamentos que diferem das classes Python, que não são realmente evidentes no início e devem ser bem conhecidos:

1. NamedTuples são imutáveis

A imutabilidade pode ser útil, mas talvez não seja o que você deseja para suas instâncias. DataClasses também pode ser imutável de alguma forma se você usar o argumento frozen=Trueno @dataclassdecorador.

2. NamedTuples __eq__se comporta como o de Tuple

Em Python, SomeNamedTuple(a=1, b=2) == AnotherNamedTuple(c=1, d=2)é True! A __eq__função de NamedTuple, usada em comparações, considera apenas os valores e as posições desses valores nas instâncias comparadas, não seus nomes de classe ou campos.

Jundiaius
fonte
Isso só deve ser usado se o objetivo da classe for armazenar dados.
JC Rocamonde
Ou para desenvolver em torno do armazenamento de dados.
JC Rocamonde
3
Você explicaria por que o dataclass deve ser usado apenas para classes que armazenam dados, em vez de ter outro comportamento também? Eu poderia criar um novo post de SO inteiramente para entender melhor seus casos de uso apropriados. Obrigado.
Vahid Pazirandeh
Data Classes can be thought of as "mutable namedtuples with defaults". - PEP557
aafulei
26

Citando o Zen do Python ,

Explícito é melhor do que implícito.

Ferdinand Beyer
fonte
10
Uma declaração de lista de inicialização não seria suficientemente explícita?
Adam Matan,
Eu acho. Mas não vejo razão para adicionar algo assim à linguagem. Eu claramente prefiro várias instruções de atribuição a algum tipo de mágica do decorador nos bastidores.
Ferdinand Beyer,
30
@Ferdinand, concordo que seria bobagem ter na linguagem algo que pode perfeitamente estar no stdlib, mas DEVE estar no stdlib, porque "bonito é melhor do que feio" tem precedência, e muitas atribuições repetitivas são feias (como qualquer forma de repetição).
Alex Martelli,
Bem, para contrariar: DWIM> DWIS
Terrence Brannon
Eu concordaria que decorador é mais bonito do que uma lista de atribuições, mas PyCharm o torna mais feio por não entendê-lo :-(
user110954
24

Outra coisa que você pode fazer:

class X(object):
    def __init__(self, a,b,c,d):
        vars = locals() # dict of local names
        self.__dict__.update(vars) # __dict__ holds and object's attributes
        del self.__dict__["self"] # don't need `self`

Mas a única solução que eu recomendaria, além de soletrar, é "faça uma macro em seu editor" ;-p

Jochen Ritzel
fonte
1
Boa pegada em deletar 'self'.
michael
15

Você poderia fazer isso facilmente com os argumentos de palavra-chave, por exemplo:

>>> class D:
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

>>> D(test='d').test
'd'

implementação semelhante para os argumentos posicionais seria:

>> class C:
    def __init__(self, *args):
        self.t, self.d = args


>>> C('abc', 'def').t
'abc'
>>> C('abc', 'def').d
'def'

que para mim não parece resolver seu problema.

SilentGhost
fonte
3
Outra variação de que gosto éself.__dict__.update( **kwargs )
S.Lott,
Pode muito bem usar locals () e colocar argumentos normais.
mk12,
7

A solução de Nádia é melhor e mais poderosa, mas acho que isso também é interessante:

def constructor(*arg_names):
  def __init__(self, *args):
    for name, val in zip(arg_names, args):
      self.__setattr__(name, val)
  return __init__


class MyClass(object):
  __init__ = constructor("var1", "var2", "var3")


>>> c = MyClass("fish", "cheese", "beans")
>>> c.var2
"cheese"
Andrew Magee
fonte
5

Eu precisava de algo para o mesmo propósito, mas nenhuma das respostas existentes cobriu todos os casos que testei. A resposta de Nadia foi a mais próxima do que eu estava procurando, então comecei com seu código como base.

O decorador abaixo funciona com todas as combinações válidas de argumentos:

Positional                                          __init__(self, a, b                )
Keyword                                             __init__(self, a=None, b=None      )
Positional + Keyword                                __init__(self, a, b, c=None, d=None)
Variable Positional                                 __init__(self, *a                  )
Variable Positional + Keyword                       __init__(self, *a, b=None          )
Variable Positional + Variable Keyword              __init__(self, *a, **kwargs        )
Positional + Variable Positional + Keyword          __init__(self, a, *b, c=None       )
Positional + Variable Positional + Variable Keyword __init__(self, a, *b, **kwargs     )
Keyword Only                                        __init__(self, *, a=None           )
Positional + Keyword Only                           __init__(self, a, *, b=None        )

Ele também implementa a _convenção -prefix padrão para permitir __init__variáveis ​​-private que não serão atribuídas a instâncias de classe.


###  StdLib  ###
from functools import wraps
import inspect


###########################################################################################################################
#//////|   Decorator   |//////////////////////////////////////////////////////////////////////////////////////////////////#
###########################################################################################################################

def auto_assign_arguments(function):

  @wraps(function)
  def wrapped(self, *args, **kwargs):
    _assign_args(self, list(args), kwargs, function)
    function(self, *args, **kwargs)

  return wrapped


###########################################################################################################################
#//////|   Utils   |//////////////////////////////////////////////////////////////////////////////////////////////////////#
###########################################################################################################################

def _assign_args(instance, args, kwargs, function):

  def set_attribute(instance, parameter, default_arg):
    if not(parameter.startswith("_")):
      setattr(instance, parameter, default_arg)

  def assign_keyword_defaults(parameters, defaults):
    for parameter, default_arg in zip(reversed(parameters), reversed(defaults)):
      set_attribute(instance, parameter, default_arg)

  def assign_positional_args(parameters, args):
    for parameter, arg in zip(parameters, args.copy()):
      set_attribute(instance, parameter, arg)
      args.remove(arg)

  def assign_keyword_args(kwargs):
    for parameter, arg in kwargs.items():
      set_attribute(instance, parameter, arg)
  def assign_keyword_only_defaults(defaults):
    return assign_keyword_args(defaults)

  def assign_variable_args(parameter, args):
    set_attribute(instance, parameter, args)

  POSITIONAL_PARAMS, VARIABLE_PARAM, _, KEYWORD_DEFAULTS, _, KEYWORD_ONLY_DEFAULTS, _ = inspect.getfullargspec(function)
  POSITIONAL_PARAMS = POSITIONAL_PARAMS[1:] # remove 'self'

  if(KEYWORD_DEFAULTS     ): assign_keyword_defaults     (parameters=POSITIONAL_PARAMS,  defaults=KEYWORD_DEFAULTS)
  if(KEYWORD_ONLY_DEFAULTS): assign_keyword_only_defaults(defaults=KEYWORD_ONLY_DEFAULTS                          )
  if(args                 ): assign_positional_args      (parameters=POSITIONAL_PARAMS,  args=args                )
  if(kwargs               ): assign_keyword_args         (kwargs=kwargs                                           )
  if(VARIABLE_PARAM       ): assign_variable_args        (parameter=VARIABLE_PARAM,      args=args                )


###########################################################################################################################$#//////|   Tests   |//////////////////////////////////////////////////////////////////////////////////////////////////////#$###########################################################################################################################$$if __name__ == "__main__":$$#######|   Positional   |##################################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, b):$      pass$$  t = T(1, 2)$  assert (t.a == 1) and (t.b == 2)$$#######|   Keyword   |#####################################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a="KW_DEFAULT_1", b="KW_DEFAULT_2"):$      pass$$  t = T(a="kw_arg_1", b="kw_arg_2")$  assert (t.a == "kw_arg_1") and (t.b == "kw_arg_2")$$#######|   Positional + Keyword   |########################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, b, c="KW_DEFAULT_1", d="KW_DEFAULT_2"):$      pass$$  t = T(1, 2)$  assert (t.a == 1) and (t.b == 2) and (t.c == "KW_DEFAULT_1") and (t.d == "KW_DEFAULT_2")$$  t = T(1, 2, c="kw_arg_1")$  assert (t.a == 1) and (t.b == 2) and (t.c == "kw_arg_1") and (t.d == "KW_DEFAULT_2")$$  t = T(1, 2, d="kw_arg_2")$  assert (t.a == 1) and (t.b == 2) and (t.c == "KW_DEFAULT_1") and (t.d == "kw_arg_2")$$#######|   Variable Positional   |#########################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *a):$      pass$$  t = T(1, 2, 3)$  assert (t.a == [1, 2, 3])$$#######|   Variable Positional + Keyword   |###############################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *a, b="KW_DEFAULT_1"):$      pass$$  t = T(1, 2, 3)$  assert (t.a == [1, 2, 3]) and (t.b == "KW_DEFAULT_1")$$  t = T(1, 2, 3, b="kw_arg_1")$  assert (t.a == [1, 2, 3]) and (t.b == "kw_arg_1")$$#######|   Variable Positional + Variable Keyword   |######################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *a, **kwargs):$      pass$$  t = T(1, 2, 3, b="kw_arg_1", c="kw_arg_2")$  assert (t.a == [1, 2, 3]) and (t.b == "kw_arg_1") and (t.c == "kw_arg_2")$$#######|   Positional + Variable Positional + Keyword   |##################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, *b, c="KW_DEFAULT_1"):$      pass$$  t = T(1, 2, 3, c="kw_arg_1")$  assert (t.a == 1) and (t.b == [2, 3]) and (t.c == "kw_arg_1")$$#######|   Positional + Variable Positional + Variable Keyword   |#########################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, *b, **kwargs):$      pass$$  t = T(1, 2, 3, c="kw_arg_1", d="kw_arg_2")$  assert (t.a == 1) and (t.b == [2, 3]) and (t.c == "kw_arg_1") and (t.d == "kw_arg_2")$$#######|   Keyword Only   |################################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, *, a="KW_DEFAULT_1"):$      pass$$  t = T(a="kw_arg_1")$  assert (t.a == "kw_arg_1")$$#######|   Positional + Keyword Only   |###################################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, *, b="KW_DEFAULT_1"):$      pass$$  t = T(1)$  assert (t.a == 1) and (t.b == "KW_DEFAULT_1")$$  t = T(1, b="kw_arg_1")$  assert (t.a == 1) and (t.b == "kw_arg_1")$$#######|   Private __init__ Variables (underscored)   |####################################################################$$  class T:$    @auto_assign_arguments$    def __init__(self, a, b, _c):$      pass$$  t = T(1, 2, 3)$  assert hasattr(t, "a") and hasattr(t, "b") and not(hasattr(t, "_c"))

Nota:

Eu incluí testes, mas os reduzi na última linha ( 58 ) para abreviar. Você pode expandir os testes, que detalham todos os casos de uso em potencial, find/replace-ing todos os $caracteres com uma nova linha.

Enteleform
fonte
3

Pode não ser necessário inicializar variáveis, pois locals () já contém os valores!

classe Dummy (objeto):

def __init__(self, a, b, default='Fred'):
    self.params = {k:v for k,v in locals().items() if k != 'self'}

d = Fictício (2, 3)

d.params

{'a': 2, 'b': 3, 'padrão': 'Fred'}

d.params ['b']

3

Claro, dentro de uma classe pode-se usar self.params

user3215769
fonte
É uma abordagem legal e original, mas d['b']é escrita na língua franca do Python, embora d.params['b']possa causar confusão para os leitores de código.
Adam Matan
3

Assim que getargspecestiver obsoleto desde o Python 3.5, aqui está a solução usando inspect.signature:

from inspect import signature, Parameter
import functools


def auto_assign(func):
    # Signature:
    sig = signature(func)
    for name, param in sig.parameters.items():
        if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
            raise RuntimeError('Unable to auto assign if *args or **kwargs in signature.')
    # Wrapper:
    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        for i, (name, param) in enumerate(sig.parameters.items()):
            # Skip 'self' param:
            if i == 0: continue
            # Search value in args, kwargs or defaults:
            if i - 1 < len(args):
                val = args[i - 1]
            elif name in kwargs:
                val = kwargs[name]
            else:
                val = param.default
            setattr(self, name, val)
        func(self, *args, **kwargs)
    return wrapper

Verifique se funciona:

class Foo(object):
    @auto_assign
    def __init__(self, a, b, c=None, d=None, e=3):
        pass

f = Foo(1, 2, d="a")
assert f.a == 1
assert f.b == 2
assert f.c is None
assert f.d == "a"
assert f.e == 3

print("Ok")
Mikhail Gerasimov
fonte
2

Para Python 3.3+:

from functools import wraps
from inspect import Parameter, signature


def instance_variables(f):
    sig = signature(f)
    @wraps(f)
    def wrapper(self, *args, **kwargs):
        values = sig.bind(self, *args, **kwargs)
        for k, p in sig.parameters.items():
            if k != 'self':
                if k in values.arguments:
                    val = values.arguments[k]
                    if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
                        setattr(self, k, val)
                    elif p.kind == Parameter.VAR_KEYWORD:
                        for k, v in values.arguments[k].items():
                            setattr(self, k, v) 
                else:
                    setattr(self, k, p.default) 
    return wrapper

class Point(object):
    @instance_variables 
    def __init__(self, x, y, z=1, *, m='meh', **kwargs):
        pass

Demo:

>>> p = Point('foo', 'bar', r=100, u=200)
>>> p.x, p.y, p.z, p.m, p.r, p.u
('foo', 'bar', 1, 'meh', 100, 200)

Uma abordagem não decoradora para Python 2 e 3 usando frames:

import inspect


def populate_self(self):
    frame = inspect.getouterframes(inspect.currentframe())[1][0]
    for k, v in frame.f_locals.items():
        if k != 'self':
            setattr(self, k, v)


class Point(object):
    def __init__(self, x, y):
        populate_self(self)

Demo:

>>> p = Point('foo', 'bar')
>>> p.x
'foo'
>>> p.y
'bar'
Ashwini Chaudhary
fonte
1

nu11ptr fez um pequeno módulo, PyInstanceVars , que inclui essa funcionalidade como um decorador de função. No README do módulo está afirmado que o " [...] desempenho agora é apenas 30-40% pior do que a inicialização explícita sob CPython ".

Exemplo de uso, retirado diretamente da documentação do módulo :

>>> from instancevars import *
>>> class TestMe(object):
...     @instancevars(omit=['arg2_'])
...     def __init__(self, _arg1, arg2_, arg3='test'):
...             self.arg2 = arg2_ + 1
...
>>> testme = TestMe(1, 2)
>>> testme._arg1
1
>>> testme.arg2_
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'TestMe' object has no attribute 'arg2_'
>>> testme.arg2
3
>>> testme.arg3
'test'
Benregn
fonte
0

Talvez seja uma questão fechada, mas gostaria de propor minha solução para saber o que você pensa a respeito. Eu usei uma metaclasse que aplica um decorador ao método init

import inspect

class AutoInit(type):
    def __new__(meta, classname, supers, classdict):
        classdict['__init__'] = wrapper(classdict['__init__'])
        return type.__new__(meta, classname, supers, classdict)

def wrapper(old_init):
    def autoinit(*args):
        formals = inspect.getfullargspec(old_init).args
        for name, value in zip(formals[1:], args[1:]):
            setattr(args[0], name, value)
    return autoinit
delca85
fonte
0

A biblioteca attrs faz algo assim.

offby1
fonte
0

no final da função init :

for var in list(locals().keys()):
    setattr(self,var,locals()[var])

Para mais informações setattr(), consulte aqui

LuWil
fonte
0

Há uma função auxiliar para fazer isso no fastcore lib https://fastcore.fast.ai/utils.html#store_attr .

from fastcore.utils import store_attr

class Process:
    def __init__(self, PID, PPID, cmd, FDs, reachable, user):
        store_attr() # this will do the same as self.PID = PID etc.
Alex
fonte