Evitar constructores largos mientras se hereda sin ocultar constructor, argumentos opcionales o funcionalidad

Tengo un problema particular, pero haré el ejemplo más general. Tengo una clase principal con un parámetro de constructor obligatorio y algunos opcionales, cada uno con un valor predeterminado. Luego, heredé a Child de él y agrego un parámetro obligatorio, y heredé GrandChild de Child y agrego otro parámetro obligatorio al constructor. El resultado es similar a este:

class Parent(): def __init__(self, arg1, opt_arg1='opt_arg1_default_val', opt_arg2='opt_arg2_default_val', opt_arg3='opt_arg3_default_val', opt_arg4='opt_arg4_default_val'): self.arg1 = arg1 self.opt_arg1 = opt_arg1 self.opt_arg2 = opt_arg2 self.opt_arg3 = opt_arg3 self.opt_arg4 = opt_arg4 class Child(Parent): def __init__(self, arg1, arg2, opt_arg1, opt_arg2, opt_arg3, opt_arg4): super().__init__(arg1, opt_arg1, opt_arg2, opt_arg3, opt_arg4) self.arg2 = arg2 class GrandChild(Child): def __init__(self, arg1, arg2, arg3, opt_arg1, opt_arg2, opt_arg3, opt_arg4): super().__init__(arg1, arg2, opt_arg1, opt_arg2, opt_arg3, opt_arg4) self.arg3 = arg3 

El problema es que esto se ve bastante feo, especialmente si quiero heredar más clases de Child, tendría que copiar / pegar todos los argumentos en el constructor de esa nueva clase.

En la búsqueda de una solución, encontré aquí que puedo resolver este problema usando ** kwargs así:

 class Parent(): def __init__(self, arg1, opt_arg1='opt_arg1_default_val', opt_arg2='opt_arg2_default_val', opt_arg3='opt_arg3_default_val', opt_arg4='opt_arg4_default_val'): self.arg1 = arg1 self.opt_arg1 = opt_arg1 self.opt_arg2 = opt_arg2 self.opt_arg3 = opt_arg3 self.opt_arg4 = opt_arg4 class Child(Parent): def __init__(self, arg1, arg2, **kwargs): super().__init__(arg1, **kwargs) self.arg2 = arg2 class GrandChild(Child): def __init__(self, arg1, arg2, arg3,**kwargs): super().__init__(arg1, arg2,**kwargs) self.arg3 = arg3 

Sin embargo, no estoy seguro de si esta es la forma correcta.

También hay un pequeño inconveniente al crear objetos de estas clases. Estoy usando PyCharm para desarrollar, y en este caso, el IDE tiene un método útil para mostrar una función / argumentos de constructor de clase. Por ejemplo, en el primer ejemplo,

introduzca la descripción de la imagen aquí

Esto hace que sea mucho más fácil de desarrollar y puede ayudar a los futuros desarrolladores, ya que pueden ver qué otros argumentos tiene la función. Sin embargo, en el segundo ejemplo, los argumentos opcionales ya no se muestran:

introduzca la descripción de la imagen aquí

Y no creo que sea una buena práctica usar ** kwargs en este caso, ya que uno tendría que profundizar en el código hasta la clase Parent para verificar qué argumentos opcionales tiene.

También he estudiado el uso del patrón de Generador, pero luego todo lo que hago es mover la lista de argumentos de mis clases a clases de generador, y tengo el mismo problema, los constructores con muchos argumentos que cuando se heredan crearán aún más argumentos en la parte superior de los ya existentes. También en Python, por lo que veo, Builder realmente no tiene mucho sentido teniendo en cuenta que todos los miembros de la clase son públicos y se puede acceder a ellos sin necesidad de configuradores y captadores.

¿Alguna idea sobre cómo resolver este problema constructor?

La idea básica es escribir código que genere el método __init__ para ti, con todos los parámetros especificados explícitamente en lugar de a través de *args y / o **kwargs , y sin necesidad de self.arg1 = arg1 con todas esas líneas self.arg1 = arg1 .

Y, idealmente, puede facilitar la adición de anotaciones de tipo que PyCharm puede usar para sugerencias emergentes y / o verificación de tipos estáticos. 1

Y, mientras lo hace, ¿por qué no construir una __repr__ que muestre los mismos valores? Y tal vez incluso un __eq__ , y un __hash__ , y tal vez operadores de comparación lexicográficos, y conversión hacia y desde un dict cuyas claves coincidan con los atributos para cada persistencia JSON, y …

O, mejor aún, use una biblioteca que se encargue de eso por usted.

Python 3.7 viene con una biblioteca de este tipo, dataclasses . O puede usar una biblioteca de terceros como attrs , que funciona con Python 3.4 y (con algunas limitaciones) 2.7. O, para casos simples (donde sus objetos son inmutables, y desea que funcionen como una tupla de sus atributos en un orden específico), puede usar namedtuple , que funciona de nuevo a 3.0 y 2.6.

Desafortunadamente, las dataclasses no funcionan para su caso de uso. Si solo escribes esto:

 from dataclasses import dataclass @dataclass class Parent: arg1: str opt_arg1: str = 'opt_arg1_default_val' opt_arg2: str = 'opt_arg2_default_val' opt_arg3: str = 'opt_arg3_default_val' opt_arg4: str = 'opt_arg4_default_val' @dataclass class Child(Parent): arg2: str 

… obtendrá un error, ya que intenta colocar el parámetro obligatorio arg2 después de los parámetros de valores predeterminados opt_arg1 través de opt_arg4 .

dataclasses no tiene ninguna forma de reordenar los parámetros ( Child(arg1, arg2, opt_arg1=… ), o de forzarlos para que sean solo palabras clave ( Child(*, arg1, opt_arg1=…, arg2) ). attrs doesn ‘ t tiene esa funcionalidad fuera de la caja, pero puede agregarla.

Entonces, no es tan trivial como esperas, pero es factible.


Pero si quisiera escribir esto usted mismo, ¿cómo crearía la función __init__ dinámicamente?

La opción más sencilla es exec .

Probablemente has escuchado que el exec es peligroso. Pero solo es peligroso si está pasando valores que provienen de su usuario. Aquí, solo estás pasando valores que provienen de tu propio código fuente.

Sigue siendo feo, pero a veces es la mejor respuesta de todos modos. El nombre de la biblioteca estándar solía ser una plantilla exec gigante. , e incluso la versión actual usa exec para la mayoría de los métodos , y también lo hacen las dataclasses .

Además, tenga en cuenta que todos estos módulos almacenan el conjunto de campos en algún lugar en un atributo de clase privada, por lo que las subclases pueden leer fácilmente los campos de la clase primaria. Si no lo hizo, podría usar el módulo de inspect para obtener la Signature para el inicializador de su clase base (o clases base, para herencia múltiple) y trabajar desde allí. Pero el uso de base._fields es obviamente mucho más simple (y permite almacenar metadatos adicionales que normalmente no van en firmas).

Aquí hay una implementación sencilla que no maneja la mayoría de las características de attrs o dataclasses , pero ordena todos los parámetros obligatorios antes que todos los opcionales.

 def makeinit(cls): fields = () optfields = {} for base in cls.mro(): fields = getattr(base, '_fields', ()) + fields optfields = {**getattr(base, '_optfields', {}), **optfields} optparams = [f"{name} = {val!r}" for name, val in optfields.items()] paramstr = ', '.join(['self', *fields, *optparams]) assignstr = "\n ".join(f"self.{name} = {name}" for name in [*fields, *optfields]) exec(f'def __init__({paramstr}):\n {assignstr}\ncls.__init__ = __init__') return cls @makeinit class Parent: _fields = ('arg1',) _optfields = {'opt_arg1': 'opt_arg1_default_val', 'opt_arg2': 'opt_arg2_default_val', 'opt_arg3': 'opt_arg3_default_val', 'opt_arg4': 'opt_arg4_default_val'} @makeinit class Child(Parent): _fields = ('arg2',) 

Ahora, tiene exactamente los métodos __init__ que deseaba en Parent and Child , totalmente inspeccionable 2 (incluida la help ), y sin tener que repetirlo.


1. No uso PyCharm, pero sé que mucho antes de que saliera la versión 3.7, sus desarrolladores participaron en la discusión de @dataclass y ya estaban trabajando para agregar soporte explícito a su IDE, por lo que ni siquiera tienen Para evaluar la definición de la clase para obtener toda esa información. No sé si está disponible en la versión actual, pero si no, supongo que lo estará. Mientras tanto, @dataclass ya me funciona con el autocompletado de IPython, emacs flycheck, etc., que es lo suficientemente bueno para mí. 🙂

2.… al menos en tiempo de ejecución. Es posible que PyCharm no pueda resolver las cosas de manera suficientemente estática como para completar el menú emergente.