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

  1. De diccionarios a clases: por qué dar el salto
  2. Definir una clase con class y __init__
  3. self: el objeto habla de sí mismo
  4. Atributos de instancia vs atributos de clase
  5. Métodos: comportamiento que viaja con los datos
  6. La clase Libro completa de Papyrus
  7. Varios objetos del mismo molde
  8. Errores comunes y consejos
  9. 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:

catalogo = {
    "La Odisea": {"precio": 12.50, "stock": 4},
    "Hamlet": {"precio": 9.95, "stock": 6},
}

Y cómo se calcula un precio, desde papyrus_utils.py:

precio = precio_final(catalogo["La Odisea"]["precio"], socio=True)

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 = stock

Desglose 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)    # 4

Observa 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.stock significa "el stock de este libro".
  • Si odisea.stock es 4 y fausto.stock es 0, odisea.hay_stock() devuelve True y fausto.hay_stock() devuelve False — mismo código, datos distintos, porque self apunta a un objeto diferente en cada llamada.

self no 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.stockhamlet.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.35

El 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 > 0

Y 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():

print(type(odisea))            # <class '__main__.Libro'>
print(type(odisea) is Libro)   # True

Un detalle que quizá ya has notado si has probado print(odisea):

print(odisea)   # <__main__.Libro object at 0x0000021F3A2B4E50>

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 self en la definición del método. def precio_socio(): sin self provoca TypeError: precio_socio() takes 0 positional arguments but 1 was given al llamarlo, porque Python siempre pasa el objeto como primer argumento. Todo método de instancia empieza por self.
  • Olvidar self. al usar un atributo. Dentro de un método, precio a secas es una variable local inexistente (NameError); el atributo del objeto es self.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__ con self.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 self para 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)       # 0

Fí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)   # 4

El 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

Módulo 2: Estructuras de Control

Módulo 3: Funciones y Módulos

Módulo 4: Estructuras de Datos

Módulo 5: Programación Orientada a Objetos

Módulo 6: Manejo de Archivos

Módulo 7: Manejo de Errores y Excepciones

Módulo 8: Temas Avanzados

Módulo 9: Pruebas y Depuración

Módulo 10: Desarrollo Web con Python

Módulo 11: Ciencia de Datos con Python

Módulo 12: Proyecto Final

© Copyright 2026. Todos los derechos reservados