Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Guía de Referencia 3: Las Operaciones del Almacén

Esta es la sala de máquinas. Aquí viven las funciones que mueven mercancía de verdad: generar identificadores, agregar productos, buscarlos, eliminarlos y, lo más importante, guardar y recuperar todo del disco. Ninguna de estas funciones habla con el usuario. Reciben datos, los procesan y devuelven resultados. La conversación con el teclado es trabajo de la siguiente guía.

Generar el ID: el Mayor más Uno

Cada producto necesita un ID único. La tentación inicial es usar el tamaño de la lista:

# MAL: parece lógico, pero tiene una bomba escondida
nuevo_id = str(len(inventario) + 1)

Parece más simple, y funciona… hasta que eliminas algo. Imagina el inventario con los IDs 1, 2 y 3. Eliminas el producto 2. Ahora la lista tiene 2 elementos, así que len() + 1 propone el ID 3. Ese ID ya está ocupado. Dos cajas distintas con la misma etiqueta: el caos administrativo perfecto.

La regla correcta es otra: el nuevo ID es el mayor ID existente más uno. No importa cuántos huecos haya dejado la historia de eliminaciones, el mayor más uno nunca choca con nadie.

def generar_id(inventario: list[dict]) -> str:
    """Genera un ID nuevo: el mayor ID existente más uno."""
    if len(inventario) == 0:
        return "1"
    ids = [int(producto["id"]) for producto in inventario]
    return str(max(ids) + 1)

Hay tres cosas que explican este código:

  1. El caso vacío va primero. Si el almacén está recién inaugurado, el primer ID es "1" y no hay nada que comparar.
  2. La comprehension [int(producto["id"]) for producto in inventario] junta todos los IDs convertidos a número, porque comparar textos daría resultados raros ("9" es “mayor” que "10" alfabéticamente).
  3. max() encuentra el mayor de la lista, le sumamos 1 y lo regresamos como str, que es el tipo del campo id.

Agregar un Producto

Con generar_id y crear_producto listos, agregar es ensamblar piezas:

def agregar_producto(inventario: list[dict], nombre: str,
                     precio: float, stock: int) -> dict:
    """Crea un producto con ID nuevo y lo añade al estante."""
    producto = crear_producto(generar_id(inventario), nombre, precio, stock)
    inventario.append(producto)
    return producto

Devuelve el producto recién creado para que quien llamó pueda confirmar la operación (por ejemplo, mostrando el ID asignado).

Buscar un Producto

Para encontrar una caja por su etiqueta, se recorre el estante y se compara:

def buscar_producto(inventario: list[dict], id_producto: str) -> dict | None:
    """Busca un producto por su ID. Devuelve None si no existe."""
    for producto in inventario:
        if producto["id"] == id_producto:
            return producto
    return None

El tipo de retorno dict | None se lee “diccionario o None”: la función devuelve el producto si lo encuentra, y None si no. El return dentro del bucle corta la búsqueda en cuanto hay coincidencia; el return None final solo se alcanza si el bucle terminó sin encontrar nada. Quien use esta función debe revisar el resultado con is None antes de tocarlo.

Eliminar un Producto

En lugar de mutilar la lista original, construimos una nueva que contiene todo excepto el producto condenado. Una comprehension con filtro lo hace en una línea:

def eliminar_producto(inventario: list[dict], id_producto: str) -> list[dict]:
    """Devuelve una lista nueva sin el producto indicado."""
    return [
        producto for producto in inventario if producto["id"] != id_producto
    ]

Cuidado: esta función devuelve la lista filtrada, no modifica la que recibió. Quien la llame debe quedarse con el resultado:

inventario = eliminar_producto(inventario, "2")

Si olvidas la asignación, la lista nueva se pierde y el producto sigue en el estante como si nada.

Guardar: la Lista al Disco

Hora de la persistencia. guardar_datos vuelca toda la lista a inventario.json de una sola operación con json.dump:

import json

def guardar_datos(ruta: Path, inventario: list[dict]) -> None:
    """Sobrescribe el archivo JSON con los datos actuales."""
    with open(ruta, mode="w", encoding="utf-8") as archivo:
        json.dump(inventario, archivo, indent=2, ensure_ascii=False)

Cada argumento carga una decisión. mode="w" reescribe el archivo entero, porque la lista en memoria es la verdad oficial del almacén y no queremos acumular versiones viejas. indent=2 deja el archivo legible para un humano que lo abra. ensure_ascii=False respeta acentos y eñes en vez de convertirlos en códigos ilegibles. Y with open cierra el muelle aunque algo falle a medio camino.

Cargar: del Disco a la Lista

El viaje inverso ocurre una sola vez, al arrancar el programa, y es igual de corto:

def cargar_datos(ruta: Path) -> list[dict]:
    """Lee el archivo JSON y devuelve la lista de productos."""
    if not ruta.exists():
        return []
    with open(ruta, encoding="utf-8") as archivo:
        return json.load(archivo)

ruta.exists() cubre el primer arranque: si el archivo todavía no existe, el almacén empieza vacío (sin esa revisión, abrir el archivo lanzaría FileNotFoundError). Si existe, json.load reconstruye la lista de diccionarios exactamente como se guardó, con los precios como float y los stocks como int, sin saltar encabezados ni convertir nada a mano.

¿Y si alguien abre inventario.json con un editor y lo deja roto? json.load lanza un json.JSONDecodeError y el programa se detiene con su parte de accidente. Para el SGI v1.0 eso basta. Un sistema en producción atraparía ese error con el try del Capítulo 10 y decidiría entre arrancar vacío o negarse a operar.

Con la sala de máquinas completa, falta el mostrador: el menú que conecta al usuario con estas funciones. Sigue con la Guía de Referencia 4.