Cerrábamos el módulo 6 con una incomodidad: TareaFácil ya sabe buscar, ordenar, recorrer árboles y medir su propio coste, pero todo eso se apoya en una lista de diccionarios que nadie vigila. Nada impide que a un diccionario le falte la clave completada, que otro tenga prioridad escrita como "Alta" o que un tercero arrastre un campo inventado; y las funciones que trabajan con tareas —mostrar_ficha, dias_totales, clave_orden— están sueltas por el fichero, lejos de los datos que manipulan. En esta lección empezamos a arreglar las dos cosas a la vez.

La herramienta se llama programación orientada a objetos, y ya la nombramos de pasada en Lenguajes de programación como uno de los grandes paradigmas. Aquí la vas a usar por primera vez con un objetivo muy concreto: dejar de tener datos por un lado y funciones por otro para pasar a tener cosas —tareas, agendas— que llevan dentro tanto lo que son como lo que saben hacer. Empezaremos por lo esencial: qué es una clase, qué es una instancia y en qué se diferencian de un diccionario.

Contenido

  1. La agenda frágil: una demostración incómoda
  2. Qué es un objeto: datos y comportamiento juntos
  3. Clase e instancia: el molde y los ejemplares
  4. La sintaxis mínima: class y creación de instancias
  5. Atributos asignados desde fuera (y por qué todavía no basta)
  6. type(), isinstance() y «en Python todo es un objeto»
  7. Atributos de clase frente a atributos de instancia
  8. El espacio de nombres del objeto: __dict__, getattr, setattr y hasattr
  9. Diccionario u objeto: cuándo compensa cada uno
  10. Los cuatro pilares de la POO, en una tabla
  11. Errores comunes y consejos
  12. Ejercicios
  13. Conclusión

  1. La agenda frágil: una demostración incómoda

Antes de presentar la solución, veamos el problema ejecutándose. Esta es una agenda como las que usamos desde 05-04, pero con tres tareas que han llegado de sitios distintos: una la escribió un programador a mano, otra vino de un JSON antiguo y otra la creó un compañero que no conocía el formato.

agenda = [
    {"titulo": "Cartel feria del libro", "responsable": "Luis",
     "prioridad": "alta", "dias": 3, "completada": False},
    {"titulo": "Logotipo Panaderia Sole", "responsable": "Nuria",
     "prioridad": "Alta", "dias": 5, "completada": False},   # <-- 'Alta' con mayuscula
    {"titulo": "Presupuesto cliente Vidal", "responsable": "Marta",
     "prioridad": "media", "dias": 2, "urgente": True},      # <-- falta 'completada'
]                                                            #     y sobra 'urgente'

def mostrar_listado(agenda):
    for numero, tarea in enumerate(agenda, start=1):
        marca = "[X]" if tarea["completada"] else "[ ]"
        print(f"{numero:>2}. {marca} {tarea['titulo']:<26}{tarea['prioridad']}")

mostrar_listado(agenda)

#  1. [ ] Cartel feria del libro    alta
#  2. [ ] Logotipo Panaderia Sole   Alta
# Traceback (most recent call last):  ...  KeyError: 'completada'

Fíjate en la forma exacta del desastre, porque es lo que justifica todo el módulo:

  • El programa imprimió medio listado antes de reventar, y el error aparece lejos de su causa: el KeyError salta en mostrar_listado, pero el fallo se cometió mucho antes, cuando alguien creó ese diccionario sin completada. En un programa grande pueden separarlos cientos de líneas y varios días.
  • El segundo problema ni siquiera dio error. "Alta" se imprimió tan tranquila. El estropicio llegará más tarde, cuando clave_orden haga ORDEN_PRIORIDAD["Alta"], o cuando un filtro tarea["prioridad"] == "alta" deje esa tarea fuera sin avisar de nada. Este es el peor caso: un resultado incorrecto que parece correcto.
  • El campo urgente es basura silenciosa. Nadie lo lee ni lo actualiza, pero está ahí, se guarda en el JSON y confunde a quien abra el fichero dentro de seis meses.

La raíz común es que en ningún sitio del programa está escrito qué es una tarea. El formato solo existe en nuestra cabeza y en la costumbre. Un diccionario acepta cualquier clave y cualquier valor: esa flexibilidad, que en 05-03 era una virtud, aquí es exactamente el problema.

  1. Qué es un objeto: datos y comportamiento juntos

Un objeto es un elemento del programa que reúne dos cosas que hasta ahora llevábamos separadas:

Parte Nombre técnico En TareaFácil
Lo que el objeto es (sus datos) Atributos título, responsable, prioridad, días, completada
Lo que el objeto sabe hacer Métodos mostrarse, marcarse como completada, decir si es urgente

Hasta ahora nuestro programa tenía los datos en diccionarios y el comportamiento en funciones sueltas repartidas por tareafacil.py. Nada conectaba mostrar_ficha con el diccionario que sabe pintar, salvo la esperanza de que quien la llame le pase el tipo de dato adecuado.

Lo curioso es que ya has usado objetos durante todo el curso sin llamarlos así. Cuando escribes titulo.upper() o titulo.count("e"), titulo no es «solo» un texto: es un objeto que contiene unos caracteres y además sabe ponerse en mayúsculas, contar letras o partirse en trozos. Ese .upper() es un método: una función que vive dentro del objeto y actúa sobre sus propios datos. Por eso no hace falta escribir upper(titulo): el objeto ya sabe sobre qué texto trabajar. Lo mismo con las listas (agenda.append(...)) y los diccionarios (tarea.get("dias")). La notación del punto que llevas usando desde el módulo 2 es, literalmente, la notación de la orientación a objetos: objeto.loQueSabeHacer().

Lo que aprenderás en este módulo es a crear tus propios tipos de objeto, para que una tarea de Estudio Alba tenga tanto sentido para Python como lo tiene un texto o una lista.

  1. Clase e instancia: el molde y los ejemplares

Aquí aparecen las dos palabras centrales del módulo, y conviene no confundirlas nunca. Una clase es la definición: describe qué datos tiene una tarea y qué sabe hacer, se escribe una sola vez y no es una tarea concreta, sino la idea de tarea. Una instancia (u objeto) es cada ejemplar creado a partir de esa clase: se crean tantas como haga falta y cada una tiene sus propios valores.

La analogía que mejor funciona es la del formulario en papel. La clase es la plantilla impresa: define las casillas «título», «responsable», «prioridad», «días» y «completada». La instancia es cada formulario relleno: uno dice «Cartel feria del libro / Luis / alta / 3 / no», otro dice «Logotipo Panaderia Solé / Nuria / alta / 5 / no». Hay una plantilla y muchos formularios, y cambiar la plantilla cambia todos los formularios futuros.

flowchart TD
    T["CLASE Tarea (el molde)<br/>titulo, responsable, prioridad, dias, completada"]
    T -->|instancia de| C["cartel<br/>Cartel feria del libro / Luis / alta / 3"]
    T -->|instancia de| L["logotipo<br/>Logotipo Sole / Nuria / alta / 5"]
    T -->|instancia de| P["presupuesto<br/>Presupuesto Vidal / Marta / media / 2"]

Dos consecuencias prácticas conviene fijarlas desde el principio: la clase se escribe una vez y sirve para un millón de tareas (si mañana toda tarea necesita un campo cliente, se toca la clase, no las mil llamadas repartidas por el programa), y las instancias son independientes entre sí (marcar una tarea como completada no toca a las demás, igual que rellenar un formulario no rellena los del resto de la carpeta).

  1. La sintaxis mínima: class y creación de instancias

La forma más simple de una clase en Python, y la creación de dos instancias a partir de ella, son estas:

class Tarea:
    """Una tarea del estudio de diseno Estudio Alba."""
    pass

cartel = Tarea()
logotipo = Tarea()
print(cartel)              # <__main__.Tarea object at 0x7f8b1c0d5e50>
print(cartel is logotipo)  # False: son dos objetos distintos

Tres detalles de sintaxis, que son los que fallan al principio:

  • La palabra reservada es class, seguida del nombre y de dos puntos, y el cuerpo va indentado, igual que en def o en un if.
  • El nombre de las clases se escribe por convenio en CamelCase: Tarea, TareaRecurrente, Agenda. Las funciones y variables siguen en snake_case (registrar_tarea). Verlo así te dice de un vistazo si un nombre es una clase.
  • pass es el relleno para un cuerpo vacío; el docstring de la primera línea cumple aquí la misma función que en las funciones.

Escribir Tarea() —con paréntesis, como una llamada a función— fabrica un objeto nuevo de esa clase y devuelve una referencia a él. A esa operación se la llama instanciar. Cada llamada crea un objeto distinto en memoria, y por eso cartel is logotipo es False, aunque por ahora ambos estén igual de vacíos: es el mismo comportamiento de las listas que viste en 05-01, donde [] is [] también era False.

Ese <__main__.Tarea object at 0x...> que imprime print es la representación por defecto: dice de qué clase es y en qué dirección de memoria vive. Es fea a propósito; en Atributos, métodos y constructor aprenderás a sustituirla por algo legible con __str__.

  1. Atributos asignados desde fuera (y por qué todavía no basta)

A una instancia se le pueden añadir atributos con la notación del punto, sin declararlos previamente:

cartel = Tarea()
cartel.titulo = "Cartel feria del libro"
cartel.responsable = "Luis"
cartel.prioridad = "alta"
cartel.dias = 3
cartel.completada = False
print(f"{cartel.titulo} ({cartel.dias}d)")   # Cartel feria del libro (3d)

Compara la lectura con la del diccionario equivalente:

Operación Diccionario Objeto
Leer un campo tarea["titulo"] tarea.titulo
Escribir un campo tarea["dias"] = 4 tarea.dias = 4
Campo inexistente KeyError AttributeError
Campos posibles cualquiera, sin control los que se definan (ver 07-02)

Ya se gana algo: cartel.titulo se lee mejor que cartel["titulo"], y el editor puede sugerirte los atributos disponibles mientras escribes. Pero seamos honestos: esto todavía no resuelve el problema de la sección 1. Nada nos obliga a rellenar los cinco campos:

logotipo = Tarea()
logotipo.titulo = "Logotipo Panaderia Sole"
logotipo.prioridad = "Alta"        # otra vez con mayuscula, nadie protesta
print(logotipo.completada)         # ... y se nos habia olvidado ese campo:
# AttributeError: 'Tarea' object has no attribute 'completada'

Hemos cambiado un KeyError por un AttributeError, y poco más: el fallo sigue apareciendo tarde y sigue sin haber un sitio que garantice que toda tarea nace completa y con valores válidos. Ese sitio existe y se llama constructor: un bloque que Python ejecuta automáticamente al crear cada instancia, que exige los datos imprescindibles y puede validarlos antes de guardarlos. Es el __init__ que estudiaremos en 07-02, y es el que convierte la clase de un simple contenedor en una garantía. Quédate con la idea: definir la clase es el primer paso; hacer que sea imposible crear una tarea inválida es el segundo.

  1. type(), isinstance() y «en Python todo es un objeto»

type(), que usamos en 02-01 para descubrir tipos de datos, también funciona con nuestras clases, y para preguntar si un objeto es de cierta clase se usa isinstance():

cartel = Tarea()
print(type(cartel))                   # <class '__main__.Tarea'>
print(type(cartel).__name__)          # Tarea
print(isinstance(cartel, Tarea))      # True
print(isinstance(cartel, dict))       # False
print(isinstance(3.5, (int, float)))  # True: admite una tupla de tipos

La forma correcta de comprobar un tipo es isinstance(x, Clase), no type(x) == Clase: además de leerse mejor, reconoce las clases derivadas, cosa que veremos al final de Colecciones de objetos.

Ahora la afirmación que da sentido a todo: en Python, absolutamente todo es un objeto. No es una frase de manual, se comprueba en cuatro líneas:

print(type(5))        # <class 'int'>    -> 5 es instancia de la clase int
print(type("hola"))   # <class 'str'>    -> "hola" es instancia de str
print(type([1, 2]))   # <class 'list'>   -> la lista, de list
print(type(Tarea))    # <class 'type'>   -> hasta las clases son objetos
Expresión Qué demuestra
"hola".upper() str es una clase y upper es uno de sus métodos
(255).bit_length() hasta un número entero tiene métodos propios
[3, 1].sort() sort es un método de la clase list

Es decir: no estás aprendiendo una técnica exótica añadida al lenguaje, estás aprendiendo cómo está construido Python por dentro, y lo único nuevo es que ahora los moldes los defines tú. Todo lo que sabes de str o de list —que se pasan por referencia, que tienen métodos, que type() los identifica— vale igual para Tarea.

  1. Atributos de clase frente a atributos de instancia

Hay dos sitios donde puede vivir un atributo, y la diferencia importa. Un atributo de instancia pertenece a un objeto concreto y cada instancia tiene el suyo: es el cartel.titulo de la sección 5. Un atributo de clase se define dentro del cuerpo de la clase y es compartido por todas las instancias: hay una sola copia, en la clase. El caso de uso clásico es un valor común a todas las tareas y un contador de cuántas se han creado:

class Tarea:
    """Una tarea del estudio Estudio Alba."""
    ESTUDIO = "Estudio Alba"    # atributo de clase: igual para todas
    creadas = 0                 # atributo de clase: contador compartido

cartel, logotipo = Tarea(), Tarea()
Tarea.creadas += 2             # normalmente se hara en el constructor (07-02)

print(Tarea.creadas)           # 2
print(cartel.creadas)          # 2  <- la instancia ve el atributo de la clase
print(logotipo.ESTUDIO)        # Estudio Alba

cartel.creadas = 99            # OJO: crea un atributo PROPIO de cartel
print(cartel.creadas)          # 99  <- el suyo
print(logotipo.creadas)        # 2   <- sigue viendo el de la clase
print(Tarea.creadas)           # 2   <- la clase no se ha tocado

Una instancia que no tiene un atributo propio lo busca en su clase: por eso cartel.creadas funciona aunque nunca se lo hayamos asignado a cartel. Es un orden de búsqueda parecido al LEGB del módulo 4: primero lo propio, después lo heredado del molde. Y ahí está la trampa: asignar a través de la instancia no modifica la clase, sino que crea un atributo de instancia que la tapa. Por eso el contador se incrementa siempre nombrando la clase, Tarea.creadas += 1.

El aviso importante: nunca pongas un objeto mutable como atributo de clase si esperas que cada instancia tenga el suyo. Es el aliasing de 05-01 en su versión más traicionera:

class Tarea:
    notas = []            # MAL: una unica lista para todas las tareas

cartel, logotipo = Tarea(), Tarea()
cartel.notas.append("Falta el logo del ayuntamiento")
print(logotipo.notas)     # ['Falta el logo del ayuntamiento'] <- la nota de otra tarea

Como cartel.notas no encuentra lista propia, sube a la clase y modifica la lista compartida, que es la misma que ve logotipo. La regla práctica es sencilla:

Tipo de dato ¿Vale como atributo de clase?
Constantes inmutables (str, int, tuple) Sí: ESTUDIO, PRIORIDADES = ("alta", "media", "baja")
Contadores que quieres compartir a propósito Sí, actualizándolos con Clase.nombre
Listas, diccionarios y conjuntos por instancia No: se crean en el constructor (07-02)

  1. El espacio de nombres del objeto: __dict__, getattr, setattr y hasattr

Por dentro, los atributos de instancia se guardan en... un diccionario. Cada objeto tiene el suyo, accesible como __dict__:

cartel = Tarea()
cartel.titulo = "Cartel feria del libro"
cartel.dias = 3
print(cartel.__dict__)      # {'titulo': 'Cartel feria del libro', 'dias': 3}
print(vars(cartel))         # lo mismo: vars() es la forma elegante de pedirlo

Esto explica de golpe varias cosas: por qué se pueden añadir atributos sobre la marcha, por qué acceder a un atributo cuesta O(1) como cualquier clave de diccionario (módulo 6), y por qué convertir un objeto a diccionario para guardarlo en JSON será tan directo en 07-03. Fíjate en que __dict__ solo contiene los atributos de instancia: ESTUDIO y creadas no aparecen porque viven en la clase.

Cuando el nombre del atributo está en una variable, existen tres funciones para trabajar con él:

print(hasattr(cartel, "dias"))                # True:  el objeto tiene ese atributo
print(hasattr(cartel, "completada"))          # False: no lo tiene
print(getattr(cartel, "completada", False))   # False: valor por defecto, no falla
setattr(cartel, "completada", True)           # equivale a cartel.completada = True
Función Equivale a Para qué sirve de verdad
getattr(obj, "x") obj.x Leer un atributo cuyo nombre está en una variable
getattr(obj, "x", defecto) Leer sin arriesgarse a AttributeError
setattr(obj, "x", v) obj.x = v Escribir un atributo con nombre calculado
hasattr(obj, "x") Comprobar antes de leer

En el día a día escribirás cartel.dias, no getattr(cartel, "dias"): estas funciones son para el caso en que el nombre no se conoce hasta que el programa se ejecuta. Un ejemplo con TareaFácil, aprovechando la constante CAMPOS que ya existe desde 05-05:

CAMPOS = ("titulo", "responsable", "prioridad", "dias", "completada")

def revisar(tarea):
    """Indica que campos obligatorios le faltan a una tarea."""
    return [campo for campo in CAMPOS if not hasattr(tarea, campo)]

print(revisar(cartel))     # ['responsable', 'prioridad']

Es un parche útil, pero sigue siendo una comprobación posterior: detecta el problema cuando la tarea inválida ya existe. La solución de verdad es impedir que nazca así, y para eso hace falta el constructor.

  1. Diccionario u objeto: cuándo compensa cada uno

Crear una clase no siempre es la respuesta correcta. Los diccionarios siguen siendo la herramienta adecuada en muchos casos, y esta es la comparación honesta:

Criterio Diccionario Objeto (clase propia)
Definición del formato No existe: cada uno pone lo que quiere Escrita una vez en la clase
Acceso t["titulo"] (fallo en ejecución) t.titulo (el editor autocompleta y avisa)
Garantía de campos Ninguna El constructor los exige (07-02)
Validación de valores Manual, en cada sitio Centralizada al crear (07-02)
Comportamiento asociado Funciones sueltas por el fichero Métodos junto a los datos
Claves dinámicas Sí, es su punto fuerte No: los atributos son fijos
Guardar en JSON Directo Hay que convertirlo antes (07-03)
Coste de escribirlo Cero líneas Unas cuantas líneas de clase

De ahí salen dos reglas prácticas. Usa un diccionario cuando los datos vengan de fuera con forma variable (un JSON descargado, una fila de CSV), cuando las claves no se conozcan de antemano (un índice responsable → tareas, un contador de palabras, la tabla de decisión de 05-04) o cuando sea una estructura temporal que vive tres líneas dentro de una función.

Usa una clase cuando el concepto se repita por todo el programa con la misma forma (una tarea, un cliente, una factura), cuando haya invariantes que cumplir (la prioridad solo puede ser alta, media o baja; los días, entre 1 y 365), cuando haya comportamiento que pertenezca a esos datos (saber si está retrasada, marcarse como completada, imprimirse) o cuando el concepto vaya a crecer: hoy son cinco campos, mañana serán ocho y tres reglas.

La tarea de TareaFácil cumple los cuatro criterios de la segunda lista, así que se lleva la clase. La agenda que llega del JSON seguirá siendo, en el momento de la lectura, una lista de diccionarios: la convertiremos en objetos justo después.

  1. Los cuatro pilares de la POO, en una tabla

La orientación a objetos se resume tradicionalmente en cuatro ideas. Aquí van nombradas, para que reconozcas los términos cuando los leas fuera del curso:

Pilar En una línea Dónde aparece en este curso
Abstracción Representar un concepto real con lo esencial y nada más Esta lección y 07-02
Encapsulación Guardar datos y comportamiento juntos y controlar el acceso 07-02 y 07-03
Herencia Crear una clase a partir de otra, reaprovechando lo que hace 07-03, solo lo básico
Polimorfismo Tratar igual a objetos distintos que comparten interfaz 07-03, solo lo básico

Un aviso de expectativas: este curso es de fundamentos y llega hasta la base. Verás abstracción y encapsulación con detalle porque son las que resuelven el problema que tenemos; herencia y polimorfismo se presentarán en su forma más simple y con la recomendación de no abusar de ellos. Todo lo demás —clases abstractas, herencia múltiple, patrones de diseño— es material de un curso específico de POO, y no lo necesitas para escribir programas correctos y ordenados.

Errores Comunes y Consejos

  • Confundir la clase con la instancia. Tarea es el molde; Tarea() fabrica un ejemplar. Si escribes cartel = Tarea (sin paréntesis), cartel no es una tarea: es la clase misma, y cartel.titulo = "..." modificaría el molde para todo el programa. Comprueba con type(cartel): debe decir <class '__main__.Tarea'>, no <class 'type'>.
  • Esperar que la clase valide algo por sí sola. Una clase vacía no protege de nada; solo da un nombre. La garantía llega con el constructor de 07-02.
  • Poner una lista o un diccionario como atributo de clase, o asignar a instancia.contador creyendo que actualizas la clase. Son las dos caras del error de la sección 7: lo mutable compartido se contagia entre instancias y lo asignado por instancia no llega nunca a la clase. Los mutables, siempre por instancia; el contador compartido, siempre como Tarea.creadas += 1.
  • Escribir clases para todo. Una clase con dos campos, sin comportamiento y usada en un solo sitio es peor que un diccionario: más líneas para el mismo resultado. Aplica la tabla de la sección 9.
  • Nombres poco claros. La clase se llama en singular y describe una cosa (Tarea, no Tareas ni GestorDeTareas), en CamelCase. El plural se reserva para las colecciones.
  • Consejo: prueba las clases en la consola interactiva. Crea una instancia, asígnale atributos, mira su __dict__, pregunta isinstance. Ver el objeto por dentro es lo que convierte estos conceptos en algo concreto.

Ejercicios

Ejercicio 1: Detectar tareas mal formadas

Partiendo de la agenda de diccionarios de la sección 1, escribe una función validar_agenda(agenda) que recorra la lista y devuelva una lista de textos describiendo todos los problemas encontrados: campos que faltan, campos sobrantes y prioridades que no estén exactamente en ("alta", "media", "baja"). Debe revisar la agenda entera, sin detenerse en el primer fallo.

Ejercicio 2: Del diccionario al objeto

Define una clase Cliente con un atributo de clase ESTUDIO = "Estudio Alba" y un contador creados. Crea dos instancias (Panadería Solé y cliente Vidal), asígnales desde fuera los atributos nombre, contacto y activo, incrementa el contador en cada creación y muestra el __dict__ de cada una junto al total de clientes creados. Después, asigna panaderia.creados = 50 y explica por escrito qué imprime Cliente.creados y por qué.

Ejercicio 3: Decidir la estructura

Para cada caso, decide si usarías un diccionario o una clase, y justifícalo en una frase apoyándote en la tabla de la sección 9: (a) el resultado de contar cuántas veces aparece cada palabra en los títulos de la agenda; (b) una factura del estudio, con número, cliente, líneas e importe, que se emite, se envía y se cobra; (c) los datos de configuración leídos de un fichero JSON al arrancar; (d) un miembro del equipo, con nombre, horas semanales disponibles y la capacidad de decir si está sobrecargado.

Soluciones

Solución 1.

CAMPOS = ("titulo", "responsable", "prioridad", "dias", "completada")
PRIORIDADES = ("alta", "media", "baja")

def validar_agenda(agenda):
    """Devuelve la lista de problemas encontrados en una agenda de diccionarios."""
    problemas = []
    for numero, tarea in enumerate(agenda, start=1):
        for campo in CAMPOS:                          # campos que faltan
            if campo not in tarea:
                problemas.append(f"Tarea {numero}: falta el campo '{campo}'")
        for clave in tarea:                           # campos que sobran
            if clave not in CAMPOS:
                problemas.append(f"Tarea {numero}: campo desconocido '{clave}'")
        prioridad = tarea.get("prioridad")            # get: no falla si no esta
        if prioridad is not None and prioridad not in PRIORIDADES:
            problemas.append(f"Tarea {numero}: prioridad invalida '{prioridad}'")
    return problemas

for aviso in validar_agenda(agenda):
    print(aviso)

# Tarea 2: prioridad invalida 'Alta'
# Tarea 3: falta el campo 'completada'
# Tarea 3: campo desconocido 'urgente'

Tres detalles: se usa .get("prioridad") en lugar de tarea["prioridad"] porque la clave podría faltar y no queremos un KeyError dentro del propio validador; se acumula en una lista en vez de imprimir, para que la función siga siendo pura y separada de la salida (módulo 4); y enumerate(..., start=1) numera las tareas como las ve el usuario. Ahora fíjate en lo incómodo del resultado: esta función hay que acordarse de llamarla, y si alguien crea una tarea después, nadie la revisa. Con el constructor de 07-02 esa comprobación pasa a ser automática e ineludible.

Solución 2.

class Cliente:
    """Un cliente del estudio."""
    ESTUDIO = "Estudio Alba"
    creados = 0

def alta(nombre, contacto, activo):
    """Crea un cliente asignando sus atributos desde fuera y cuenta el alta."""
    cliente = Cliente()
    cliente.nombre, cliente.contacto, cliente.activo = nombre, contacto, activo
    Cliente.creados += 1
    return cliente

panaderia = alta("Panaderia Sole", "[email protected]", True)
vidal = alta("Cliente Vidal", "[email protected]", False)

print(panaderia.__dict__)
print(vidal.__dict__)
print(f"{Cliente.ESTUDIO} tiene {Cliente.creados} clientes registrados.")

panaderia.creados = 50
print(panaderia.creados, Cliente.creados)    # 50 2

Cliente.creados sigue valiendo 2. La asignación panaderia.creados = 50 no toca la clase: crea un atributo de instancia dentro del __dict__ de panaderia que a partir de ahora tapa al de la clase para ese objeto concreto. vidal.creados sigue leyendo el de la clase y vale 2. Comprobación directa: print(panaderia.__dict__) ahora incluye 'creados': 50, mientras que el de vidal no.

Solución 3.

Caso Elección Motivo
(a) Recuento de palabras Diccionario Claves dinámicas y desconocidas de antemano; estructura temporal sin comportamiento
(b) Factura Clase Concepto repetido, con invariantes (el importe debe cuadrar) y comportamiento propio (emitir, enviar, cobrar)
(c) Configuración del JSON Diccionario Llega de fuera con forma variable y solo se lee; convertirla a clase añade trabajo sin ganar nada
(d) Miembro del equipo Clase Forma fija, se repite por todo el programa y tiene comportamiento propio: esta_sobrecargado()

El criterio que más peso tiene es el de la última columna: si el concepto hace cosas, quiere ser una clase; si solo transporta datos de forma variable, un diccionario le basta.

Conclusión

Un objeto reúne en un mismo sitio los datos (atributos) y el comportamiento (métodos) que van juntos, y llevas usándolos desde el módulo 2 sin saberlo: "hola".upper() y agenda.append(...) son exactamente eso. La clase es el molde que define cómo son los objetos de un tipo, se escribe una vez con class Tarea: en CamelCase, y cada instancia se fabrica llamándola como una función, Tarea(). Los atributos se leen y escriben con la notación del punto y viven realmente en el __dict__ del objeto, con getattr, setattr y hasattr para cuando el nombre no se conoce hasta la ejecución. Los atributos de clase son compartidos por todas las instancias —perfectos para constantes y contadores, peligrosos si son mutables—, mientras que los de instancia pertenecen a un solo objeto y tapan a los de la clase cuando coinciden. type() e isinstance() identifican de qué clase es cada cosa y demuestran que en Python todo es un objeto, incluidas las propias clases. Y la elección entre diccionario y clase no es de moda, sino de criterio: diccionario para datos externos, claves dinámicas y estructuras temporales; clase cuando hay invariantes, comportamiento y un concepto que se repite por todo el programa.

Pero lo que hemos construido hoy es todavía un molde sin cerrar. Una Tarea a la que se le asignan atributos desde fuera puede seguir naciendo sin completada y con la prioridad en "Alta": hemos cambiado el KeyError por un AttributeError y hemos mejorado la lectura, poco más. Falta la pieza que convierte la clase en una garantía: un bloque que se ejecute automáticamente al crear cada instancia, que exija los datos imprescindibles, los valide contra PRIORIDADES y EQUIPO, y que además permita guardar junto a los datos las funciones que hoy andan sueltas por tareafacil.py. En Atributos, métodos y constructor llega __init__, se explica de una vez qué es ese self que aparece en todos los ejemplos de internet, y TareaFácil da el salto a la v0.15: una clase Tarea de verdad, con validación en el constructor y métodos propios, y un programa que deja de manejar diccionarios para manejar objetos.

Fundamentos de la Programación

Módulo 1: Introducción a la Programación

Módulo 2: Conceptos Básicos

Módulo 3: Estructuras de Control

Módulo 4: Funciones y Procedimientos

Módulo 5: Estructuras de Datos

Módulo 6: Algoritmos Básicos

Módulo 7: Objetos y Organización del Código

Módulo 8: Buenas Prácticas y Herramientas

Módulo 9: Proyecto Final y Cierre del Curso

© Copyright 2026. Todos los derechos reservados