Saltar al contenido
Portada » Lenguajes » 7. Programación Orientada a Objetos (POO)

7. Programación Orientada a Objetos (POO)

Programación Orientada a Objetos (POO). En este capítulo exploraremos uno de los paradigmas de programación más importantes en Python, utilizado en el desarrollo de aplicaciones de cualquier escala. La programación orientada a objetos permite organizar el código en torno a objetos y clases, lo cual ayuda a estructurar programas más complejos y a mejorar la modularidad y reutilización del código.

Este capítulo comenzará explicando los conceptos fundamentales de la POO: clases y objetos. Aprenderemos cómo definir clases en Python, crear instancias u objetos de estas clases y utilizar atributos y métodos para realizar acciones específicas. Exploraremos además el concepto de herencia, que permite crear nuevas clases basadas en otras ya existentes, así como el polimorfismo, para que diferentes objetos puedan responder de distintas maneras a la misma acción o método. También trataremos los principios de encapsulamiento y abstracción, que ayudan a ocultar los detalles internos de un objeto y permiten interactuar con él de una forma más sencilla y segura.

Finalmente, conoceremos los métodos mágicos de Python, como __init__ y __str__, que permiten definir comportamientos especiales en nuestros objetos y dotarlos de más funcionalidad. Este capítulo te proporcionará una buena base de la POO en Python, lo que te ayudará a estructurar y organizar mejor tu código en proyectos reales.

Este es un tutorial que te ayudará a introducirte en la programación orientada a objetos en Python, pero si quieres profundizar te podemos aconsejar el libro Aprende la Programación Orientada a Objetos con el lenguaje Python. Autor: Vincent Boucheny (Editorial Ediciones ENI)

Clases y objetos

Clases

En programación orientada a objetos, una clase es una plantilla que define las características y comportamientos que tendrán los objetos creados a partir de ella. Básicamente es una descripción de un tipo de objeto, donde se describe la estructura y el comportamiento común a todos los objetos que se definan a partir de esa clase, o sea, establece qué datos deben tener los objetos y qué operaciones pueden realizar con esos datos. A través de esta estructura, es posible organizar el código de una forma más intuitiva y modular.

En Python, se define una clase con la palabra clave class, seguida del nombre de la clase (convencionalmente en mayúsculas). Dentro de una clase, se definen atributos (o propiedades), que representan los datos o el estado de un objeto, y métodos, que son funciones que definen el comportamiento del objeto.

Ejemplo básico de clase

Supongamos que queremos modelar un «Coche». La clase Coche representará a todos los coches de forma general. Aquí definimos los atributos básicos (como marca y color) y métodos que describan su comportamiento (como arrancar o acelerar):

class Coche:
    def __init__(self, marca, color):
        self.marca = marca
        self.color = color
    
    def arrancar(self):
        print(f"El coche {self.marca} está arrancando.")

    def acelerar(self, velocidad):
        print(f"El coche {self.marca} acelera a {velocidad} km/h.")

Aquí:

  • __init__: Es un método especial llamado constructor que se ejecuta automáticamente cuando se crea un nuevo objeto de esta clase. Inicializa los atributos del objeto.
  • marca y color: Son atributos que representan el estado del objeto.
  • arrancar y acelerar: Son métodos que describen comportamientos o acciones del objeto.

Objetos

Un objeto es una instancia de una clase, es decir, es un ejemplar concreto creado a partir de una clase. Cuando se crea un objeto, se reserva un espacio en la memoria que almacena sus datos (sus atributos) y que puede realizar las operaciones definidas por los métodos de su clase. Cada objeto tiene sus propios valores para los atributos definidos en la clase, pero comparte la misma estructura y métodos con otros objetos de la misma clase.

Para crear un objeto, se llama a la clase como si fuera una función, pasando los parámetros que espera el constructor __init__. Los atributos de un objeto son accesibles y modificables de forma individual, y los métodos se pueden invocar sobre el objeto para hacer que realice acciones.

Ejemplo de creación de objetos

Siguiendo el ejemplo anterior de la clase Coche, ahora podemos crear objetos específicos de esta clase, que representen coches con características particulares:

# Crear objetos de la clase Coche
mi_coche = Coche("Toyota", "Rojo")
coche_amigo = Coche("Honda", "Azul")

# Acceder a los atributos y métodos de cada objeto
print(mi_coche.marca)  # Salida: Toyota
print(coche_amigo.color)  # Salida: Azul

mi_coche.arrancar()  # Salida: El coche Toyota está arrancando.
coche_amigo.acelerar(100)  # Salida: El coche Honda acelera a 100 km/h.

Aquí:

  • mi_coche y coche_amigo son dos objetos distintos de la clase Coche.
  • Cada uno tiene valores específicos para marca y color, que son independientes uno del otro.
  • Cuando se llama a arrancar() o acelerar() en cada objeto, este ejecuta el método con su propio estado (marca y color específicos de cada objeto).

Atributos y Métodos

Atributos

En POO, los atributos son variables que representan el estado o las características de un objeto. Los atributos pueden ser específicos de cada objeto (atributos de instancia) o compartidos entre todos los objetos de una clase (atributos de clase).

  • Atributos de instancia: Son aquellos que se definen para cada objeto en particular, por lo que cada objeto puede tener valores diferentes en sus atributos de instancia.
  • Atributos de clase: Son atributos que se definen una sola vez a nivel de la clase y son compartidos por todos los objetos de esa clase.

En nuestro ejemplo Coche, podemos definir el color, marca y velocidad como atributos de instancia, ya que cada coche puede tener su propio color y velocidad. Podemos agregar, por ejemplo, un atributo de clase llamado num_ruedas, que es común para todos los coches.

class Coche:
    num_ruedas = 4  # Atributo de clase, compartido entre todos los objetos

    def __init__(self, marca, color, velocidad=0):
        self.marca = marca    # Atributo de instancia
        self.color = color    # Atributo de instancia
        self.velocidad = velocidad  # Atributo de instancia

    def arrancar(self):
        print(f"El coche {self.marca} está arrancando.")
    
    def acelerar(self, incremento):
        self.velocidad += incremento
        print(f"El coche {self.marca} ha acelerado a {self.velocidad} km/h.")
    
    def frenar(self, decremento):
        self.velocidad -= decremento
        print(f"El coche {self.marca} ha frenado a {self.velocidad} km/h.")
  • num_ruedas es un atributo de clase, por lo que todos los objetos Coche comparten este valor.
  • marca, color y velocidad son atributos de instancia, únicos para cada objeto.

Métodos

Los métodos son funciones definidas dentro de una clase que operan sobre los objetos de esa clase. Estos métodos permiten que los objetos realicen acciones y manipulen sus propios atributos.

En el ejemplo:

  • arrancar() es un método que no cambia el estado, solo imprime un mensaje.
  • acelerar() y frenar() son métodos que modifican el estado de velocidad del objeto.

Podemos crear y utilizar un objeto de esta clase de la siguiente manera:

mi_coche = Coche("Toyota", "Rojo")
mi_coche.arrancar()        # Salida: El coche Toyota está arrancando.
mi_coche.acelerar(50)       # Salida: El coche Toyota ha acelerado a 50 km/h.
mi_coche.frenar(20)         # Salida: El coche Toyota ha frenado a 30 km/h.

¿Qué es self y por qué se utiliza?

En Python, self es una referencia al objeto actual (la instancia de la clase) desde el cual se llama a un método o se accede a un atributo. Cada vez que se crea un nuevo objeto, self hace referencia a esa instancia específica. A diferencia de algunos lenguajes, donde this es una palabra clave reservada, en Python self es solo una convención, pero es esencial para que el método identifique correctamente al objeto que lo llama.

¿Cuándo y cómo se usa self?

1. En el constructor __init__ y otros métodos de instancia

Al definir métodos en una clase, self siempre debe ser el primer parámetro en los métodos de instancia, como el constructor __init__. Esto permite a Python identificar que el método pertenece a la instancia específica del objeto.

class Coche:
    def __init__(self, marca, color):
        self.marca = marca      # Aquí, self.marca se refiere al atributo marca del objeto actual
        self.color = color      # self.color se refiere al atributo color del objeto actual

En este caso:

  • self.marca y self.color son los atributos que pertenecen al objeto, mientras que marca y color son los parámetros pasados al constructor.

2. Para acceder a atributos y métodos de instancia

self permite acceder y modificar los atributos y métodos de la misma instancia desde otros métodos de la clase. Cada vez que queremos acceder o modificar un atributo, self nos asegura que estamos trabajando con la instancia correcta.

class Coche:
    def __init__(self, marca, color, velocidad=0):
        self.marca = marca
        self.color = color
        self.velocidad = velocidad

    def acelerar(self, incremento):
        self.velocidad += incremento    # Accedemos a self.velocidad del objeto actual
        print(f"El coche {self.marca} ha acelerado a {self.velocidad} km/h.")

Aquí:

  • self.velocidad hace referencia a la velocidad del coche específico que está ejecutando acelerar.
  • self.marca y self.color se usan para obtener la marca y color del coche en el que estamos trabajando.

3. Diferencia entre self y variables locales

Las variables definidas con self pertenecen al objeto en particular, mientras que las variables definidas sin self son variables locales, propias del método que se está ejecutando, y desaparecen cuando el método termina.

Ejemplo completo utilizando self

class Coche:
    def __init__(self, marca, color, velocidad=0):
        self.marca = marca          # Atributo de instancia
        self.color = color          # Atributo de instancia
        self.velocidad = velocidad  # Atributo de instancia

    def arrancar(self):
        print(f"El coche {self.marca} ha arrancado.")

    def acelerar(self, incremento):
        self.velocidad += incremento
        print(f"El coche {self.marca} acelera a {self.velocidad} km/h.")

# Crear una instancia de Coche
mi_coche = Coche("Toyota", "Rojo")
mi_coche.arrancar()        # Llama a arrancar() con self referenciando a mi_coche
mi_coche.acelerar(20)      # Llama a acelerar() con self referenciando a mi_coche
  • Cuando llamamos a mi_coche.arrancar(), self referencia a mi_coche, permitiendo acceder a los atributos y métodos de esa instancia específica.
  • self facilita la personalización de los atributos para cada instancia, manteniendo independientes los estados de diferentes objetos de la misma clase.

Herencia

La herencia permite crear una nueva clase (subclase) basada en una clase existente (superclase). La subclase hereda todos los atributos y métodos de la superclase y, además, puede añadir nuevos atributos y métodos, o sobrescribir los existentes.

Esto es útil porque permite reutilizar código y extender funcionalidades sin duplicación. Siguiendo con el ejemplo, podríamos crear una subclase CocheDeportivo que herede de Coche, y que además añada un atributo y métodos específicos.

class CocheDeportivo(Coche):
    def __init__(self, marca, color, velocidad=0, turbo=False):
        super().__init__(marca, color, velocidad)  # Llamamos al constructor de la superclase
        self.turbo = turbo    # Nuevo atributo específico de CocheDeportivo
    
    def activar_turbo(self):
        if not self.turbo:
            self.turbo = True
            print("Turbo activado.")
        else:
            print("El turbo ya está activado.")
    
    def acelerar(self, incremento):
        # Sobrescribimos el método acelerar para usar turbo si está activado
        if self.turbo:
            incremento *= 2  # Doble de aceleración
        super().acelerar(incremento)

En CocheDeportivo:

  • turbo es un nuevo atributo que no está en la clase Coche.
  • activar_turbo() es un nuevo método para activar el turbo.
  • acelerar() ha sido sobrescrito para aumentar el incremento de velocidad si el turbo está activado.

Ejemplo de uso:

mi_coche_deportivo = CocheDeportivo("Ferrari", "Rojo")
mi_coche_deportivo.arrancar()       # Heredado de Coche
mi_coche_deportivo.activar_turbo()  # Turbo activado.
mi_coche_deportivo.acelerar(50)     # Aceleración de 100 km/h si turbo está activo.

Polimorfismo

El polimorfismo permite que objetos de diferentes clases puedan ser tratados como si fueran del mismo tipo cuando comparten métodos o atributos comunes. En Python, el polimorfismo se expresa principalmente cuando se definen métodos con el mismo nombre en diferentes clases y estos se comportan de manera diferente según la clase.

Imaginemos otra clase Bicicleta que también tiene un método acelerar, aunque su implementación será diferente a la de Coche.

class Bicicleta:
    def __init__(self, tipo, velocidad=0):
        self.tipo = tipo
        self.velocidad = velocidad
    
    def acelerar(self, incremento):
        self.velocidad += incremento
        print(f"La bicicleta {self.tipo} acelera a {self.velocidad} km/h.")

Tanto Coche como Bicicleta tienen un método acelerar, aunque implementado de manera diferente en cada caso. Gracias al polimorfismo, podemos iterar sobre una lista de Coche y Bicicleta y llamar al método acelerar sin preocuparnos por el tipo específico del objeto.

# Creamos instancias de Coche y Bicicleta
mi_coche = Coche("Toyota", "Rojo")
mi_bici = Bicicleta("Montaña")

# Los almacenamos en una lista
vehiculos = [mi_coche, mi_bici]

# Usamos polimorfismo para acelerar cada vehículo
for vehiculo in vehiculos:
    vehiculo.acelerar(10)

Dependiendo del tipo de vehiculo, se ejecutará la versión de acelerar correspondiente:

  • Para Coche, se aplicará acelerar de Coche.
  • Para Bicicleta, se aplicará acelerar de Bicicleta.

Para concluir este capítulo repasemos algunos de los aspectos claves.

Ventajas de Clases y Objetos

  1. Modularidad y Reutilización: Una clase puede definirse una vez y luego usarse en múltiples partes de un programa, o incluso en otros proyectos.
  2. Organización del Código: Las clases agrupan atributos y métodos relacionados, facilitando el mantenimiento y lectura del código.
  3. Facilidad para Modelar el Mundo Real: La POO permite crear modelos de cosas o conceptos del mundo real, lo que hace más intuitivo el diseño de aplicaciones complejas.
  4. Encapsulamiento: La estructura de una clase permite ocultar detalles internos y proteger datos, exponiendo solo lo necesario.

Diferencia entre Clases y Objetos

  • Una clase es un concepto abstracto, una plantilla o diseño general para crear objetos.
  • Un objeto es una entidad concreta que existe en el programa y que tiene características individuales definidas por la clase de la que proviene.

Por ejemplo, Coche es la clase, mientras que mi_coche (el objeto) es una representación específica de esa clase en la memoria del programa.

Analogía

Imagina que la clase Coche es el plano de un coche, mientras que un objeto es el coche físico construido a partir de esos planos. Puedes construir varios coches a partir del mismo plano, y aunque todos sean coches, cada uno puede tener su propio color, modelo, etc.

Vista la parte teórica ahora te toca a ti. Te propongo dos ejercicios para que practiques.

Ejercicio 1: Herencia y Métodos en Subclases (Persona, Hombre, Mujer)

Define una clase base Persona que contenga los siguientes atributos y métodos:
Atributos:nombre: El nombre de la persona.
edad: La edad de la persona.
nacionalidad: La nacionalidad de la persona.
Métodos:__init__(self, nombre, edad, nacionalidad): Inicializa nombre, edad y nacionalidad.
saludar(self): Método que imprime un saludo usando los atributos de la persona.
es_mayor_edad(self): Método que devuelve True si la persona tiene 18 años o más, o False en caso contrario.
Crea dos subclases llamadas Hombre y Mujer, que hereden de Persona. Además:
En Hombre: Agrega un método saludar(self) que extienda el saludo indicando «Soy un hombre».
En Mujer: Agrega un método saludar(self) que extienda el saludo indicando «Soy una mujer».
# Clase base Persona
class Persona:
    def __init__(self, nombre, edad, nacionalidad):
        self.nombre = nombre
        self.edad = edad
        self.nacionalidad = nacionalidad

    def saludar(self):
        # Saludo general para cualquier persona
        print(f"Hola, me llamo {self.nombre}, tengo {self.edad} años y soy de {self.nacionalidad}.")

    def es_mayor_edad(self):
        # Devuelve si la persona es mayor de edad
        return self.edad >= 18

# Subclase Hombre
class Hombre(Persona):
    def saludar(self):
        # Saludo específico para la subclase Hombre
        super().saludar()  # Llama al saludo de la clase base
        print("Soy un hombre.")

# Subclase Mujer
class Mujer(Persona):
    def saludar(self):
        # Saludo específico para la subclase Mujer
        super().saludar()  # Llama al saludo de la clase base
        print("Soy una mujer.")

# Crear instancias de cada clase
persona1 = Hombre("Carlos", 30, "Español")
persona2 = Mujer("Ana", 25, "Argentina")

# Llamadas a los métodos
persona1.saludar()      # Saludo específico de Hombre
print(persona1.es_mayor_edad())  # Verificación de edad

persona2.saludar()      # Saludo específico de Mujer
print(persona2.es_mayor_edad())  # Verificación de edad

Explicación:

  1. Herencia: La clase Persona actúa como clase base, y Hombre y Mujer heredan sus atributos y métodos. Esta estructura permite que Hombre y Mujer tengan comportamientos similares (como el método es_mayor_edad).
  2. Polimorfismo: Cada subclase tiene su propio método saludar(), extendiendo el saludo estándar para cada caso específico. super().saludar() permite invocar el método saludar() de la clase base y luego añadir la distinción de género.
  3. Aplicación de los Métodos: La invocación de saludar() y es_mayor_edad() en las instancias de Hombre y Mujer demuestra cómo las subclases pueden modificar y ampliar el comportamiento de la clase base según la necesidad.
Ejercicio 2: Polimorfismo y Métodos en Subclases (Persona, Hombre, Mujer con método profesion)

Amplía la clase Persona añadiendo un método profesion que indique la profesión de la persona. Este método debe ser implementado de manera diferente en cada subclase, ya que cada persona puede tener una profesión distinta.
En Persona: Define el método profesion que simplemente imprima «Profesión desconocida».
En Hombre: Redefine profesion para imprimir «Soy ingeniero».
En Mujer: Redefine profesion para imprimir «Soy doctora».
# Clase base Persona
class Persona:
    def __init__(self, nombre, edad, nacionalidad):
        self.nombre = nombre
        self.edad = edad
        self.nacionalidad = nacionalidad

    def saludar(self):
        print(f"Hola, soy {self.nombre}, tengo {self.edad} años y soy de {self.nacionalidad}.")

    def profesion(self):
        # Método genérico en la clase base
        print("Profesión desconocida.")

# Subclase Hombre
class Hombre(Persona):
    def profesion(self):
        # Profesión específica para Hombre
        print("Soy ingeniero.")

# Subclase Mujer
class Mujer(Persona):
    def profesion(self):
        # Profesión específica para Mujer
        print("Soy doctora.")

# Crear instancias de Hombre y Mujer
persona1 = Hombre("Carlos", 32, "Mexicano")
persona2 = Mujer("Laura", 29, "Chilena")

# Llamadas a los métodos
persona1.saludar()      # Saludo general
persona1.profesion()    # Profesión de hombre

persona2.saludar()      # Saludo general
persona2.profesion()    # Profesión de mujer

Explicación:

  1. Polimorfismo en Acción: El método profesion se comporta de manera distinta en cada subclase (Hombre y Mujer). Aunque el nombre del método es el mismo, cada clase muestra su propio mensaje, aplicando polimorfismo.
  2. Herencia y Modificación de Comportamiento: Hombre y Mujer heredan de Persona pero redefinen el método profesion, lo que permite personalizar el comportamiento de acuerdo a cada subclase.
  3. Implementación de un Método Genérico: Al definir profesion en Persona, hemos creado un método genérico que permite establecer un valor por defecto que se puede sobrescribir en las subclases.

Estos ejercicios demuestran cómo usar herencia para evitar la duplicación de código, además de aplicar polimorfismo para implementar comportamientos específicos en subclases.

Bibliografía de Python de interés.

Logo Python. Programación orientada a Objetos

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *