{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "596f06bc",
   "metadata": {},
   "source": [
    "# 06 · Técnicas pythónicas\n",
    "\n",
    "Los patrones que distinguen a quien escribe Python con soltura: iteradores y generadores, decoradores,\n",
    "gestores de contexto, herramientas funcionales, anotaciones de tipo, registro (logging), pruebas automáticas y\n",
    "concurrencia. Cada uno aparece constantemente en las bibliotecas que usarás y en las revisiones de código.\n",
    "\n",
    "**Aprenderás**\n",
    "1. Iteradores y generadores, y la caja de herramientas `itertools`\n",
    "2. Decoradores: medir tiempos, cachear, reintentar\n",
    "3. Gestores de contexto: limpieza garantizada con `with`\n",
    "4. Herramientas funcionales: `functools.partial`, `reduce`, `map`/`filter`\n",
    "5. Anotaciones de tipo que las herramientas pueden comprobar\n",
    "6. `logging` en lugar de `print`\n",
    "7. Pruebas con `pytest`\n",
    "8. Concurrencia: hilos, procesos y `asyncio`, y cuál encaja con cada problema (medido)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac2910fa",
   "metadata": {},
   "source": [
    "## 1. Iteradores, generadores e `itertools`\n",
    "Todo lo que se puede recorrer con un bucle es un *iterable*; `iter()` da un *iterador* que entrega los\n",
    "elementos de uno en uno. Los generadores son la forma fácil de escribir iteradores, e `itertools` los combina\n",
    "sin construir listas en memoria."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "cbaa12b8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:34.334657Z",
     "iopub.status.busy": "2026-10-18T10:10:34.334209Z",
     "iopub.status.idle": "2026-10-18T10:10:34.349544Z",
     "shell.execute_reply": "2026-10-18T10:10:34.347442Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "10 20 [30]\n",
      "[[0, 1, 2], [3, 4, 5], [6]]\n",
      "[1, 4, 7, 10, 13]\n",
      "[1, 3, 6, 10]\n",
      "['a', 'b', 'c', 'd'] [('a', 'a'), ('a', 'b'), ('b', 'a'), ('b', 'b')]\n",
      "{'i': ['i', 'i', 'i', 'i'], 'm': ['m'], 'p': ['p', 'p'], 's': ['s', 's', 's', 's']}\n"
     ]
    }
   ],
   "source": [
    "import itertools as it\n",
    "\n",
    "nums = iter([10, 20, 30])\n",
    "print(next(nums), next(nums), list(nums))\n",
    "\n",
    "def read_in_batches(items, size):\n",
    "    batch = []\n",
    "    for item in items:\n",
    "        batch.append(item)\n",
    "        if len(batch) == size:\n",
    "            yield batch\n",
    "            batch = []\n",
    "    if batch:\n",
    "        yield batch\n",
    "\n",
    "print(list(read_in_batches(range(7), 3)))\n",
    "print(list(it.islice(it.count(start=1, step=3), 5)))                 # contador infinito, los 5 primeros\n",
    "print(list(it.accumulate([1, 2, 3, 4])))                              # total acumulado\n",
    "print(list(it.chain(\"ab\", \"cd\")), list(it.product(\"ab\", repeat=2)))\n",
    "print({k: list(g) for k, g in it.groupby(sorted(\"mississippi\"))})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa89539e",
   "metadata": {},
   "source": [
    "## 2. Decoradores\n",
    "Un decorador es una función que recibe una función y devuelve otra nueva con comportamiento añadido. Poner\n",
    "`@timer` encima de una definición significa `f = timer(f)`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "a180024a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:34.355080Z",
     "iopub.status.busy": "2026-10-18T10:10:34.354625Z",
     "iopub.status.idle": "2026-10-18T10:10:34.508496Z",
     "shell.execute_reply": "2026-10-18T10:10:34.506932Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "slow_sum tardó 137.9 ms\n",
      "intento 1 fallido: corte de red momentáneo\n",
      "intento 2 fallido: corte de red momentáneo\n",
      "12499997500000 ok\n"
     ]
    }
   ],
   "source": [
    "import functools, time\n",
    "\n",
    "def timer(func):\n",
    "    @functools.wraps(func)                  # conserva el nombre y la documentación originales\n",
    "    def wrapper(*args, **kwargs):\n",
    "        t0 = time.perf_counter()\n",
    "        result = func(*args, **kwargs)\n",
    "        print(f\"{func.__name__} tardó {(time.perf_counter() - t0) * 1000:.1f} ms\")\n",
    "        return result\n",
    "    return wrapper\n",
    "\n",
    "def retry(times: int, exceptions=(Exception,)):\n",
    "    '''Un decorador con argumentos: una función que devuelve un decorador.'''\n",
    "    def decorate(func):\n",
    "        @functools.wraps(func)\n",
    "        def wrapper(*args, **kwargs):\n",
    "            for attempt in range(1, times + 1):\n",
    "                try:\n",
    "                    return func(*args, **kwargs)\n",
    "                except exceptions as e:\n",
    "                    print(f\"intento {attempt} fallido: {e}\")\n",
    "            raise RuntimeError(f\"{func.__name__} falló {times} veces\")\n",
    "        return wrapper\n",
    "    return decorate\n",
    "\n",
    "@timer\n",
    "def slow_sum(n): return sum(range(n))\n",
    "\n",
    "calls = {\"n\": 0}\n",
    "@retry(times=3, exceptions=(ConnectionError,))\n",
    "def flaky():\n",
    "    calls[\"n\"] += 1\n",
    "    if calls[\"n\"] < 3:\n",
    "        raise ConnectionError(\"corte de red momentáneo\")\n",
    "    return \"ok\"\n",
    "\n",
    "print(slow_sum(5_000_000), flaky())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "c1a284f6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:34.512589Z",
     "iopub.status.busy": "2026-10-18T10:10:34.512158Z",
     "iopub.status.idle": "2026-10-18T10:10:34.520910Z",
     "shell.execute_reply": "2026-10-18T10:10:34.518766Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "280571172992510140037611932413038677189525\n",
      "0.58 ms con caché\n",
      "CacheInfo(hits=198, misses=201, maxsize=None, currsize=201)\n"
     ]
    }
   ],
   "source": [
    "@functools.lru_cache(maxsize=None)          # memoización de la biblioteca estándar\n",
    "def fib(n): return n if n < 2 else fib(n - 1) + fib(n - 2)\n",
    "\n",
    "t0 = time.perf_counter(); print(fib(200)); print(f\"{(time.perf_counter() - t0) * 1000:.2f} ms con caché\")\n",
    "print(fib.cache_info())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bfea2d53",
   "metadata": {},
   "source": [
    "## 3. Gestores de contexto\n",
    "`with` garantiza que cada preparación vaya acompañada de su limpieza, incluso si se lanza una excepción. Los\n",
    "archivos, los cerrojos, las transacciones de bases de datos y los ajustes temporales lo usan."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "86b8f7fc",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:34.525598Z",
     "iopub.status.busy": "2026-10-18T10:10:34.525200Z",
     "iopub.status.idle": "2026-10-18T10:10:34.657966Z",
     "shell.execute_reply": "2026-10-18T10:10:34.656014Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "dentro: True ['notes.txt']\n",
      "de vuelta: True\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "el bloque tardó 119.1 ms\n"
     ]
    }
   ],
   "source": [
    "from contextlib import contextmanager, suppress\n",
    "import os, tempfile\n",
    "\n",
    "@contextmanager\n",
    "def working_directory(path):\n",
    "    old = os.getcwd()\n",
    "    os.chdir(path)\n",
    "    try:\n",
    "        yield path\n",
    "    finally:\n",
    "        os.chdir(old)                         # se ejecuta siempre\n",
    "\n",
    "tmp = tempfile.mkdtemp()\n",
    "with working_directory(tmp):\n",
    "    with open(\"notes.txt\", \"w\") as f:         # el archivo se cierra al terminar el bloque\n",
    "        f.write(\"hello\")\n",
    "    print(\"dentro:\", os.getcwd() == os.path.realpath(tmp), os.listdir(\".\"))\n",
    "print(\"de vuelta:\", os.getcwd() != os.path.realpath(tmp))\n",
    "\n",
    "with suppress(FileNotFoundError):             # ignora un error concreto y esperado\n",
    "    os.remove(\"does-not-exist.txt\")\n",
    "\n",
    "class Timer:\n",
    "    '''La forma de clase: __enter__ y __exit__.'''\n",
    "    def __enter__(self):\n",
    "        self.t0 = time.perf_counter(); return self\n",
    "    def __exit__(self, exc_type, exc, tb):\n",
    "        self.elapsed = time.perf_counter() - self.t0\n",
    "        return False                          # no se tragan las excepciones\n",
    "\n",
    "with Timer() as t:\n",
    "    sum(i * i for i in range(1_000_000))\n",
    "print(f\"el bloque tardó {t.elapsed * 1000:.1f} ms\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f2da825",
   "metadata": {},
   "source": [
    "## 4. Herramientas funcionales"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "525a78ef",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:34.662675Z",
     "iopub.status.busy": "2026-10-18T10:10:34.662261Z",
     "iopub.status.idle": "2026-10-18T10:10:34.674327Z",
     "shell.execute_reply": "2026-10-18T10:10:34.672294Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "49 27\n",
      "120\n",
      "[('bag', 25), ('book', 12), ('pen', 3)]\n",
      "[('book', 12), ('bag', 25)] ['book', 'bag']\n"
     ]
    }
   ],
   "source": [
    "from functools import partial, reduce\n",
    "from operator import mul, itemgetter\n",
    "\n",
    "power = lambda base, exp: base ** exp\n",
    "square, cube = partial(power, exp=2), partial(power, exp=3)\n",
    "print(square(7), cube(3))\n",
    "\n",
    "print(reduce(mul, range(1, 6)))                        # 5! = 120\n",
    "orders = [(\"pen\", 3), (\"book\", 12), (\"bag\", 25)]\n",
    "print(sorted(orders, key=itemgetter(1), reverse=True))\n",
    "print(list(filter(lambda o: o[1] > 5, orders)), [name for name, price in orders if price > 5])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b5635b82",
   "metadata": {},
   "source": [
    "## 5. Anotaciones de tipo\n",
    "Las anotaciones documentan la intención y permiten que las herramientas (`mypy`, `pyright`, tu editor)\n",
    "detecten errores antes de ejecutar el código."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "68b85c50",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:34.679530Z",
     "iopub.status.busy": "2026-10-18T10:10:34.679103Z",
     "iopub.status.idle": "2026-10-18T10:10:34.690726Z",
     "shell.execute_reply": "2026-10-18T10:10:34.688675Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'name': 'ben', 'role': 'admin'}\n",
      "{'items': typing.Iterable[~T], 'predicate': typing.Callable[[~T], bool], 'return': typing.Optional[~T]}\n"
     ]
    }
   ],
   "source": [
    "from typing import TypedDict, Literal, Callable, Iterable, TypeVar\n",
    "\n",
    "T = TypeVar(\"T\")\n",
    "\n",
    "class User(TypedDict):\n",
    "    name: str\n",
    "    role: Literal[\"admin\", \"viewer\"]\n",
    "\n",
    "def first_match(items: Iterable[T], predicate: Callable[[T], bool]) -> T | None:\n",
    "    return next((i for i in items if predicate(i)), None)\n",
    "\n",
    "users: list[User] = [{\"name\": \"ana\", \"role\": \"viewer\"}, {\"name\": \"ben\", \"role\": \"admin\"}]\n",
    "print(first_match(users, lambda u: u[\"role\"] == \"admin\"))\n",
    "print(first_match.__annotations__)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00c38a6a",
   "metadata": {},
   "source": [
    "## 6. `logging` en lugar de `print`\n",
    "`logging` añade niveles, marcas de tiempo y destinos (consola, archivo, un servicio de registros) que puedes\n",
    "cambiar sin tocar el código."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "dd23dcc2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:34.695482Z",
     "iopub.status.busy": "2026-10-18T10:10:34.695071Z",
     "iopub.status.idle": "2026-10-18T10:10:34.705268Z",
     "shell.execute_reply": "2026-10-18T10:10:34.703599Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "12:10:34 INFO    pipeline: cargadas 1200 filas\n",
      "12:10:34 WARNING pipeline: a 3 filas les faltaba el precio\n",
      "12:10:34 ERROR   pipeline: la división falló\n",
      "Traceback (most recent call last):\n",
      "  File \"/tmp/ipykernel_1035851/2157448921.py\", line 12, in <module>\n",
      "    1 / 0\n",
      "    ~~^~~\n",
      "ZeroDivisionError: division by zero\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import logging, io\n",
    "\n",
    "stream = io.StringIO()\n",
    "logging.basicConfig(stream=stream, level=logging.INFO, force=True,\n",
    "                    format=\"%(asctime)s %(levelname)-7s %(name)s: %(message)s\", datefmt=\"%H:%M:%S\")\n",
    "log = logging.getLogger(\"pipeline\")\n",
    "\n",
    "log.debug(\"no se muestra: por debajo del nivel INFO\")\n",
    "log.info(\"cargadas %d filas\", 1200)              # argumentos, no f-strings: solo se formatea si se emite\n",
    "log.warning(\"a 3 filas les faltaba el precio\")\n",
    "try:\n",
    "    1 / 0\n",
    "except ZeroDivisionError:\n",
    "    log.exception(\"la división falló\")          # incluye la traza\n",
    "print(stream.getvalue())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ed583cc7",
   "metadata": {},
   "source": [
    "## 7. Pruebas con pytest\n",
    "Las pruebas son funciones pequeñas cuyo nombre empieza por `test_`; basta con `assert`. `pytest` las encuentra\n",
    "y las ejecuta. A continuación se escriben en disco un módulo diminuto y sus pruebas, y se ejecutan."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "eb658209",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:34.710164Z",
     "iopub.status.busy": "2026-10-18T10:10:34.709735Z",
     "iopub.status.idle": "2026-10-18T10:10:35.465553Z",
     "shell.execute_reply": "2026-10-18T10:10:35.463829Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m                                                                    [100%]\u001b[0m\n",
      "\u001b[32m\u001b[32m\u001b[1m5 passed\u001b[0m\u001b[32m in 0.02s\u001b[0m\u001b[0m\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import subprocess, sys, textwrap\n",
    "proj = tempfile.mkdtemp()\n",
    "with open(os.path.join(proj, \"pricing.py\"), \"w\") as f:\n",
    "    f.write(textwrap.dedent(\"\"\"\n",
    "        def apply_discount(price: float, percent: float) -> float:\n",
    "            if not 0 <= percent <= 100:\n",
    "                raise ValueError(\"percent must be between 0 and 100\")\n",
    "            return round(price * (1 - percent / 100), 2)\n",
    "    \"\"\"))\n",
    "with open(os.path.join(proj, \"test_pricing.py\"), \"w\") as f:\n",
    "    f.write(textwrap.dedent(\"\"\"\n",
    "        import pytest\n",
    "        from pricing import apply_discount\n",
    "\n",
    "        def test_basic():\n",
    "            assert apply_discount(100, 20) == 80\n",
    "\n",
    "        @pytest.mark.parametrize(\"price, pct, expected\", [(50, 0, 50), (50, 100, 0), (19.99, 15, 16.99)])\n",
    "        def test_many(price, pct, expected):\n",
    "            assert apply_discount(price, pct) == expected\n",
    "\n",
    "        def test_rejects_bad_percent():\n",
    "            with pytest.raises(ValueError):\n",
    "                apply_discount(10, 120)\n",
    "    \"\"\"))\n",
    "r = subprocess.run([sys.executable, \"-m\", \"pytest\", \"-q\", proj], capture_output=True, text=True, cwd=proj)\n",
    "print(r.stdout[-400:])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c37d7a8f",
   "metadata": {},
   "source": [
    "## 8. Concurrencia: hilos, procesos, asyncio\n",
    "Python tiene un *bloqueo global del intérprete* (GIL): en el intérprete estándar solo un hilo ejecuta bytecode\n",
    "de Python a la vez. La consecuencia:\n",
    "\n",
    "- Trabajo **limitado por E/S** (esperar a redes, discos, bases de datos): los hilos o `asyncio` solapan las esperas.\n",
    "- Trabajo **limitado por CPU** (cálculo en Python puro): solo los **procesos** usan varios núcleos (o\n",
    "  bibliotecas como NumPy que liberan el GIL internamente). Python 3.13 añade una compilación opcional sin GIL\n",
    "  (free-threaded); todavía es experimental.\n",
    "\n",
    "Medido a continuación con una tarea de E/S simulada (dormir) y una tarea de CPU (contar primos):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "3837b780",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:35.470656Z",
     "iopub.status.busy": "2026-10-18T10:10:35.470102Z",
     "iopub.status.idle": "2026-10-18T10:10:39.406671Z",
     "shell.execute_reply": "2026-10-18T10:10:39.404942Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "E/S: secuencial                     1.60 s\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "E/S: 8 hilos                        0.20 s\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "E/S: asyncio                        0.20 s\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU: secuencial                     0.65 s\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU: 4 hilos                        0.93 s\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU: 4 procesos                     0.30 s\n"
     ]
    }
   ],
   "source": [
    "import asyncio\n",
    "from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor\n",
    "\n",
    "def io_task(_):\n",
    "    time.sleep(0.2)                          # representa la espera de una petición de red\n",
    "    return 1\n",
    "\n",
    "def cpu_task(n):\n",
    "    return sum(1 for k in range(2, n) if all(k % d for d in range(2, int(k ** 0.5) + 1)))\n",
    "\n",
    "def timed(label, fn):\n",
    "    t0 = time.perf_counter(); fn(); dt = time.perf_counter() - t0\n",
    "    print(f\"{label:<34}{dt:6.2f} s\"); return dt\n",
    "\n",
    "JOBS, N = 8, 60_000\n",
    "results = {}\n",
    "results[\"E/S secuencial\"] = timed(\"E/S: secuencial\", lambda: [io_task(i) for i in range(JOBS)])\n",
    "with ThreadPoolExecutor(8) as ex:\n",
    "    results[\"E/S hilos\"] = timed(\"E/S: 8 hilos\", lambda: list(ex.map(io_task, range(JOBS))))\n",
    "\n",
    "async def io_async(_):\n",
    "    await asyncio.sleep(0.2)\n",
    "async def main():\n",
    "    await asyncio.gather(*(io_async(i) for i in range(JOBS)))\n",
    "# los notebooks ya tienen un bucle de eventos en marcha, así que asyncio se ejecuta en un hilo nuevo para esta demo\n",
    "with ThreadPoolExecutor(1) as ex:\n",
    "    results[\"E/S asyncio\"] = timed(\"E/S: asyncio\", lambda: ex.submit(asyncio.run, main()).result())\n",
    "\n",
    "results[\"CPU secuencial\"] = timed(\"CPU: secuencial\", lambda: [cpu_task(N) for _ in range(4)])\n",
    "with ThreadPoolExecutor(4) as ex:\n",
    "    results[\"CPU hilos\"] = timed(\"CPU: 4 hilos\", lambda: list(ex.map(cpu_task, [N] * 4)))\n",
    "# Los procesos de trabajo deben poder encontrar cpu_task. Las funciones definidas en un notebook solo las ven\n",
    "# con el método de arranque \"fork\" (Linux, Colab). En un script .py funciona cualquier método.\n",
    "import multiprocessing as mp\n",
    "ctx = mp.get_context(\"fork\") if \"fork\" in mp.get_all_start_methods() else None\n",
    "if ctx:\n",
    "    with ProcessPoolExecutor(4, mp_context=ctx) as ex:\n",
    "        results[\"CPU procesos\"] = timed(\"CPU: 4 procesos\", lambda: list(ex.map(cpu_task, [N] * 4)))\n",
    "else:\n",
    "    print(\"CPU: en esta plataforma los procesos necesitan un archivo .py (sin 'fork'); mira el ejercicio de abajo\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "d58df3ed",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-18T10:10:39.411119Z",
     "iopub.status.busy": "2026-10-18T10:10:39.410723Z",
     "iopub.status.idle": "2026-10-18T10:10:40.685797Z",
     "shell.execute_reply": "2026-10-18T10:10:40.683988Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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ldhHXV0dhYWFSm1ZWVnQ19cHABgZGZU7jypUqeXFGubNm4c//vgDnp6eAIDMzEz89ttvJf5BUBptbe1S15aIiIhIblT+QAQ1NTXMnj0bGzZswLVr14ptj4iIUDlUubu7IzAwsNg1mocOHYK7u3uJYxITE6X+jo6OmDVrFjp27IjLly+jcePGcHBwgJ+fX7FxUVFRAAAnJyfY29tj48aNSttjYmKk/7eyspLuDAAA6enpSh8woMo8qlClluclJycjPz8fjRo1ktp27dpVoRBLREREVNtU+K4FkZGR6Ny5M8aNGyfdR/batWu4fv06bt26VeJlAS+aMWMGfv/9d7i7u2P8+PHQ1dXF4cOHcfPmTZw9e7bEMbGxsfD09ISnpyecnZ0REhKCCxcuYOnSpVAoFNi8eTP69euHPn36oF+/fkhKSsLhw4cxdOhQzJkzR7q/bf/+/fH06VN0794dd+/exYMHD/DXX38BePZGruHDh8PMzAwWFhbSdbBFVJlHFarU8jwLCwt06tQJ48aNw6RJk3D//n1s2bIFGhov9XkWRERERLVChT6iVk1NDevWrcO5c+dgaWmJK1euIC0tDUOGDMHNmzehpvZsd126dEHfvn1L3Y+Ojg6Cg4OxcOFCJCYmIjw8HP369UNoaCiaNm1a4pimTZsiKCgItra2uHHjBurWrYtr165Jt8Xq0qULQkJC0LNnT9y6dQsA4OfnpxQuPTw8cOvWLbi6uuL27dto3rw59uzZI2338vLCwYMHkZOTg7i4OPj5+eGLL75QejOVKvP06dNHutyhiLu7O3r06KFyLT169FA6O33kyBGMHDkSN27cgIGBAf755x9MmjRJ6c13L44hIiIiqs0Ugq9Pv7FSU1NhZGSE737ZC11dvfIHEFG5Phqj+p1QiIiouKJ8kpKSUuzDpF5UoTOyREREREQ1BYMsEREREckSgywRERERyRKDLBERERHJEoMsEREREckSgywRERERyRKDLBERERHJEoMsEREREckSP+OUMHV4z3JvOExERERU0/CMLBERERHJEoMsEREREckSgywRERERyRKDLBERERHJEoMsEREREckSgywRERERyRKDLBERERHJEu8jS7D+PQ6KOtnVXQYR0UtJGVW/uksgomrCM7JEREREJEsMskREREQkSwyyRERERCRLDLJEREREJEsMskREREQkSwyyRERERCRLDLJEREREJEsMskREREQkSwyyRERERCRLDLKVxMvLC76+vmX2GT16NObPn1/qYyIiIiIqXa0Osqmpqfjyyy/Rrl07NGjQAM2bN8fYsWNx9epVqc+HH34IY2NjGBsbw8zMDG3atMGyZcuQm5sLAOjVqxdmzJhRbN+///47jI2NkZOTU+Lc6enpyMrKKrO+jIwMZGZmlvqYiIiIiEqnUd0FVJa4uDh07doVhoaGWLp0KVxdXZGUlITr169j6tSpCAoKgrq6OjIzM9G8eXP4+/sjLy8PAQEBGDduHJKSkvDtt98iLS2txHCZm5uLlJQUCCFeusZt27ZBTa1W/y1BREREVGlqbYqaOXMmUlJScOrUKfTt2xcNGjRA06ZNMWLECFy8eBHq6upSXw0NDemM7KBBgzB+/Hjs3r37lWtIS0vD9OnT0aRJEzg5OWHRokUoLCyUtk+dOhWLFy8udXxhYSG++uorNG3aFObm5ujVqxf++ecfpT6BgYF4++23YWVlBTc3N/zyyy+vXDcRERGRHNTKIJuZmYk9e/bgo48+gr6+foXHa2trIy8v75XrWLNmDRwcHHD8+HH89NNPWLlyJXbs2CFtL+9Sgi+//BKrV6/Gf//7XwQFBaF58+bo0aMHHj16BADIysrCu+++ix49euDixYtYv349rl27hitXrrxy7UREREQ1Xa0Msvfu3UNeXh6aNm1a4bHXr1/H1q1b0adPn1euo1+/fvjkk09gY2OD3r17o3///jh9+rRKY3Nzc/Hdd9/hq6++Qt++fdGoUSOsXLkStra2+OGHHwAAMTExSEtLw4QJE2BpaQlXV1f8/PPPaNOmTYn7zMnJQWpqqtIXERERkVzVyiBbUFAA4NklA6oIDAyEsbEx9PX10b17d3h6emLlypWvXMeLQbpevXpISEhQaez9+/eRmZmJzp07S20KhQJdunTBzZs3AQCNGjVCly5d4OHhgSVLliAwMFA69pIsW7YMRkZG0peNjc1LHBURERFRzVArg2yjRo2gpqaGsLAwlfq7u7sjMjISsbGxSEpKwoYNG2BkZAQA0NfXR0ZGRrExGRkZ0NLSgpaWVqn7LemNXKq+OSw/Px9A8TCuoaEhbVNTU8Pp06fx3XffITY2FuPGjUOTJk1w7969Eve5YMECpKSkSF8PHz5UqRYiIiKimqhWBlkjIyP06dMH69atU+la16I3e+np6RXb5uTkhNu3bxdrv3XrFhwcHCrtrgMODg7Q0NDA5cuXldovX74MZ2dn6bGGhgYGDhwIPz8/hIWFQV9fH2vXri1xn9ra2jA0NFT6IiIiIpKrWhlkAWDVqlVITk7GkCFDcPfuXQDP3hwVFBSEIUOGlPkS/POmTZuG27dv4/vvv0dWVhYKCgpw9OhRbN68GdOnT6+0+nV1dfHBBx9g0aJFCA0NRV5eHlatWoXLly9L8166dAmffvopoqKiAAAPHz5EfHw8rK2tK60uIiIiopqi1t5H1tHREZcvX8aiRYvQqVMnZGZmQlNTEy1btsScOXOUbr9VlpYtW+Lw4cP47LPPsHjxYhQWFsLKygpff/01Pvjgg0o9hm+++QZZWVlwdXVFXl4e7OzssGfPHumMbIsWLXDq1Cl069YNsbGx0NHRwbhx4/DRRx9Val1ERERENYFCvMod/WUkKysLurq6xdozMzNRWFio0m26hBDIz8+HpqZmuX0zMjKgrq4OHR0dpRoKCwulSxgyMjKgpqYm1fXi4yKFhYXIyckpsf4iOTk50NbWLreu56WmpsLIyAgG68OhqGNQobFERDVFyqj61V0CEb1GRfkkJSWl3Msga+0Z2ReVFgLr1Kmj8j4UCoVKIRZAidfbvljDi31KGgOgxHD7ooqGWCIiIiK5q7XXyBIRERFR7cYgS0RERESyxCBLRERERLLEIEtEREREssQgS0RERESyxCBLRERERLLEIEtEREREssQgS0RERESy9MZ8IAKVLnqYWbmfnEFERERU0/CMLBERERHJEoMsEREREckSgywRERERyRKDLBERERHJEoMsEREREckSgywRERERyRKDLBERERHJEu8jSxj53mNoaqRXdxlvrD+PWlZ3CURERLLEM7JEREREJEsMskREREQkSwyyRERERCRLDLJEREREJEsMskREREQkSwyyRERERCRLDLJEREREJEsMskREREQkSwyyRERERCRLVfLJXiEhIfjtt99w+/ZtaGpqwtnZGePGjUPjxo0BAOfPn8eiRYsAAAqFAubm5ujYsSMmT54MbW1t7Ny5E7t378bevXuV9pudnY3+/ftj4cKF6N69e1UcChERERHVEJV+Rvann35Cq1atEBcXhzFjxmDUqFHQ19fH4MGDceLECQDA06dPcfLkSUyfPh2f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",
      "text/plain": [
       "<Figure size 700x340 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "plt.figure(figsize=(7, 3.4))\n",
    "colors = [\"#94a3b8\", \"#0ea5e9\", \"#22c55e\", \"#94a3b8\", \"#0ea5e9\", \"#4f46e5\"]\n",
    "plt.barh(list(results), list(results.values()), color=colors); plt.gca().invert_yaxis()\n",
    "plt.xlabel(\"segundos\"); plt.title(\"Los hilos ayudan a esperar; los procesos, a calcular\"); plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0a00c812",
   "metadata": {},
   "source": [
    "Los hilos y asyncio terminan las tareas de E/S en aproximadamente el tiempo de una sola, porque las esperas se\n",
    "solapan. Para las tareas de CPU los hilos no aceleran nada, a menudo incluso ralentizan un poco (el GIL más el\n",
    "coste de alternar), mientras que los procesos se ejecutan en núcleos distintos y reducen el tiempo a menos de la\n",
    "mitad. La ganancia queda por debajo del ideal de 4 veces porque arrancar procesos de trabajo y enviarles el\n",
    "trabajo tiene un coste, que pesa en tareas tan cortas.\n",
    "\n",
    "## Ejercicios\n",
    "1. Escribe un decorador `@count_calls` que registre cuántas veces se ha llamado a una función.\n",
    "2. Convierte `Timer` en decorador además de gestor de contexto (`contextlib.ContextDecorator`).\n",
    "3. Añade una prueba para `apply_discount` con un precio negativo y decide qué debería hacer la función.\n",
    "4. Reescribe la prueba de CPU con NumPy (criba vectorizada) y compárala con cuatro procesos."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
