El módulo 4 terminó con un diagnóstico claro: en Papyrus, los datos viven en un diccionario (catalogo) y el comportamiento vive en otro sitio (papyrus_utils.py). precio_final() confía en que le pases un número correcto; buscar_libro() confía en que el diccionario tenga la forma esperada. Nada une ambas mitades. La programación orientada a objetos (POO) resuelve exactamente eso: una clase es un molde que empaqueta datos y comportamiento en una sola pieza con nombre propio, y cada objeto creado a partir de ese molde lleva sus datos consigo y sabe operar sobre ellos. En esta lección crearás la clase Libro de Papyrus: cada libro dejará de ser una entrada de diccionario para convertirse en un objeto que calcula su propio precio de socio.
Contenido
- De diccionarios a clases: por qué dar el salto
- Definir una clase con
classy__init__ self: el objeto habla de sí mismo- Atributos de instancia vs atributos de clase
- Métodos: comportamiento que viaja con los datos
- La clase
Librocompleta de Papyrus - Varios objetos del mismo molde
- Errores comunes y consejos
- Ejercicios con soluciones
De diccionarios a clases: por qué dar el salto
Recuerda cómo quedó el catálogo tras el gran refactor de 04-03:
Y cómo se calcula un precio, desde papyrus_utils.py:
Funciona, pero fíjate en las costuras:
- Nada garantiza la estructura: si alguien escribe
{"precio": "12,50"}, ninguna parte del programa protesta hasta que explota en otro lugar. - El conocimiento está disperso: para saber qué se puede hacer con un libro tienes que rebuscar en
papyrus_utils.py. - La función y el dato no se conocen:
precio_final()acepta cualquier número; no sabe que trabaja con libros.
Una clase invierte la relación: en lugar de funciones que reciben datos, tienes datos que llevan sus funciones incorporadas.
| Enfoque | Datos | Comportamiento | ¿Quién garantiza la coherencia? |
|---|---|---|---|
| Dict + funciones (módulo 4) | catalogo["Hamlet"] |
papyrus_utils.precio_final(...) |
Nadie: disciplina del programador |
| Clase (este módulo) | hamlet.precio |
hamlet.precio_socio() |
La propia clase |
Definir una clase con class y __init__
La sintaxis mínima:
class Libro:
"""Un libro del catálogo de Papyrus."""
def __init__(self, titulo, precio, stock):
self.titulo = titulo
self.precio = precio
self.stock = stockDesglose línea a línea:
class Libro:— declara la clase. Por convención PEP 8, los nombres de clase van en CapWords (Libro,LibroDigital), a diferencia de funciones y variables (precio_final,stock_total).- El docstring funciona igual que en las funciones de 03-01: documenta para qué sirve la clase.
__init__es el inicializador: un método especial que Python ejecuta automáticamente cada vez que creas un objeto. Recibe los datos iniciales y los guarda.self.titulo = titulo— crea un atributo de instancia: una variable que pertenece a ese objeto concreto, no a la clase en general.
Para crear un objeto (una instancia), llamas a la clase como si fuera una función:
odisea = Libro("La Odisea", 12.50, 4)
hamlet = Libro("Hamlet", 9.95, 6)
print(odisea.titulo) # La Odisea
print(hamlet.precio) # 9.95
print(odisea.stock) # 4Observa que no pasas self al llamar: escribes Libro("La Odisea", 12.50, 4) con tres argumentos, y Python coloca el objeto recién creado en self por ti.
self: el objeto habla de sí mismo
self es el parámetro que recibe el propio objeto sobre el que se está trabajando. Cuando Ana escribe odisea.precio_socio(), Python lo traduce internamente a Libro.precio_socio(odisea): el objeto viaja como primer argumento.
class Libro:
def __init__(self, titulo, precio, stock):
self.titulo = titulo
self.precio = precio
self.stock = stock
def hay_stock(self):
return self.stock > 0- Dentro de
hay_stock,self.stocksignifica "el stock de este libro". - Si
odisea.stockes 4 yfausto.stockes 0,odisea.hay_stock()devuelveTrueyfausto.hay_stock()devuelveFalse— mismo código, datos distintos, porqueselfapunta a un objeto diferente en cada llamada.
selfno es una palabra reservada: es una convención universal. Podrías llamarlo de otra forma, pero ningún programador Python lo hace. Respétala siempre.
Atributos de instancia vs atributos de clase
El IVA de los libros y el descuento de socio son iguales para todos los libros de Papyrus. No tiene sentido guardarlos en cada objeto: son atributos de clase, compartidos por todas las instancias.
class Libro:
IVA_LIBROS = 0.04 # atributo de clase: compartido
DESCUENTO_SOCIO = 0.05 # atributo de clase: compartido
def __init__(self, titulo, precio, stock):
self.titulo = titulo # atributos de instancia: propios de cada objeto
self.precio = precio
self.stock = stock| Característica | Atributo de instancia | Atributo de clase |
|---|---|---|
| Se define en | __init__, con self. |
Cuerpo de la clase, sin self |
| Pertenece a | Cada objeto por separado | La clase (todos lo comparten) |
| Ejemplo en Papyrus | titulo, precio, stock |
IVA_LIBROS, DESCUENTO_SOCIO |
| Cambia entre objetos | Sí (odisea.stock ≠ hamlet.stock) |
No (es el mismo para todos) |
| Acceso | odisea.precio |
Libro.IVA_LIBROS o odisea.IVA_LIBROS |
print(Libro.IVA_LIBROS) # 0.04 — desde la clase
print(odisea.IVA_LIBROS) # 0.04 — desde la instancia (lo hereda de la clase)Cuando accedes a odisea.IVA_LIBROS, Python busca primero en el objeto; si no lo encuentra, sube a la clase. Por eso funciona desde ambos sitios. Fíjate en que las constantes que en el módulo 1 vivían sueltas (NOMBRE_TIENDA, IVA_LIBROS) ahora encuentran un hogar natural dentro de la clase a la que pertenecen conceptualmente.
Métodos: comportamiento que viaja con los datos
Un método es una función definida dentro de una clase. La fórmula de precio de Papyrus (base × (1−descuento) × (1+IVA), redondeada a 2 decimales) deja de vivir en papyrus_utils.py y pasa a ser conocimiento del propio libro:
def precio_socio(self):
"""Precio final para socios: descuento del 5 % y IVA del 4 %."""
bruto = self.precio * (1 - Libro.DESCUENTO_SOCIO) * (1 + Libro.IVA_LIBROS)
return round(bruto, 2)Compara las dos llamadas:
# Módulo 4: función externa que recibe el dato
precio_final(catalogo["La Odisea"]["precio"], socio=True) # 12.35
# Módulo 5: el objeto se lo calcula solo
odisea.precio_socio() # 12.35El resultado es idéntico (12.35 €), pero el segundo estilo no puede equivocarse de dato: el método siempre opera sobre el precio de su libro.
La clase Libro completa de Papyrus
class Libro:
"""Un libro del catálogo de Papyrus."""
IVA_LIBROS = 0.04
DESCUENTO_SOCIO = 0.05
def __init__(self, titulo, precio, stock):
self.titulo = titulo
self.precio = precio
self.stock = stock
def precio_socio(self):
"""Precio final para socios, redondeado a 2 decimales."""
bruto = self.precio * (1 - Libro.DESCUENTO_SOCIO) * (1 + Libro.IVA_LIBROS)
return round(bruto, 2)
def hay_stock(self):
"""True si queda al menos un ejemplar."""
return self.stock > 0Y en acción, con los cuatro títulos canónicos:
odisea = Libro("La Odisea", 12.50, 4)
hamlet = Libro("Hamlet", 9.95, 6)
quijote = Libro("El Quijote", 15.90, 8)
fausto = Libro("Fausto", 21.00, 0)
for libro in (odisea, hamlet, quijote, fausto):
disponible = "disponible" if libro.hay_stock() else "AGOTADO"
print(f"{libro.titulo:<12} socio: {libro.precio_socio():>6.2f} € [{disponible}]")La Odisea socio: 12.35 € [disponible] Hamlet socio: 9.83 € [disponible] El Quijote socio: 15.71 € [disponible] Fausto socio: 20.75 € [AGOTADO]
Los precios coinciden exactamente con los que Luis (socio LUIS-001) paga desde el módulo 4: la lógica es la misma, solo ha cambiado de casa.
Varios objetos del mismo molde
La distinción clave del día: la clase es el molde (se escribe una vez); cada instancia es un objeto independiente con sus propios valores.
classDiagram
class Libro {
+IVA_LIBROS = 0.04
+DESCUENTO_SOCIO = 0.05
+titulo
+precio
+stock
+precio_socio()
+hay_stock()
}
Libro <.. odisea : instancia
Libro <.. hamlet : instancia
Libro <.. fausto : instancia
class odisea {
titulo = "La Odisea"
precio = 12.50
stock = 4
}
class hamlet {
titulo = "Hamlet"
precio = 9.95
stock = 6
}
class fausto {
titulo = "Fausto"
precio = 21.00
stock = 0
}
Cada objeto tiene identidad propia: modificar fausto.stock no afecta a odisea.stock (nada de las trampas de alias de 04-01, porque son objetos distintos desde su creación). Puedes comprobar el tipo de cualquier objeto con type():
Un detalle que quizá ya has notado si has probado print(odisea):
Sale "feo": Python no sabe cómo mostrar un Libro en texto legible y enseña su dirección de memoria. Hay una solución elegante, pero pertenece a los métodos mágicos de la lección 05-05. Hasta entonces, imprime atributos concretos (libro.titulo) cuando necesites salida legible.
Errores Comunes y Consejos
- Olvidar
selfen la definición del método.def precio_socio():sinselfprovocaTypeError: precio_socio() takes 0 positional arguments but 1 was givenal llamarlo, porque Python siempre pasa el objeto como primer argumento. Todo método de instancia empieza porself. - Olvidar
self.al usar un atributo. Dentro de un método,precioa secas es una variable local inexistente (NameError); el atributo del objeto esself.precio. Revisa el ámbito de variables de 03-01: las reglas no han cambiado. - Escribir
Libro()sin argumentos cuando__init__los exige:TypeError: __init__() missing 3 required positional arguments. Los parámetros de__init__funcionan exactamente como los de cualquier función (03-02), incluidos los valores por defecto:def __init__(self, titulo, precio, stock=0)permitiría crear libros sin stock inicial. - Confundir clase con instancia.
Libro.precio_socio()falla porque no hay ningún objeto sobre el que operar;odisea.precio_socio()funciona. La clase es el plano; la instancia, el edificio. - Usar un atributo de clase mutable como valor por instancia.
reservas = []en el cuerpo de la clase sería compartido por todos los libros — la misma trampa del argumento mutable por defecto de 03-02. Las listas y diccionarios por objeto se crean siempre en__init__conself.reservas = []. - Consejo: empieza toda clase respondiendo dos preguntas: ¿qué datos describe? (atributos) y ¿qué sabe hacer con ellos? (métodos). Si un método no usa
selfpara nada, probablemente deba ser una función normal fuera de la clase.
Ejercicios
Ejercicio 1: la clase Socio
Papyrus gestiona socios como Luis (LUIS-001) o Marta (MARTA-002). Crea una clase Socio con atributos de instancia nombre y codigo, y un método saludo() que devuelva "Hola, Luis (socio LUIS-001)". Crea los socios Luis, Marta (MARTA-002) y Pau (PAU-003) e imprime los tres saludos con un bucle.
Ejercicio 2: ampliar Libro con ventas
Añade a la clase Libro un método vender(unidades) que reste unidades al stock solo si hay suficiente, devolviendo True si la venta se realizó y False si no. Prueba: Júlia quiere 2 ejemplares de "La Odisea" (stock 4) y Omar quiere 1 "Fausto" (stock 0).
Ejercicio 3: contador de instancias con atributo de clase
Añade a Libro un atributo de clase total_libros = 0 que se incremente en 1 dentro de __init__ cada vez que se crea un libro (pista: Libro.total_libros += 1). Crea los cuatro libros del catálogo y comprueba que Libro.total_libros vale 4.
Soluciones
Solución 1:
class Socio:
"""Un socio del club de lectores de Papyrus."""
def __init__(self, nombre, codigo):
self.nombre = nombre
self.codigo = codigo
def saludo(self):
return f"Hola, {self.nombre} (socio {self.codigo})"
socios = [Socio("Luis", "LUIS-001"), Socio("Marta", "MARTA-002"), Socio("Pau", "PAU-003")]
for socio in socios:
print(socio.saludo())Cada objeto guarda su propio nombre y codigo; el método saludo() es el mismo para los tres, pero self hace que cada uno hable de sí mismo.
Solución 2:
class Libro:
IVA_LIBROS = 0.04
DESCUENTO_SOCIO = 0.05
def __init__(self, titulo, precio, stock):
self.titulo = titulo
self.precio = precio
self.stock = stock
def hay_stock(self):
return self.stock > 0
def vender(self, unidades):
"""Descuenta stock si hay suficiente; devuelve True/False."""
if unidades <= self.stock:
self.stock -= unidades
return True
return False
odisea = Libro("La Odisea", 12.50, 4)
fausto = Libro("Fausto", 21.00, 0)
print(odisea.vender(2)) # True → Júlia se lleva 2
print(odisea.stock) # 2
print(fausto.vender(1)) # False → Omar se queda sin Fausto (a la cola de reservas de 04-06)
print(fausto.stock) # 0Fíjate en que la comprobación protege el dato: el stock nunca puede quedar negativo si todo el mundo usa vender(). Que nadie pueda saltárselo es justo el tema de la encapsulación (05-04).
Solución 3:
class Libro:
total_libros = 0 # atributo de clase: contador compartido
def __init__(self, titulo, precio, stock):
self.titulo = titulo
self.precio = precio
self.stock = stock
Libro.total_libros += 1 # se toca a través de la CLASE, no de self
odisea = Libro("La Odisea", 12.50, 4)
hamlet = Libro("Hamlet", 9.95, 6)
quijote = Libro("El Quijote", 15.90, 8)
fausto = Libro("Fausto", 21.00, 0)
print(Libro.total_libros) # 4El contador vive en la clase, por eso todas las instancias lo comparten y cada creación lo incrementa. Si escribieras self.total_libros += 1, crearías un atributo de instancia que taparía al de clase — un error clásico.
Conclusión
Hoy has unido lo que el módulo 4 dejó separado: la clase Libro empaqueta los datos (titulo, precio, stock) con su comportamiento (precio_socio(), hay_stock()) en una sola pieza. Has aprendido que class define el molde, __init__ inicializa cada objeto, self permite que cada instancia hable de sí misma, y que los atributos de instancia (distintos por objeto) conviven con los de clase (IVA_LIBROS, DESCUENTO_SOCIO, compartidos por todos). Cada libro de Papyrus calcula ahora su propio precio de socio: 12.35 € para "La Odisea", 20.75 € para el agotado "Fausto". Pero la tienda de Ana no vende solo libros: también hay revistas y, pronto, libros digitales sin stock físico. ¿Habrá que copiar y pegar la clase Libro tres veces? En absoluto: la siguiente lección presenta la herencia, el mecanismo con el que una clase Producto compartirá lo común y cada tipo de artículo añadirá solo lo que le hace diferente.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
