lunes, 24 de febrero de 2014

Python exquisite II, clases y la función super




Esta y las siguientes entradas tratarán sobre algunos aspectos relacionados con las clases. Primeramente, tenemos super, que es una función build-in que sirve para acceder a atributos que pertenecen a una clase superior. Un ejemplo típico:

class Madre(object):
    def apellido(self):
        print " Gonzalez "

class Hijo(Madre):
    def nombre(self):
        print " Manuel "        
        super(Hijo,self).apellido()

hijoA = Hijo()
hijoA.nombre()

Como puede verse, su uso resulta muy sencillo y evidente. No obstante en caso de herencia múltiple el uso de super puede resultar mucho menos evidente. Compliquemos el ejemplo anterior:

 
class Madre(object):
    def apellido(self):
        print " Gonzalez "
class Padre(object):
    def apellido(self):
        print " Sanz "
class Hijo(Madre, Padre):
    def nombre(self):
        print " Manuel "        
        super(Hijo,self).apellido()
class Hija(Padre,Madre):
    def nombre(self):
        print " Sofia "        
        super(Hija,self).apellido()
hijoA = Hijo()
hijoA.nombre()      
hijoB = Hija()
hijoB.nombre()  
print Hijo.__mro__
print Hija.__mro__ 

En este caso el asunto se complica, tanto las clase Padre, como la clase Madre tienen un método nombre y cuando se llama desde Hijo o desde Hija, el resultado es diferente. Para conocer la prioridad en cuanto a la herencia se puede emplear MiClase.__mro__, esto nos ofrece la secuencia de herencia y desvela las prioridades. Para la salida de Hijo.__mro__ tenemos:




La salida es bastante esclarecedora, la primera herencia es de madre, seguida de padre, por lo que como se podría esperar super llama al método de la clase Madre. En este caso la cosa resultaba bastante evidente pero en el caso de herencias más complejas __mro__ aporta información imprescindible.

El uso de super pude resultar muy peligroso si se emplea mezclado con llamadas __init__ a las clases superiores. Veamos el siguiente ejemplo:


class ClaseA(object):
    def __init__(self):
        print "Clase A"
        super(ClaseA,self).__init__()
class ClaseB(object):
    def __init__(self):
        print "Clase B"
        super(ClaseB,self).__init__()
class ClaseC(ClaseA,ClaseB):
    def __init__(self):
        print "Clase C"
        ClaseA.__init__(self)
        ClaseB.__init__(self)

clase = ClaseC() 



El uso de __init__ desde las clases que heredan para llamar a los constructores de las clases superiores es, por decirlo de alguna manera, el método clásico, mientras que super es una forma más actual de realizar el trabajo. La mezcla de lo nuevo y lo clásico no parece resultar muy correcta. Desde luego no es el resultado esperado, la segunda llamada a B no debía haberse producido. Esto es el resultado de mezclar la clásica forma de llamar a los constructores __init__ , con la más actual de usar super. Como regla general, si se emplea super en alguna clase, debe mantenerse esta forma en todas las clases que puedan heredar de ella. En este caso el problema se resuelve sustituyendo las llamadas a los constructores __init__ de las clases ClaseA y ClaseB que se realiza desde ClaseC por un super(ClaseC,self).__init__().

Otro conflicto importante ocurre cuando queremos pasar argumentos desde la clases hijas hacia las clases superiores. El siguiente ejemplo da un error.

class ClaseA(object):
    def __init__(self):
        print "Clase A"
        super(ClaseA,self).__init__()
class ClaseB(object):
    def __init__(self):
        print "Clase B"
        super(ClaseB,self).__init__()
class ClaseC(ClaseA):
    def __init__(self,arg):
        print "Clase C", "arg = ", arg
        super(ClaseC,self).__init__()
class ClaseD(ClaseB):
    def __init__(self,arg):
        print "Clase D", "arg = ", arg
        super(ClaseD,self).__init__()
class ClaseE(ClaseC,ClaseD):
    def __init__(self,arg):
        print "Clase E", "arg = ", arg
        super(ClaseE,self).__init__(arg)
        
clase0 = ClaseE(10)


Lo que ocurre, es que los argumentos se pierden y ya no están presentes en ClaseA por lo que tampoco los va a recibir ClaseD, que si los necesita en el constructor. La regla general para evitar esto; siempre hay que pasar todos los argumentos recibidos al super, y si las clases incluyen varios argumentos, añadir  *args y **kwargs. La solución quedaría como:

 
class ClaseA(object):
    def __init__(self, *args, **kwargs):
        print "Clase A"
        super(ClaseA,self).__init__(*args, **kwargs)
class ClaseB(object):
    def __init__(self,*args, **kwargs):
        print "Clase B"
        super(ClaseB,self).__init__(*args, **kwargs)
class ClaseC(ClaseA):
    def __init__(self,arg,*args, **kwargs):
        print "Clase C", "arg = ", arg
        super(ClaseC,self).__init__(arg, *args, **kwargs)
class ClaseD(ClaseB):
    def __init__(self,arg, *args, **kwargs):
        print "Clase D", "arg = ", arg
        super(ClaseD,self).__init__(*args, **kwargs)
class ClaseE(ClaseC,ClaseD):
    def __init__(self,arg,*args, **kwargs):
        print "Clase E", "arg = ", arg
        super(ClaseE,self).__init__(arg,*args, **kwargs)
        
clase0 = ClaseE(10)

El resultado es el esperado:



Algunos ejemplos en esta línea los encontramos aquí. A modo de resumen:
  • Si se emplea super hay que documentarlo correctamente.
  • Tener mucho cuidado con los argumentos con los que se llama a super, en cualquier caso usar todos los argumentos que la clase reciba.
  • Emplear *args, **kwargs, es decir,  hacer super(MiClase,self).metodo(todos_los_argumentos_pasados, *args, **kwargs)
  • No mezclar super con llamadas __init__ a las clases padres. 
En líneas generales es preferible usar super únicamente cuando se tiene una herencia múltiple en esquema de diamante.  

3 comentarios: