Curso de Python #07 (Nivel básico)

Programación orientada a objetos

Fotografía de Chris Ried, disponible en Unsplash.

1. Programación OO I

1.1. Vídeo

1.2. Notas personales

Python es un lenguaje de programación orientado a objetos (POO). Existen, principalemente, dos paradigmas de programación:

  1. Programación orientada a procedimientos.
  2. Programación orientada a objetos.

1.3. Programación OP

Algunos ejemplos de lenguajes de programación que siguen este paradigma son: Fortan, Cobol, Basic…

Entre sus principales desventajas encontramos:

  • Las unidades de código son muy grandes en aplicaciones complejas (resultando en un número de líneas significativamente elevado).
  • En aplicaciones complejas, el código resulta difícil de descifrar.
  • Las aplicaciones generadas suelen ser poco reutilizables.
  • Si existen fallos en alguna línea del código, es muy probable que el programa caiga en su totalidad.
  • Aparición frecuente de código espaguetti (saltos en el flujo de ejecución del programa).
  • Es difícil de depurar el código por otros programadores en caso de necesidad o error.

1.4. Programación OO

La programación orientada a objetos consiste en trasladar el comportamiento que tienen los objetos en la vida real al código de programación. Los objetos tienen un estado, un comportamiento (¿qué puede hacer?) y unas propiedades.

Por ejemplo, pensemos en el objeto ‘‘coche’’:

  • ¿Cuál es el estado de un coche? Puede estar parado, circulando, aparcado…
  • ¿Qué propiedades tiene un coche? Tiene un color, un peso, un tamaño…
  • ¿Qué comportamiento tiene un coche? Puede arrancar, frenar, acelerar, girar…
Objeto Coche
Propiedades (atributos) Color, peso, alto, largo…
Comportamiento Arrancar, frenar, girar, acelerar…

Algunos ejemplos de lenguajes de programación que emplean este paradigma son: C++, Java, VisualNet…

Entre las principales ventajas encontramos:

  • Los programas están divididos en ‘’trozos’’, ‘‘partes’’, ‘‘módulos’’ o ‘‘clases’’, es decir, existe modularización.
  • El código es muy reutilizable. Aparece en el concepto de herencia.
  • Si existen fallos en alguna línea del código, el programa es posible que continue con su funcionamiento, debido al control de excepciones.
  • Surge el concepto de encapsulamiento.

El vocabulario más frecuente de este paradigma de programación incluye palabras o expresiones como:

  • Clase.
  • Objeto.
  • Ejemplar de clase. Instancia de clase. Ejemplarizar una clase. Instanciar una clase.
  • Modularización.
  • Encapsulamiento / encapsulación.
  • Herencia.
  • Polimorfismo.

2. Programación OO II

2.1. Vídeo

2.2. Notas personales

Una clase es un modelo donde se redactan las características comunes de un grupo de objetos.

Una instancia (o ejemplar u objeto) es un miembro concreto de una clase.

La modularización surge cuando un programa está compuesto de diversas clases. Cada una de ellas funciona de manera independiente (facilitando así enormemente su mantenimiento y control de excepciones) y es posible su reutilización en otros programas.

La encapsulación nos permite proteger el funcionamiento interno de cierto bloque de código, para que no pueda accederse o alterarse desde el exterior de manera inadecuada. No obstante, todas las clases de un programa estarán ‘‘conectadas’’ entre sí de cierta manera (mediante métodos de acceso a ciertas características de cada una de las clases).

El mencionado acceso se llevará a cabo empleando la nomenclatura del punto. Por ejemplo, supongamos que hemos creado un objeto, de la clase coche, llamado miCoche. Para acceder a sus propiedades, utilizaremos la sintaxis:

  • miCoche.color = ''rojo''
  • miCoche.peso = 1500
  • miCoche.ancho = 2000
  • miCoche.alto = 900

De forma similar, el acceso al comportamiento de este objeto se realizará mediante la mencionada nomenclatura:

  • miCoche.arranca()
  • miCoche.frena()
  • miCoche.gira()
  • miCoche.acelera()

3. Programación OO III

3.1. Vídeo

3.2. Notas personales

Traslademos a código fuente algunos de los conceptos examinados en las dos lecciones anteriores. La sintaxis para crear la clase Coche sería:

class Coche():
    instrucciones

Empecemos declarando las propiedades de la clase Coche:

class Coche():
    largo_chasis = 250
    ancho_chasis = 120
    ruedas = 4
    en_marcha = False

Definamos comportamientos para los futuros objetos que pertenezcan a esta clase, que vienen determinados por distintos métodos:

class Coche():
    largo_chasis = 250
    ancho_chasis = 120
    ruedas = 4
    en_marcha = False

    def function(self):
        pass

En Sublime Text 3 cuando empezamos a escribir def nos ofrece dos opciones, crear una función o un método. La principal diferencia radica en que la primera no pertenece a ninguna clase, al contrario que la segunda. Podemos, a través de los cursores, escoger en el editor la opción que apunta a un método y se nos proporciona la sintaxis de uno por defecto, como el que se muestra arriba.

Una vez editado, el código queda:

class Coche():
    largo_chasis = 250
    ancho_chasis = 120
    ruedas = 4
    en_marcha = False

    def arrancar(self):
        pass

Nota: self hace referencia al propio objeto perteneciente a la clase, es decir, a la instancia perteneciente a la clase.

Construyamos un objeto de la clase Coche y veamos cómo acceder a sus propiedades:

mi_coche = Coche()  # Instanciación de una clase

print("Largo del coche: ", mi_coche.largo_chasis)  # Largo del coche:  250
print("Número de ruedas: ", mi_coche.ruedas)  # Número de ruedas:  4

Trabajemos en el método declarado, para ello, escribimos:

class Coche():
    largo_chasis = 250
    ancho_chasis = 120
    ruedas = 4
    en_marcha = False

    def arrancar(self):
        self.en_marcha = True

Así, ahora:

print("En marcha: ", mi_coche.en_marcha)  # En marcha:  False
mi_coche.arrancar()
print("En marcha: ", mi_coche.en_marcha)  # En marcha:  True

Esta última acción la podríamos haber llevado a cabo a través de otro comportamiento, estado:

class Coche():
    largo_chasis = 250
    ancho_chasis = 120
    ruedas = 4
    en_marcha = False

    def arrancar(self):
        self.en_marcha = True

    def estado(self):
        if self.en_marcha:
            return "El coche está en marcha."
        else:
            return "El coche está parado."


print(mi_coche.estado())  # El coche está parado.
mi_coche.arrancar()
print(mi_coche.estado())  # El coche está en marcha.

En resumen, hemos creado la clase Coche, que se caracteriza por poseer cuatro propiedades y dos comportamientos.

3.3. Código fuente

El código fuente y los posibles ficheros externos generados correspondientes a esta lección se encuentran disponibles para su consulta en la carpeta /lecciones/26/ del repositorio.

4. Programación OO IV

4.1. Vídeo

4.2. Notas personales

Partamos del código del último ejemplo de la lección anterior:

class Coche():
    largo_chasis = 250
    ancho_chasis = 120
    ruedas = 4
    en_marcha = False

    def arrancar(self):
        self.en_marcha = True

    def estado(self):
        if self.en_marcha:
            return "El coche está en marcha."
        else:
            return "El coche está parado."

Generemos dos objetos y comparémoslos:

mi_coche1 = Coche()
mi_coche2 = Coche()

print("Largo mi_coche1: ", mi_coche1.largo_chasis)  # Largo mi_coche1:  250
print("Largo mi_coche2: ", mi_coche2.largo_chasis)  # Largo mi_coche2:  250

mi_coche1.arrancar()
print(mi_coche1.estado())  # El coche está en marcha.
print(mi_coche2.estado())  # El coche está parado.

Sería buena idea que el método arrancar(), además de arrancar el coche, nos informase de su estado (en marcha o parado). También, programaremos el método estado() para que nos ofrezca información del coche:

class Coche():
    largo_chasis = 250
    ancho_chasis = 120
    ruedas = 4
    en_marcha = False

    def arrancar(self, arrancamos):
        self.en_marcha = arrancamos
        if self.en_marcha:
            return "El coche está en marcha."
        else:
            return "El coche está parado."

    def estado(self):
        print("El coche tiene", self.ruedas, "ruedas. Un ancho de",
              self.ancho_chasis, "cm y un largo de", self.largo_chasis, "cm.")


mi_coche1 = Coche()
mi_coche2 = Coche()

print(mi_coche1.arrancar(True))  # El coche está en marcha.
print(mi_coche2.arrancar(False))  # # El coche está parado.
mi_coche1.estado()
# El coche tiene 4 ruedas. Un ancho de 120 cm y un largo de 250 cm.
mi_coche2.estado()
# El coche tiene 4 ruedas. Un ancho de 120 cm y un largo de 250 cm.

En programación orientada a objetos, las características comunes suelen formar parte de lo que se conoce como estado inicial. Para especificar dicho estado utilizaremos un constructor, que es un método especial que le da estado a los objetos que pertenecen a una clase. Su sintaxis vendrá dada por

def __init__(self):
    propiedades

Así, el código de la clase quedaría:

class Coche():
    def __init__(self):
        self.largo_chasis = 250
        self.ancho_chasis = 120
        self.ruedas = 4
        self.en_marcha = False

    def arrancar(self, arrancamos):
        self.en_marcha = arrancamos
        if self.en_marcha:
            return "El coche está en marcha."
        else:
            return "El coche está parado."

    def estado(self):
        print("El coche tiene", self.ruedas, "ruedas. Un ancho de",
              self.ancho_chasis, "cm y un largo de", self.largo_chasis, "cm.")

¿Qué sucede si intentamos ahora incrementar a cinco el número de ruedas del segundo coche?

mi_coche2 = Coche()

print(mi_coche2.arrancar(False))

mi_coche2.ruedas = 5

mi_coche2.estado()
El coche está parado.
El coche tiene 5 ruedas. Un ancho de 120 cm y un largo de 250 cm.

Esta acción, en ciertos casos, no debería estar permitida. Para ello, entra en juego el concepto de encapsulación, que nos permitirá proteger propiedades para que no se puedan modificar desde fuera de la propia clase. Su aplicación es tan sencilla como preceder con __ el nombre de la propiedad a proteger y en aquellos lugares donde luego aparezca:

class Coche():
    def __init__(self):
        self.largo_chasis = 250
        self.ancho_chasis = 120
        self.__ruedas = 4
        self.en_marcha = False

    def arrancar(self, arrancamos):
        self.en_marcha = arrancamos
        if self.en_marcha:
            return "El coche está en marcha."
        else:
            return "El coche está parado."

    def estado(self):
        print("El coche tiene", self.__ruedas, "ruedas. Un ancho de",
              self.ancho_chasis, "cm y un largo de", self.largo_chasis, "cm.")
mi_coche2 = Coche()

print(mi_coche2.arrancar(False))

mi_coche2.__ruedas = 5

mi_coche2.estado()
El coche está parado.
El coche tiene 4 ruedas. Un ancho de 120 cm y un largo de 250 cm.

En esta ocasión, las cuatro propiedades deberían encapsularse. Incluso en_marcha, ya que queremos modificarla únicamente desde el interior de la clase.

La clase entonces quedaría:

class Coche():
    def __init__(self):
        self.__largo_chasis = 250
        self.__ancho_chasis = 120
        self.__ruedas = 4
        self.__en_marcha = False

    def arrancar(self, arrancamos):
        self.__en_marcha = arrancamos
        if self.__en_marcha:
            return "El coche está en marcha."
        else:
            return "El coche está parado."

    def estado(self):
        print("El coche tiene", self.__ruedas, "ruedas. Un ancho de",
              self.__ancho_chasis, "cm y un largo de", self.__largo_chasis,
              "cm.")

De la misma manera, se pueden encapsular métodos, opción que estudiaremos en futuras lecciones.

4.3. Código fuente

El código fuente y los posibles ficheros externos generados correspondientes a esta lección se encuentran disponibles para su consulta en la carpeta /lecciones/27/ del repositorio.

5. Programación OO V

5.1. Vídeo

5.2. Notas personales

A continuación, abordaremos la encapsulación de métodos partiendo del último ejemplo de la lección anterior:

class Coche():
    def __init__(self):
        self.__largo_chasis = 250
        self.__ancho_chasis = 120
        self.__ruedas = 4
        self.__en_marcha = False

    def arrancar(self, arrancamos):
        self.__en_marcha = arrancamos
        if self.__en_marcha:
            return "El coche está en marcha."
        else:
            return "El coche está parado."

    def estado(self):
        print("El coche tiene", self.__ruedas, "ruedas. Un ancho de",
              self.__ancho_chasis, "cm y un largo de", self.__largo_chasis,
              "cm.")

Encapsular un método es hacer que sea únicamente accesible desde la propia clase, no desde fuera.

Como aplicación práctica de este procedimiento, generemos un método que compruebe que todo está en orden antes de arrancar, chequeo_interno(self), que llamaremos desde arrancar().

class Coche():
    def __init__(self):
        self.__largo_chasis = 250
        self.__ancho_chasis = 120
        self.__ruedas = 4
        self.__en_marcha = False

    def arrancar(self, arrancamos):
        self.__en_marcha = arrancamos

        if self.__en_marcha:
            chequeo = self.chequeo_interno()

        if self.__en_marcha and chequeo:
            return "El coche está en marcha."
        elif self.__en_marcha and not chequeo:
            return "Algo ha ido mal en el chequeo. No podemos arrancar."
        else:
            return "El coche está parado."

    def estado(self):
        print("El coche tiene", self.__ruedas, "ruedas. Un ancho de",
              self.__ancho_chasis, "cm y un largo de", self.__largo_chasis,
              "cm.")

    def chequeo_interno(self):
        print("Realizando chequeo interno.")

        self.gas = "Ok"
        self.aceite = "Ok"
        self.puertas = "Ok"

        if self.gas == "Ok" and self.aceite == "Ok" and self.puertas == "Ok":
            return True
        else:
            return False

Así, si ahora tecleamos:

mi_coche1 = Coche()

print(mi_coche1.arrancar(True))

mi_coche1.estado()

print("---- Segundo vehículo ----")

mi_coche2 = Coche()

print(mi_coche2.arrancar(False))

mi_coche2.estado()

El resultado será:

Realizando chequeo interno.
El coche está en marcha.
El coche tiene 4 ruedas. Un ancho de 120 cm y un largo de 250 cm.
---- Segundo vehículo ----
El coche está parado.
El coche tiene 4 ruedas. Un ancho de 120 cm y un largo de 250 cm.

El método chequeo_interno() es accesible desde fuera de la clase (no está encapsulado), pero, ¿es lógico que podamos acceder a él en cualquier momento? ¿Incluso si está parado? Si el método está diseñado para ejecutarse únicamente en el momento previo a arrancar, hemos de ‘‘protegerlo’’.

mi_coche2 = Coche()

print(mi_coche2.arrancar(False))
print(mi_coche2.chequeo_interno())  # Absurdo en este caso
mi_coche2.estado()
El coche está parado.
Realizando chequeo interno.
True
El coche tiene 4 ruedas. Un ancho de 120 cm y un largo de 250 cm.

Para encapsular el mencionado método, utilizamos la estrategia de __:

class Coche():
    def __init__(self):
        self.__largo_chasis = 250
        self.__ancho_chasis = 120
        self.__ruedas = 4
        self.__en_marcha = False

    def arrancar(self, arrancamos):
        self.__en_marcha = arrancamos

        if self.__en_marcha:
            chequeo = self.__chequeo_interno()

        if self.__en_marcha and chequeo:
            return "El coche está en marcha."
        elif self.__en_marcha and not chequeo:
            return "Algo ha ido mal en el chequeo. No podemos arrancar."
        else:
            return "El coche está parado."

    def estado(self):
        print("El coche tiene", self.__ruedas, "ruedas. Un ancho de",
              self.__ancho_chasis, "cm y un largo de", self.__largo_chasis,
              "cm.")

    def __chequeo_interno(self):
        print("Realizando chequeo interno.")

        self.gas = "Ok"
        self.aceite = "Ok"
        self.puertas = "Ok"

        if self.gas == "Ok" and self.aceite == "Ok" and self.puertas == "Ok":
            return True
        else:
            return False

De forma que si ahora escribimos:

mi_coche2 = Coche()

print(mi_coche2.arrancar(False))
print(mi_coche2.__chequeo_interno())
mi_coche2.estado()

El resultado es:

El coche está parado.
Traceback (most recent call last):
  File "prac28_poo5_1.py", line 50, in <module>
    print(mi_coche2.__chequeo_interno())
AttributeError: 'Coche' object has no attribute '__chequeo_interno'

Es decir, Python arroja un error. No nos deja llamar al método __chequeo_interno() desde fuera de la propia clase Coche porque está encapsulado.

¿Cuándo encapsular una variable o un método? No existe una ‘‘regla de oro’’, esto es, habremos de hacerlo cuando la clase así lo precise, dependiendo del comportamiento que posea una clase y en función del criterio del propio programador.

5.3. Código fuente

El código fuente y los posibles ficheros externos generados correspondientes a esta lección se encuentran disponibles para su consulta en la carpeta /lecciones/28/ del repositorio.

6. Programación OO VI

6.1. Vídeo

6.2. Notas personales

En programación orientada a objetos, el concepto de herencia intenta dar una réplica aproximada de su contrapartida en la vida real. De una clase, denominada en ocasiones clase padre o superclase, heredarán otras clases atributos, métodos… Se conocen estas últimas como subclases de la anterior (y también como superclases si de ellas también heredan otras).

La principal utilidad de la herencia es la reutilización de código cuando se generan clases ‘‘similares’’. Hemos de estudiar las características y comportamientos que poseen en común todos los objetos con los que deseamos trabajar. Todo ello lo englobaremos en una superclase, de la cual luego heredarán otras clases, que aun teniendo características en común, también es cierto que existen otras peculiaridades que las diferencian.

Veamos un ejemplo práctico:

# Clase Padre
class Vehiculo():
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo
        self.enmarcha = False
        self.acelera = False
        self.frena = False

    def arrancar(self):
        self.enmarcha = True

    def acelerar(self):
        self.acelera = True

    def frenar(self):
        self.frena = True

    def estado(self):
        print("Marca:", self.marca, "\nModelo:", self.modelo, "\nEn marcha:",
              self.enmarcha, "\nAcelerando:", self.acelera, "\nFrenando:",
              self.frena)


# Clase hija
class Moto(Vehiculo):
    pass


mi_moto = Moto("Honda", "CBR")

mi_moto.estado()
Marca: Honda 
Modelo: CBR 
En marcha: False 
Acelerando: False 
Frenando: False

Estamos utilizando métodos de la clase Vehiculo a través de la clase Moto gracias a la herencia.

6.3. Código fuente

El código fuente y los posibles ficheros externos generados correspondientes a esta lección se encuentran disponibles para su consulta en la carpeta /lecciones/29/ del repositorio.

7. Programación OO VII

7.1. Vídeo

7.2. Notas personales

Continuemos con el ejemplo de la lección anterior:

# Clase Padre
class Vehiculo():
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo
        self.enmarcha = False
        self.acelera = False
        self.frena = False

    def arrancar(self):
        self.enmarcha = True

    def acelerar(self):
        self.acelera = True

    def frenar(self):
        self.frena = True

    def estado(self):
        print("Marca:", self.marca, "\nModelo:", self.modelo, "\nEn marcha:",
              self.enmarcha, "\nAcelerando:", self.acelera, "\nFrenando:",
              self.frena)


# Clase hija
class Moto(Vehiculo):
    pass

Construyamos la clase Moto, añadiendo un comportamiento nuevo, caballito, que se va a sumar a los cuatro heredados de la clase Vehiculo:

class Moto(Vehiculo):
    hcaballito = ""

    def caballito(self):
        hcaballito = "Voy haciendo el caballito."

Ahora podríamos teclear:

mi_moto = Moto("Honda", "CBR")

mi_moto.caballito()

mi_moto.estado()
Marca: Honda 
Modelo: CBR 
En marcha: False 
Acelerando: False 
Frenando: False

El programa ‘‘funciona’’ (al menos no arroja errores), pero no nos está informando si estamos haciendo el caballito o no.

Abordemos esta situación sobreescribiendo el método estado heredado de la clase Vehiculo, para así incorporar la información sobre el nuevo comportamiento de la clase Moto.

Para sobreescribir un método de la clase padre definimos uno en la clase hija que se caracterice por tener el mismo nombre y número de parámetros:

class Moto(Vehiculo):
    hcaballito = ""

    def caballito(self):
        self.hcaballito = "Voy haciendo el caballito."

    def estado(self):
        print("Marca:", self.marca, "\nModelo:", self.modelo, "\nEn marcha:",
              self.enmarcha, "\nAcelerando:", self.acelera, "\nFrenando:",
              self.frena, "\n", self.hcaballito)

De esta manera, la ejecución del siguiente bloque de código:

mi_moto = Moto("Honda", "CBR")

mi_moto.caballito()

mi_moto.estado()

Produce como resultado ahora:

Marca: Honda 
Modelo: CBR 
En marcha: False 
Acelerando: False 
Frenando: False 
 Voy haciendo el caballito.

Modifiquemos el código para albergar la posibilidad de trabajar con furgonetas:

class Furgoneta(Vehiculo):
    def carga(self, cargar):
        self.cargado = cargar
        if self.cargado:
            return "La furgoneta está cargada."
        else:
            return "La furgoneta no está cargada."

Así,

mi_furgo = Furgoneta("Renault", "Kangoo")

mi_furgo.arrancar()

mi_furgo.estado()

mi_furgo.carga(True)
Marca: Renault 
Modelo: Kangoo 
En marcha: True 
Acelerando: False 
Frenando: False

Todo funciona de manera adecuada, con la salvedad de que no estamos viendo que la furgoneta está cargada. Como el método carga() devuelve una cadena de texto, añadiendo una función print() solucionamos el entuerto:

mi_furgo = Furgoneta("Renault", "Kangoo")

mi_furgo.arrancar()

mi_furgo.estado()

print(mi_furgo.carga(True))
Marca: Renault 
Modelo: Kangoo 
En marcha: True 
Acelerando: False 
Frenando: False
La furgoneta está cargada.

Obviamente, una instrucción del tipo mi_moto.carga() arroja un error, ya que no hereda de Furgoneta la clase Moto, sino de Vehiculo.

Añademos soporte para vehículos electrónicos:

class VehiculoElec():
    def __init__(self):
        self.autonomia = 100

    def cargar_energia(self):
        self.cargando = True

Y, a continuación, generemos una clase para trabajar con biciletas eléctricas. Estas tienen marca, modelo, pueden arrancar, frenar… y a la vez también poseen autonomia y la posibilidad de cargar energía. Python nos permite heredar de dos o más clases, que se conoce como herencia múltiple:

class BicicletaElec(VehiculoElec, Vehiculo):
    pass

Hemos de tener en cuenta que cuando se da herencia múltiple, a la hora de tomar el constructor o los diferentes métodos, se da la preferencia según hayamos ordenado las clases padres de las que hereda. En este caso, no podemos iniciar una bicicleta eléctrica con marca y modelo, aprovechando así el constructor de la clase Vehiculo, ya que la clase VehiculoElec posee su propio constructor y este último tiene preferencia por haber colocado esta clase primero en la definición de BicicletaElec.

mi_bici = BicicletaElec("Orbea", "HCI30")
Traceback (most recent call last):
  File "prac30_poo7_3.py", line 58, in <module>
    mi_bici = BicicletaElec("Orbea", "HCI30")
TypeError: __init__() takes 1 positional argument but 3 were given

No obstante, si intercambiamos el orden de las clases padre en la definición de BicicletaElec:

class BicicletaElec(Vehiculo, VehiculoElec):
    pass


mi_bici = BicicletaElec("Orbea", "HCI30")

mi_bici.estado()

La ejecución ya no arroja errores, mostrando el siguiente resultado:

Marca: Orbea 
Modelo: HCI30 
En marcha: False 
Acelerando: False 
Frenando: False

7.3. Código fuente

El código fuente y los posibles ficheros externos generados correspondientes a esta lección se encuentran disponibles para su consulta en la carpeta /lecciones/30/ del repositorio.

8. Programación OO VIII

8.1. Vídeo

8.2. Notas personales

En esta lección estudiaremos el uso de las funciones

  • super() e
  • isinstance().

Partimos del último ejemplo de la lección anterior:

class Vehiculo():
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo
        self.enmarcha = False
        self.acelera = False
        self.frena = False

    def arrancar(self):
        self.enmarcha = True

    def acelerar(self):
        self.acelera = True

    def frenar(self):
        self.frena = True

    def estado(self):
        print("Marca:", self.marca, "\nModelo:", self.modelo, "\nEn marcha:",
              self.enmarcha, "\nAcelerando:", self.acelera, "\nFrenando:",
              self.frena)


class Furgoneta(Vehiculo):
    def carga(self, cargar):
        self.cargado = cargar
        if self.cargado:
            return "La furgoneta está cargada."
        else:
            return "La furgoneta no está cargada."


class Moto(Vehiculo):
    hcaballito = ""

    def caballito(self):
        self.hcaballito = "Voy haciendo el caballito."

    def estado(self):
        print("Marca:", self.marca, "\nModelo:", self.modelo, "\nEn marcha:",
              self.enmarcha, "\nAcelerando:", self.acelera, "\nFrenando:",
              self.frena, "\n", self.hcaballito)


class VehiculoElec():
    def __init__(self):
        self.autonomia = 100

    def cargar_energia(self):
        self.cargando = True


class BicicletaElec(VehiculoElec, Vehiculo):
    pass

Si queremos crear un objeto de la clase BicicletaElec que posea una marca y un modelo, tenemos dos opciones disponibles:

  • Acudir al método __init__() de la clase VehiculoElec y copiar las líneas que nos interesen del método homónimo de la clase Vehiculo. No obstante, este enfoque es, cuanto menos, poco elegante.
  • Utilizar la función super(), cuya utilidad reside en que procede a llamar al método de la clase padre.

Veamos esta segunda aproximación con un ejemplo un tanto más sencillo:

class Persona():
    def __init__(self, nombre, edad, residencia):
        self.nombre = nombre
        self.edad = edad
        self.residencia = residencia

    def describir(self):
        print("Nombre:", self.nombre, "\nEdad:", self.edad, "\nResidencia:",
              self.residencia)


class Empleado(Persona):
    def __init__(self, salario, antiguedad):
        self.salario = salario
        self.antiguedad = antiguedad

Ahora podríamos comenzar a construir objetos de ambas clases:

antonio = Persona("Antonio", 55, "España")

antonio.describir()
Nombre: Antonio 
Edad: 55 
Residencia: España

Para constuir un objeto de la clase Empleado, hemos de tener en cuenta que tomará como argumentos los parámetros declarados en dicha clase, y no los de Persona. No obstante, a la hora de emplear el método describir() encontraremos problemas tal y como están programadas ambas clases:

juan = Empleado(1500, 15)

juan.describir()
Traceback (most recent call last):
  File "prac31_poo8_2.py", line 24, in <module>
    juan.describir()
  File "prac31_poo8_2.py", line 8, in describir
    print("Nombre:", self.nombre, "\nEdad:", self.edad, "\nResidencia:",
AttributeError: 'Empleado' object has no attribute 'nombre'

Podemos reescribir el constructor de la clase Empleado, haciendo uso de la función super(), como sigue:

class Empleado(Persona):
    def __init__(self, salario, antiguedad):
        super().__init__("Juan", 33, "Italia")
        self.salario = salario
        self.antiguedad = antiguedad

De manera que ahora el código no arrojará error alguno:

juan = Empleado(1500, 15)

juan.describir()
Nombre: Juan 
Edad: 33 
Residencia: Italia

No obstante, programado así, todos nuestros empleados se llamarían Juan, tendrían 33 años y serían italianos. Veamos cómo generalizar el funcionamiento de la anterior clase:

class Empleado(Persona):
    def __init__(self, salario, antiguedad, nombre_empleado, edad_empleado,
                 residencia_empleado):
        super().__init__(nombre_empleado, edad_empleado, residencia_empleado)
        self.salario = salario
        self.antiguedad = antiguedad

    def describir(self):
        super().describir()
        print("Salario:", self.salario, "\nAntigüedad:", self.antiguedad)

De paso, hemos mejorado también el método describir(), que procede a llamar al de la clase padre y, además, añade la información correspondiente al salario y a la antigüedad. De este modo,

juan = Empleado(1500, 15, "Juan", 33, "Italia")

juan.describir()
Nombre: Juan 
Edad: 33 
Residencia: Italia
Salario: 1500 
Antigüedad: 15

La función isinstance() nos informa si un objeto es instancia de una clase determinada:

juan = Empleado(1500, 15, "Juan", 33, "Italia")

juan.describir()

print(isinstance(juan, Persona))  # True
print(isinstance(juan, Empleado))  # True

marco = Persona("Marco", 51, "Francia")

print(isinstance(marco, Persona))  # True
print(isinstance(marco, Empleado))  # False

Con todo, modifiquemos el ejemplo inicial para permitir que una bicicleta eléctrica admita marca y modelo.

class Vehiculo():
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo
        self.enmarcha = False
        self.acelera = False
        self.frena = False

    def arrancar(self):
        self.enmarcha = True

    def acelerar(self):
        self.acelera = True

    def frenar(self):
        self.frena = True

    def estado(self):
        print("Marca:", self.marca, "\nModelo:", self.modelo, "\nEn marcha:",
              self.enmarcha, "\nAcelerando:", self.acelera, "\nFrenando:",
              self.frena)


class Furgoneta(Vehiculo):
    def carga(self, cargar):
        self.cargado = cargar
        if self.cargado:
            return "La furgoneta está cargada."
        else:
            return "La furgoneta no está cargada."


class Moto(Vehiculo):
    hcaballito = ""

    def caballito(self):
        self.hcaballito = "Voy haciendo el caballito."

    def estado(self):
        print("Marca:", self.marca, "\nModelo:", self.modelo, "\nEn marcha:",
              self.enmarcha, "\nAcelerando:", self.acelera, "\nFrenando:",
              self.frena, "\n", self.hcaballito)


class VehiculoElec(Vehiculo):
    def __init__(self, marca, modelo):
        super().__init__(marca, modelo)
        self.autonomia = 100

    def cargar_energia(self):
        self.cargando = True


class BicicletaElec(VehiculoElec, Vehiculo):
    pass

Notemos que:

  • Hemos utilizado la función super() en el método __init__() de VehiculoElec.
  • Hemos declarado que la clase VehiculoElec hereda de Vehiculo.

Así,

mi_bici = BicicletaElec("Orbea", "HCI30")

mi_bici.estado()
Marca: Orbea 
Modelo: HCI30 
En marcha: False 
Acelerando: False 
Frenando: False

8.3. Código fuente

El código fuente y los posibles ficheros externos generados correspondientes a esta lección se encuentran disponibles para su consulta en la carpeta /lecciones/31/ del repositorio.

9. Programación OO IX

9.1. Vídeo

9.2. Notas personales

En esta lección, abordaremos el concepto de polimorfismo. Un objeto puede cambiar de forma dependiendo del contexto en el que se utilice y, por tanto, modificar tanto sus propiedades como sus comportamientos asociados.

Como Python es un lenguaje de tipado dinámico, esta característica es sencilla de utilizar.

Veamos un ejemplo:

class Coche():
    def desplazamiento(self):
        print("Me desplazo utilizando cuatro ruedas.")


class Moto():
    def desplazamiento(self):
        print("Me desplazo utilizando dos ruedas.")


class Camion():
    def desplazamiento(self):
        print("Me desplazo utilizando seis ruedas.")

Así, si ahora tecleamos:

mi_vehiculo = Moto()

mi_vehiculo.desplazamiento()

mi_vehiculo2 = Coche()

mi_vehiculo2.desplazamiento()

mi_vehiculo3 = Camion()

mi_vehiculo3.desplazamiento()
Me desplazo utilizando dos ruedas.
Me desplazo utilizando cuatro ruedas.
Me desplazo utilizando seis ruedas.

Si tuviésemos cientos de vehículos y quisiéramos utilizar sus comportamientos, habríamos de seguir el patrón esbozado arriba.

No obstante, nos podemos aprovechar de la magia del polimorfismo creando una función como se muestra a continuación:

def desplazamiento_vehiculo(vehiculo):
    vehiculo.desplazamiento()

Y como el objeto vehiculo posee la capacidad de adquirir el rol de cualquiera de los vehículos programados arriba (coche, moto o camión), Python en todo momento sabrá a qué método desplazamiento() acudir en cada instante.

Así, si escribimos:

mi_vehiculo = Camion()

desplazamiento_vehiculo(mi_vehiculo)

mi_vehiculo = Coche()

desplazamiento_vehiculo(mi_vehiculo)

mi_vehiculo = Moto()

desplazamiento_vehiculo(mi_vehiculo)
Me desplazo utilizando seis ruedas.
Me desplazo utilizando cuatro ruedas.
Me desplazo utilizando dos ruedas.

9.3. Código fuente

El código fuente y los posibles ficheros externos generados correspondientes a esta lección se encuentran disponibles para su consulta en la carpeta /lecciones/32/ del repositorio.

Alexis Sáez
Alexis Sáez
Profesor de matemáticas

Cazador de problemas matemáticos en parajes opositores.