¿Inicializar automáticamente las variables de instancia?

89

Tengo una clase de Python que se ve así:

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

seguido por:

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

¿Hay alguna forma de autoinicializar estas variables de instancia, como la lista de inicialización de C ++? Ahorraría mucho código redundante.

Adam Matan
fuente
1
Consulte también la discusión de la autoassignreceta del estado activo y una autoargsimplementación alternativa en: ¿Cuál es la mejor manera de realizar la asignación automática de atributos en Python? ¿Es una buena idea? - Stack Overflow
nealmcb

Respuestas:

104

Puedes usar un 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

Úselo para decorar el __init__método:

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

Salida:

>>> c = process(1, 2, 3, 4, 5, 6)
>>> c.PID
1
>>> dir(c)
['FDs', 'PID', 'PPID', '__doc__', '__init__', '__module__', 'cmd', 'reachable', 'user'
Nadia Alramli
fuente
5
Esto funciona y responde la pregunta, así que voté a favor. Pero me quedé con la respuesta de Ferdidand Beyer: "Lo explícito es mejor que lo implícito"
Lucas Gabriel Sánchez
14
+1 Por una gran respuesta que resolvió mi problema. ¿Pero no debería ser una funcionalidad básica del lenguaje? ¿Crees que vale la pena escribir un PEP?
Adam Matan
3
Esta es una respuesta realmente buena, esto ha ido directamente a mi caja de herramientas.
Michael van der Westhuizen
3
@NadiaAlramli Creo que hay un pequeño error en el código. Mira aquí gist.github.com/pmav99/137dbf4681be9a58de74 (original.py)
pmav99
2
El ejemplo actual tiene un error y no funcionará si la firma no incluye argumentos predeterminados. Debe incluir una verificación para asegurarse de que los nombres y los valores predeterminados no sean Ninguno. Ej: si nombres y valores predeterminados:
36

Si está utilizando Python 2.6 o superior, puede utilizar collections.namedtuple :

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

Esto es apropiado especialmente cuando su clase es realmente una gran bolsa de valores.

Kiv
fuente
2
"Esto es apropiado especialmente cuando su clase es realmente una gran bolsa de valores". En tal escenario, también podría hacer esto: https://docs.python.org/3.3/tutorial/classes.html#odds-and-ends
Big Sharpie
34

Para Python 3.7+, puede usar una clase de datos , que es una forma muy pitónica y fácil de mantener para hacer lo que quiera.

Le permite definir campos para su clase, que son sus variables de instancia inicializadas automáticamente.

Se vería algo así:

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

El __init__método ya estará en tu clase.

Tenga en cuenta que aquí se requieren sugerencias de tipo , por eso he usado inty stren el ejemplo. Si no conoce el tipo de su campo, puede usar Cualquiera del typingmódulo .

La clase de datos tiene muchas ventajas en comparación con las soluciones propuestas:

  • Es explícito : todos los campos son visibles, lo que respeta el Zen de Python y lo hace legible y fácil de mantener. Compárelo con el uso de **kwargs.
  • Puede tener métodos . Como cualquier otra clase.
  • Te permite ir más allá de lo automático __init__ usando el __post_init__método.

EDITAR: Razones para evitar el uso de NamedTuples

Algunos sugieren el uso de namedtuplepara este caso, pero namedtuples tienen algunos comportamientos que difieren de las clases de Python, que no son realmente evidentes al principio y deberían ser bien conocidos:

1. Las tuplas con nombre son inmutables

La inmutabilidad puede ser útil, pero tal vez no sea lo que desea para sus instancias. DataClasses también puede ser de alguna manera inmutable si usa el argumento frozen=Trueen el@dataclass decorador.

2. Tuplas nombradas __eq__ comporta como Tuple

En Python, SomeNamedTuple(a=1, b=2) == AnotherNamedTuple(c=1, d=2)es True! La __eq__función de NamedTuple, utilizada en comparaciones, solo considera los valores y las posiciones de esos valores en las instancias comparadas, no sus clases o nombres de campos.

Jundiaius
fuente
Esto solo debe usarse si el propósito de la clase es almacenar datos.
JC Rocamonde
O desarrollar en torno al almacenamiento de datos.
JC Rocamonde
3
¿Podría explicar por qué la clase de datos debería usarse solo para clases que almacenan datos, en lugar de tener otro comportamiento también? Podría crear una nueva publicación SO para que esto comprenda mejor sus casos de uso apropiados. Gracias.
Vahid Pazirandeh
Data Classes can be thought of as "mutable namedtuples with defaults". - PEP557
aafulei
26

Citando el Zen de Python ,

Explícito es mejor que implícito.

Ferdinand Beyer
fuente
10
¿No sería suficientemente explícita una declaración de lista de inicialización?
Adam Matan
Supongo. Pero no veo una razón para agregar algo así al lenguaje. Claramente prefiero declaraciones de asignaciones múltiples a algún tipo de magia decorativa detrás de escena.
Ferdinand Beyer
29
@Ferdinand, estoy de acuerdo en que sería una tontería tener en el idioma algo que puede estar perfectamente en stdlib, pero DEBERÍA estar en stdlib, porque "lo bello es mejor que lo feo" tiene prioridad, y muchas asignaciones repetitivas son feas (como es cualquier forma de repetición).
Alex Martelli
Bueno, para contrarrestar: DWIM> DWIS
Terrence Brannon
Estoy de acuerdo en que el decorador es más hermoso que una lista de asignaciones, pero PyCharm lo hace más feo al no entenderlo :-(
user110954
23

Otra cosa que puedes hacer:

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`

Pero la única solución que recomendaría, además de deletrearla, es "hacer una macro en su editor" ;-p

Jochen Ritzel
fuente
1
Buen truco para eliminar "yo".
Michael
15

Puede hacerlo fácilmente con los argumentos de palabras clave, por ejemplo, así:

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

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

una implementación similar para los argumentos posicionales sería:

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


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

que para mí no parece resolver su problema.

SilentGhost
fuente
3
Otra variación que me gusta esself.__dict__.update( **kwargs )
S.Lott
También podría usar locals () y poner argumentos normales.
mk12
7

La solución de Nadia es mejor y más poderosa, pero creo que esto también es interesante:

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

Necesitaba algo para el mismo propósito, pero ninguna de las respuestas existentes cubría todos los casos que probé. La respuesta de Nadia fue la más cercana a lo que estaba buscando, así que comencé con su código como base.

El siguiente decorador funciona con todas las combinaciones 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        )

También implementa la convención de _prefijo estándar para permitir __init__variables privadas que no se asignarán a instancias de clase.


###  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:

Incluí pruebas, pero las colapsé en la última línea ( 58 ) por brevedad. Puede expandir las pruebas, que detallan todos los casos de uso potenciales, find/replacecolocando $una nueva línea en todos los caracteres.

Enteleform
fuente
3

Puede que no sea necesario inicializar las variables, ya que locals () ya contiene los valores.

clase Dummy (objeto):

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

d = Maniquí (2, 3)

d.params

{'a': 2, 'b': 3, 'predeterminado': 'Fred'}

d.params ['b']

3

Por supuesto, dentro de una clase uno podría usar self.params

user3215769
fuente
Es un enfoque agradable y original, pero d['b']está escrito en la lengua franca de Python, aunque d.params['b']causará confusión a los lectores de código.
Adam Matan
3

Tan pronto como getargspecesté en desuso desde Python 3.5, aquí hay una solución que usa 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

Compruebe si 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
fuente
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

Manifestación:

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

Un enfoque de no decorador para Python 2 y 3 usando marcos:

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)

Manifestación:

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

nu11ptr ha realizado un pequeño módulo, PyInstanceVars , que incluye esta funcionalidad como decorador de funciones. En el README del módulo se establece que el " [...] rendimiento ahora es sólo un 30-40% peor que la inicialización explícita bajo CPython ".

Ejemplo de uso, extraído directamente de la documentación del 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
fuente
0

Quizás esta sea una pregunta cerrada, pero me gustaría proponer mi solución para saber qué piensas al respecto. He usado una metaclase que aplica un decorador al 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
fuente
0

La biblioteca attrs hace algo como esto.

offby1
fuente
0

al final de la función init :

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

Para obtener más información setattr(), consulte aquí.

LuWil
fuente
0

Hay una función auxiliar para hacer esto en la biblioteca fastcore 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
fuente