{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "8f28136f",
   "metadata": {},
   "source": [
    "# 06 · Pythonic Techniques\n",
    "\n",
    "The patterns that distinguish fluent Python: iterators and generators, decorators, context managers,\n",
    "functional tools, type hints, logging, automated tests and concurrency. Each one appears constantly in\n",
    "libraries you will use and in code reviews.\n",
    "\n",
    "**You will learn**\n",
    "1. Iterators and generators, and the `itertools` toolbox\n",
    "2. Decorators: timing, caching, retrying\n",
    "3. Context managers: guaranteed cleanup with `with`\n",
    "4. Functional tools: `functools.partial`, `reduce`, `map`/`filter`\n",
    "5. Type hints that tools can check\n",
    "6. Logging instead of `print`\n",
    "7. Testing with `pytest`\n",
    "8. Concurrency: threads, processes and `asyncio`, and which one fits which problem (measured)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d199db96",
   "metadata": {},
   "source": [
    "## 1. Iterators, generators and `itertools`\n",
    "Anything you can loop over is an *iterable*; `iter()` gives an *iterator* that hands out items one at a\n",
    "time. Generators are the easy way to write iterators, and `itertools` combines them without building\n",
    "lists in memory."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "f694b096",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:23:44.223306Z",
     "iopub.status.busy": "2026-10-17T23:23:44.222572Z",
     "iopub.status.idle": "2026-10-17T23:23:44.242510Z",
     "shell.execute_reply": "2026-10-17T23:23:44.241086Z"
    }
   },
   "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)))                 # infinite counter, first 5\n",
    "print(list(it.accumulate([1, 2, 3, 4])))                              # running total\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": "755d175f",
   "metadata": {},
   "source": [
    "## 2. Decorators\n",
    "A decorator is a function that takes a function and returns a new one with extra behaviour. `@timer`\n",
    "above a definition means `f = timer(f)`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "5c8b2377",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:23:44.248399Z",
     "iopub.status.busy": "2026-10-17T23:23:44.247830Z",
     "iopub.status.idle": "2026-10-17T23:23:44.423195Z",
     "shell.execute_reply": "2026-10-17T23:23:44.421969Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "slow_sum took 159.7 ms\n",
      "attempt 1 failed: network blip\n",
      "attempt 2 failed: network blip\n",
      "12499997500000 ok\n"
     ]
    }
   ],
   "source": [
    "import functools, time\n",
    "\n",
    "def timer(func):\n",
    "    @functools.wraps(func)                  # keep the original name and docstring\n",
    "    def wrapper(*args, **kwargs):\n",
    "        t0 = time.perf_counter()\n",
    "        result = func(*args, **kwargs)\n",
    "        print(f\"{func.__name__} took {(time.perf_counter() - t0) * 1000:.1f} ms\")\n",
    "        return result\n",
    "    return wrapper\n",
    "\n",
    "def retry(times: int, exceptions=(Exception,)):\n",
    "    '''A decorator with arguments: a function that returns a decorator.'''\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\"attempt {attempt} failed: {e}\")\n",
    "            raise RuntimeError(f\"{func.__name__} failed {times} times\")\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(\"network blip\")\n",
    "    return \"ok\"\n",
    "\n",
    "print(slow_sum(5_000_000), flaky())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "c8ddbb01",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:23:44.427966Z",
     "iopub.status.busy": "2026-10-17T23:23:44.427578Z",
     "iopub.status.idle": "2026-10-17T23:23:44.437495Z",
     "shell.execute_reply": "2026-10-17T23:23:44.435237Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "280571172992510140037611932413038677189525\n",
      "0.53 ms with caching\n",
      "CacheInfo(hits=198, misses=201, maxsize=None, currsize=201)\n"
     ]
    }
   ],
   "source": [
    "@functools.lru_cache(maxsize=None)          # memoisation from the standard library\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 with caching\")\n",
    "print(fib.cache_info())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fd2bb92e",
   "metadata": {},
   "source": [
    "## 3. Context managers\n",
    "`with` guarantees that setup is paired with cleanup, even when an exception is raised. Files, locks,\n",
    "database transactions and temporary settings all use it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "ee283f81",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:23:44.441604Z",
     "iopub.status.busy": "2026-10-17T23:23:44.441258Z",
     "iopub.status.idle": "2026-10-17T23:23:44.588167Z",
     "shell.execute_reply": "2026-10-17T23:23:44.586216Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "inside: True ['notes.txt']\n",
      "back to: True\n",
      "block took 132.5 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)                         # always runs\n",
    "\n",
    "tmp = tempfile.mkdtemp()\n",
    "with working_directory(tmp):\n",
    "    with open(\"notes.txt\", \"w\") as f:         # the file is closed when the block ends\n",
    "        f.write(\"hello\")\n",
    "    print(\"inside:\", os.getcwd() == os.path.realpath(tmp), os.listdir(\".\"))\n",
    "print(\"back to:\", os.getcwd() != os.path.realpath(tmp))\n",
    "\n",
    "with suppress(FileNotFoundError):             # ignore one specific, expected error\n",
    "    os.remove(\"does-not-exist.txt\")\n",
    "\n",
    "class Timer:\n",
    "    '''The class form: __enter__ and __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                          # do not swallow exceptions\n",
    "\n",
    "with Timer() as t:\n",
    "    sum(i * i for i in range(1_000_000))\n",
    "print(f\"block took {t.elapsed * 1000:.1f} ms\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e27db20a",
   "metadata": {},
   "source": [
    "## 4. Functional tools"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "b5811fa6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:23:44.593698Z",
     "iopub.status.busy": "2026-10-17T23:23:44.593280Z",
     "iopub.status.idle": "2026-10-17T23:23:44.603412Z",
     "shell.execute_reply": "2026-10-17T23:23:44.601515Z"
    }
   },
   "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": "7923ae05",
   "metadata": {},
   "source": [
    "## 5. Type hints\n",
    "Hints document intent and let tools (`mypy`, `pyright`, your editor) catch bugs before the code runs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "8f29eb79",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:23:44.608090Z",
     "iopub.status.busy": "2026-10-17T23:23:44.607543Z",
     "iopub.status.idle": "2026-10-17T23:23:44.618733Z",
     "shell.execute_reply": "2026-10-17T23:23:44.616574Z"
    }
   },
   "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": "29de42bb",
   "metadata": {},
   "source": [
    "## 6. Logging instead of print\n",
    "`logging` adds levels, timestamps and destinations (console, file, a log service) that you can change\n",
    "without touching the code."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "c992f303",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:23:44.621903Z",
     "iopub.status.busy": "2026-10-17T23:23:44.621561Z",
     "iopub.status.idle": "2026-10-17T23:23:44.630693Z",
     "shell.execute_reply": "2026-10-17T23:23:44.628626Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "01:23:44 INFO    pipeline: loaded 1200 rows\n",
      "01:23:44 WARNING pipeline: 3 rows had missing prices\n",
      "01:23:44 ERROR   pipeline: division failed\n",
      "Traceback (most recent call last):\n",
      "  File \"/tmp/ipykernel_821678/3560669218.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(\"not shown: below the INFO level\")\n",
    "log.info(\"loaded %d rows\", 1200)              # arguments, not f-strings: formatted only if emitted\n",
    "log.warning(\"3 rows had missing prices\")\n",
    "try:\n",
    "    1 / 0\n",
    "except ZeroDivisionError:\n",
    "    log.exception(\"division failed\")          # includes the traceback\n",
    "print(stream.getvalue())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "de2c3e1a",
   "metadata": {},
   "source": [
    "## 7. Testing with pytest\n",
    "Tests are small functions whose names start with `test_`; `assert` is all you need. `pytest` finds and\n",
    "runs them. Below, a tiny module and its tests are written to disk and run."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "f658de33",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:23:44.635787Z",
     "iopub.status.busy": "2026-10-17T23:23:44.635290Z",
     "iopub.status.idle": "2026-10-17T23:23:45.955922Z",
     "shell.execute_reply": "2026-10-17T23:23:45.954014Z"
    }
   },
   "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": "fd99816c",
   "metadata": {},
   "source": [
    "## 8. Concurrency: threads, processes, asyncio\n",
    "Python has a *global interpreter lock* (GIL): in the standard interpreter only one thread runs Python\n",
    "bytecode at a time. The consequence:\n",
    "\n",
    "- **I/O-bound** work (waiting for networks, disks, databases): threads or `asyncio` overlap the waiting.\n",
    "- **CPU-bound** work (pure Python computation): only **processes** use several cores (or libraries like\n",
    "  NumPy that release the GIL internally). Python 3.13 adds an optional free-threaded build that removes\n",
    "  the GIL; it is still experimental.\n",
    "\n",
    "Measured below with a simulated I/O task (sleeping) and a CPU task (counting primes):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "5c1dd133",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:23:45.960101Z",
     "iopub.status.busy": "2026-10-17T23:23:45.959756Z",
     "iopub.status.idle": "2026-10-17T23:23:49.928798Z",
     "shell.execute_reply": "2026-10-17T23:23:49.926795Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "I/O: sequential                     1.60 s\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "I/O: 8 threads                      0.20 s\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "I/O: asyncio                        0.20 s\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU: sequential                     0.70 s\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU: 4 threads                      0.90 s\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU: 4 processes                    0.31 s\n"
     ]
    }
   ],
   "source": [
    "import asyncio\n",
    "from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor\n",
    "\n",
    "def io_task(_):\n",
    "    time.sleep(0.2)                          # stands for waiting on a network request\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[\"I/O sequential\"] = timed(\"I/O: sequential\", lambda: [io_task(i) for i in range(JOBS)])\n",
    "with ThreadPoolExecutor(8) as ex:\n",
    "    results[\"I/O threads\"] = timed(\"I/O: 8 threads\", 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",
    "# notebooks already run an event loop, so run asyncio in a fresh thread for this demo\n",
    "with ThreadPoolExecutor(1) as ex:\n",
    "    results[\"I/O asyncio\"] = timed(\"I/O: asyncio\", lambda: ex.submit(asyncio.run, main()).result())\n",
    "\n",
    "results[\"CPU sequential\"] = timed(\"CPU: sequential\", lambda: [cpu_task(N) for _ in range(4)])\n",
    "with ThreadPoolExecutor(4) as ex:\n",
    "    results[\"CPU threads\"] = timed(\"CPU: 4 threads\", lambda: list(ex.map(cpu_task, [N] * 4)))\n",
    "# Worker processes must be able to find cpu_task. Functions defined in a notebook are only visible to\n",
    "# them with the \"fork\" start method (Linux, Colab). In a .py script any start method works.\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 processes\"] = timed(\"CPU: 4 processes\", lambda: list(ex.map(cpu_task, [N] * 4)))\n",
    "else:\n",
    "    print(\"CPU: processes need a .py file on this platform (no 'fork'); see the exercise below\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "abd0c603",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-17T23:23:49.934996Z",
     "iopub.status.busy": "2026-10-17T23:23:49.934315Z",
     "iopub.status.idle": "2026-10-17T23:23:51.302040Z",
     "shell.execute_reply": "2026-10-17T23:23:51.299739Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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/UcffSSOHTsmmjVrJrS1tUXdunWL3UVflhe9N+q+z6XN2cTEpNij5YQo+UkC5ZlPbm6umDZtmrCwsBAGBgaiT58+4t69eyX+jgwODhZeXl7S4/i8vLyka2Wt6/NKev+ysrLExx9/LGrWrCn09fVFt27dREREhGjWrJno3r17sT7o5SmEqOC/fRERVaDJkydj27ZtJd4AQv9n+vTpWLNmDR4/fix94MLKlSvxv//9D5GRkcXOLT6P6/zmMDQ0xOjRo8v1AQjV2Zs0H0tLS/Tq1QsbN26s6lDeGDxaQEQkc/n5+di3bx/atm2r8qlhR48excyZM1+YxBJR5Tt16hSSk5Mr7AgKPaX14ipERFRdPHz4EDNnzoSvry/q16+PmJgYzJ8/H9HR0fj1119V6u7bt6+KoiR6u23cuBHp6eno06cPzMzMcP78eUyePBn169fH0KFDqzq8Nwp3ZImIZMTc3BxdunTBRx99BGtra7Ru3RrJyck4fPgwP8OdqJro168fIiMj0b17d1haWmLMmDHo0qULTpw4AT09vaoO743CM7JEREREJEvckSUiIiIiWWIiS0RERESyxJu93kKFhYWIj4+HkZFRuT4ikIiIiKiyCSGQkZEBGxsbaGiUvefKRPYtFB8fD3t7+6oOg4iIiKhUsbGxsLOzK7MOE9m3UNHH+cXGxsLY2LiKoyEiIiL6P+np6bC3ty/z45SLMJF9CxUdJzA2NmYiS0RERNWSOscfebMXEREREckSE1kiIiIikiUmskREREQkS0xkiYiIiEiWmMgSERERkSwxkSUiIiIiWWIiS0RERESyxESWiIiIiGSJH4jwFlu//V/o6RlUdRhEREQkEx+N6FHVIajgjiwRERERyRITWSIiIiKSJSayRERERCRLTGSJiIiISJaYyBIRERGRLDGRJSIiIiJZYiJLRERERLLERJaIiIiIZImJLBERERHJEhPZambFihX4/fffK70NERERkdxVaCK7Zs0afP7558XKt27dirVr16qUnThxArNmzcKoUaMwa9YsHDt2rCJDkYWvvvoKmzZtUik7ffo0Ll++XK5+XqYNERERkdxVaCIbEBBQYkK6cuVK6OrqAgDy8/MxZMgQvP/++9DV1UXXrl1hYGCAQYMGYciQIcjPz6/IkKq1U6dO4cqVKypls2bNwtixY6soIiIiIiL50KrsAZKS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OHEiXF1dpTO506dPf+n4iYiIiOSmWieyZmZmaN26NbS0tJCVlYUTJ05g1apVxeotWLAAnTt3xsaNG7FgwQLo6urC09MT69evh5WVVan9N23aFGvXrsXWrVuRkpKCKVOmoFatWsWOIpiYmEhPSQBQYh0AGDx4MOrUqYN169Zh6dKlsLGxQe/evTF06FAAgEKhwD///IMlS5bg888/h7W1NbZv344FCxaoHBv4+OOPoa+vjx07dsDIyAjDhw9H+/btpeMBLVq0wG+//YY1a9Zg+/btsLKywo4dO9CrV6/yLC8RERGRrCmEEKKqg1DH3r17MXv2bISFhVV1KLKXnp4OExMTGP18Bwp9o6oOp8qlDav54kpERET0WhTlKWlpaS98Pr5szshGRERgwoQJVR0GEREREVUT1fpowbOefXwUEREREZFsdmSJiIiIiJ7FRJaIiIiIZImJLBERERHJEhNZIiIiIpIlJrJEREREJEtMZImIiIhIlpjIEhEREZEsyeY5slTx4gZZvvATM4iIiIiqK+7IEhEREZEsMZElIiIiIlliIktEREREssREloiIiIhkiYksEREREckSE1kiIiIikiU+fust1jRgEjQMtKs6jCoV0XZjVYdAREREL4k7skREREQkS0xkiYiIiEiWmMgSERERkSwxkSUiIiIiWWIiS0RERESyxESWiIiIiGSJiSwRERERyRITWSIiIiKSJSayRERERCRLTGSruVmzZuHrr7+u6jCIiIiIqp1q/RG1ixYtQnx8PNatW6dS/t133yE7OxufffYZACAvLw8bN26En58fEhMTUbNmTfTq1QtjxoyBUqmsitArzJ07d/DkyZOqDoOIiIio2qnWiezdu3cRFRVVrPy3337D/PnzAQCPHz9G9+7dkZiYiHnz5qFevXqIiIjA0qVLsWHDBhw+fBiGhoavOfKKs2LFCtkn40RERESVQXZHC2JiYhAWFoYePXoAAObOnYvg4GCcPXsWo0ePRtu2bTFy5EicPXsWYWFhmDNnTql9LV++HK1atUKrVq3Qo0cPLFiwAJmZmSp1wsPDMWnSJHTo0AFDhw7FsWPHAAA7duxAz549i/W5aNEi+Pr6AgA++ugjrFy5EitXrkTv3r3Rs2dPbNq0qVibkydPYtiwYWjXrh2mTJmC+/fvS9fWrFmD7du3q9Tfvn07+vTpAy8vL3z4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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(\"seconds\"); plt.title(\"Threads help waiting; processes help computing\"); plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a31532f",
   "metadata": {},
   "source": [
    "Threads and asyncio finish the I/O jobs in about the time of one job, because the waiting overlaps.\n",
    "For the CPU jobs threads give no speed-up, often a small slow-down (the GIL plus switching overhead),\n",
    "while processes run on separate cores and cut the time by more than half. The gain is below the ideal\n",
    "4x because starting worker processes and sending work to them has a cost, which matters for jobs this short.\n",
    "\n",
    "## Exercises\n",
    "1. Write a decorator `@count_calls` that records how many times a function has been called.\n",
    "2. Turn `Timer` into a decorator as well as a context manager (`contextlib.ContextDecorator`).\n",
    "3. Add a test for `apply_discount` with a negative price and decide what the function should do.\n",
    "4. Rewrite the CPU benchmark with NumPy (vectorised sieve) and compare it with four processes."
   ]
  }
 ],
 "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
}
