Anulación del método Python, ¿importa la firma?

5 minutos de lectura

digamos que tengo

class Super():
  def method1():
    pass

class Sub(Super):
  def method1(param1, param2, param3):
      stuff

¿Es esto correcto? ¿Las llamadas al método 1 siempre irán a la subclase? Mi plan es tener 2 subclases, cada una de las cuales anula el método 1 con diferentes parámetros

avatar de usuario
sha de mierda

En Python, los métodos son solo pares clave-valor en el diccionario adjunto a la clase. Cuando está derivando una clase de una clase base, esencialmente está diciendo que el nombre del método se buscará primero en el diccionario de la clase derivada y luego en el diccionario de la clase base. Para “anular” un método, simplemente vuelva a declarar el método en la clase derivada.

Entonces, ¿qué sucede si cambia la firma del método anulado en la clase derivada? Todo funciona correctamente si la llamada está en la instancia derivada, pero si realiza la llamada en la instancia base, obtendrá un error porque la clase base usa una firma diferente para el mismo nombre de método.

Sin embargo, hay escenarios frecuentes en los que desea que el método de clase derivada tenga adicional los parámetros y también desea que la llamada al método funcione sin errores en la base. Esto se llama “principio de sustitución de Liskov” (o LSP) que garantiza que si la persona cambia de instancia base a derivada o viceversa, no tiene que renovar su código. Para hacer esto en Python, debe diseñar su clase base con la siguiente técnica:

class Base:
    # simply allow additional args in base class
    def hello(self, name, *args, **kwargs):
        print("Hello", name)

class Derived(Base):
      # derived class also has unused optional args so people can
      # derive new class from this class as well while maintaining LSP
      def hello(self, name, age=None, *args, **kwargs):
          super(Derived, self).hello(name, age, *args, **kwargs) 
          print('Your age is ', age)

b = Base()
d = Derived()

b.hello('Alice')        # works on base, without additional params
b.hello('Bob', age=24)  # works on base, with additional params
d.hello('Rick')         # works on derived, without additional params
d.hello('John', age=30) # works on derived, with additional params

Arriba se imprimirá:

    Hello Alice
    Hello Bob
    Hello Rick
    Your age is  None
    Hello John
    Your age is  30

.
Juega con este código

  • ¡Gracias por esta actualización varios años después con una discusión mucho más clara y procesable, y un ejemplo de trabajo más un parque infantil!

    – nealmcb

    26 de febrero de 2020 a las 3:17

  • ¿Deberíamos colocar la edad en la firma de hola después? *args? Si no, código como, d.hello("John", "blue", age=30) no trabajará. Quiero decir, en general, los argumentos posicionales siempre deben definirse antes que los kwargs.

    – Quickbeam2k1

    5 de marzo de 2020 a las 9:46

  • Creo que la respuesta a su pregunta depende de si queremos permitir param con la opción predeterminada (age aquí) para establecerse también por posición o solo como kwarg. Tenga en cuenta que su sugerencia funcionará solo en Python 3, donde puede especificar argumentos solo de palabras clave con *. Véase, por ejemplo, esto.

    – Nerxis

    3 de julio de 2020 a las 8:15


  • Buena respuesta, pero en “LSP que garantiza que si la persona cambia de instancia base a derivada o viceversa”, la parte “viceversa” es incorrecta.

    – Michał Jabłoński

    29 de septiembre de 2020 a las 12:43

  • si omites *args, **kwargs de la firma del método de la clase derivada, ¿sigue funcionando?

    – papa tonto

    7 mayo 2021 a las 14:13

Python permitirá esto, pero si method1() está destinado a ejecutarse desde un código externo, entonces es posible que desee reconsiderarlo, ya que viola LSP y por lo tanto no siempre funcionará correctamente.

  • ¿La violación de LSP se debe al hecho de que Sub.method1 toma 3 argumentos mientras que Super.method1 no toma ninguno, lo que los convierte en interfaces diferentes?

    – unode

    18 mayo 2011 a las 16:59


  • @Unode: Correcto. Esto podría resolverse haciendo que todos los argumentos del método de la subclase tengan valores predeterminados, pero luego se obtiene qué valores predeterminados serían apropiados.

    – Ignacio Vázquez-Abrams

    18 de mayo de 2011 a las 17:02

  • Ya veo. Pero luego solo para aclarar. Si el método principal1 se definió como Super.method1(param1=None, param2=None, param3=None) todavía violaría LSP si en las subclases se define como Sub.method1(param1, param2, param3) ¿Correcto? Como atributos son obligatorios en un caso pero no en el otro. Por lo tanto, según tengo entendido, sin cambiar la interfaz de la subclase, la única forma de no violar LSP sería tener los parámetros sin valores predeterminados en el padre. ¿Tengo razón en esto o estoy sobreinterpretando el LSP?

    – unode

    18 mayo 2011 a las 18:44


  • @Unode: También correcto. Hacer que el contrato se vuelva menos restrictivo en la subclase viola LSP.

    – Ignacio Vázquez-Abrams

    18 mayo 2011 a las 18:47

  • excepto cuando method1 es __init__donde LSP no se aplica

    – joel

    13 de febrero de 2020 a las 13:29

Podría hacer algo como esto si está bien usar argumentos predeterminados:

>>> class Super():
...   def method1(self):
...     print("Super")
...
>>> class Sub(Super):
...   def method1(self, param1="X"):
...     super(Sub, self).method1()
...     print("Sub" + param1)
...
>>> sup = Super()
>>> sub = Sub()
>>> sup.method1()
Super
>>> sub.method1()
Super
SubX

En Python, todos los métodos de clase son “virtuales” (en términos de C++). Entonces, en el caso de su código, si desea llamar method1() en super clase, tiene que ser:

class Super():
    def method1(self):
        pass

class Sub(Super):
    def method1(self, param1, param2, param3):
       super(Sub, self).method1() # a proxy object, see http://docs.python.org/library/functions.html#super
       pass

Y la firma del método sí importa. No puedes llamar a un método como este:

sub = Sub()
sub.method1() 

avatar de usuario
cuasistoico

Funcionará:

>>> class Foo(object):
...   def Bar(self):
...     print 'Foo'
...   def Baz(self):
...     self.Bar()
... 
>>> class Foo2(Foo):
...   def Bar(self):
...     print 'Foo2'
... 
>>> foo = Foo()
>>> foo.Baz()
Foo
>>> 
>>> foo2 = Foo2()
>>> foo2.Baz()
Foo2

Sin embargo, esto generalmente no se recomienda. Eche un vistazo a la respuesta de S. Lott: los métodos con el mismo nombre y diferentes argumentos son un olor a código.

¿Ha sido útil esta solución?