{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Make a class iterable or container-like\n",
    "\n",
    "**The question.** You have a class that wraps a collection — a playlist of songs, an inventory of items, a page of records — and you want it to behave like one. `for x in obj`, `len(obj)`, `x in obj`, and `obj[i]` should all work without callers having to reach into `obj.items`.\n",
    "\n",
    "Each of those comes from a different dunder method — `__iter__`, `__len__`, `__contains__`, `__getitem__` — and you can pick the ones you need. The complete container shape is a handful of one-liners, each delegating to the underlying list."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Playlist:\n",
    "    def __init__(self, songs=()):\n",
    "        self._songs = list(songs)\n",
    "\n",
    "    def add(self, song):\n",
    "        self._songs.append(song)\n",
    "\n",
    "    def __iter__(self):\n",
    "        # for song in playlist — return a fresh iterator each call\n",
    "        return iter(self._songs)\n",
    "\n",
    "    def __len__(self):\n",
    "        # len(playlist) — also gives you truthiness for free\n",
    "        return len(self._songs)\n",
    "\n",
    "    def __contains__(self, song):\n",
    "        # 'Finale' in playlist\n",
    "        return song in self._songs\n",
    "\n",
    "    def __getitem__(self, index):\n",
    "        # playlist[0], playlist[-1], playlist[::2] — slicing is free\n",
    "        return self._songs[index]\n",
    "\n",
    "    def __repr__(self):\n",
    "        return f'Playlist({self._songs!r})'\n",
    "\n",
    "\n",
    "p = Playlist(['Prelude', 'Interlude', 'Finale'])\n",
    "p.add('Encore')\n",
    "print(p)\n",
    "print('length:', len(p))\n",
    "print('Finale in p:', 'Finale' in p)\n",
    "print('first:', p[0])\n",
    "print('as list:', list(p))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Shortcut: inherit from collections.abc for the full suite\n",
    "from collections.abc import Sequence\n",
    "\n",
    "class Playlist2(Sequence):\n",
    "    def __init__(self, songs=()):\n",
    "        self._songs = list(songs)\n",
    "\n",
    "    # Sequence requires __getitem__ and __len__; everything else is free.\n",
    "    def __getitem__(self, index):\n",
    "        return self._songs[index]\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self._songs)\n",
    "\n",
    "\n",
    "p2 = Playlist2(['Prelude', 'Interlude', 'Finale'])\n",
    "print('index:', p2.index('Interlude'))\n",
    "print('count:', p2.count('Prelude'))\n",
    "print('reversed:', list(reversed(p2)))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why it works\n",
    "\n",
    "Each dunder plugs into a separate Python protocol, which is why they're independent. `for x in obj` looks for `__iter__`; `len(obj)` looks for `__len__`; `x in obj` looks for `__contains__`; `obj[i]` looks for `__getitem__`. Python will fall back sensibly when you don't implement one — `in` will iterate if there's no `__contains__`, and iteration itself will call `__getitem__(0)`, `(1)`, `(2)` until `IndexError` if there's no `__iter__` either. You only implement the ones where you want to supply the behaviour (or outperform the default — a set-backed `__contains__` is O(1) versus the O(n) iterate-and-compare fallback).\n",
    "\n",
    "`__iter__` returning `iter(self._songs)` matters: it hands back a fresh iterator each call, so two nested `for` loops over the same playlist both see every song. Returning `self` and implementing `__next__` makes the object single-use — fine for a generator-like class, wrong for a container.\n",
    "\n",
    "The `__getitem__` line does a lot of work because `self._songs[index]` handles both integers and slices. If you were implementing indexing without a backing list, you'd need to branch on `isinstance(index, slice)` and build the sliced view yourself."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Trade-offs\n",
    "\n",
    "**Implement what you need, not the full set.** A one-shot pipeline wrapper might need `__iter__` and nothing else. A read-only view might need `__getitem__` and `__len__`. Resist the temptation to add every dunder just because you can — each one is a contract you now have to maintain.\n",
    "\n",
    "**`collections.abc.Sequence` or `MutableSequence`** is the shortcut when you want the complete container API. Implement `__getitem__` and `__len__` (plus `__setitem__`, `__delitem__`, `insert` for `MutableSequence`) and the ABC gives you `index`, `count`, `__contains__`, `__iter__`, and `__reversed__` for free. The cost is a subtle API surface — `Sequence` commits you to indexing semantics, which is overkill for, say, a stream-like object.\n",
    "\n",
    "**Hashability and equality** aren't in this recipe, but they're worth naming: if your class is going into a `set` or is a `dict` key, it also needs `__hash__` and `__eq__` — and those have their own gotchas (see \"Avoid common class mistakes\")."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Related reading\n",
    "\n",
    "- [Iterators and generators](https://agilearn.co.uk/guides/iterators-and-generators) — the protocol under `__iter__`, and when `__next__` earns its place.\n",
    "- [Avoid common class mistakes](https://agilearn.co.uk/guides/classes-and-objects/recipes/avoid-common-class-mistakes) — including the mutable-default trap that bites containers especially hard.\n",
    "- [Truthiness rules](https://agilearn.co.uk/guides/conditional-logic/reference/truthiness-rules) — what `__len__` gives you for free.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}