Clases, objetos, __init__ y self
Una clase es una plantilla (qué atributos y métodos tienen sus instancias). Un objeto (o instancia) es una materialización de esa plantilla. Solo tienes que escribir la clase una vez y luego crear tantos objetos como quieras.
class Perro:
def __init__(self, nombre, edad):
self.nombre = nombre # atributo de instancia
self.edad = edad
def ladrar(self):
return f"¡Guau! soy {self.nombre}"
rex = Perro("Rex", 3)
luna = Perro("Luna", 5)
print(rex.ladrar()) # ¡Guau! soy Rex
print(luna.edad) # 5
1.1 · self: la referencia al propio objeto
self es el primer parámetro de todos los métodos y apunta a la instancia concreta.
Por eso rex.ladrar() sabe que self es rex y no luna. No lo
pasas cuando llamas: Python lo rellena solo.
self SIEMPRE va primero, aunque
no uses nada del objeto. Se llama self por convención universal en Python.1.2 · Atributos de clase vs de instancia
Los atributos de instancia van con self. dentro de métodos. Los de clase se
declaran a nivel de clase y son compartidos por todas las instancias:
class Contador:
total = 0 # atributo de clase (compartido)
def __init__(self, nombre):
self.nombre = nombre # atributo de instancia
Contador.total += 1
a = Contador("a")
b = Contador("b")
print(Contador.total) # 2 (compartido)
Encapsulación y propiedades
La encapsulación protege el estado interno de un objeto: que no se pueda manipular el
atributo de cualquier manera. En Python los atributos «privados» se nombran con _ y se accede a
ellos mediante propiedades (@property).
class Cuenta:
def __init__(self, saldo=0):
self._saldo = saldo
@property
def saldo(self):
return self._saldo
@saldo.setter
def saldo(self, valor):
if valor < 0:
raise ValueError("El saldo no puede ser negativo")
self._saldo = valor
c = Cuenta()
c.saldo = 100
print(c.saldo) # 100
_saldo con guion bajo es una convención: indica «privado, no
tocar desde fuera». El doble guion bajo (__saldo) activa name mangling, pero se usa poco.
Lo importante es la API: propiedades para leer/escribir con reglas.2.1 · Métodos estáticos y de clase
@staticmethod no recibe ni self ni la clase: es una utilidad sin estado.
@classmethod recibe la clase (cls) y se usa para constructores alternativos:
class Util:
@staticmethod
def es_par(n):
return n % 2 == 0
@classmethod
def desde_texto(cls, texto):
return cls(texto.strip())
Herencia y super()
La herencia crea clases hijas que reutilizan (y amplían o cambian) el comportamiento de la
clase padre. super() invoca el método de la clase base, por ejemplo para reutilizar su
__init__.
class Vehiculo:
def __init__(self, marca, ruedas=4):
self.marca = marca
self.ruedas = ruedas
def describir(self):
return f"{self.marca} con {self.ruedas} ruedas"
class Moto(Vehiculo):
def __init__(self, marca):
super().__init__(marca, ruedas=2)
m = Moto("Yamaha")
print(m.describir()) # Yamaha con 2 ruedas
print(isinstance(m, Vehiculo)) # True
3.1 · Sobreescritura
Una hija puede sobreescribir un método del padre (mismo nombre). También puede extenderlo
llamando a super().método() y añadiendo comportamiento:
class Perro:
def sonido(self):
return "Guau"
class Cachorro(Perro):
def sonido(self):
# extiende: usa lo del padre + más
return super().sonido() + " en miniatura"
Moto es un Vehiculo). Si solo quieres compartir código, prefiere composición (un objeto
contiene a otro) antes que forzar jerarquías.Polimorfismo, métodos mágicos y dataclasses
4.1 · Polimorfismo: misma firma, distinto comportamiento
Si varias clases comparten un método con el mismo nombre, un bucle puede llamarlo sin saber de qué clase es cada objeto. Eso es polimorfismo:
class Gato:
def sonido(self):
return "Miau"
class Perro:
def sonido(self):
return "Guau"
animales = [Gato(), Perro(), Gato()]
for a in animales:
print(a.sonido()) # Miau, Guau, Miau
4.2 · Métodos mágicos
Los métodos con doble guion bajo (__str__, __repr__, __eq__,
__len__…) definen cómo se comporta el objeto con la sintaxis nativa de Python. El más útil para
empezar es __str__:
class Punto:
def __init__(self, x, y):
self.x, self.y = x, y
def __str__(self):
return f"Punto({self.x}, {self.y})"
def __eq__(self, otro):
return self.x == otro.x and self.y == otro.y
print(Punto(1, 2)) # Punto(1, 2)
print(Punto(1, 2) == Punto(1, 2)) # True
4.3 · dataclasses: clases sin boilerplate
El decorador @dataclass genera automáticamente __init__, __repr__ y
__eq__. Ideal para clases «contenedoras de datos»:
from dataclasses import dataclass
@dataclass
class Libro:
titulo: str
autor: str
paginas: int = 0
libro = Libro("Dune", "Frank Herbert", 680)
print(libro) # Libro(titulo='Dune', autor='Frank Herbert', paginas=680)
print(libro.titulo) # Dune
__init__/__eq__/__repr__, una dataclass te las regala.
Úsala para modelos de datos; sigue escribiendo clases normales cuando haya lógica propia.Excepciones propias y diseño de clases
5.1 · Levantar y capturar excepciones propias
Puedes lanzarte errores con raise y crear tus propios tipos de excepción
heredando de Exception. Así el código que usa tu clase puede reaccionar a problemas
específicos:
class SaldoInsuficiente(Exception):
pass
class Cuenta:
def __init__(self, saldo=0):
self._saldo = saldo
def retirar(self, monto):
if monto > self._saldo:
raise SaldoInsuficiente("No hay saldo suficiente")
self._saldo -= monto
c = Cuenta(20)
try:
c.retirar(50)
except SaldoInsuficiente as error:
print(error)
5.2 · Diseño limpio de clases
Libro no debe guardar usuarios._).borrar(), no hacer1().✍️ Los 15 ejercicios del bloque
Crea la carpeta python-b4/ y un archivo .py por ejercicio. Recuerda: cada ejercicio
debe comprobarse ejecutándolo con varios casos (print()).
Sección A · Clases básicas
1 Clase Coche
Crea la clase Coche con __init__(marca, modelo, combustible="gasolina") y un
método describir() que devuelva algo como "Seat Ibiza gasolina". Crea 2 coches y
muéstralos.
Ver solución
class Coche:
def __init__(self, marca, modelo, combustible="gasolina"):
self.marca = marca
self.modelo = modelo
self.combustible = combustible
def describir(self):
return f"{self.marca} {self.modelo} {self.combustible}"
c1 = Coche("Seat", "Ibiza")
c2 = Coche("Tesla", "Model 3", "eléctrico")
print(c1.describir())
print(c2.describir())
2 Contador con clase
La clase Contador tendrá __init__ que inicializa self.valor = 0, y
métodos sumar(n) y ver(). Crea un contador, suma varias veces y muestra el valor.
Ver solución
class Contador:
def __init__(self):
self.valor = 0
def sumar(self, n):
self.valor += n
def ver(self):
return self.valor
c = Contador()
c.sumar(3)
c.sumar(5)
print(c.ver()) # 8
3 Frigorífico con stock
Clase Frigorífico con __init__ que acepte items (dict,
por defecto None → inicias a dict vacío), método meter(producto, cantidad) y
total_productos() que sume las cantidades.
Ver solución
class Frigorífico:
def __init__(self, items=None):
self.items = items if items is not None else {}
def meter(self, producto, cantidad):
self.items[producto] = self.items.get(producto, 0) + cantidad
def total_productos(self):
return sum(self.items.values())
f = Frigorífico()
f.meter("leche", 2)
f.meter("leche", 1)
print(f.total_productos()) # 3
4 Atributo de clase compartido
Crea Venta con atributo de clase total_ventas = 0; en
__init__(producto, importe) suma a Venta.total_ventas. Crea 3 ventas y muestra el
total compartido.
Ver solución
class Venta:
total_ventas = 0
def __init__(self, producto, importe):
self.producto = producto
self.importe = importe
Venta.total_ventas += 1
v1 = Venta("ratón", 12)
v2 = Venta("teclado", 45)
v3 = Venta("pad", 8)
print(Venta.total_ventas) # 3
Sección B · Encapsulación y propiedades
5 Saldo protegido
Clase Cuenta con self._saldo, propiedad saldo de solo lectura y
ingresar(monto). Crea una cuenta de 100, ingresa 50 y muestra el saldo.
Ver solución
class Cuenta:
def __init__(self, saldo=0):
self._saldo = saldo
@property
def saldo(self):
return self._saldo
def ingresar(self, monto):
self._saldo += monto
c = Cuenta(100)
c.ingresar(50)
print(c.saldo) # 150
6 Setter con validación
Amplía la de antes: propiedad saldo con @setter que lance
ValueError("El saldo no puede ser negativo") si se intenta asignar un valor negativo. Pruébalo
dentro de un try/except.
Ver solución
class Cuenta:
def __init__(self, saldo=0):
self.saldo = saldo
@property
def saldo(self):
return self._saldo
@saldo.setter
def saldo(self, valor):
if valor < 0:
raise ValueError("El saldo no puede ser negativo")
self._saldo = valor
c = Cuenta()
try:
c.saldo = -5
except ValueError as e:
print(e)
7 Métodos estáticos
Crea la clase Util con @staticmethod es_par(n) y
@staticmethod es_primo(n). Pueba ambos con varios números.
Ver solución
class Util:
@staticmethod
def es_par(n):
return n % 2 == 0
@staticmethod
def es_primo(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
print(Util.es_par(4), Util.es_par(5))
print(Util.es_primo(7), Util.es_primo(8))
8 Constructores con @classmethod
Clase Punto normal con x e y y un
@classmethod desde_origen() que devuelva cls(0, 0). Crea un punto en el origen.
Ver solución
class Punto:
def __init__(self, x, y):
self.x, self.y = x, y
@classmethod
def desde_origen(cls):
return cls(0, 0)
origen = Punto.desde_origen()
print(origen.x, origen.y) # 0 0
Sección C · Herencia, polimorfismo y mágicos
9 Vehículo y Moto
Clase Vehiculo con __init__(marca, ruedas) y describir(). Clase
Moto que hereda con ruedas fijas a 2 usando super(). Crea una moto y muéstrala.
Ver solución
class Vehiculo:
def __init__(self, marca, ruedas):
self.marca = marca
self.ruedas = ruedas
def describir(self):
return f"{self.marca} con {self.ruedas} ruedas"
class Moto(Vehiculo):
def __init__(self, marca):
super().__init__(marca, ruedas=2)
m = Moto("Yamaha")
print(m.describir())
10 Sobreescritura con super()
Clase Persona con __init__(nombre) y presentarse(). Clase
Estudiante que añade curso y reutiliza super().__init__(nombre),
sobreescribiendo presentarse() para incluir el curso.
Ver solución
class Persona:
def __init__(self, nombre):
self.nombre = nombre
def presentarse(self):
return f"Hola, soy {self.nombre}"
class Estudiante(Persona):
def __init__(self, nombre, curso):
super().__init__(nombre)
self.curso = curso
def presentarse(self):
return super().presentarse() + f" y estudio {self.curso}"
e = Estudiante("Ana", "Python")
print(e.presentarse())
11 Polimorfismo con animales
Crea Perro y Gato (sin herencia) con método sonido(). Pon varios en
una lista y recorre con un for imprimiendo cada sonido.
Ver solución
class Perro:
def sonido(self):
return "Guau"
class Gato:
def sonido(self):
return "Miau"
animales = [Perro(), Gato(), Perro()]
for a in animales:
print(a.sonido())
12 __str__ bonito
Clase Libro con titulo, autor y paginas, y
__str__ que devuelva "Dune · Frank Herbert (680 páginas)". Crea uno y haz
print(libro).
Ver solución
class Libro:
def __init__(self, titulo, autor, paginas):
self.titulo = titulo
self.autor = autor
self.paginas = paginas
def __str__(self):
return f"{self.titulo} · {self.autor} ({self.paginas} páginas)"
l = Libro("Dune", "Frank Herbert", 680)
print(l)
13 __eq__ de igualdad
Clase Punto con __eq__ que compare x e y. Crea pares
de puntos iguales y distintos y verifica con ==.
Ver solución
class Punto:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, otro):
return self.x == otro.x and self.y == otro.y
print(Punto(1, 2) == Punto(1, 2)) # True
print(Punto(1, 2) == Punto(3, 2)) # False
14 dataclass Producto
Con @dataclass define Producto(nombre: str, precio: float, stock: int = 0).
Crea dos productos, muestra uno y compara con ==.
Ver solución
from dataclasses import dataclass
@dataclass
class Producto:
nombre: str
precio: float
stock: int = 0
p1 = Producto("ratón", 12.5, 10)
p2 = Producto("ratón", 12.5, 10)
print(p1)
print(p1 == p2) # True (dataclass compara por valores)
15 🏆 Proyecto integrador: «biblioteca digital»
Crea biblioteca.py: un mini sistema con modelos y préstamos. Requisitos:
- Clase
Librocontitulo,autory__str__. - Clase
Bibliotecaque guarde_libros(lista) y_prestados(set de títulos). - Métodos
agregar(libro: Libro),prestar(titulo)(conraise ValueErrorsi no existe o ya está prestado) ydevolver(titulo). - Herencia: clase
LibroDigital(Libro)con atributoformato="epub"por defecto. - Polimorfismo: método
mostrar()en las dos clases y un bucle que lo llame a todas. - Excepción propia
LibroNoDisponible(Exception). - Un
try/exceptde demostración capturandoLibroNoDisponible.
Ver una solución posible
class LibroNoDisponible(Exception):
pass
class Libro:
def __init__(self, titulo, autor):
self.titulo = titulo
self.autor = autor
def mostrar(self):
return f"{self.titulo} · {self.autor}"
class LibroDigital(Libro):
def __init__(self, titulo, autor, formato="epub"):
super().__init__(titulo, autor)
self.formato = formato
def mostrar(self):
return super().mostrar() + f" [{self.formato}]"
class Biblioteca:
def __init__(self):
self._libros = []
self._prestados = set()
def agregar(self, libro):
self._libros.append(libro)
def prestar(self, titulo):
disponible = any(l.titulo == titulo for l in self._libros)
if not disponible or titulo in self._prestados:
raise LibroNoDisponible(f"No se puede prestar: {titulo}")
self._prestados.add(titulo)
def devolver(self, titulo):
self._prestados.discard(titulo)
libro1 = Libro("Dune", "Frank Herbert")
libro2 = LibroDigital("El Quijote", "Cervantes")
biblio = Biblioteca()
biblio.agregar(libro1)
biblio.agregar(libro2)
for l in biblio._libros:
print(l.mostrar())
biblio.prestar("Dune")
try:
biblio.prestar("Dune")
except LibroNoDisponible as e:
print(e)
📝 Examen del Bloque 4
Dos partes: un test teórico autoevaluable (mínimo 70 %) y un reto práctico de autocorrección con laboratorio.
Parte B · Reto «biblioteca en miniatura»
Crea en biblioteca_mini.py: una clase Libro, una clase hija
LibroDigital, una clase Estantería que los contenga, un método estático o de clase
útil y una excepción propia. Valida tú mismo/a estos criterios. Mínimo para aprobar: 6 de 8
casillas.
Cumplidos: 0 / 8