La Programación Orientada a Objetos (POO) es un paradigma de programación que se centra en la organización del código en objetos, los cuales encapsulan datos y comportamientos relacionados. Python es un lenguaje de programación que soporta completamente la POO y permite crear clases y objetos de manera sencilla. A continuación mostraremos la sintaxis relacionada con la POO en Python, incluyendo la creación de clases, la definición de atributos y métodos, la herencia y el encapsulamiento, utilizando explicaciones y ejemplos prácticos.
Clases y Objetos
En POO, una clase es una plantilla que define la estructura y el comportamiento de un objeto. Un objeto es una instancia particular de una clase, es decir, una entidad real con sus propios datos y métodos. Para definir una clase en Python, utilizamos la palabra clave class
, seguida del nombre de la clase (por convención, los nombres de clase suelen empezar con mayúscula):
# Definición de una clase simple class MiClase: pass # Creación de un objeto (instancia) de la clase MiClase objeto1 = MiClase()
Atributos y métodos
Los atributos son variables que pertenecen a una clase y representan las características de los objetos. Los métodos son funciones definidas dentro de una clase y describen el comportamiento de los objetos. Los métodos siempre tienen como primer parámetro self
, que se refiere al objeto en sí mismo:
class Persona: def __init__(self, nombre, edad): self.nombre = nombre self.edad = edad def saludar(self): print(f"Hola, mi nombre es {self.nombre} y tengo {self.edad} años.") # Crear un objeto de la clase Persona persona1 = Persona("Juan", 30) # Acceder a los atributos print(persona1.nombre) # Salida: "Juan" print(persona1.edad) # Salida: 30 # Llamar a un método del objeto persona1.saludar() # Salida: "Hola, mi nombre es Juan y tengo 30 años."
En este ejemplo, definimos la clase Persona
con los atributos nombre
y edad
, y el método saludar()
que muestra un mensaje con los valores de los atributos.
Constructor __init__()
El método __init__()
es especial en Python y se llama automáticamente cuando se crea un nuevo objeto de la clase. Se utiliza habitualmente para inicializar los atributos del objeto:
class Persona: def __init__(self, nombre, edad): self.nombre = nombre self.edad = edad def saludar(self): print(f"Hola, mi nombre es {self.nombre} y tengo {self.edad} años.") # Crear un objeto de la clase Persona persona1 = Persona("María", 25) persona1.saludar() # Salida: "Hola, mi nombre es María y tengo 25 años."
Atributos de clase y atributos de instancia
En Python, los atributos de clase son comunes a todas las instancias de la clase, mientras que los atributos de instancia son específicos para cada objeto:
class Circulo: # Atributo de clase (común a todos los círculos) pi = 3.14159 def __init__(self, radio): # Atributo de instancia (especifico para cada círculo) self.radio = radio def calcular_area(self): return self.pi * self.radio ** 2 # Crear objetos de la clase Circulo circulo1 = Circulo(5) circulo2 = Circulo(10) # Acceder a los atributos de clase (usando el nombre de la clase) print(Circulo.pi) # Salida: 3.14159 # Acceder a los atributos de instancia (usando el objeto) print(circulo1.radio) # Salida: 5 print(circulo2.radio) # Salida: 10 # Llamar a un método del objeto print(circulo1.calcular_area()) # Salida: 78.53975 print(circulo2.calcular_area()) # Salida: 314.159
Herencia
La herencia es un concepto clave en la POO y permite crear una nueva clase basada en una clase existente. La clase nueva hereda atributos y métodos de la clase existente y puede añadir sus propios atributos y métodos:
class Animal: def __init__(self, especie): self.especie = especie def hacer_sonido(self): pass class Perro(Animal): def __init__(self, raza): super().__init__("Perro") self.raza = raza def hacer_sonido(self): return "Guau Guau!" class Gato(Animal): def __init__(self, color): super().__init__("Gato") self.color = color def hacer_sonido(self): return "Miau Miau!" # Crear objetos de las clases Perro y Gato perro1 = Perro("Labrador") gato1 = Gato("Negro") # Acceder a atributos y llamar a métodos de las clases y de la clase base (Animal) print(perro1.especie) # Salida: "Perro" print(perro1.raza) # Salida: "Labrador" print(perro1.hacer_sonido()) # Salida: "Guau Guau!" print(gato1.especie) # Salida: "Gato" print(gato1.color) # Salida: "Negro" print(gato1.hacer_sonido()) # Salida: "Miau Miau!"
Métodos de clase y métodos estáticos
Los métodos de clase (@classmethod
) son métodos que se definen en una clase y operan en la clase en sí misma, en lugar de en instancias individuales de la clase. Estos métodos pueden ser accedidos directamente desde la clase:
class MiClase: contador = 0 def __init__(self): MiClase.contador += 1 @classmethod def obtener_contador(cls): return cls.contador objeto1 = MiClase() objeto2 = MiClase() # Llamar al método de clase desde la clase print(MiClase.obtener_contador()) # Salida: 2
Los métodos estáticos (@staticmethod
) son similares a los métodos de clase, pero no tienen acceso a la instancia (self
) ni a la clase (cls
). Se utilizan para realizar tareas que están relacionadas con la clase, pero no dependen de los atributos o métodos de la clase:
class Utilidades: @staticmethod def sumar(a, b): return a + b @staticmethod def restar(a, b): return a - b resultado_suma = Utilidades.sumar(5, 3) resultado_resta = Utilidades.restar(10, 4) print(resultado_suma) # Salida: 8 print(resultado_resta) # Salida: 6
Encapsulamiento
El encapsulamiento es un concepto que se refiere a ocultar detalles internos de una clase y proteger sus atributos y métodos. En Python no disponemos de modificadores de acceso como en otros lenguajes (p. ej., public, private). Sin embargo, podemos indicar que un atributo o método es «privado» mediante una convención de nomenclatura agregando dos guiones bajos al inicio del nombre (p. ej., __atributo
):
class CuentaBancaria: def __init__(self, titular, saldo): self.__titular = titular self.__saldo = saldo def depositar(self, cantidad): self.__saldo += cantidad def retirar(self, cantidad): if cantidad <= self.__saldo: self.__saldo -= cantidad else: print("Saldo insuficiente.") def obtener_titular(self): return self.__titular def obtener_saldo(self): return self.__saldo # Crear un objeto de la clase CuentaBancaria cuenta = CuentaBancaria("Juan", 1000) # Usar los métodos para interactuar con los atributos de la clase cuenta.depositar(500) print("Depositados 500 euros") cuenta.retirar(200) print("Retirados 200 euros") # Usar los métodos para obtener el titular y el saldo de la cuenta print(f"{cuenta.obtener_titular()} tiene {cuenta.obtener_saldo()} euros") # Salida: Juan tiene 1300 euros
Propiedades
Las propiedades son una forma de controlar el acceso a los atributos de una clase, y permiten además la ejecución de código adicional al consultar o asignar valores, pudiendo realizar validaciones de los datos. Se utilizan para definir métodos especiales (getter
y setter
) que se comportan como atributos:
class Persona: def __init__(self, nombre, edad): self.__nombre = nombre self.__edad = edad # Getter para obtener el nombre @property def nombre(self): return self.__nombre # Setter para asignar el nombre y validar la longitud @nombre.setter def nombre(self, valor): if len(valor) > 3: self.__nombre = valor else: print("El nombre debe tener más de 3 caracteres.") # Getter para obtener la edad @property def edad(self): return self.__edad # Setter para asignar la edad y validar que sea mayor que 0 @edad.setter def edad(self, valor): if valor > 0: self.__edad = valor else: print("La edad debe ser mayor que 0.") # Crear un objeto de la clase Persona persona = Persona("Juan", 30) # Acceder al atributo usando la propiedad (getter) print(persona.nombre) # Salida: "Juan" # Asignar un nuevo valor al atributo usando la propiedad (setter) persona.nombre = "Ana" # Salida: "El nombre debe tener más de 3 caracteres." # Asignar un nuevo valor válido al atributo usando la propiedad (setter) persona.nombre = "María" print(persona.nombre) # Salida: "María" # Intentar asignar un valor no válido a la edad persona.edad = -5 # Salida: "La edad debe ser mayor que 0." # Asignar un valor válido a la edad persona.edad = 25 print(persona.edad) # Salida: 25
Métodos Especiales
En Python, los métodos especiales (también conocidos como «métodos mágicos») son métodos que tienen doble guion bajo al inicio y al final del nombre. Son utilizados para sobrecargar operadores y permitir la personalización del comportamiento de una clase.
En el siguiente ejemplo vamos a sobrecargar los métodos __str__
, __add__
, __sub__
, __eq__
y __lt__
para personalizar la representación de un objeto Punto
como una cadena y también para permitir la suma, la resta y la comparación utilizando las coordenadas x
e y
de cada punto. Estos métodos especiales nos permiten definir el comportamiento de los operadores en la clase Punto
, lo que proporciona una mayor flexibilidad y facilidad de uso cuando interactuamos con objetos de esta clase. A continuación mostramos cómo podemos especificar dicha funcionalidad y sobrecargar los operadores ==
, <
, +
y -
en la clase Punto
:
class Punto: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"Punto({self.x}, {self.y})" def __add__(self, otro_punto): return Punto(self.x + otro_punto.x, self.y + otro_punto.y) def __sub__(self, otro_punto): return Punto(self.x - otro_punto.x, self.y - otro_punto.y) def __eq__(self, otro_punto): return self.x == otro_punto.x and self.y == otro_punto.y def __lt__(self, otro_punto): distancia_origen_self = (self.x ** 2 + self.y ** 2) ** 0.5 distancia_origen_otro = (otro_punto.x ** 2 + otro_punto.y ** 2) ** 0.5 return distancia_origen_self < distancia_origen_otro # Crear objetos de la clase Punto punto1 = Punto(1, 2) punto2 = Punto(3, 4) punto3 = Punto(1, 2) # Uso del método especial __str__() al imprimir el objeto print(punto1) # Salida: Punto(1, 2) print(punto2) # Salida: Punto(3, 4) print(punto3) # Salida: Punto(1, 2) # Sobrecarga del operador == print(punto1 == punto2) # Salida: False print(punto1 == punto3) # Salida: True # Sobrecarga del operador < punto4 = Punto(5, 5) punto5 = Punto(3, 4) print(punto4 < punto5) # Salida: False (distancia al origen de punto4 es mayor) # Sobrecarga del operador + resultado_suma = punto1 + punto2 print(resultado_suma) # Salida: Punto(4, 6) # Sobrecarga del operador - resultado_resta = punto1 - punto2 print(resultado_resta) # Salida: Punto(-2, -2)
En este ejemplo, la clase Punto
tiene el método especial __str__()
, que devuelve una cadena que representa el objeto en un formato personalizado. Cuando imprimimos los objetos punto1
, punto2
y punto3
Python utiliza automáticamente el método __str__()
para obtener la representación de cadena del objeto, lo que nos permite ver las coordenadas del punto de una forma legible.
Además, hemos sobrecargado los métodos __add__()
y __sub__()
para definir la suma y resta de objetos Punto
. Esto nos permite realizar operaciones aritméticas directamente entre objetos Punto
y obtener nuevos objetos Punto
con las coordenadas sumadas o restadas.
Y por último hemos sobrecargado los métodos __eq__()
y __lt__()
para utilizar los operadores ==
y <
, respectivamente.
El método __eq__()
permite comparar si dos objetos Punto
son iguales. En este caso, estamos comparando las coordenadas x
e y
de ambos puntos. Si ambas coordenadas son iguales, entonces los objetos Punto
son considerados iguales.
El método __lt__()
permite comparar si un objeto Punto
es menor que otro, en función de su distancia al origen (punto en coordenadas [0, 0]
). Calculamos la distancia al origen de ambos puntos y devolvemos True
si la distancia del primer punto es menor que la del segundo, de lo contrario, devolvemos False
.
Herencia múltiple y «mixins»
En Python, una clase puede heredar de varias clases, lo que se conoce como herencia múltiple. Esto permite que una clase obtenga atributos y métodos de varias clases base:
class A: def metodo_a(self): print("Método A") class B: def metodo_b(self): print("Método B") class C(A, B): def metodo_c(self): print("Método C") objeto_c = C() objeto_c.metodo_a() # Salida: "Método A" objeto_c.metodo_b() # Salida: "Método B" objeto_c.metodo_c() # Salida: "Método C"
También podemos utilizar «mixins» con este propósito. Los «mixins» son pequeñas clases que se utilizan para agregar funcionalidades específicas a otras clases. La idea es que contengan piezas de código que puedan mezclarse o combinarse con otras clases para agregar ciertas características. Los «mixins» no están diseñados para ser instanciados por sí mismos, sino para ser usados como componentes adicionales. Permiten que las clases compartan comportamientos comunes sin la necesidad de heredar de una clase base que puede tener otros métodos y atributos que no son necesarios para todas las subclases.
Veamos un ejemplo de cómo podemos utilizar un «mixin» para agregar funcionalidad a una clase Vehiculo
que representa un vehículo genérico:
class Vehiculo: def __init__(self, marca, modelo): self.marca = marca self.modelo = modelo def conducir(self): print(f"Conduciendo el {self.marca} {self.modelo}")
Ahora queremos agregar funcionalidad adicional a la clase Vehiculo
para que algunos vehículos puedan navegar por el agua. Para ello creamos un «mixin » llamado NavegableMixin
:
class NavegableMixin: def navegar(self): print(f"Navegando con el {self.marca} {self.modelo}")
Finalmente, combinamos el «mixin» NavegableMixin
con la clase Vehiculo
para crear una nueva clase Barco
que puede navegar:
class Barco(Vehiculo, NavegableMixin): pass barco = Barco("Transatlántico", "Titanic") barco.conducir() # Salida: "Conduciendo el Transatlántico Titanic" barco.navegar() # Salida: "Navegando con el Transatlántico Titanic"
En este ejemplo, hemos creado una clase NavegableMixin
que tiene un método navegar()
. Luego, creamos la clase Barco
que hereda de la clase Vehiculo
y mezcla el mixin NavegableMixin
. De esta manera, la clase Barco
hereda la funcionalidad de la clase Vehiculo
y también obtiene la capacidad de navegar del «mixin» NavegableMixin
.
La utilización de «mixins» permite una mayor modularidad y reutilización de código, ya que podemos crear «mixins» con funcionalidades específicas y combinarlos con diferentes clases según sea necesario.
Es importante tener en cuenta que el orden en que se heredan las clases y mixins puede afectar el comportamiento de la clase final. En el ejemplo anterior, heredamos primero de Vehiculo
y luego del mixin NavegableMixin
, lo que asegura que los métodos de la clase base (Vehiculo
) tengan prioridad en caso de existir métodos con el mismo nombre en el mixin. Si el orden fuera invertido, los métodos del «mixin» tendrían prioridad. Por lo tanto, es recomendable tener cuidado con el orden de herencia y mezcla de clases y mixins para evitar posibles conflictos.
Test
Evalúa tus conocimientos mediante este test que incluye preguntas relacionadas con esta unidad.