¿Cómo evito el "self.x = x; self.y = y; self.z = z ”patrón en __init__?

170

Veo patrones como

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

con bastante frecuencia, a menudo con muchos más parámetros. ¿Hay una buena manera de evitar este tipo de repetitividad tediosa? ¿Debería la clase heredar de su namedtuplelugar?

MaxB
fuente
31
No toda receptividad es mala. Tenga en cuenta que el modelo de clase de Python no incluye la definición explícita de los atributos de instancia, por lo que estas asignaciones son los equivalentes autodocumentados.
chepner
44
@chepner: Bueno, no requiere una definición explícita. Sin __slots__embargo , puede usar para este propósito ; es levemente poco fónico (más detallado para obtener ahorros de memoria), pero me gusta en gran medida para evitar el riesgo de auto-vivificar un atributo completamente nuevo si escribo el nombre.
ShadowRanger
2
Cualquier buen editor tendrá plantillas. Escribes ini <shortcut> x, y, z): <shortcut>y listo.
Gerenuk
3
Los Namedtuples son increíbles, si quieres un objeto de valor inmutable. Si quieres una clase regular y mutable, no puedes usarlos.
RemcoGerlich
44
"No" es una buena opción, cualquier opción disponible eliminará la firma del método (y, por lo tanto, potencialmente toda la interfaz). Además, si sus clases tienen una cantidad insoportable de campos para inicializar, puede considerar dividirlos.
Kroltan

Respuestas:

87

Editar: si tiene Python 3.7+, solo use clases de datos

Una solución decoradora que mantiene la firma:

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
fuente
2
buena respuesta, pero no funcionará con python2.7: nosignature
MaxB
3
@alexis, el decorador "decorator.decorator" envuelve automáticamente la función
Siphor
44
Estoy bastante dividido sobre si amar esto u odiarlo. Agradezco preservar la firma.
Kyle Strand
14
"... explícito es mejor que implícito. Simple es mejor que complejo ..." -Zen of Python
Jack Stout
9
-1 Francamente, esto es horrible. No tengo idea de lo que está haciendo este código de un vistazo, y es literalmente diez veces la cantidad de código. Ser inteligente se siente genial y todo, pero esto es un mal uso de su inteligencia obvia.
Ian Newson
108

Descargo de responsabilidad: Parece que varias personas están preocupadas por presentar esta solución, por lo que proporcionaré un descargo de responsabilidad muy claro. No deberías usar esta solución. Solo lo proporciono como información, para que sepa que el idioma es capaz de esto. El resto de la respuesta es solo mostrar capacidades de lenguaje, no respaldar su uso de esta manera.


Realmente no hay nada malo en copiar explícitamente los parámetros en los atributos. Si tiene demasiados parámetros en el ctor, a veces se considera un olor a código y tal vez debería agrupar estos parámetros en menos objetos. Otras veces, es necesario y no tiene nada de malo. De todos modos, hacerlo explícitamente es el camino a seguir.

Sin embargo, dado que usted pregunta CÓMO se puede hacer (y no si se debe hacer), entonces una solución es esta:

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
fuente
16
buena respuesta +1 ... aunque self.__dict__.update(kwargs)podría ser marginalmente más pitónico
Joran Beasley
44
El problema con este enfoque es que no hay registro de lo que los argumentos A.__init__realmente esperan, y no hay verificación de errores para nombres de argumentos mal escritos.
MaxB
77
@JoranBeasley La actualización del diccionario de instancias a ciegas kwargste deja abierto al equivalente de un ataque de inyección SQL. Si su objeto tiene un método llamado my_methody le pasa un argumento my_methodal constructor, entonces update()el diccionario, simplemente sobrescribió el método.
Pedro
3
Como otros dijeron, la sugerencia es un estilo de programación realmente pobre. Oculta información crucial. Puede mostrarlo, pero debe desalentar explícitamente que el OP lo use.
Gerenuk
3
@Pedro ¿Hay alguna diferencia semántica entre la sintaxis de gruzczy y JoranBeasley?
gerrit
29

Como otros han mencionado, la repetición no es mala, pero en algunos casos una tupla con nombre puede ser una gran opción para este tipo de problema. Esto evita el uso de locales () o kwargs, que generalmente son una mala idea.

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

He encontrado un uso limitado para él, pero puede heredar una tupla nombrada como con cualquier otro objeto (ejemplo continuado):

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()
Un script de shell pequeño
fuente
55
Estas son tuplas, por lo que su propertiesmétodo puede escribirse como justo return tuple(self), lo que es más fácil de mantener si en el futuro se agregan más campos a la definición de tupla nombrada.
PaulMcG
1
Además, su cadena de declaración namedtuple no requiere comas entre los nombres de campo, XYZ = namedtuple("XYZ", "x y z")funciona igual de bien.
PaulMcG
Gracias @PaulMcGuire. Estaba tratando de pensar en un complemento realmente simple para mostrar la herencia y un poco de espacio en eso. ¡Tienes 100% de razón y es una excelente taquigrafía con otros objetos heredados también! Menciono que los nombres de campo pueden estar separados por comas o espacios - Prefiero CSV de hábito
Un script de Shell pequeño
1
A menudo uso namedtuples para este propósito exacto, especialmente en el código matemático donde una función puede estar altamente parametrizada y tener un montón de coeficientes que solo tienen sentido juntos.
desviarse
El problema namedtuplees que son de solo lectura. No puedes hacer abc.x += 1ni nada de eso.
hamstergene
29

explícito es mejor que implícito ... así que seguro que podría hacerlo más conciso:

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

La mejor pregunta es ¿debería?

... Dicho esto, si desea una tupla con nombre, recomendaría usar una tupla con nombre (recuerde que las tuplas tienen ciertas condiciones asociadas) ... tal vez desee una sentencia ordenada o incluso solo una sentencia ...

Joran Beasley
fuente
Entonces el objeto necesitará recolección de basura cíclica ya que se tiene a sí mismo como un atributo
John La Rooy
3
@bernie (¿o es bemie?), a veces, ke ke ning es difícil
gato
44
Para pruebas un poco más eficientes, if k != "self":podría cambiarse a if v is not self:prueba de identidad barata en lugar de comparación de cadenas. Supongo que técnicamente __init__podría llamarse por segunda vez después de la construcción y pasar selfcomo un argumento posterior, pero realmente no quiero pensar qué tipo de monstruo haría eso. :-)
ShadowRanger
Que se podrían hacer en una función que toma el valor de retorno de locals: set_fields_from_locals(locals()). Entonces ya no es más que las soluciones basadas en decoradores más mágicos.
Lii
20

Para ampliar gruszczyla respuesta de s, he usado un patrón 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))

Me gusta este método porque:

  • evita la repetición
  • es resistente a los errores tipográficos al construir un objeto
  • funciona bien con subclases (solo puede super().__init(...))
  • permite la documentación de los atributos en un nivel de clase (donde pertenecen) en lugar de en X.__init__

Antes de Python 3.6, esto no proporciona control sobre el orden en que se establecen los atributos, lo que podría ser un problema si algunos atributos son propiedades con setters que acceden a otros atributos.

Probablemente podría mejorarse un poco, pero soy el único usuario de mi propio código, por lo que no me preocupa ninguna forma de desinfección de entrada. Quizás un AttributeErrorsería más apropiado.

gerrit
fuente
10

También puedes hacer:

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

Por supuesto, tendría que importar el inspectmódulo.

zondo
fuente
8

Esta es una solución sin ninguna importación adicional.

Función auxiliar

Una pequeña función auxiliar lo hace más conveniente y reutilizable:

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)

Solicitud

Necesitas llamarlo con locals():

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

Prueba

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

Salida:

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

Sin cambiar locals()

Si no te gusta cambiar locals()usa esta versión:

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
fuente
docs.python.org/2/library/functions.html#locals locals() no debe modificarse (puede afectar al intérprete, en su caso, eliminarlo selfdel alcance de la función de llamada)
MaxB
@MaxB De los documentos que cita: ... los cambios pueden no afectar los valores de las variables locales y libres utilizadas por el intérprete. selfestá disponible en __init__.
Mike Müller
Correcto, el lector espera que afecte las variables locales, pero puede o no , dependiendo de una serie de circunstancias. El punto es que es UB.
MaxB
Cita: "El contenido de este diccionario no debe modificarse"
MaxB
@MaxB Agregué una versión que no cambia los locales ().
Mike Müller
7

Una biblioteca interesante que maneja esto (y evita muchas otras repeticiones) es attrs . Su ejemplo, por ejemplo, podría reducirse a esto (suponga que se llama a la clase MyClass):

import attr

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

Ya ni siquiera necesita un __init__método, a menos que también haga otras cosas. Aquí hay una buena introducción de Glyph Lefkowitz .

RafG
fuente
¿Hasta qué punto es attrredundante la funcionalidad de dataclasses?
gerrit
1
@gerrit Esto se trata en la documentación del paquete attrs . Tbh, las diferencias ya no parecen tan grandes.
Ivo Merchiers
5

Mi 0.02 $. Está muy cerca de la respuesta de Joran Beasley, pero más 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)

Además, la respuesta de Mike Müller (la mejor para mi gusto) se puede reducir con esta técnica:

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

Y la simple llamada auto_init(locals())de tu__init__

bgusach
fuente
1
docs.python.org/2/library/functions.html#locals locals() no debe modificarse (comportamiento indefinido)
MaxB
4

Es una forma natural de hacer cosas en Python. No intentes inventar algo más inteligente, conducirá a un código demasiado inteligente que nadie en tu equipo entenderá. Si quieres ser un jugador de equipo y luego seguir escribiéndolo de esta manera.

Peter Krumins
fuente
4

Python 3.7 en adelante

En Python 3.7, puede (ab) usar el dataclassdecorador, disponible desde el dataclassesmódulo. De la documentación:

Este módulo proporciona un decorador y funciones para agregar automáticamente métodos especiales generados como __init__()y__repr__() a las clases definidas por el usuario. Fue descrito originalmente en PEP 557.

Las variables miembro a usar en estos métodos generados se definen usando anotaciones de tipo PEP 526. Por ejemplo 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

Agregará, entre otras cosas, un __init__()aspecto similar a:

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

Tenga en cuenta que este método se agrega automáticamente a la clase: no se especifica directamente en la definición de InventoryItem que se muestra arriba.

Si su clase es grande y compleja, puede ser inapropiado usar a dataclass. Escribo esto el día del lanzamiento de Python 3.7.0, por lo que los patrones de uso aún no están bien establecidos.

gerrit
fuente