Herramientas de usuario

Herramientas del sitio


informatica:programacion:python:cursos:introduccion_programacion_python:programacion_orientada_a_objetos

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.
  • self hace 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:

  1. 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
  2. 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 modulo
    • from 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()
informatica/programacion/python/cursos/introduccion_programacion_python/programacion_orientada_a_objetos.txt · Última modificación: por tempwin