Objetivos de Aprendizaje
Al finalizar esta clase, serás capaz de:
- Definir clases en Python usando
class - Crear objetos (instancias) de una clase
- Usar
__init__para inicializar objetos - Comprender el propósito de
self - Crear métodos para definir comportamientos
- Implementar el modelo Usuario para TaskFlow
Video Recomendado
Introducción a la Programación Orientada a Objetos en Python:
Ver "POO en Python - Introducción" 16:46 min | Sergio A. Castaño Giraldo¿Qué es una Clase? (10 min)
La POO (Programación Orientada a Objetos) es un paradigma basado en objetos. Una clase es un molde o plantilla para crear objetos. Define atributos (datos) y métodos (comportamientos).
Sintaxis Básica
class NombreClase:
"""Descripción de la clase."""
# Atributo de clase (compartido por todos los objetos)
atributo_clase = "valor"
def __init__(self, parametro1, parametro2):
"""Constructor: inicializa el objeto."""
# Atributos de instancia (únicos para cada objeto)
self.atributo1 = parametro1
self.atributo2 = parametro2
def metodo(self):
"""Método de instancia."""
return f"Atributo 1: {self.atributo1}"
# Crear objetos (instancias)
objeto1 = NombreClase("valor1", "valor2")
objeto2 = NombreClase("otro1", "otro2")
print(objeto1.atributo1) # "valor1"
print(objeto2.atributo1) # "otro1"
Ejemplo: Clase Perro
class Perro:
"""Representa un perro con nombre y edad."""
especie = "Canis familiaris" # Atributo de clase
def __init__(self, nombre, edad):
self.nombre = nombre # Atributo de instancia
self.edad = edad # Atributo de instancia
def ladrar(self):
return f"{self.nombre} dice: ¡Guau!"
def es_cachorro(self):
return self.edad < 2
# Crear perros
mi_perro = Perro("Fido", 3)
otro_perro = Perro("Rex", 1)
print(mi_perro.ladrar()) # "Fido dice: ¡Guau!"
print(mi_perro.es_cachorro()) # False
print(otro_perro.es_cachorro()) # True
Constructor __init__ (15 min)
El método __init__ se ejecuta automáticamente cuando se crea un objeto. Sirve para inicializar los atributos.
Ejemplo: Clase Persona
class Persona:
def __init__(self, nombre, edad, email=None):
"""Constructor: inicializa una persona."""
self.nombre = nombre
self.edad = edad
self.email = email
self.activo = True # Valor por defecto
def presentarse(self):
return f"Hola, soy {self.nombre} y tengo {self.edad} años."
# Crear personas
juan = Persona("Juan", 25, "juan@email.com")
ana = Persona("Ana", 30)
print(juan.presentarse())
print(f"Email de Juan: {juan.email}")
print(f"Email de Ana: {ana.email}") # None
Valores por Defecto
class Producto:
def __init__(self, nombre, precio, cantidad=0):
self.nombre = nombre
self.precio = precio
self.cantidad = cantidad # Si no se pasa, es 0
# Uso
p1 = Producto("Laptop", 1000, 5) # cantidad = 5
p2 = Producto("Mouse", 25) # cantidad = 0 (por defecto)
print(f"{p1.nombre}: {p1.cantidad} unidades")
print(f"{p2.nombre}: {p2.cantidad} unidades")
El parámetro self (10 min)
self representa la instancia actual del objeto. Permite acceder a sus atributos y métodos.
self es el primer parámetro de los métodos, NO lo pasas al llamar el método. Python lo hace automáticamente.
class CuentaBancaria:
def __init__(self, titular, saldo_inicial=0):
self.titular = titular
self.saldo = saldo_inicial
def depositar(self, cantidad):
"""self permite modificar los atributos del objeto."""
if cantidad > 0:
self.saldo += cantidad
return f"Depósito exitoso. Nuevo saldo: ${self.saldo}"
return "Cantidad inválida"
def retirar(self, cantidad):
"""self permite leer y modificar atributos."""
if cantidad <= self.saldo:
self.saldo -= cantidad
return f"Retiro exitoso. Nuevo saldo: ${self.saldo}"
return "Saldo insuficiente"
def consultar_saldo(self):
"""self permite leer atributos."""
return f"Saldo de {self.titular}: ${self.saldo}"
# Uso - ¡No pasamos self!
cuenta = CuentaBancaria("Juan", 1000)
print(cuenta.consultar_saldo()) # Saldo de Juan: $1000
print(cuenta.depositar(500)) # Depósito exitoso...
print(cuenta.retirar(200)) # Retiro exitoso...
print(cuenta.consultar_saldo()) # Saldo de Juan: $1300
Métodos Especiales (10 min)
Python tiene métodos especiales (dunder methods) que comienzan y terminan con __.
| Método | Cuándo se usa | Ejemplo |
|---|---|---|
__init__ |
Crear objeto | obj = Clase() |
__str__ |
Imprimir objeto | print(obj) |
__repr__ |
Representación técnica | repr(obj) |
__eq__ |
Comparar objetos | obj1 == obj2 |
class Punto:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
"""Representación amigable para usuarios."""
return f"({self.x}, {self.y})"
def __repr__(self):
"""Representación técnica para debugging."""
return f"Punto(x={self.x}, y={self.y})"
def __eq__(self, otro):
"""Comparar dos puntos."""
return self.x == otro.x and self.y == otro.y
p1 = Punto(3, 4)
p2 = Punto(3, 4)
p3 = Punto(1, 2)
print(p1) # (3, 4) - usa __str__
print(repr(p1)) # Punto(x=3, y=4) - usa __repr__
print(p1 == p2) # True - usa __eq__
print(p1 == p3) # False
Ejercicio Rápido 1: Clase Rectángulo (5 min)
Crea una clase Rectángulo que tenga:
- Atributos: base y altura
- Método area() que retorne base × altura
- Método perimetro() que retorne 2×(base+altura)
- Método __str__ que muestre "Rectángulo de base=X y altura=Y"
Crea 2 rectángulos diferentes y compara sus áreas.
Ejercicio: Clase Usuario (25 min)
Vamos a crear el modelo Usuario para el proyecto TaskFlow.
Requisitos
- Atributos: username, email, nombre_completo (opcional)
- Métodos: presentarse(), es_valido(), activar(), desactivar()
- Validar que username tenga al menos 3 caracteres
- Validar que email contenga "@"
# usuario.py
class Usuario:
"""Representa un usuario en el sistema TaskFlow."""
def __init__(self, username, email, nombre_completo=None):
"""Inicializa un usuario."""
self.username = username
self.email = email
self.nombre_completo = nombre_completo
self.activo = True
def __str__(self):
return f"@{self.username}"
def __repr__(self):
return f"Usuario('{self.username}', '{self.email}')"
def presentarse(self):
"""Retorna una presentación del usuario."""
if self.nombre_completo:
return f"Hola, soy {self.nombre_completo} (@{self.username})"
return f"Hola, soy @{self.username}"
def es_valido(self):
"""Valida los datos del usuario."""
if len(self.username) < 3:
return False, "Username debe tener al menos 3 caracteres"
if "@" not in self.email:
return False, "Email debe contener @"
return True, "Válido"
def activar(self):
"""Activa la cuenta del usuario."""
self.activo = True
def desactivar(self):
"""Desactiva la cuenta del usuario."""
self.activo = False
def esta_activo(self):
"""Retorna True si el usuario está activo."""
return self.activo
# === PRUEBAS ===
if __name__ == "__main__":
# Crear usuarios
usuario1 = Usuario("juan123", "juan@example.com", "Juan Pérez")
usuario2 = Usuario("ana", "ana@example.com")
usuario3 = Usuario("ab", "email-invalido") # Inválido
# Probar presentación
print(usuario1.presentarse())
print(usuario2.presentarse())
# Probar validación
for u in [usuario1, usuario2, usuario3]:
valido, mensaje = u.es_valido()
estado = "✅" if valido else "❌"
print(f"{estado} {u}: {mensaje}")
# Probar activación
usuario1.desactivar()
print(f"\n{usuario1} activo: {usuario1.esta_activo()}")
usuario1.activar()
print(f"{usuario1} activo: {usuario1.esta_activo()}")
# Mostrar representación
print(f"\nRepresentación: {repr(usuario1)}")
Retos Adicionales
- Agrega atributo
fecha_registroque se establezca automáticamente - Agrega método
cambiar_email(nuevo_email)con validación - Agrega método
to_dict()que retorne un diccionario con los datos
Laboratorio 2: Sistema de Cuentas Bancarias (25 min)
Crea un sistema de cuentas bancarias con múltiples cuentas y transferencias entre ellas.
Requisitos
- Clase CuentaBancaria con número de cuenta único, titular y saldo
- Métodos: depositar(), retirar(), transferir(), consultar_saldo()
- Historial de transacciones (lista de diccionarios)
- Validaciones: saldo insuficiente, montos negativos
- Interfaz de menú para gestionar múltiples cuentas
# sistema_bancario.py
import random
import datetime
class CuentaBancaria:
"""Representa una cuenta bancaria."""
def __init__(self, titular, saldo_inicial=0):
self.numero = f"10{random.randint(10000000, 99999999)}"
self.titular = titular
self.saldo = saldo_inicial
self.transacciones = []
# Registrar saldo inicial
if saldo_inicial > 0:
self._registrar_transaccion("APERTURA", saldo_inicial, "Depósito inicial")
def _registrar_transaccion(self, tipo, monto, descripcion):
"""Registra una transacción en el historial."""
transaccion = {
"fecha": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"tipo": tipo,
"monto": monto,
"descripcion": descripcion,
"saldo_resultante": self.saldo
}
self.transacciones.append(transaccion)
def depositar(self, monto, descripcion="Depósito"):
"""Deposita dinero en la cuenta."""
if monto <= 0:
return False, "El monto debe ser positivo"
self.saldo += monto
self._registrar_transaccion("DEPÓSITO", monto, descripcion)
return True, f"Depósito exitoso. Nuevo saldo: ${self.saldo:.2f}"
def retirar(self, monto, descripcion="Retiro"):
"""Retira dinero de la cuenta."""
if monto <= 0:
return False, "El monto debe ser positivo"
if monto > self.saldo:
return False, "Saldo insuficiente"
self.saldo -= monto
self._registrar_transaccion("RETIRO", monto, descripcion)
return True, f"Retiro exitoso. Nuevo saldo: ${self.saldo:.2f}"
def transferir(self, monto, cuenta_destino, descripcion="Transferencia"):
"""Transfiere dinero a otra cuenta."""
if monto <= 0:
return False, "El monto debe ser positivo"
if monto > self.saldo:
return False, "Saldo insuficiente para transferencia"
# Retirar de esta cuenta
self.saldo -= monto
self._registrar_transaccion(
"TRANSFERENCIA SALIDA",
monto,
f"{descripcion} a cuenta {cuenta_destino.numero}"
)
# Depositar en cuenta destino
cuenta_destino.saldo += monto
cuenta_destino._registrar_transaccion(
"TRANSFERENCIA ENTRADA",
monto,
f"{descripcion} desde cuenta {self.numero}"
)
return True, f"Transferencia exitosa. Nuevo saldo: ${self.saldo:.2f}"
def consultar_saldo(self):
"""Retorna el saldo actual."""
return self.saldo
def obtener_historial(self):
"""Muestra el historial de transacciones."""
return self.transacciones
def __str__(self):
return f"Cuenta {self.numero} - {self.titular}: ${self.saldo:.2f}"
def __repr__(self):
return f"CuentaBancaria(numero='{self.numero}', titular='{self.titular}', saldo={self.saldo})"
# Sistema de gestión de cuentas
class SistemaBancario:
"""Gestiona múltiples cuentas bancarias."""
def __init__(self):
self.cuentas = {}
def crear_cuenta(self, titular, saldo_inicial=0):
"""Crea una nueva cuenta."""
cuenta = CuentaBancaria(titular, saldo_inicial)
self.cuentas[cuenta.numero] = cuenta
print(f"✅ Cuenta creada: {cuenta.numero}")
return cuenta
def obtener_cuenta(self, numero):
"""Busca una cuenta por número."""
return self.cuentas.get(numero)
def listar_cuentas(self):
"""Lista todas las cuentas."""
if not self.cuentas:
print("📭 No hay cuentas registradas")
return
print("\n📊 CUENTAS REGISTRADAS:")
print("=" * 60)
for cuenta in self.cuentas.values():
print(f" • {cuenta}")
print("=" * 60)
# Programa principal
sistema = SistemaBancario()
while True:
print("\n" + "="*50)
print("🏦 SISTEMA BANCARIO")
print("="*50)
print("1. Crear cuenta")
print("2. Listar cuentas")
print("3. Depositar")
print("4. Retirar")
print("5. Transferir")
print("6. Consultar saldo")
print("7. Ver historial")
print("8. Salir")
opcion = input("\nSelecciona una opción: ")
if opcion == "1":
titular = input("Nombre del titular: ").strip()
try:
saldo = float(input("Saldo inicial: $"))
sistema.crear_cuenta(titular, saldo)
except ValueError:
print("❌ Saldo inválido")
elif opcion == "2":
sistema.listar_cuentas()
elif opcion == "3":
num = input("Número de cuenta: ").strip()
cuenta = sistema.obtener_cuenta(num)
if cuenta:
try:
monto = float(input("Monto a depositar: $"))
exito, mensaje = cuenta.depositar(monto)
print(f"{'✅' if exito else '❌'} {mensaje}")
except ValueError:
print("❌ Monto inválido")
else:
print("❌ Cuenta no encontrada")
elif opcion == "4":
num = input("Número de cuenta: ").strip()
cuenta = sistema.obtener_cuenta(num)
if cuenta:
try:
monto = float(input("Monto a retirar: $"))
exito, mensaje = cuenta.retirar(monto)
print(f"{'✅' if exito else '❌'} {mensaje}")
except ValueError:
print("❌ Monto inválido")
else:
print("❌ Cuenta no encontrada")
elif opcion == "5":
num_origen = input("Cuenta origen: ").strip()
num_destino = input("Cuenta destino: ").strip()
cuenta_origen = sistema.obtener_cuenta(num_origen)
cuenta_destino = sistema.obtener_cuenta(num_destino)
if cuenta_origen and cuenta_destino:
try:
monto = float(input("Monto a transferir: $"))
exito, mensaje = cuenta_origen.transferir(monto, cuenta_destino)
print(f"{'✅' if exito else '❌'} {mensaje}")
except ValueError:
print("❌ Monto inválido")
else:
print("❌ Una o ambas cuentas no existen")
elif opcion == "6":
num = input("Número de cuenta: ").strip()
cuenta = sistema.obtener_cuenta(num)
if cuenta:
print(f"\n💰 Saldo actual: ${cuenta.consultar_saldo():.2f}")
else:
print("❌ Cuenta no encontrada")
elif opcion == "7":
num = input("Número de cuenta: ").strip()
cuenta = sistema.obtener_cuenta(num)
if cuenta:
historial = cuenta.obtener_historial()
if historial:
print(f"\n📜 HISTORIAL DE {cuenta.titular}:")
print("=" * 80)
print(f"{'Fecha':<20} {'Tipo':<20} {'Monto':<12} {'Descripción'}")
print("=" * 80)
for t in historial:
print(f"{t['fecha']:<20} {t['tipo']:<20} ${t['monto']:<11.2f} {t['descripcion']}")
print("=" * 80)
else:
print("📭 No hay transacciones")
else:
print("❌ Cuenta no encontrada")
elif opcion == "8":
print("👋 ¡Hasta luego!")
break
else:
print("❌ Opción no válida")
Retos Adicionales
- Interés: Agrega método para calcular y aplicar interés mensual
- Límites: Establece límites diarios de retiro por cuenta
- Estadísticas: Calcula total de dinero en el sistema bancario
- Reportes: Genera reporte de cuentas con saldo negativo
Git: Ramas (Branches) y Merge (20 min)
Las ramas te permiten desarrollar funcionalidades aisladas sin afectar el código principal.
¿Qué son las Ramas?
main es la versión publicada. Cuando quieres agregar un capítulo nuevo, creas una copia (rama), trabajas en ella, y cuando está listo la fusionas (merge) con el libro principal.
Comandos de Ramas
# Ver en qué rama estás
git branch
# Crear nueva rama
git branch nombre-rama
# Cambiar a una rama
git checkout nombre-rama
# Crear y cambiar en un solo paso
git checkout -b nombre-rama
# Ver todas las ramas (locales y remotas)
git branch -a
Flujo de Trabajo con Ramas
# 1. Estás en main y todo funciona
git checkout main
# 2. Creas rama para nueva funcionalidad
git checkout -b feature/clase-usuario
# 3. Trabajas en tu código (creas archivos, modificas, etc.)
# ... código código código ...
# 4. Guardas cambios en la rama
git add .
git commit -m "feat: agregada clase Usuario con validaciones"
# 5. Vuelves a main
git checkout main
# 6. Fusionas la rama
git merge feature/clase-usuario
# 7. Opcional: eliminar rama ya fusionada
git branch -d feature/clase-usuario
Ejercicio Práctico: Tu Primera Rama (15 min)
Vamos a crear una nueva funcionalidad usando ramas:
- En tu repo local:
git status(deberías estar en main) - Crea rama:
git checkout -b feature/clase-cuenta-bancaria - Crea el archivo
clase-03/cuenta_bancaria.pycon la clase CuentaBancaria - Prueba que funcione
- Agrega a Git:
git add . - Commit:
git commit -m "feat: agregada clase CuentaBancaria" - Vuelve a main:
git checkout main - Fusiona:
git merge feature/clase-cuenta-bancaria - Sube a GitHub:
git push - Verifica en GitHub que aparecen los archivos
Resolución de Conflictos Básica
A veces al hacer merge aparecen conflictos cuando dos personas modificaron las mismas líneas:
# Intentas hacer merge
$ git merge otra-rama
# Aparece conflicto
Auto-merging archivo.py
CONFLICT (content): Merge conflict in archivo.py
Automatic merge failed; fix conflicts and then commit the result.
# Abres el archivo y verás marcadores:
<<<<<<< HEAD
print("Mi versión")
=======
print("Otra versión")
>>>>>>> otra-rama
# Editas y eliges qué quedarse
print("Versión final combinada")
# Guardas y completas el merge
git add archivo.py
git commit -m "Merge: resuelto conflicto en archivo.py"
Git Ignore para Python
Actualiza tu .gitignore para proyectos Python más profesionales:
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Entornos virtuales
venv/
ENV/
env/
.venv
# IDEs
.vscode/
.idea/
*.swp
*.swo
# Archivos de sistema
.DS_Store
Thumbs.db
# Variables de entorno
.env
.env.local
Ejercicio: Flujo Completo (10 min)
Practica el flujo profesional completo:
- Crea una rama
feature/mejora-readme - Mejora tu README.md con emojis y mejor formato
- Commit:
git commit -m "docs: mejorado README con formato profesional" - Merge a main
- Push a GitHub
- Verifica que el README se ve bien en GitHub
feature/nueva-funcionalidad- Nueva característicafix/correccion-bug- Corrección de errordocs/actualizacion-readme- Documentaciónrefactor/mejorar-codigo- Refactorización
Ejercicios Adicionales (10 min)
Crea una clase Libro con:
- Atributos: título, autor, ISBN, año, disponible (bool)
- Métodos: prestar(), devolver(), info()
- __str__ que muestre "Título por Autor (Año)"
Crea una clase Estudiante con:
- Lista de calificaciones
- Métodos: agregar_nota(), promedio(), aprobo()
- Método para mostrar todas las notas
Resumen
Python - POO
- Clase: Plantilla que define atributos y métodos
- Objeto: Instancia creada a partir de una clase
- __init__: Constructor que inicializa el objeto
- self: Referencia al objeto actual (no se pasa al llamar)
- Atributos: Variables que almacenan datos del objeto
- Métodos: Funciones que definen comportamientos del objeto
Git - Ramas
| Comando | Función |
|---|---|
git branch |
Ver ramas existentes |
git checkout -b nombre |
Crear y cambiar a rama |
git checkout main |
Cambiar a main |
git merge rama |
Fusionar rama a actual |
git branch -d rama |
Eliminar rama fusionada |