En De los datos a los objetos definimos una clase Tarea y le asignamos atributos desde fuera, y comprobamos que eso no arreglaba nada: seguíamos pudiendo crear una tarea sin completada y con la prioridad escrita como "Alta". Lo único que cambió fue el nombre del error, de KeyError a AttributeError. Esta lección aporta la pieza que faltaba, y es el corazón de todo el módulo. Esa pieza es el constructor, __init__: un bloque que Python ejecuta automáticamente al crear cada instancia, y que por tanto es el único punto del programa por el que pasan, obligatoriamente, todas las tareas que llegarán a existir. Ahí se exigen los datos imprescindibles y se validan sus valores. Junto a él llegan los métodos, que permiten recoger las funciones sueltas que llevan desde el módulo 4 dando vueltas por tareafacil.py y guardarlas donde pertenecen: dentro de la propia tarea. Al final de la lección, TareaFácil será la v0.15 y trabajará con objetos.
Contenido
__init__: el constructorself, explicado de verdad- Validar dentro del constructor
- Métodos que leen el estado
- Métodos que cambian el estado
__str__frente a__repr__- Comparar objetos con
__eq__(y una nota sobre__len__) - Docstrings de clase y de método
@property: valores calculados sin duplicar estado@staticmethody@classmethod@dataclass: el atajo para clases que solo guardan datos- TareaFácil v0.15: la clase
Tarea - Errores comunes y consejos
- Ejercicios
- Conclusión
__init__: el constructor
__init__: el constructorEl constructor es un método especial llamado __init__ (dos guiones bajos a cada lado) que Python ejecuta justo después de crear una instancia. Su trabajo es dejar el objeto listo para usarse: dar valor a todos sus atributos.
class Tarea:
"""Una tarea del estudio Estudio Alba."""
def __init__(self, titulo, responsable, prioridad="media", dias=1):
self.titulo = titulo
self.responsable = responsable
self.prioridad = prioridad
self.dias = dias
self.completada = False # toda tarea nace pendiente
cartel = Tarea("Cartel feria del libro", "Luis", "alta", 3)
print(cartel.titulo, cartel.prioridad, cartel.completada) # ... alta False
Tarea("Presupuesto Vidal")
# TypeError: __init__() missing 1 required positional argument: 'responsable'Hay varias cosas nuevas a la vez, y conviene leerlas despacio:
__init__no se llama nunca a mano. EscribesTarea("Cartel...", "Luis", "alta", 3)y Python crea el objeto y le pasa esos argumentos a__init__por ti. Es el mismo mecanismo por el quelist("abc")oint("42")construyen objetos.- Los parámetros sin valor por defecto son los datos obligatorios.
tituloyresponsableno lo tienen, así que es imposible crear una tarea sin ellos: el error salta en el sitio y el momento correctos, como muestra la última línea del ejemplo. Los que sí lo tienen funcionan igual que en las funciones (04-02):prioridad="media"ydias=1permiten escribirTarea("Revisar bocetos", "Nuria"), y deben ir después de los obligatorios. completadano es un parámetro: se fija dentro. Ninguna tarea nace completada, así que no tiene sentido preguntarlo al crearla. Hay atributos que se reciben y atributos que se fijan o calculan, y todos salen del constructor con valor. Antes, la garantía de que una tarea tuviera sus cinco campos dependía de que cada programador se acordara; ahora la garantía es del lenguaje: si el objeto existe, sus atributos existen, y elAttributeErrora mitad del listado ha dejado de ser posible.
self, explicado de verdad
self, explicado de verdadself es la palabra que más confunde al principio y en realidad no tiene misterio: self es el objeto sobre el que se está trabajando. Cuando escribes Tarea("Cartel feria del libro", "Luis"), Python hace dos cosas: fabrica un objeto vacío y llama a __init__ pasándole ese objeto como primer argumento. Dentro del método, ese primer parámetro se llama self por convenio, y self.titulo = titulo significa «guarda en este objeto el valor recibido».
| Lo que escribes | Lo que Python ejecuta realmente |
|---|---|
cartel = Tarea("Cartel", "Luis") |
crea el objeto y llama a Tarea.__init__(objeto, "Cartel", "Luis") |
cartel.completar() |
llama a Tarea.completar(cartel) |
cartel.reasignar("Nuria") |
llama a Tarea.reasignar(cartel, "Nuria") |
De ahí salen las dos reglas que hay que memorizar: self es siempre el primer parámetro de todo método de instancia, aunque no reciba nada más; y self no se pasa al llamar, lo pone Python a partir del objeto que hay a la izquierda del punto. Por eso cartel.completar() se escribe sin argumentos aunque su definición sea def completar(self). Dos errores clásicos, con su mensaje exacto para que los reconozcas:
class Tarea:
def __init__(titulo, responsable): # MAL: falta self
...
Tarea("Cartel", "Luis")
# TypeError: __init__() takes 2 positional arguments but 3 were givenEl mensaje parece absurdo —«has dado 3 y acepta 2»— hasta que recuerdas que Python añade el objeto como primer argumento. Sin self, las cuentas nunca cuadran. El segundo error no da mensaje ninguno, y por eso es peor: escribir dentro del constructor titulo = titulo en lugar de self.titulo = titulo. Eso asigna a una variable local que muere al terminar el método (es el ámbito del módulo 4 en acción); el objeto se queda sin titulo y el fallo aparecerá más tarde y en otro sitio. La regla es simple: todo lo que deba sobrevivir a la llamada se guarda en self.. Por último, self no es una palabra reservada —podrías llamarlo yo y funcionaría—, pero no lo hagas: todo el mundo escribe self.
- Validar dentro del constructor
Que la tarea tenga los cinco atributos no basta: sus valores también deben ser correctos. Y como el constructor es el paso obligatorio de toda tarea, es el sitio natural para comprobarlo.
PRIORIDADES = ("alta", "media", "baja")
EQUIPO = ("marta", "luis", "nuria")
class Tarea:
def __init__(self, titulo, responsable, prioridad="media", dias=1): # v0.15
self.titulo = titulo.strip() # fuera espacios sobrantes
limpio = responsable.strip().lower()
self.responsable = limpio.capitalize() if limpio in EQUIPO else "Marta"
prioridad = prioridad.strip().lower() # 'Alta' -> 'alta'
self.prioridad = prioridad if prioridad in PRIORIDADES else "media"
self.dias = dias if 1 <= dias <= 365 else 1
self.completada = False
t = Tarea(" Logotipo Panaderia Sole ", "NURIA", "Alta", 5)
print(f"[{t.titulo}] {t.responsable} {t.prioridad} {t.dias}d")
# [Logotipo Panaderia Sole] Nuria alta 5dFíjate en lo que acaba de ocurrir: "Alta", el valor que en el módulo 6 iba a reventar el ORDEN_PRIORIDAD, ya no puede entrar en el programa. La clase lo normaliza en el único punto por el que pasan todas las tareas, y lo mismo hace con los espacios del título y con "NURIA". Hay tres estrategias posibles ante un valor inválido:
| Estrategia | Qué hace | Cuándo conviene |
|---|---|---|
| Normalizar | Arregla lo arreglable: "Alta" → "alta", quita espacios |
Diferencias de formato, no de contenido |
| Valor seguro | Sustituye lo irrecuperable por un valor por defecto | Datos dudosos que no deben parar el programa |
| Avisar y rechazar | Impide crear el objeto y comunica el fallo | Lo correcto en un programa serio |
Por ahora usamos las dos primeras a conciencia. La tercera —lanzar un error con raise y capturarlo con try/except— es el contenido de Depuración y manejo de errores; cuando lo estudies volverás a esta clase y sustituirás los valores seguros por errores explícitos, sin cambiar la estructura. Mientras tanto hay que tener cuidado con un peligro: un valor seguro silencioso oculta el problema. Si alguien escribe Tarea("Cartel", "Pepe") y la tarea acaba asignada a Marta sin decir nada, el error existe pero nadie lo ve; como mínimo hay que avisar por pantalla con un print(f"Aviso: '{responsable}' no esta en el equipo; se asigna a Marta.") en esa rama, y así lo hará la versión definitiva de la sección 12.
- Métodos que leen el estado
Un método es una función definida dentro de la clase, con self como primer parámetro, que por tanto accede directamente a los atributos del objeto. Los hay de dos familias, y distinguirlas ayuda a diseñar bien: los que consultan el estado sin tocarlo y los que lo modifican. Empezamos por los primeros, que recogen lógica hasta ahora dispersa en funciones sueltas (la clase tiene además un atributo self.hechos = 0, los días ya invertidos):
def esta_retrasada(self):
"""Indica si se han invertido mas dias de los estimados."""
return not self.completada and self.hechos > self.dias
def urgencia(self):
"""Devuelve la urgencia real: 'ninguna', 'critica' o la prioridad."""
if self.completada:
return "ninguna"
return "critica" if self.esta_retrasada() else self.prioridad
cartel = Tarea("Cartel feria del libro", "Luis", "alta", 3)
cartel.hechos = 5 # 5 dias invertidos de 3 previstos
print(cartel.esta_retrasada(), cartel.urgencia()) # True criticaTres observaciones que valen para todos los métodos que escribas. No reciben la tarea como parámetro: antes escribíamos esta_retrasada(tarea) y ahora la tarea es self y llega sola, con lo que cartel.urgencia() dice quién y qué sin ambigüedad. Un método puede llamar a otro del mismo objeto, siempre a través de self, como hace urgencia con self.esta_retrasada(); sin el self., Python buscaría una función global con ese nombre y no la encontraría. Y estos métodos no imprimen: devuelven un valor, que es la separación entre entrada/salida y lógica de 04-04 aplicada dentro de las clases; quien llama decide si lo imprime, lo suma o lo usa para filtrar.
- Métodos que cambian el estado
La otra familia modifica los atributos del objeto. Aquí es donde marcar_completada y cambiar_prioridad, que desde el módulo 4 recibían un diccionario y lo modificaban en el sitio, encuentran por fin su lugar:
def completar(self):
"""Marca la tarea como terminada. Devuelve False si ya lo estaba."""
if self.completada:
return False
self.completada = True
self.hechos = max(self.hechos, self.dias)
return True
def reasignar(self, persona):
"""Cambia el responsable si la persona pertenece al equipo."""
limpio = persona.strip().lower()
if limpio not in EQUIPO:
return False
self.responsable = limpio.capitalize()
return True
def avanzar(self, dias_trabajados):
"""Suma dias de trabajo ya invertidos en la tarea."""
self.hechos += max(0, dias_trabajados)
cartel = Tarea("Cartel feria del libro", "Luis", "alta", 3)
print(cartel.reasignar("nuria"), cartel.reasignar("Pepe")) # True False
print(cartel.completar(), cartel.completar()) # True False: la segunda no cambia nadaDos decisiones de diseño que merece la pena copiar. Devuelven True/False para indicar si el cambio se hizo, de modo que quien llama avisa al usuario sin que el método imprima nada: if not cartel.reasignar(nombre): print("Esa persona no esta en el equipo."). Y el método protege el invariante: reasignar no admite un responsable fuera de EQUIPO, igual que hacía el constructor. Esa es la regla general de la encapsulación: si un dato tiene reglas, todos los caminos que lo modifican deben respetarlas, y por eso se cambia mediante métodos y no escribiendo cartel.responsable = "Pepe" desde fuera.
__str__ frente a __repr__
__str__ frente a __repr__Ya viste que print(cartel) muestra algo ilegible: <__main__.Tarea object at 0x7f8b1c0d5e50>. Se arregla con dos métodos especiales:
def __str__(self): # texto para el usuario final
marca = "[X]" if self.completada else "[ ]"
return f"{marca} {self.titulo:<28}{self.responsable:<8}{self.prioridad:<6}{self.dias}d"
def __repr__(self): # texto para el programador
return f"Tarea({self.titulo!r}, {self.responsable!r}, {self.prioridad!r}, {self.dias})"
cartel = Tarea("Cartel feria del libro", "Luis", "alta", 3)
print(cartel) # [ ] Cartel feria del libro Luis alta 3d
print(repr(cartel)) # Tarea('Cartel feria del libro', 'Luis', 'alta', 3)
print([cartel]) # [Tarea('Cartel feria del libro', 'Luis', 'alta', 3)]__str__ |
__repr__ |
|
|---|---|---|
| Para quién | El usuario del programa | El programador |
| Lo usan | print(obj), str(obj), f-strings |
La consola, repr(obj), los objetos dentro de listas |
| Objetivo | Que se lea bien | Que sea preciso e inequívoco |
| Si solo defines uno | print funciona, pero las listas siguen feas |
print también lo usa como recambio |
La última fila es el consejo práctico: si solo vas a escribir uno, escribe __repr__, porque Python lo usa de sustituto cuando falta __str__ y además es el que se ve al depurar. La sorpresa más habitual es la de print([cartel]): al imprimir una lista, Python no usa el __str__ de sus elementos sino su __repr__, así que una lista de tareas sin __repr__ sigue mostrando direcciones de memoria. El !r de la f-string, por cierto, es el atajo para aplicar repr() a un valor: por eso los textos salen entrecomillados.
- Comparar objetos con
__eq__ (y una nota sobre __len__)
__eq__ (y una nota sobre __len__)Por defecto, dos objetos distintos nunca son iguales, aunque contengan los mismos datos. Es coherente con lo que vimos en 05-01 sobre is y ==, pero casi nunca es lo que queremos. Definiendo __eq__ decidimos nosotros qué significa que dos tareas sean la misma:
def __eq__(self, otra):
"""Dos tareas son la misma si coinciden titulo y responsable."""
if not isinstance(otra, Tarea):
return NotImplemented # comparar con otra cosa no es cosa nuestra
return (self.titulo.lower() == otra.titulo.lower()
and self.responsable == otra.responsable)
a = Tarea("Cartel feria del libro", "Luis", "alta", 3)
b = Tarea("cartel feria del libro", "Luis", "baja", 9)
print(a == b, a == "Cartel") # True False (sin __eq__, la primera seria False)
print(a in [b]) # True: el operador 'in' usa ==Lo interesante es la última línea: al definir __eq__, operadores y funciones que ya conoces empiezan a funcionar con tus objetos. in, .count(), .index() y .remove() sobre listas usan == internamente, así que ahora localizan tareas por contenido. Devolver NotImplemented cuando el otro objeto no es una Tarea es la forma educada de decir «yo no sé comparar esto»; Python entonces responde False. En la misma familia está __len__, que define qué devuelve len(obj). En una tarea suelta no tiene sentido —¿la longitud de qué?—, pero en la clase Agenda de Colecciones de objetos será evidente: len(agenda) debe dar el número de tareas. A estos métodos con guiones bajos se les llama métodos especiales o dunder (de double underscore), y su gracia es que conectan tus clases con la sintaxis del lenguaje.
- Docstrings de clase y de método
Igual que las funciones, las clases y sus métodos llevan docstring: un texto entre triples comillas en la primera línea del cuerpo. Ya lo has visto en todos los ejemplos de esta lección —"""Marca la tarea como terminada. Devuelve False si ya lo estaba."""—, con una diferencia: la docstring de la clase suele ocupar varias líneas, porque además de decir qué representa el objeto describe qué garantiza, como en """Una tarea del estudio. Nace pendiente, validada y con todos sus campos.""".
Se consultan con help(Tarea) o Tarea.completar.__doc__, y el editor las muestra al escribir. La regla mínima: la clase explica qué representa y qué garantiza; cada método, qué hace y qué devuelve. La guía completa está en Documentación y comentarios.
@property: valores calculados sin duplicar estado
@property: valores calculados sin duplicar estadoImagina que quieres saber cuántos días de trabajo le quedan a una tarea. La tentación es guardarlo como un atributo más, self.dias_restantes = dias, y ahí empieza el problema: cada vez que alguien llame a avanzar() o a completar() habrá que acordarse de recalcularlo, y el día que se olvide el objeto mostrará un dato falso. Eso es estado duplicado, una de las fuentes de error más silenciosas que existen. La solución es no guardarlo, sino calcularlo cuando se pida. Y para que se lea como un atributo en vez de como un método, se usa el decorador @property:
@property
def dias_restantes(self):
"""Dias de trabajo que faltan; cero si la tarea ya esta completada."""
return 0 if self.completada else max(0, self.dias - self.hechos)
cartel = Tarea("Cartel feria del libro", "Luis", "alta", 3)
print(cartel.dias_restantes) # 3 <- sin parentesis: parece un atributo
cartel.avanzar(2)
print(cartel.dias_restantes) # 1 <- siempre coherente: se recalcula
cartel.completar()
print(cartel.dias_restantes) # 0 <- y sigue siendo coherenteLa diferencia con un atributo guardado está en la coherencia: ambos se leen igual, con obj.x y sin paréntesis, pero el atributo hay que actualizarlo a mano en cada método que lo afecte, mientras que la @property se calcula en el momento y por tanto nunca miente. A cambio paga un pequeño cálculo por lectura, que salvo en bucles enormes es irrelevante. Los decoradores ya te suenan de @lru_cache (06-03): son funciones que envuelven a otra para modificar su comportamiento. La regla práctica: si un valor se puede deducir de otros atributos, hazlo @property; si es un dato independiente, guárdalo. Y ojo, una @property de solo lectura no admite asignación —cartel.dias_restantes = 5 da AttributeError—, lo cual es una protección, no un inconveniente.
@staticmethod y @classmethod
@staticmethod y @classmethodNo todos los métodos necesitan un objeto concreto. Un @staticmethod es una función normal que vive dentro de la clase por afinidad temática: no recibe self ni toca ningún objeto. Sirve para utilidades del dominio, como comprobar un dato antes de crear nada:
@staticmethod
def prioridad_valida(texto):
"""Indica si un texto es una prioridad aceptable."""
return texto.strip().lower() in PRIORIDADES
print(Tarea.prioridad_valida("ALTA")) # True: desde la clase, sin instanciaUn @classmethod recibe como primer parámetro la clase (cls) en vez de la instancia, y su uso más valioso es el constructor alternativo: otra forma de fabricar objetos a partir de datos con distinto formato. Justo lo que necesitamos para el JSON de 05-05, donde cada tarea está guardada como diccionario:
@classmethod
def desde_diccionario(cls, datos):
"""Crea una Tarea a partir de un diccionario leido del JSON."""
tarea = cls(datos["titulo"], datos["responsable"],
datos.get("prioridad", "media"), datos.get("dias", 1))
tarea.hechos = datos.get("hechos", 0)
tarea.completada = datos.get("completada", False)
return tarea
d = {"titulo": "Presupuesto Vidal", "responsable": "Marta", "prioridad": "Alta"}
print(Tarea.desde_diccionario(d)) # [ ] Presupuesto Vidal Marta alta 1dObserva el detalle que lo hace robusto: datos.get("prioridad", "media") tolera que la clave falte y el constructor normaliza el "Alta" que venía del fichero. Es decir, los diccionarios sucios del módulo 6 entran, y salen tareas limpias. La operación inversa —convertir el objeto en diccionario para poder guardarlo— es to_dict(), y llega en 07-03 junto a la clase Agenda, porque json.dump no sabe escribir objetos.
@dataclass: el atajo para clases que solo guardan datos
@dataclass: el atajo para clases que solo guardan datosCuando una clase se limita a agrupar datos, escribir __init__, __repr__ y __eq__ a mano es puro trámite. El módulo dataclasses de la biblioteca estándar los genera por ti. El mismo Cliente, antes y después:
class Cliente: # ANTES: todo a mano
def __init__(self, nombre, contacto, activo=True):
self.nombre, self.contacto, self.activo = nombre, contacto, activo
def __repr__(self): ... # el f-string con los tres campos
def __eq__(self, otro): ... # isinstance + comparacion campo a campo
from dataclasses import dataclass # DESPUES: lo mismo, generado
@dataclass
class Cliente:
"""Un cliente del estudio."""
nombre: str
contacto: str
activo: bool = True
sole = Cliente("Panaderia Sole", "[email protected]")
print(sole, sole == Cliente("Panaderia Sole", "[email protected]"))
# Cliente(nombre='Panaderia Sole', contacto='[email protected]', activo=True) TrueEl decorador lee las líneas nombre: str —anotaciones de tipo, que Python no comprueba pero sirven de documentación y ayudan al editor— y genera __init__, __repr__ y __eq__. Cuándo usar cada cosa: si la clase solo agrupa datos, @dataclass; si hay validación, normalización, invariantes o comportamiento propio, clase normal con __init__ escrito a mano. Nuestra Tarea valida, normaliza y decide, así que se queda como clase normal; Cliente, que solo transporta datos, es un @dataclass de manual.
- TareaFácil v0.15: la clase
Tarea
TareaAplicamos todo. Las funciones sueltas que trabajaban sobre diccionarios se convierten en métodos, y la agenda pasa a ser una lista de objetos Tarea.
# tareafacil.py - Estudio Alba / Version 0.15: la tarea es un objeto
CAMPOS = ("titulo", "responsable", "prioridad", "dias", "hechos", "completada")
# --- Resto de constantes y funciones de E/S: sin cambios respecto a la v0.14 ---
class Tarea:
"""Una tarea del estudio. Nace pendiente, validada y con todos sus campos."""
# __init__ como en la seccion 3, mas el aviso de responsable invalido y
# self.hechos = 0; dias_restantes (@property), esta_retrasada, urgencia,
# completar, reasignar, cambiar_prioridad, avanzar, desde_diccionario,
# __str__, __repr__ y __eq__ tal como se han escrito en esta leccion
def clave_orden(self):
"""Criterio del listado: prioridad, luego dias, luego titulo."""
return (ORDEN_PRIORIDAD[self.prioridad], self.dias, self.titulo)
def registrar_tarea(agenda):
"""Pide una tarea nueva y la anade a la agenda."""
agenda.append(Tarea(pedir_texto("Titulo : "),
pedir_opcion("Responsable : ", EQUIPO),
pedir_opcion("Prioridad : ", PRIORIDADES),
pedir_entero("Dias (1-365) : ", 1, 365)))
def mostrar_listado(agenda):
"""Muestra la agenda ordenada por prioridad y dias."""
for numero, tarea in enumerate(sorted(agenda, key=Tarea.clave_orden), start=1):
print(f"{numero:>2}. {tarea}") # aqui actua __str__Dos detalles nuevos. En el print del bucle, {tarea} invoca __str__ automáticamente: la función ya no sabe nada de la estructura interna de una tarea. Y key=Tarea.clave_orden funciona porque un método al que se accede desde la clase es una función normal que recibe el objeto como primer argumento, así que sorted le pasará cada tarea como self; también valdría key=lambda t: t.clave_orden(). Antes y después, línea a línea:
| En la v0.14 (diccionarios) | En la v0.15 (objetos) |
|---|---|
tarea["titulo"] |
tarea.titulo |
mostrar_ficha(tarea) |
print(tarea), gracias a __str__ |
clave_orden(tarea) / marcar_completada(tarea) |
tarea.clave_orden() / tarea.completar() |
Validación repartida por registrar_tarea |
Centralizada en __init__ |
Un KeyError posible en cualquier listado |
Imposible: si el objeto existe, tiene sus campos |
Queda un cabo suelto evidente: guardar_tareas ya no funciona. json.dump sabe escribir diccionarios, pero no objetos Tarea, y protesta con TypeError: Object of type Tarea is not JSON serializable. La solución —un método to_dict() que devuelva el diccionario equivalente, con desde_diccionario para el camino de vuelta— llega en la próxima lección.
Errores Comunes y Consejos
- Olvidar
selfen la definición del método, con el mensajetakes 1 positional argument but 2 were given. Recuerda que Python añade el objeto: si el método recibenargumentos al llamarlo, su definición necesitan + 1parámetros. Y olvidar elself.al asignar dentro del constructor no da error ninguno: crea una variable local que se pierde, así que si un atributo «desaparece», revisa esto primero. - Poner un mutable como valor por defecto de un parámetro,
def __init__(self, notas=[]). Es el mismo error del módulo 4 y aquí es peor, porque esa lista se comparte entre todas las instancias. Usanotas=Noney dentroself.notas = notas if notas else []. Tampoco hace falta nunca llamar a__init__a mano (cartel.__init__(...)): escribeTarea(...). - Definir solo
__str__y esperar que las listas se vean bien.print([tarea])usa__repr__: define los dos, o al menos__repr__. Y no modifiques atributos desde fuera saltándote los métodos (tarea.prioridad = "Alta"), porque rompe justo la garantía que da la clase: si un dato tiene reglas, pasa por el método. - Consejo: pon los métodos en orden. Primero
__init__, luego las@property, después los métodos que consultan, los que modifican y al final los especiales. Una clase ordenada se lee en diagonal.
Ejercicios
Ejercicio 1: La clase Cliente
Escribe una clase Cliente cuyo constructor reciba nombre, contacto y dias_credito=30; que normalice el nombre (sin espacios sobrantes y con la primera letra de cada palabra en mayúscula), obligue a que dias_credito esté entre 0 y 90 (si no, 30) y fije facturas = []. Añade un método facturar(importe) que añada el importe a la lista si es positivo y devuelva True/False, y una @property total_facturado. Pruébala con Panadería Solé.
Ejercicio 2: Un método a partir de una función suelta
Esta función trabajaba con diccionarios en la v0.14. Conviértela en un método resumen() de Tarea sin cambiar su comportamiento y explica por escrito qué mejora: return f"{tarea['titulo']} ({tarea['responsable']}): {estado}, {tarea['dias']} dias", donde estado vale "completada" o "pendiente" según tarea["completada"].
Ejercicio 3: @property frente a atributo guardado
Añade a Tarea una @property llamada porcentaje que devuelva el avance en tanto por ciento (días hechos sobre días estimados, con máximo 100, y 100 si está completada). Después responde por escrito: ¿qué pasaría si en lugar de una @property se guardara self.porcentaje en el constructor? Escribe la secuencia de llamadas que demostraría el fallo.
Soluciones
Solución 1.
class Cliente:
"""Un cliente del estudio, con su credito y sus facturas."""
def __init__(self, nombre, contacto, dias_credito=30):
self.nombre = nombre.strip().title()
self.contacto = contacto.strip().lower()
self.dias_credito = dias_credito if 0 <= dias_credito <= 90 else 30
self.facturas = [] # mutable: siempre creado en el constructor
@property
def total_facturado(self):
"""Suma de todas las facturas emitidas al cliente."""
return sum(self.facturas)
def facturar(self, importe):
"""Anade una factura si el importe es positivo."""
if importe <= 0:
return False
self.facturas.append(importe)
return True
sole = Cliente(" panaderia sole ", "[email protected]", 120)
sole.facturar(450.0)
print(sole.nombre, sole.dias_credito, sole.facturar(-10), sole.total_facturado)
# Panaderia Sole 30 False 450.0 <- nombre normalizado, credito seguro, rechazoLa clave está en self.facturas = [] dentro del constructor: si estuviera en el cuerpo de la clase sería un atributo de clase compartido y las facturas de un cliente aparecerían en los demás (07-01, sección 7). Y total_facturado es una @property porque es una suma deducible de facturas: guardarla obligaría a actualizarla en cada facturar.
Solución 2.
def resumen(self):
"""Devuelve una linea descriptiva de la tarea."""
estado = "completada" if self.completada else "pendiente"
return f"{self.titulo} ({self.responsable}): {estado}, {self.dias} dias"Tres mejoras concretas. Vive junto a los datos: si mañana la tarea gana un campo, el resumen está a tres líneas de distancia y no en otro punto del fichero. No puede recibir algo que no sea una tarea: resumen_tarea({"titulo": "x"}) reventaba con KeyError, mientras que tarea.resumen() solo existe si tarea es una Tarea. Y se lee mejor en el punto de uso: print(cartel.resumen()) dice de quién es el resumen sin necesidad de mirar la firma de ninguna función.
Solución 3.
@property
def porcentaje(self): # avance, entre 0 y 100
if self.completada:
return 100
return min(100, round(self.hechos / self.dias * 100))
# Si en vez de la @property se guardara self.porcentaje = 0 en el constructor:
t = Tarea("Cartel feria del libro", "Luis", "alta", 4)
t.avanzar(2) # ha hecho la mitad
print(t.porcentaje) # 0 <- MENTIRA: nadie actualizo el atributoavanzar modifica hechos, pero el atributo porcentaje conserva el valor que le puso el constructor. Es estado duplicado: el mismo hecho —cuánto se ha avanzado— guardado en dos sitios que pueden discrepar. Con la @property, el porcentaje no existe hasta que se pide y por tanto no puede estar desactualizado. La misma trampa aparecería con completar(), que también cambia hechos sin tocar porcentaje.
Conclusión
El constructor __init__ es el paso obligatorio de todos los objetos de una clase, y por eso es donde se garantiza que toda tarea nace completa: los parámetros sin valor por defecto son los datos imprescindibles, los que sí lo tienen cubren lo opcional, y los atributos que no se reciben —como completada— se fijan dentro. Ahí mismo se valida: normalizar "Alta" a "alta" y comprobar el responsable contra EQUIPO elimina de raíz los KeyError del módulo 6, y cuando estudies raise y try/except en 08-02 sustituirás los valores seguros por errores explícitos. self no es magia: es el propio objeto, que Python pasa como primer argumento a partir de lo que hay a la izquierda del punto, y todo lo que deba sobrevivir a la llamada se guarda en self.. Los métodos recogen la lógica que estaba suelta, distinguiendo los que consultan el estado (esta_retrasada, urgencia, clave_orden) de los que lo modifican (completar, reasignar, cambiar_prioridad), que devuelven True/False en vez de imprimir. Los métodos especiales conectan la clase con el lenguaje: __str__ para el usuario, __repr__ para el programador —y es el que usan las listas—, __eq__ para que == e in comparen contenido. @property convierte en atributo de lectura cualquier valor deducible de los demás y elimina el estado duplicado; @staticmethod aloja utilidades sin objeto y @classmethod da constructores alternativos como Tarea.desde_diccionario(d), la puerta de entrada de los datos del JSON. Y @dataclass ahorra el trabajo manual en las clases que solo agrupan datos.
TareaFácil es ya la v0.15: la agenda es una lista de objetos Tarea, el listado se imprime con {tarea} y siete funciones sueltas se han mudado dentro de la clase. Pero fíjate en lo que ha quedado descolgado. La agenda sigue siendo una lista pelada: cualquier parte del programa puede hacerle append de lo que quiera —incluido un diccionario suelto o un número—, las operaciones que la manejan (buscar, filtrar, ordenar, resumir) continúan repartidas por el fichero, y guardar_tareas está literalmente roto porque json.dump no sabe escribir objetos. En Colecciones de objetos daremos el mismo salto de hoy, pero un nivel más arriba: aparece la clase Agenda, que contiene las tareas y ofrece agregar, buscar, filtrar y resumen; aprenderás a recorrer, ordenar e indexar objetos, verás por qué una colección debe proteger su lista interna y recuperarás la persistencia con to_dict/from_dict para seguir guardando en el mismo JSON de 05-05.
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
