{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# namedtuple and friends\n",
    "\n",
    "This notebook covers the rest of the module: `namedtuple`, which gives a plain tuple's fields *names*, and the two smaller types `OrderedDict` and `ChainMap`. `namedtuple` is the one you'll reach for most — it's the lightest way to make a small, readable record."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `namedtuple`: a tuple with named fields\n",
    "\n",
    "`(51.5, -0.12)` is a fine pair of coordinates until you forget which is latitude. A `namedtuple` keeps the tuple's lightness and immutability but lets you access fields by name, so the code reads for itself."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "from collections import namedtuple\n",
    "\n",
    "Point = namedtuple('Point', ['x', 'y'])    # or 'x y' as one string\n",
    "p = Point(3, 4)\n",
    "\n",
    "print(p)            # Point(x=3, y=4) — a helpful repr, for free\n",
    "print(p.x, p.y)     # 3 4   — access by name\n",
    "print(p[0])         # 3     — still indexable like a tuple\n",
    "\n",
    "px, py = p          # still unpacks like a tuple\n",
    "print(px, py)       # 3 4"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It *is* a tuple — it compares, hashes, and unpacks like one, so it slots into anything that expects a tuple while being far more readable."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Immutable — change by making a copy\n",
    "\n",
    "Like all tuples, a `namedtuple` can't be modified in place. To get a changed version, use `_replace`, which returns a **new** instance with some fields swapped. (The leading underscore avoids clashing with your field names; it's a public method, not a private one.)"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "from collections import namedtuple\n",
    "Point = namedtuple('Point', 'x y')\n",
    "p = Point(3, 4)\n",
    "\n",
    "try:\n",
    "    p.x = 10                 # tuples are immutable\n",
    "except AttributeError as exc:\n",
    "    print('AttributeError:', exc)\n",
    "\n",
    "moved = p._replace(x=10)     # returns a new Point\n",
    "print(p, '->', moved)        # Point(x=3, y=4) -> Point(x=10, y=4)"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The useful underscore methods\n",
    "\n",
    "`namedtuple` adds a few helpers, all underscore-prefixed: `_asdict()` for a dict, `_fields` for the field names, `_make()` to build one from an iterable, and a `defaults` argument for optional fields."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "from collections import namedtuple\n",
    "\n",
    "Point = namedtuple('Point', 'x y')\n",
    "p = Point(3, 4)\n",
    "\n",
    "print(p._asdict())          # {'x': 3, 'y': 4}\n",
    "print(Point._fields)        # ('x', 'y')\n",
    "print(Point._make([5, 6]))  # Point(x=5, y=6) — build from a sequence (e.g. a CSV row)\n",
    "\n",
    "# defaults fill from the right:\n",
    "Server = namedtuple('Server', 'host port', defaults=[8080])\n",
    "print(Server('localhost'))  # Server(host='localhost', port=8080)"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Where `namedtuple` sits\n",
    "\n",
    "It fills the gap between a bare tuple and a full class:\n",
    "\n",
    "- **vs a tuple** — same speed and immutability, but fields have names and a readable `repr`.\n",
    "- **vs a dict** — immutable, ordered, lighter in memory, and accessed with `.field` rather than `['field']`.\n",
    "- **vs a dataclass** — a `namedtuple` is immutable and *is* a tuple (so it unpacks and compares as one); a `dataclass` is mutable by default and better when you want methods or many fields. The [classes guide](https://agilearn.co.uk/guides/classes-and-objects/index) compares them directly.\n",
    "\n",
    "Reach for `namedtuple` for a small, fixed, immutable record — a coordinate, an RGB colour, a parsed row."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `OrderedDict`: order with extra tools\n",
    "\n",
    "Since Python 3.7, regular dicts remember insertion order, so you rarely *need* `OrderedDict` just for that. It earns its place through a few order-specific abilities a plain dict lacks: `move_to_end`, popping from either end, and order-sensitive equality."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "from collections import OrderedDict\n",
    "\n",
    "od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])\n",
    "od.move_to_end('a')              # send 'a' to the back\n",
    "print(list(od))                  # ['b', 'c', 'a']\n",
    "od.move_to_end('a', last=False)  # ...or to the front\n",
    "print(list(od))                  # ['a', 'b', 'c']\n",
    "\n",
    "print(od.popitem(last=False))    # ('a', 1) — pop from the FRONT (a plain dict can't)"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One more difference: `OrderedDict` equality is **order-sensitive**, while plain dicts compare equal regardless of order. This matters when order is part of the meaning (an LRU cache, an ordered config)."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "from collections import OrderedDict\n",
    "\n",
    "print({'a': 1, 'b': 2} == {'b': 2, 'a': 1})                              # True — order ignored\n",
    "print(OrderedDict(a=1, b=2) == OrderedDict([('b', 2), ('a', 1)]))        # False — order matters"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `ChainMap`: search several dicts as one\n",
    "\n",
    "`ChainMap` groups several dicts into a single view and searches them in order — first match wins. The classic use is layered configuration: command-line overrides on top, then environment, then built-in defaults, all looked up through one mapping without copying anything."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "from collections import ChainMap\n",
    "\n",
    "defaults = {'colour': 'black', 'size': 'medium', 'border': True}\n",
    "user     = {'colour': 'blue'}\n",
    "\n",
    "settings = ChainMap(user, defaults)   # search 'user' first, then 'defaults'\n",
    "print(settings['colour'])             # 'blue'   — from user (it wins)\n",
    "print(settings['size'])               # 'medium' — falls through to defaults\n",
    "print(dict(settings))                 # merged view: {'colour': 'blue', ...}"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Writes go to the **first** mapping only, so you can layer changes without touching the defaults — and `new_child()` adds a fresh top layer (handy for nested scopes)."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "from collections import ChainMap\n",
    "\n",
    "defaults = {'colour': 'black', 'size': 'medium'}\n",
    "settings = ChainMap({}, defaults)\n",
    "settings['colour'] = 'red'            # written to the first (empty) map\n",
    "print(settings['colour'])             # 'red'\n",
    "print(defaults)                       # {'colour': 'black', 'size': 'medium'} — untouched\n",
    "print(settings.maps)                  # [{'colour': 'red'}, {'colour': 'black', 'size': 'medium'}]"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Recap\n",
    "\n",
    "- `namedtuple` is a tuple with named fields: readable, immutable, lightweight; change via `_replace`, inspect via `_asdict`/`_fields`, build via `_make`.\n",
    "- It sits between a tuple and a class — use it for small fixed records.\n",
    "- `OrderedDict` is rarely needed for ordering now, but adds `move_to_end`, front-popping, and order-sensitive equality.\n",
    "- `ChainMap` searches several dicts as one (first wins) — ideal for layered configuration; writes hit only the first map.\n",
    "\n",
    "That's the module. The [Recipes](https://agilearn.co.uk/guides/collections/recipes) put these to work, and the [Concepts](https://agilearn.co.uk/guides/collections/concepts) cover why they exist and how to choose."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}