Uno de los conceptos más importantes referentes a la programación orientada a objetos es el de la Herencia.
Recordemos de la sesión pasada que en la clase Abuelo definimos el constructor como
class Abuelo():
# Constructor
def __init__(self, nombre):
# Atributos
self.nombre = nombre
self.apellido = 'Apáez'
self.color_ojos = 'negro'
self.color_cabello = 'marrón'
self.tez = 'morena'
self.hablando = False
self.corriendo = False
de donde, en la clase Hijo (subclase de la superclase Abuelo) también creamos un método inicial:
class Hijo(Abuelo):
# Constructor:
# Agregamos el parámetro (o los parámetros) del constructor de la superclase
def __init__(self, nombre, num_celular):
# hacemos referencia de los parámetros de la superclase
super().__init__(nombre)
# Atributos
self.num_celular = num_celular
self.idiomas = ['Inglés', 'Francés', 'Italianos']
self.estudiar = False
self.pedalear = False
en el cual "pasamos" los parámetros del constructor de la clase Abuelo e hicimos la referencia de ello utilizando super().__init__(nombre)
.
En otras palabras, incorporamos el método __init__
desde la superclase Abuelo a la subclase Hijo, agregando algunas características propias dentro de este método en la clase Hijo
El comportamiento anterior respecto al método __init__
se conoce como sobreescritura de métodos, la cual consiste básicamente en definir un método en la superclase y volverlo a definir en una subclase; además de efectuar esta re-definición podemos agregar cosas distintas al método sobreescrito.
De tal manera, sobreescribimos el método __init__
de la superclase Abuelo en la subclase Hijo, agregando algunos atributos adicionales que no figuran en el método __init__
de la superclase.
Abordemos otro ejemplo sobre la sobreesctritura de métodos, para ello recordemos el código de la superclase Abuelo agregando un método para mostrar objetos (__str__
)
# Creamos la superclase Abuelo
class Abuelo():
# Constructor
def __init__(self, nombre):
# Atributos
self.nombre = nombre
self.apellido = 'Apáez'
self.color_ojos = 'negro'
self.color_cabello = 'marrón'
self.tez = 'morena'
# Método para mostrar objetos
def __str__(self):
return f'Hola, soy {self.nombre} {self.apellido}'
y también el código de la subclase Hijo (cuya superclase es la clase Abuelo)
# Creamos la clase Hijo como subclase de la clase Abuelo
class Hijo(Abuelo):
# Constructor:
# Agregamos el parámetro (o los parámetros) del constructor de la superclase
def __init__(self, nombre, num_celular):
# hacemos referencia de los parámetros de la superclase
super().__init__(nombre)
# Atributos
self.num_celular = num_celular
self.idiomas = ['Inglés', 'Francés', 'Italianos']
podemos después crear un objeto de la clase Hijo y observar como se hereda el método __str__
de la superclase Abuelo a la subclase Hijo
# Creamos un objeto de la clase Hijo
Zin = Hijo('Zin', '520-3145')
# Invocamos el método __str__ de la superclase Abuelo
print(Zin)
Hola, soy Zin Apáez
donde el método __str__
nos devuelve una cadena de texto con un saludo y el nombre del objeto. Sin embargo, en la subclase Hijo tenemos un atributo importante que la superclase Abuelo no tiene, nos referimos al atributo self.num_celular
. De tal manera, es de nuestro interés que el método __str__
también muestre el número teléfonico referente a dicho atributo, por lo que debemos efectuar una sobreescitura del método __str__
en la subclase Hijo para que se muestre lo que deseamos. Para ello escribimos
# Creamos la clase Hijo como subclase de la clase Abuelo
class Hijo(Abuelo):
# Constructor:
def __init__(self, nombre, num_celular):
# hacemos referencia de los parámetros de la superclase
super().__init__(nombre)
# Atributos
self.num_celular = num_celular
self.idiomas = ['Inglés', 'Francés', 'Italianos']
# Sobreescritura del método __str__
def __str__(self):
return f'{super().__str__()} y mi número es {self.num_celular}'
donde al declarar el método __str__
dentro de la subclase Hijo estamos realizando una sobreescritura de métodos. Luego, en la primer parte del f-string dentro del método __str__
hacemos la referencia correspondiente al mensaje que habíamos escrito en el método __str__
de la superclase Abuelo, consiguiendo lo anterior al escribir super().__str__()
, de manera análoga a cuando lo hicimos con el método __init__
. De tal manera en el método __str__
de la subclase Hijo:
{super().__str__()}
en el f-string nos devolvera un saludo con el nombre del objeto en cuestión, lo cual proviene propiamente del método __str__
de la superclase Abuelo;y mi número es {self.num_celular}
en el f-string nos devuelve el número telefónico correspondiente al atributo self.num_celular
propio de la subclase Hijo, de tal manera esta parte es propia del método __str__
de la subclase Hijo.Veamos pues que
# Creamos un objeto de la clase Hijo
Zen = Hijo('Zen', '524-7842')
# Invocamos el método __str__ de la superclase Abuelo
# efectuando una sobreescritura de métodos
print(Zen)
Hola, soy Zen Apáez y mi número es 524-7842
Consideremos la clase Punto2 que hace alusión a puntos sobre un espacio de dos dimensiones ($\mathbb{R}^{2}$), además de los habituales métodos __init__
y __str__
escribiremos el método norma
para calcular la norma de dicho punto.
Observación: Considerando un punto sobre el plano cartesiano, $P=(x,y)\in\mathbb{R}^{2}$, la norma de $P$ se define como el número $\sqrt{x^{2}+y^{2}}$.
Para ejemplificar la sobreescritura de métodos más adelante, haremos la construcción del método para calcular la norma de un punto en dos partes como veremos a continuación
import numpy as np
class Punto2():
# Constructor
def __init__(self, x, y):
self.x = x
self.y = y
# __str__
def __str__(self):
return f'{self.x, self.y}'
# Método norma:
def auxiliar(self):
return self.x ** 2 + self.y ** 2
def norma2(self):
return np.sqrt(self.auxiliar())
# Creamos un punto
p2 = Punto2(3,4)
# Probamos el método norma2
p2.norma2()
5.0
Y creamos otra clase denominada Punto3 que haga alusión a los puntos en un espacio de tres dimensiones y que sea totalmente análoga a la clase Punto2, es decir que posea los mismos atributos y métodos adaptados al espacio de tres dimensiones. Tenemos la opción de crear dicha clase escribiendo
class Punto3():
# Constructor
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
# __str__
def __str__(self):
return f'{self.x, self.y, self.z}'
# Método norma:
def auxiliar(self):
return self.x ** 2 + self.y ** 2 + self.z ** 2
def norma3(self):
return np.sqrt(self.auxiliar())
pero también podemos efectuar sobreescritura de métodos en los métodos __init__
, __str__
y auxiliar
de la clase Punto2 como sigue
# ----------------------------------------------------------------------------------------
# SOBREESCRITURA DE LOS MÉTODOS __init__, __str__ Y norma
# ----------------------------------------------------------------------------------------
# Creamos la clase Punto3 como subclase de la superclase Punto2
class Punto3(Punto2):
# Constructor
def __init__(self, x, y, z):
# Hacemos referencia de los parámetros del método __init__ de la superclase
super().__init__(x, y)
# Atributo propio de la subclase Punto3
self.z = z
# __str__
def __str__(self):
# Hacemos referencia de la salida del método __str__ de la superclase
return f'({super().__str__()},{self.z})'
# Método norma
def auxiliar(self):
return super().auxiliar() + self.z ** 2
def norma3(self):
return np.sqrt(self.auxiliar())
# Creamos un objeto de la clase Punto3
p3 = Punto3(3,4,5)
# Probamos el método norma3
p3.norma3()
7.0710678118654755
Así como hemos construido la clase Punto3 como subclase de Punto2, podríamos construir la clase Punto4 como subclase de Punto3, de tal manera
La idea anterior puede ser más clara si recordamos le herencia entre personas
considerando cada rango jerárquico como una clase, tenemos entonces que la clase Abuelo es superclase de las clases Hijo, Nieto1 y Nieto2, donde éstas últimas son subclases de la superclase Hijo. Es por ello que la clase Punto4 seguiría la misma idea de las clases Nieto1 y Nieto2 respecto a la imagen anterior. Esto es
Escribamos pues el código de la clase Punto4 en la cual volveremos a efectuar sobreescritura de métodos en los métodos __init__
, __str__
y norma
# Creamos la clase Punto4 como subclase de la superclase Punto3
class Punto4(Punto3):
# Constructor
def __init__(self, x, y, z, w):
# Hacemos referencia de los parámetros del método __init__ de la superclase
super().__init__(x, y, z)
# Atributo propio de la subclase Punto4
self.w = w
# __str__
def __str__(self):
# Hacemos referencia de la salida del método __str__ de la superclase
return f'({super().__str__()},{self.w})'
# Método norma
def auxiliar(self):
return super().auxiliar() + self.w ** 2
def norma4(self):
return np.sqrt(self.auxiliar())
# Creamos un objeto de la clase Punto4
p4 = Punto4(3,4,5,6)
# Probamos el método norma3
p4.norma4()
9.273618495495704
# Además...
# método __str__
print(p4)
(((3, 4),5),6)
lo cual ejemplifica perfectamente la sobreescritura del método __str__
, donde el primer paréntesis $(3,4)$ hace alusión al método __str__
de la clase Punto2, el segundo $((3,4),5)$ a la clase Punto3 y el último $(((3,4),5),6)$ a la propia clase Punto4.
Hasta el momento, todo lo que hemos trabajado sobre herencia hace referencia al concepto de herencia simple, la cual se caracteriza cuando las subclases tienen sólo una superclase en cuestión. Por ejemplo, sobre la clase Abuelo y la subclase Hijo tenemos herencia simple; sobre la clase Punto2 y la subclase Punto3 tenemos herencia simple, asimismo sobre la clase Punto3 y Punto4 tenemos herencia simple.
Observación: Debemos ser cautelosos cuando la herencia tenga más de un nivel jerárquico y ser claros en que la herencia se involucra en todos los niveles de forma conjunta. Es decir, no podemos pensar la herencia de la clase Punto2 sobre la subclase Punto4 de manera aislada, pues en medio interviene la clase Punto3. Lo cual es claro, pues por ejemplo (en la herencia entre personas) no podemos pensar a los nietos como descendencia directa de los abuelos, pues en medio interviene de manera contundente los padres en cuestión (hijos de los abuelos).
Notemos, en realidad, que en la herencia entre persona hay dos papeles decisivos en vez de uno
o trasladado a lenguaje de programación tendríamos algo del estilo
donde la Subclase2 y Subclase3 tienen dos superclases en cuestión y por ello podemos hablar en este caso de herencia múltiple. Además, la Subclase1
sólo tiene una superclase en cuestión por lo que en este caso hablamos sobre herencia simple. En las subsecuentes sesiones abordaremos más a detalle el tema de la herencia múltiple.