Práctica recomendada de herencia: * args, ** kwargs o especificando explícitamente los parámetros

A menudo me encuentro sobreescribiendo los métodos de una clase padre, y nunca puedo decidir si debo enumerar explícitamente los parámetros dados o simplemente usar una manta *args, **kwargs construct. ¿Una versión es mejor que la otra? ¿Hay una mejor práctica? ¿Qué (dis-) ventajas me estoy perdiendo?

 class Parent(object): def save(self, commit=True): # ... class Explicit(Parent): def save(self, commit=True): super(Explicit, self).save(commit=commit) # more logic class Blanket(Parent): def save(self, *args, **kwargs): super(Blanket, self).save(*args, **kwargs) # more logic 

Beneficios percibidos de la variante explícita.

  • Más explícito (Zen de Python)
  • más fácil de entender
  • parámetros de función de fácil acceso

Beneficios percibidos de la variante de manta.

  • más seco
  • la clase padre es fácilmente intercambiable
  • el cambio de los valores predeterminados en el método principal se propaga sin tocar otro código

Principio de Sustitución de Liskov

En general, no desea que la firma de su método varíe en los tipos derivados. Esto puede causar problemas si desea intercambiar el uso de tipos derivados. Esto se refiere a menudo como el principio de sustitución de Liskov .

Beneficios de las firmas explícitas

Al mismo tiempo, no creo que sea correcto que todos sus métodos tengan una firma de *args , **kwargs . Firmas explícitas:

  • Ayuda a documentar el método a través de buenos nombres de argumentos.
  • ayuda para documentar el método especificando qué argumentos son necesarios y cuáles tienen valores predeterminados
  • Proporcionar validación implícita (faltan argumentos necesarios para lanzar excepciones obvias)

Argumentos de longitud variable y acoplamiento

No confunda los argumentos de longitud variable con una buena práctica de acoplamiento. Debe haber una cierta cantidad de cohesión entre una clase padre y las clases derivadas, de lo contrario no estarían relacionadas entre sí. Es normal que el código relacionado dé como resultado un acoplamiento que refleje el nivel de cohesión.

Lugares para usar argumentos de longitud variable

El uso de argumentos de longitud variable no debería ser su primera opción. Se debe usar cuando tengas una buena razón como:

  • Definir una envoltura de función (es decir, un decorador).
  • Definiendo una función polimórfica paramétrica.
  • Cuando los argumentos que puede tomar realmente son completamente variables (por ejemplo, una función de conexión de base de datos generalizada). Las funciones de conexión de la base de datos usualmente toman una cadena de conexión en muchas formas diferentes, tanto en forma arg única como en forma multi-arg. También hay diferentes conjuntos de opciones para diferentes bases de datos.

¿Estás haciendo algo mal?

Si encuentra que a menudo está creando métodos que toman muchos argumentos o métodos derivados con diferentes firmas, puede tener un problema mayor en la forma en que organiza su código.

Mi elección sería:

 class Child(Parent): def save(self, commit=True, **kwargs): super(Child, self).save(commit, **kwargs) # more logic 

Evita el acceso al argumento de confirmación de *args y **kwargs y mantiene las cosas seguras si cambia la firma de Parent:save (por ejemplo, agregar un nuevo argumento predeterminado).

Actualización : en este caso, tener * args puede causar problemas si se agrega un nuevo argumento posicional al padre. Mantendría solo **kwargs y manejaría solo los nuevos argumentos con valores predeterminados. Evitaría errores de propagación.

Si está seguro de que el niño mantendrá la firma, seguramente el enfoque explícito es preferible, pero cuando el niño cambie la firma, yo personalmente prefiero usar ambos enfoques:

 class Parent(object): def do_stuff(self, a, b): # some logic class Child(Parent): def do_stuff(self, c, *args, **kwargs): super(Child, self).do_stuff(*args, **kwargs) # some logic with c 

De esta manera, los cambios en la firma son bastante legibles en Child, mientras que la firma original es bastante legible en Parent.

En mi opinión, esta es también la mejor manera cuando tienes herencia múltiple, porque llamar super algunas veces es bastante desagradable cuando no tienes args y kwargs.

Para lo que vale, esta es también la forma preferida en bastantes libs y frameworks de Python (Django, Tornado, Requests, Markdown, por nombrar algunos). Aunque uno no debería basar sus elecciones en tales cosas, simplemente estoy insinuando que este enfoque está bastante extendido.

No es realmente una respuesta, sino más bien una nota al margen: si realmente quiere asegurarse de que los valores predeterminados de la clase principal se propaguen a las clases secundarias, puede hacer algo como:

 class Parent(object): default_save_commit=True def save(self, commit=default_save_commit): # ... class Derived(Parent): def save(self, commit=Parent.default_save_commit): super(Derived, self).save(commit=commit) 

Sin embargo, debo admitir que esto parece bastante feo y solo lo usaría si siento que realmente lo necesito.

Prefiero los argumentos explícitos porque la función de autocompletar te permite ver la firma del método de la función mientras realizas la llamada de la función.

Además de las otras respuestas:

Tener argumentos variables puede “desacoplar” al padre del hijo, pero crea un acoplamiento entre el objeto creado y el padre, lo que creo que es peor, porque ahora creó una pareja de “larga distancia” (más difícil de detectar, más difícil de identificar). mantener, porque puede crear varios objetos en su aplicación)

Si está buscando un desacoplamiento, eche un vistazo a la composición sobre la herencia