{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "adv-0001",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "# Advanced OOP Topics\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "adv-0002",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [],
   "source": [
    "import sys\n",
    "from pathlib import Path\n",
    "\n",
    "current = Path.cwd()\n",
    "for parent in [current, *current.parents]:\n",
    "    if (parent / '_config.yml').exists():\n",
    "        project_root = parent\n",
    "        break\n",
    "else:\n",
    "    project_root = Path.cwd().parent.parent\n",
    "\n",
    "sys.path.insert(0, str(project_root))\n",
    "\n",
    "from shared import thinkpython, diagram, jupyturtle, download\n",
    "\n",
    "sys.modules['thinkpython'] = thinkpython\n",
    "sys.modules['diagram'] = diagram\n",
    "sys.modules['jupyturtle'] = jupyturtle\n",
    "sys.modules['download'] = download\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0003",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "This notebook covers five topics that extend the core OOP material:\n",
    "\n",
    "| Topic | What it adds |\n",
    "|---|---|\n",
    "| Comparison dunder methods | Value equality, ordering, and hash behavior for custom objects |\n",
    "| Operator overloading | Making custom objects work with operators like `+` and `*` |\n",
    "| `@dataclass` in depth | Auto-generated `__init__`, `__repr__`, `__eq__`, ordering, frozen instances |\n",
    "| Class vs. instance variables | A common source of bugs, explained clearly |\n",
    "| Static and class methods | Utility behavior and alternative constructors |\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0010",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "## Comparison Dunder Methods\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0011",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "By default, `==` on a custom object tests **identity** (same as `is`), not value equality.\n",
    "Defining `__eq__` changes that behavior to value comparison.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "adv-0012",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "False\n",
      "False\n"
     ]
    }
   ],
   "source": [
    "class Point:\n",
    "    def __init__(self, x, y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "\n",
    "    def __repr__(self):\n",
    "        return f'Point({self.x}, {self.y})'\n",
    "\n",
    "p1 = Point(1, 2)\n",
    "p2 = Point(1, 2)\n",
    "\n",
    "print(p1 == p2)   # False — identity check by default\n",
    "print(p1 is p2)   # False\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0013",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "Adding `__eq__` makes `==` compare by value instead.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "adv-0014",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "True\n",
      "False\n",
      "False\n"
     ]
    }
   ],
   "source": [
    "class Point:\n",
    "    def __init__(self, x, y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "\n",
    "    def __repr__(self):\n",
    "        return f'Point({self.x}, {self.y})'\n",
    "\n",
    "    def __eq__(self, other):\n",
    "        if not isinstance(other, Point):\n",
    "            return NotImplemented\n",
    "        return self.x == other.x and self.y == other.y\n",
    "\n",
    "p1 = Point(1, 2)\n",
    "p2 = Point(1, 2)\n",
    "p3 = Point(3, 4)\n",
    "\n",
    "print(p1 == p2)   # now True\n",
    "print(p1 is p2)   # still False\n",
    "print(p1 == p3)   # False\n",
    "\n",
    "# print(p1)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0015",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "### Ordering with `__lt__`\n",
    "\n",
    "To support sorting (`sorted()`, `min()`, `max()`), define `__lt__` (less than).\n",
    "You don't need to write all six comparison methods by hand. The\n",
    "`@functools.total_ordering` decorator **derives the missing ordering methods**\n",
    "from two definitions you supply:\n",
    "\n",
    "1. `__eq__`: required; the decorator never derives it (equality has different\n",
    "   semantics from ordering and is deliberately left to you).\n",
    "2. **Any one** of `__lt__`, `__le__`, `__gt__`, or `__ge__`: the decorator\n",
    "   fills in the remaining three.\n",
    "\n",
    "| You define | Decorator derives |\n",
    "|---|---|\n",
    "| `__eq__` | *(nothing; you must always supply this yourself)* |\n",
    "| one of `__lt__` / `__le__` / `__gt__` / `__ge__` | the other three ordering methods |\n",
    "\n",
    "Using `__lt__` is conventional: `<` reads naturally as \"is *a* less than *b*?\",\n",
    "which maps cleanly onto sort order, but any of the four works.\n",
    "\n",
    "For example, if you define `__lt__`, then `a > b` becomes `b < a`,\n",
    "and `a >= b` becomes `not (a < b)`.\n",
    "\n",
    "**Performance note:** because the derived methods add an extra function call\n",
    "layer, `@total_ordering` is marginally slower than writing all six methods\n",
    "explicitly. The difference is negligible in typical code; only consider\n",
    "hand-writing them if profiling shows comparison is a bottleneck in a tight\n",
    "inner loop.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "adv-0016",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[Point(1, 1), Point(0, 2), Point(3, 4)]\n",
      "Point(1, 1)\n",
      "True\n",
      "False\n"
     ]
    }
   ],
   "source": [
    "from functools import total_ordering\n",
    "import math\n",
    "\n",
    "@total_ordering\n",
    "class Point:\n",
    "    def __init__(self, x, y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "\n",
    "    def __repr__(self):\n",
    "        return f'Point({self.x}, {self.y})'\n",
    "\n",
    "    def __eq__(self, other):\n",
    "        if not isinstance(other, Point):\n",
    "            return NotImplemented\n",
    "        return self.x == other.x and self.y == other.y\n",
    "\n",
    "    def __lt__(self, other):                        ### compare distances from the origin\n",
    "        \"\"\"Sort by distance from the origin.\"\"\"\n",
    "        if not isinstance(other, Point):\n",
    "            return NotImplemented\n",
    "        return math.hypot(self.x, self.y) < math.hypot(other.x, other.y)    \n",
    "\n",
    "points = [Point(3, 4), Point(1, 1), Point(0, 2)]\n",
    "print(sorted(points))   # sorted by distance from origin\n",
    "print(min(points))\n",
    "\n",
    "# __gt__ is derived automatically — no extra code needed\n",
    "p_near = Point(1, 1)    # distance ≈ 1.41\n",
    "p_far  = Point(3, 4)    # distance = 5.0\n",
    "\n",
    "print(p_far > p_near)   # True  — total_ordering derives __gt__ from __lt__\n",
    "print(p_near > p_far)   # False\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0017",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "### Hashability and `__hash__`\n",
    "\n",
    "When you define `__eq__`, Python **automatically sets `__hash__` to `None`**,\n",
    "making instances unhashable (cannot be used in sets or as dict keys).\n",
    "Define `__hash__` explicitly to restore that ability.\n",
    "\n",
    "```python\n",
    "# Rule of thumb: objects that compare equal must have the same hash.\n",
    "def __hash__(self):\n",
    "    return hash((self.x, self.y))\n",
    "```\n",
    "\n",
    "If your object is **mutable**, do not define `__hash__`; mutable objects\n",
    "should not be hashed because their value (and their hash) could change after insertion.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "adv-0018",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{Point(1, 2), Point(3, 4)}\n",
      "origin-ish\n"
     ]
    }
   ],
   "source": [
    "from functools import total_ordering\n",
    "import math\n",
    "\n",
    "@total_ordering\n",
    "class Point:\n",
    "    def __init__(self, x, y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "\n",
    "    def __repr__(self):\n",
    "        return f'Point({self.x}, {self.y})'\n",
    "\n",
    "    def __eq__(self, other):\n",
    "        if not isinstance(other, Point):\n",
    "            return NotImplemented\n",
    "        return self.x == other.x and self.y == other.y\n",
    "\n",
    "    def __lt__(self, other):\n",
    "        if not isinstance(other, Point):\n",
    "            return NotImplemented\n",
    "        return math.hypot(self.x, self.y) < math.hypot(other.x, other.y)\n",
    "\n",
    "    def __hash__(self):\n",
    "        return hash((self.x, self.y))\n",
    "\n",
    "p1 = Point(1, 2)\n",
    "p2 = Point(1, 2)\n",
    "p3 = Point(3, 4)\n",
    "\n",
    "point_set = {p1, p2, p3}\n",
    "print(point_set)          # p1 and p2 are equal — only one appears\n",
    "\n",
    "lookup = {p1: 'origin-ish', p3: 'far'}\n",
    "print(lookup[p2])         # works because p2 == p1 and hash(p2) == hash(p1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "05ac5f6b",
   "metadata": {},
   "source": [
    "## Operator Overloading\n",
    "\n",
    "By defining special methods, you can control how Python operators\n",
    "behave on your own types. For every operator there is a corresponding\n",
    "dunder method:\n",
    "\n",
    "| Operator | Method | Example |\n",
    "|----------|--------|---------|\n",
    "| `+` | `__add__` | `a + b` |\n",
    "| `==` | `__eq__` | `a == b` |\n",
    "| `<` | `__lt__` | `a < b` |\n",
    "| `len()` | `__len__` | `len(a)` |\n",
    "\n",
    "This section focuses on arithmetic operators; comparison operators\n",
    "(`__eq__`, `__lt__`, `__hash__`) are covered above.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "95f870e8",
   "metadata": {},
   "source": [
    "Here is `__add__` defined on `BankAccount`.\n",
    "When two accounts are added together, a new merged account is returned:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "d5a6f5e0",
   "metadata": {},
   "outputs": [],
   "source": [
    "class BankAccount:\n",
    "    \"\"\"BankAccount with operator overloading.\"\"\"\n",
    "\n",
    "    def __init__(self, owner=\"Unknown\", balance=0.0):\n",
    "        self.owner = owner\n",
    "        self._balance = balance\n",
    "\n",
    "    @property\n",
    "    def balance(self):\n",
    "        return self._balance\n",
    "\n",
    "    def __str__(self):\n",
    "        return f\"BankAccount(owner={self.owner}, balance=${self._balance:.2f})\"\n",
    "\n",
    "    def __repr__(self):\n",
    "        return f\"BankAccount('{self.owner}', {self._balance})\"\n",
    "\n",
    "    def __add__(self, other):\n",
    "        \"\"\"Merge two accounts into one.\"\"\"\n",
    "        if not isinstance(other, BankAccount):\n",
    "            return NotImplemented\n",
    "        merged_owner = f\"{self.owner}&{other.owner}\"\n",
    "        merged_balance = self._balance + other._balance\n",
    "        return BankAccount(merged_owner, merged_balance)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "320441b2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BankAccount(owner=Ava&Ben, balance=$20.00)\n"
     ]
    }
   ],
   "source": [
    "a = BankAccount(\"Ava\", 12.00)\n",
    "b = BankAccount(\"Ben\", 8.00)\n",
    "print(a + b)   # BankAccount(owner=Ava&Ben, balance=$20.00)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "454003f4",
   "metadata": {},
   "source": [
    "When Python evaluates `a + b`, it calls `a.__add__(b)` automatically.\n",
    "Changing the behavior of an operator so that it works with programmer-defined\n",
    "types is called **operator overloading**.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0020",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "## `@dataclass` \n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0021",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "`@dataclass` is a decorator from the standard library that auto-generates common dunder methods (`__init__`, `__repr__`, `__eq__`) for a class based on its annotated fields, eliminating boilerplate.\n",
    "\n",
    "Dataclasses give type annotations a second job. In a regular class, `name: str` is mostly a hint for readers and tools. In a dataclass, an annotated class variable also becomes a **field** that `@dataclass` uses to generate `__init__`, `__repr__`, and comparison behavior.\n",
    "\n",
    "Here we look at its most useful options.\n",
    "\n",
    "| Option | Effect |\n",
    "|---|---|\n",
    "| `eq=True` (default) | Auto-generates `__eq__` based on fields |\n",
    "| `order=True` | Also generates `__lt__`, `__le__`, `__gt__`, `__ge__` |\n",
    "| `frozen=True` | Makes instances immutable; also enables `__hash__` |\n",
    "| `field(default_factory=...)` | Safe default for mutable fields like lists |\n",
    "\n",
    "When `order=True` is set, fields are compared **in declaration order** — here `name` first, then `gpa` — so `sorted([s1, s2])` places Alice before Bob because `'Alice' < 'Bob'` alphabetically."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "adv-0022",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "True\n",
      "[Student(name='Alice', gpa=3.8, courses=[]), Student(name='Bob', gpa=3.5, courses=[])]\n",
      "Student(name='Alice', gpa=3.8, courses=['CS101'])\n"
     ]
    }
   ],
   "source": [
    "from dataclasses import dataclass, field\n",
    "\n",
    "@dataclass(order=True)\n",
    "class Student:\n",
    "    name: str\n",
    "    gpa: float\n",
    "    courses: list[str] = field(default_factory=list)   # safe mutable default\n",
    "\n",
    "s1 = Student('Alice', 3.8)\n",
    "s2 = Student('Bob', 3.5)\n",
    "s3 = Student('Alice', 3.8)\n",
    "\n",
    "print(s1 == s3)          # True — same field values\n",
    "print(sorted([s1, s2]))  # sorted lexicographically by (name, gpa)\n",
    "s1.courses.append('CS101')\n",
    "print(s1)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0023",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "### Frozen Dataclasses\n",
    "\n",
    "`frozen=True` prevents attribute mutation after creation and automatically\n",
    "provides a correct `__hash__`, making instances usable in sets and as dict keys.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "adv-0024",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Color(r=255, g=0, b=0)\n",
      "4091835460043580556\n",
      "{Color(r=0, g=255, b=0), Color(r=255, g=0, b=0), Color(r=0, g=0, b=255)}\n",
      "FrozenInstanceError cannot assign to field 'r'\n"
     ]
    }
   ],
   "source": [
    "from dataclasses import dataclass\n",
    "\n",
    "@dataclass(frozen=True)\n",
    "class Color:\n",
    "    r: int\n",
    "    g: int\n",
    "    b: int\n",
    "\n",
    "red = Color(255, 0, 0)\n",
    "print(red)\n",
    "print(hash(red))   # hashable\n",
    "\n",
    "palette = {red, Color(0, 255, 0), Color(0, 0, 255)}\n",
    "print(palette)\n",
    "\n",
    "try:\n",
    "    red.r = 128    # raises FrozenInstanceError\n",
    "except Exception as e:\n",
    "    print(type(e).__name__, e)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0040",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "## Class vs. Instance Variables\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0041",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "A **class variable** is defined directly in the class body, outside any method.\n",
    "It is shared across all instances. An **instance variable** is set on `self`\n",
    "inside a method and belongs only to that one object.\n",
    "\n",
    "Confusing the two is one of the most common OOP bugs in Python.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "adv-0042",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Canis lupus familiaris\n",
      "Canis lupus familiaris\n",
      "Rex\n",
      "Fido\n"
     ]
    }
   ],
   "source": [
    "class Dog:\n",
    "    species = 'Canis lupus familiaris'   # class variable — shared by all dogs\n",
    "\n",
    "    def __init__(self, name):\n",
    "        self.name = name                  # instance variable — unique per dog\n",
    "\n",
    "d1 = Dog('Rex')\n",
    "d2 = Dog('Fido')\n",
    "\n",
    "print(d1.species)    # 'Canis lupus familiaris'\n",
    "print(d2.species)    # same — shared\n",
    "print(d1.name)       # 'Rex'\n",
    "print(d2.name)       # 'Fido'\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0043",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "### The Mutation Trap\n",
    "\n",
    "Assigning to a class variable **via an instance** creates a new instance\n",
    "variable that *shadows* the class variable — it does **not** change the class\n",
    "variable for all instances.\n",
    "\n",
    "But **mutating** a mutable class variable (like a list) *does* affect all\n",
    "instances, because no new variable is created.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "adv-0044",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "99\n",
      "0\n",
      "0\n",
      "['event']\n",
      "['event']\n"
     ]
    }
   ],
   "source": [
    "class Counter:\n",
    "    count = 0            # class variable\n",
    "    history = []         # mutable class variable — danger zone\n",
    "\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "\n",
    "a = Counter('a')\n",
    "b = Counter('b')\n",
    "\n",
    "# Reassignment via instance — creates a new instance variable on `a` only\n",
    "a.count = 99\n",
    "print(a.count)           # 99  — instance variable on a\n",
    "print(b.count)           # 0   — class variable unchanged\n",
    "print(Counter.count)     # 0\n",
    "\n",
    "# Mutation via instance — modifies the shared class-level list\n",
    "a.history.append('event')\n",
    "print(b.history)         # ['event'] — b sees the change!\n",
    "print(Counter.history)   # ['event']\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0045",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "**Rule of thumb:**\n",
    "\n",
    "- Use class variables for constants or data that *truly* belongs to the class (e.g., `species`, `MAX_SIZE`).\n",
    "- Use instance variables (set in `__init__`) for data that belongs to individual objects.\n",
    "- Never use a mutable class variable as a default container — use `field(default_factory=list)` with `@dataclass`, or set the list in `__init__`.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0060",
   "metadata": {},
   "source": [
    "## Static and Class Methods\n",
    "\n",
    "Not every method needs an instance. Python provides two decorators\n",
    "for methods that are attached to the *class* itself rather than an instance:\n",
    "\n",
    "| Decorator | First parameter | Typical use |\n",
    "|-----------|-----------------|-------------|\n",
    "| `@staticmethod` | *(none)* | Utility function logically grouped with the class |\n",
    "| `@classmethod` | `cls` (the class itself) | Alternative constructors / factory methods |\n",
    "\n",
    "Both decorators come up naturally alongside class variables,\n",
    "because all three belong to the *class* rather than any one instance.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0061",
   "metadata": {},
   "source": [
    "### Static Methods\n",
    "\n",
    "A **static method** is a regular function that lives inside a class for\n",
    "organizational reasons. It receives neither `self` nor `cls`,\n",
    "so it cannot access instance or class state directly.\n",
    "\n",
    "A common use-case is a **validation or parsing helper** that supports\n",
    "other methods without depending on object state:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "adv-0062",
   "metadata": {},
   "outputs": [],
   "source": [
    "class BankAccount:\n",
    "    \"\"\"Bank account used to demonstrate static and class methods.\"\"\"\n",
    "\n",
    "    def __init__(self, owner=\"Unknown\", balance=0.0):\n",
    "        self.owner = owner\n",
    "        self._balance = balance\n",
    "\n",
    "    @property\n",
    "    def balance(self):\n",
    "        return self._balance\n",
    "\n",
    "    def deposit(self, amount):\n",
    "        self._balance += amount\n",
    "        return self\n",
    "\n",
    "    def withdraw(self, amount):\n",
    "        if amount > self._balance:\n",
    "            raise ValueError(\"Insufficient funds\")\n",
    "        self._balance -= amount\n",
    "        return self\n",
    "\n",
    "    def __str__(self):\n",
    "        return f\"BankAccount(owner={self.owner}, balance=${self._balance:.2f})\"\n",
    "\n",
    "    def __repr__(self):\n",
    "        return f\"BankAccount('{self.owner}', {self._balance})\"\n",
    "\n",
    "    # -- static method ------------------------------------------------\n",
    "    @staticmethod\n",
    "    def parse_record(s):\n",
    "        \"\"\"Parse an 'owner:balance' string into raw values.\"\"\"\n",
    "        owner, balance = s.split(\":\")\n",
    "        return owner, float(balance)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0063",
   "metadata": {},
   "source": [
    "Because `parse_record` is a static method, it has no `self` or `cls` parameter.\n",
    "It is a utility helper that can be called on the class directly:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "adv-0064",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BankAccount(owner=Taylor, balance=$348.00)\n"
     ]
    }
   ],
   "source": [
    "owner, balance = BankAccount.parse_record(\"Taylor:348.00\")\n",
    "acct = BankAccount(owner, balance)\n",
    "print(acct)   # BankAccount(owner=Taylor, balance=$348.00)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0065",
   "metadata": {},
   "source": [
    "### Class Methods\n",
    "\n",
    "A **class method** receives the *class* as its first argument (`cls`).\n",
    "This makes it better than a static method when subclasses are involved:\n",
    "`cls(...)` creates an instance of the *actual* subclass, not the hardcoded\n",
    "parent class.\n",
    "\n",
    "Here is `from_string` rewritten as a class method:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "adv-0066",
   "metadata": {},
   "outputs": [],
   "source": [
    "class BankAccount:\n",
    "    \"\"\"Bank account — class-method version of from_string.\"\"\"\n",
    "\n",
    "    def __init__(self, owner=\"Unknown\", balance=0.0):\n",
    "        self.owner = owner\n",
    "        self._balance = balance\n",
    "\n",
    "    @property\n",
    "    def balance(self):\n",
    "        return self._balance\n",
    "\n",
    "    def deposit(self, amount):\n",
    "        self._balance += amount\n",
    "        return self\n",
    "\n",
    "    def withdraw(self, amount):\n",
    "        if amount > self._balance:\n",
    "            raise ValueError(\"Insufficient funds\")\n",
    "        self._balance -= amount\n",
    "        return self\n",
    "\n",
    "    def __str__(self):\n",
    "        return f\"BankAccount(owner={self.owner}, balance=${self._balance:.2f})\"\n",
    "\n",
    "    def __repr__(self):\n",
    "        return f\"BankAccount('{self.owner}', {self._balance})\"\n",
    "\n",
    "    # ── class method ────────────────────────────────────────────\n",
    "    @classmethod\n",
    "    def from_string(cls, s):\n",
    "        \"\"\"Create an instance from an 'owner:balance' string.\"\"\"\n",
    "        owner, balance = s.split(\":\")\n",
    "        return cls(owner, float(balance))\n",
    "\n",
    "    @classmethod\n",
    "    def zero_balance(cls, owner):\n",
    "        \"\"\"Return an account with a zero balance.\"\"\"\n",
    "        return cls(owner, 0.0)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "id": "c27055c7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BankAccount(owner=Taylor, balance=$348.00)\n"
     ]
    }
   ],
   "source": [
    "acct1 = BankAccount.from_string(\"Taylor:348.00\")\n",
    "print(acct1)  # BankAccount(owner=Taylor, balance=$348.00)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "adv-0067",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BankAccount(owner=Casey, balance=$94.50)\n",
      "BankAccount(owner=Rin, balance=$0.00)\n"
     ]
    }
   ],
   "source": [
    "# Alternative constructors — both work on subclasses automatically\n",
    "acct1 = BankAccount.from_string(\"Casey:94.50\")\n",
    "acct2 = BankAccount.zero_balance(\"Rin\")\n",
    "print(acct1)   # BankAccount(owner=Casey, balance=$94.50)\n",
    "print(acct2)   # BankAccount(owner=Rin, balance=$0.00)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0068",
   "metadata": {},
   "source": [
    "**When to use which:**\n",
    "\n",
    "| | `@staticmethod` | `@classmethod` |\n",
    "|---|---|---|\n",
    "| Receives class? | No | Yes (`cls`) |\n",
    "| Subclass-safe? | No — hardcodes class name | Yes — `cls(...)` creates the right type |\n",
    "| Typical use | Pure utility / validation helper | Alternative constructors |\n",
    "\n",
    "Prefer `@classmethod` for constructors; use `@staticmethod` only for\n",
    "helpers that truly need no access to the class.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "adv-0069",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Class Methods\n",
    "#   Create a subclass ExtendedBankAccount(BankAccount) and add a class\n",
    "#   method from_balance_str(cls, owner, s) that parses a dollar string\n",
    "#   like \"12.34\" and returns a new account.\n",
    "#   Test:\n",
    "#       print(ExtendedBankAccount.from_balance_str(\"Kai\", \"12.34\"))\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "### Your code ends here.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "adv-0070",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BankAccount(owner=Kai, balance=$12.34)\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "class ExtendedBankAccount(BankAccount):\n",
    "    @classmethod\n",
    "    def from_balance_str(cls, owner, s):\n",
    "        \"\"\"Create account from a balance string like '12.34'.\"\"\"\n",
    "        return cls(owner, float(s))\n",
    "\n",
    "print(ExtendedBankAccount.from_balance_str(\"Kai\", \"12.34\"))\n",
    "# BankAccount(owner=Kai, balance=$12.34)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adv-0050",
   "metadata": {
    "language": "markdown"
   },
   "source": [
    "## Summary\n",
    "\n",
    "| Topic | Key takeaway |\n",
    "|---|---|\n",
    "| `__eq__` / `__lt__` / `__hash__` | Define these to make objects sortable and hashable; remember the mutability rule for `__hash__` |\n",
    "| `@dataclass` | Use `order=True` for sorting, `frozen=True` for hashable immutable objects, `field(default_factory=...)` for mutable defaults |\n",
    "| Class vs. instance variables | Keep mutable state in instance variables; treat class variables as shared constants |\n",
    "| Static / class methods | `@staticmethod` for pure helpers; `@classmethod` for alternative constructors (subclass-safe) |\n",
    "| Operator overloading | Define `__add__`, `__eq__`, etc. to give your objects natural operator syntax |\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv (3.13.7)",
   "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.13.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
