Tabla de Contenidos
Programación Orientada a Objetos en Python
Bloque perteneciente al curso Introducción a la programación con Python.
Antes de la aparición de la programación estructurada, cuando los programas crecían, se hacía cada vez más difícil estructurarlos y mantenerlos. Código espagueti.
La Programación Orientada a Objetos responde a la necesidad de poder mantener programas de gran complejidad y envergadura y ciclos de vida a muy largo plazo. Prácticamente cualquier proyecto de software que quiera mantenerse en el tiempo se desarrolla siguiendo el paradigma de Programación Orientada a Objetos.
La Programación Orientada a Objetos es un método de organización del código.
El lenguaje Java está puramente orientado a objetos. Python está también orientado a objetos, pero se pueden realizar programas sin usar este paradigma.
Atributos
Son como las variables.
- Describen el estado de cada objeto (o instancia) de esa clase
- Definidos en la clase
Métodos
También llamadas funciones miembro.
- Pertenecen a la clase
- Modelan el comportamiento de los objetos
Creación de una clase en Python
class ContadorVisitas: # Método constructor def __init__(self): self.visitas = 0 # Método para registrar una visita def registrarVisita(self): self.visitas += 1 # Método para visualizar el número de visitas def __repr__(self) -> str: return f"{self.visitas} visitas contabilizadas"
__init__es el constructor, define las características iniciales que tendrá el objeto.selfhace referencia al propio objeto.__repr__representa textualmente el objeto.
Hemos definido la clase (una estructura), para poder usarla tenemos que crear un objeto a partir de ella
Creación de las instancias u objetos
A partir de la clase ContadorVisitas definida anteriormente, crearemos un objeto:
cv = ContadorVisitas()
Cuando se crea un nuevo objeto, ocurren dos cosas:
- El intérprete del lenguaje Python solicita al sistema un espacio de memoria suficiente para alojar en él toda la información del nuevo objeto. Esa parcela de memoria reservada para el objeto de nueva creación es referida como
self - Se ejecuta automáticamente el código registrado en el método
__init__
Usamos el objeto:
cv # 0 visitas contabilizadas cv.registrarVisitas() cv # 1 visitas contabilizadas cv.registrarVisitas() cv # 2 visitas contabilizadas
Para “complicarlo” un poco y ver la utilidad de todo esto, creamos dos objetos:
# Creación de dos instancias u objetos (y se asignan a sendas variables) cv1 = ContadorVisitas() cv2 = ContadorVisitas() cv1.registrarVisitas() cv2.registrarVisitas() cv1 # 1 visitas contabilizadas cv2 # 1 visitas contabilizadas cv2.registrarVisitas() cv1 # 1 visitas contabilizadas cv2 # 2 visitas contabilizadas
Vemos que hemos podido crear dos objetos del mismo tipo y son independientes, cada uno guarda su propio estado.
Inicialización de objetos
Algunas veces necesitamos dar un valor inicial a los atributos o realizar tareas iniciales en el momento que un objeto es creado.
Para ello escribimos código de inicialización…
Características de la POO
- Abstracción: separar detalles de implementación de la declaración. Un código puede usar características de un objeto sin conocer cómo está programado.
- Encapsulación: el código está encerrado en una estructura (clase).
- Herencia: una clase (madre) aporta una serie de elementos comunes a otras clases (hijas)
- Polimorfismo: un mismo método tiene comportamientos distintos.
Organización del código
Proyecto con múltiples ficheros.
La Programación Orientada a Objetos es útil cuando hay varias personas trabajando sobre un mismo proyecto.
- Organización en módulos
.py - Cada módulo puede contener varias clases.
- Los módulos se importan desde otros módulos para componer todo el programa:
import modulofrom modulo import Clase
# Las clases heredadas las ponemos entre paréntesis cuando definimos una clase class JugadorIA(Jugador): # Código
Las clases como nuevos tipos de datos
class Pais: def __init__(self, nombre, capital, poblacion, extension): self.nombre = nombre self.capital = capital self.poblacion = poblacion self.extension = extension # Representación formal del objeto def __repr__(self): return f" Pais('{self.nombre}', '{self.capital}', {self.poblacion} {self.extension})" # Conversión a string para visualizar el objeto de manera conveniente def __str__(self): return f" {self.nombre}" # Con el decorador '@property' se pueden definir propiedades, que se comportan # como atributos pero que en realidad son funciones miembro @ property def densiadadPoblacion(self): return self.poblacion / self.extension # Un método llamado __lt__ representa el operador de comparación '<' # Esto permite definir, para este tipo de datos, un criterio de ordenación por defecto # https://docs.python.org/es/3/reference/datamodel.html?highlight=model#object.__lt__ def __lt__(self, otro): return self.extension < otro.extension
Desde Python 3.6 (y en el PEP 515)se permite representar literales numéricos grandes separando los millares con el guion bajo (_) por legibilidad.
p1 = Pais("España", "Madrid", 49_000_000, 595_990) p2 = Pais("Portugal", "Lisboa", 10_726_963, 91_568) p3 = Pais("Alemania", "Berlín", 81_881_238, 357_021) # No tenemos que llamar al método con los paréntesis porque usamos el decorador '@property' p1.densiadadPoblacion p2.densiadadPoblacion p3.densiadadPoblacion p1 < p2 # False sorted(paises) # [Portugal, Alemania, España] # Llamada al método __repr__ p1 # Pais('España', 'Madrid', 49000000, 505990) # Llamada al método __str__ print(p1) # España
Para más información sobre métodos especiales (sobrecarga de operadores y funciones especiales con objetos: https://docs.python.org/es/3/reference/datamodel.html#special-method-names
Caso práctico de Herencia y polimorfismo
# Clase base, totalmente abstracta; no tiene sentido por sí sola, pero aporta # unas definiciones que serán declaradas y extendidass en las clases que se # deriven de esta. class FiguraGeometrica: @property def area(self): ''' Cálculo del área de la figura ''' pass @property def perimetro(self): ''' Cálculo del perímetro de la figura ''' pass def __lt__(self, otra): return self.area < otra.area
Usaremos la clase anterior para definir nuevas clases:
# Clase que representa un rectángulo, y que hereda de la clase FiguraGeometrica: class Rectangulo(FiguraGeometrica): def __init__(self, base, altura): self.base = base self.altura = altura @property def area(self): return self.base * self.altura @property def perimetro(self): return 2 * (self.base + self.altura) def __repr__(self): return f"Rectangulo({self.base:g}, {self.altura:g})"
import math # Clase que representa un círculo, y que hereda de la clase FiguraGeometrica: class Circulo(FiguraGeometrica): def __init__(self, radio): self.radio = radio @property def area(self): return self.radio * self.radio * math.pi @property def perimetro(self): return self.radio * math.pi * 2 def __repr__(self): return f"Circulo({self.radio:g})"
Creamos y usamos los objetos:
r1 = Rectangulo(8, 5) r1.area, r1.perimetro # (40, 26) c1 = Circulo(10) c1.area, c1.perimetro # (314.159265, 62.83185) figuras = [r1, c1] sorted(figuras, key=lambda f:f.area) # [Rectangulo(26), Circulo(62.83185)] # Ahora se ordenará por área que es lo que definimos sorted(figuras) # [Rectangulo(40), Circulo(314.159265)]
Ejemplo práctico: 4 en raya
En una misma carpeta, crearemos varios ficheros para desarrollar el programa del 4 en raya o conecta 4. Empezamos por tablero.py:
Hay que instalar previamente el módulo Colorama
from colorama import Fore class Tablero: ROJO = 1 AMARILLO = 2 def __init__(self): self.casillas = [[0 for _ in range(6)] for _ in range(7)] def __str__(self) -> str: s = "+---"*7 + "+\n" for fila in range(6): for columna in range(7): match self.casillas[columna][fila]: case 1: s += "|" + Fore.RED + " O " + Fore.RESET case 2: s += "|" + Fore.YELLOW + " X " + Fore.RESET case _: s += "| " s += "|\n" s += "+---"*7 + "+\n" s += " 0 1 2 3 4 5 6\n" return s def ponerFicha(self, columna, color): libres = sum(c==0 for c in self.casillas[columna]) if libres > 0: self.casillas[columna][libres - 1] = color else: raise ValueError(f"Columna {columna} llena") def comprobarGanador(self): # Comprobar si hay 4 en raya en sentido vertical for fila in range(3): for columna in range(7): if self.casillas[columna][fila] != 0 \ and self.casillas[columna][fila] == self.casillas[columna][fila+1] \ and self.casillas[columna][fila] == self.casillas[columna][fila+2] \ and self.casillas[columna][fila] == self.casillas[columna][fila+3]: return self.casillas[columna][fila] # Tenemos ganador # Comprobar si hay 4 en raya en sentido horizontal for fila in range(6): for columna in range(4): if self.casillas[columna][fila] != 0 \ and self.casillas[columna][fila] == self.casillas[columna+1][fila] \ and self.casillas[columna][fila] == self.casillas[columna+2][fila] \ and self.casillas[columna][fila] == self.casillas[columna+3][fila]: return self.casillas[columna][fila] # Tenemos ganador # Comprobar si hay 4 en raya en sentido diagonal / o \ for fila in range(3): for columna in range(4): if self.casillas[columna][fila] != 0 \ and self.casillas[columna][fila] == self.casillas[columna+1][fila+1] \ and self.casillas[columna][fila] == self.casillas[columna+2][fila+2] \ and self.casillas[columna][fila] == self.casillas[columna+3][fila+3]: return self.casillas[columna][fila] # Tenemos ganador for columna in range(3, 7): if self.casillas[columna][fila] != 0 \ and self.casillas[columna][fila] == self.casillas[columna-1][fila+1] \ and self.casillas[columna][fila] == self.casillas[columna-2][fila+2] \ and self.casillas[columna][fila] == self.casillas[columna-3][fila+3]: return self.casillas[columna][fila] # Tenemos ganador # Si no se da alguna de las anteriores, devolver 0 return 0 @property def lleno(self): ''' Devuelve True si el tablero está lleno; False en caso contrario. La condición de lleno se da cuando todas las columnas tienen todas las celdas con valores distintos de 0. ''' return all([all(self.casillas[columna]) for columna in range(7)]) if __name__ == '__main__': tablero = Tablero() tablero.ponerFicha(0, Tablero.ROJO) tablero.ponerFicha(0, Tablero.AMARILLO) tablero.ponerFicha(6, Tablero.ROJO) print(tablero) tablero.ponerFicha(0, Tablero.ROJO) tablero.ponerFicha(0, Tablero.AMARILLO) tablero.ponerFicha(0, Tablero.ROJO) tablero.ponerFicha(0, Tablero.AMARILLO) try: tablero.ponerFicha(0, Tablero.ROJO) # Esta no se llega a colocar except ValueError as err: print("No puedes colocar ahí:", err) print("Ganador:", tablero.comprobarGanador()) tablero.ponerFicha(1, Tablero.ROJO) tablero.ponerFicha(1, Tablero.AMARILLO) tablero.ponerFicha(2, Tablero.ROJO) tablero.ponerFicha(2, Tablero.AMARILLO) tablero.ponerFicha(6, Tablero.ROJO) tablero.ponerFicha(3, Tablero.AMARILLO) tablero.ponerFicha(6, Tablero.ROJO) tablero.ponerFicha(1, Tablero.AMARILLO) print(tablero) print("Ganador:", tablero.comprobarGanador())
jugadores.py:
import random from tablero import Tablero class Jugador: def __init__(self, color): self.color = color def jugar(self): pass class JugadorHumano(Jugador): def jugar(self): while True: try: columna = int(input("Introduce columna (entre 0 y 6): ")) if columna < 0 or columna > 6: raise ValueError("La columna debe estar entre 0 y 6") # Si no hay errores antes, llegamos al return que devuelve la columna elegida return columna except ValueError as err: print(f"Columna incorrecta: {err}") class JugadorIA(Jugador): def jugar(self): return random.randint(0, 6) if __name__ == '__main__': jh = JugadorHumano(Tablero.ROJO) eleccion = jh.jugar() print(f"Has elegido la columna {eleccion}") tab = Tablero() tab.ponerFicha(eleccion, jh.color) print(tab)
game.py:
from jugadores import JugadorHumano, JugadorIA from tablero import Tablero class Game: def __init__(self, modoHumano = False): self.tablero = Tablero() self.jugadores = [ JugadorHumano(Tablero.ROJO), # jugador 0 JugadorHumano(Tablero.AMARILLO) if modoHumano else JugadorIA(Tablero.AMARILLO) # jugador 1 ] self.turno = 0 def cambiarTurno(self): ''' Alterna el turno de los jugadores: 0, 1, 0, 1, ... ''' self.turno = 0 if self.turno else 1 def esFinDeJuego(self) -> bool: ''' Compruebas si el juego ha llegado a su fin; bien porque uno de los dos jugadores pudo colocar 4 en raya, bien porque se ha llenado el tablero y no hay ganador. ''' return self.tablero.lleno or self.tablero.comprobarGanador()!=0 def iniciar(self): ''' Inicia el juego. Mientras no se de la condición de final del juego, se alternarán los turnos pidiédole a cada jugador que efectúen su jugada y se colocará la ficha en la posición escogida por cada uno de ellos. Cuando el juego termine, se anunciará al ganador, si lo hay. ''' while not self.esFinDeJuego(): # Al comienzo de cada vuelta, dibujamos el tablero print(self.tablero) eleccion = self.jugadores[self.turno].jugar() try: self.tablero.ponerFicha(eleccion, self.jugadores[self.turno].color) except ValueError as err: # Si hay algún problema colocando la ficha, se mostrará el error # y se volverá a pedir al jugador que realice su jugada print(err) continue self.cambiarTurno() # Al salir del bucle, el juego ha terminado print(self.tablero) ganador = self.tablero.comprobarGanador() if ganador: print(f"El ganador es el jugador {ganador}") else: print("Empate")
Fichero __init__.py:
from game import Game if __name__ == '__main__': while True: opcion = input("Quieres jugar contra la máquina o contra otro jugador? (m/j), s para salir:") try: match opcion.lower(): case "m": g = Game() break case "j": g = Game(modoHumano=True) break case "s": exit() case _: raise ValueError("Opción no válida") except ValueError as err: print(err) g.iniciar()
