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
- La agenda frágil: una demostración incómoda
- Qué es un objeto: datos y comportamiento juntos
- Clase e instancia: el molde y los ejemplares
- La sintaxis mínima:
classy creación de instancias - Atributos asignados desde fuera (y por qué todavía no basta)
type(),isinstance()y «en Python todo es un objeto»- Atributos de clase frente a atributos de instancia
- El espacio de nombres del objeto:
__dict__,getattr,setattryhasattr - Diccionario u objeto: cuándo compensa cada uno
- Los cuatro pilares de la POO, en una tabla
- Errores comunes y consejos
- Ejercicios
- Conclusión
- 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
KeyErrorsalta enmostrar_listado, pero el fallo se cometió mucho antes, cuando alguien creó ese diccionario sincompletada. 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, cuandoclave_ordenhagaORDEN_PRIORIDAD["Alta"], o cuando un filtrotarea["prioridad"] == "alta"deje esa tarea fuera sin avisar de nada. Este es el peor caso: un resultado incorrecto que parece correcto. - El campo
urgentees 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.
- 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.
- 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).
- La sintaxis mínima:
class y creación de instancias
class y creación de instanciasLa 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 distintosTres 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 endefo en unif. - El nombre de las clases se escribe por convenio en
CamelCase:Tarea,TareaRecurrente,Agenda. Las funciones y variables siguen ensnake_case(registrar_tarea). Verlo así te dice de un vistazo si un nombre es una clase. passes 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__.
- 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.
type(), isinstance() y «en Python todo es un objeto»
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 tiposLa 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.
- 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 tocadoUna 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 tareaComo 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) |
- El espacio de nombres del objeto:
__dict__, getattr, setattr y hasattr
__dict__, getattr, setattr y hasattrPor 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 pedirloEsto 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.
- 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.
- 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.
Tareaes el molde;Tarea()fabrica un ejemplar. Si escribescartel = Tarea(sin paréntesis),cartelno es una tarea: es la clase misma, ycartel.titulo = "..."modificaría el molde para todo el programa. Comprueba contype(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.contadorcreyendo 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 comoTarea.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, noTareasniGestorDeTareas), enCamelCase. El plural se reserva para las colecciones. - Consejo: prueba las clases en la consola interactiva. Crea una instancia, asígnale atributos, mira su
__dict__, preguntaisinstance. 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 2Cliente.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
- ¿Qué es la programación?
- Historia de la programación
- Lenguajes de programación
- Entornos de desarrollo
- Del problema al algoritmo
Módulo 2: Conceptos Básicos
- Variables y tipos de datos
- Operadores y expresiones
- Entrada y salida de datos
- Conversión de tipos y validación de datos
Módulo 3: Estructuras de Control
Módulo 4: Funciones y Procedimientos
- Definición y uso de funciones
- Parámetros y retorno de valores
- Ámbito de variables
- Descomponer un programa en funciones
- Funciones como valores: lambda y orden superior
Módulo 5: Estructuras de Datos
- Listas y arreglos
- Cadenas de caracteres
- Diccionarios y conjuntos
- Tuplas y estructuras anidadas
- Guardar datos en archivos: texto, CSV y JSON
Módulo 6: Algoritmos Básicos
Módulo 7: Objetos y Organización del Código
- De los datos a los objetos: clases e instancias
- Atributos, métodos y constructor
- Colecciones de objetos
- Módulos, paquetes e importaciones
Módulo 8: Buenas Prácticas y Herramientas
- Documentación y comentarios
- Depuración y manejo de errores
- Control de versiones
- Pruebas automatizadas
- Estilo, legibilidad y refactorización
