{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-000",
   "metadata": {},
   "source": [
    "# Lesson 4 (Part A) — Classes & Objects\n",
    "\n",
    "**Companion notebook** for the Lesson 4A slides: classes, `self`, properties, and the\n",
    "special methods that make your objects behave like Python's built-ins."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-001",
   "metadata": {},
   "outputs": [],
   "source": [
    "def check(label, got, expected, tol=1e-6):\n",
    "    \"\"\"Compare your answer with the expected value; prints ✅ or ❌ (never raises).\"\"\"\n",
    "    import math\n",
    "    try:\n",
    "        if isinstance(expected, float) or isinstance(got, float):\n",
    "            ok = math.isclose(float(got), float(expected), rel_tol=tol, abs_tol=tol)\n",
    "        elif isinstance(expected, (list, tuple)) and expected and isinstance(expected[0], str):\n",
    "            ok = list(got) == list(expected)\n",
    "        elif hasattr(expected, \"__len__\") and not isinstance(expected, str):\n",
    "            import numpy as np\n",
    "            ok = np.allclose(np.asarray(got, dtype=float), np.asarray(expected, dtype=float), rtol=tol, atol=tol)\n",
    "        else:\n",
    "            ok = got == expected\n",
    "    except Exception:\n",
    "        ok = False\n",
    "    print((\"✅ \" if ok else \"❌ \") + f\"{label}: got {got!r}, expected {expected!r}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-002",
   "metadata": {},
   "source": [
    "## 1. A class with a constructor, attributes, and methods"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-003",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Policy:\n",
    "    \"\"\"A single life insurance policy.\"\"\"\n",
    "\n",
    "    def __init__(self, policy_id: str, age: int, sum_assured: float, annual_premium: float):\n",
    "        if sum_assured <= 0:\n",
    "            raise ValueError(f\"sum_assured must be positive, got {sum_assured}\")\n",
    "        self.policy_id = policy_id\n",
    "        self.age = age\n",
    "        self.sum_assured = sum_assured\n",
    "        self.annual_premium = annual_premium\n",
    "\n",
    "    def premium_per_mille(self) -> float:\n",
    "        return self.annual_premium / self.sum_assured * 1000\n",
    "\n",
    "    def birthday(self) -> None:\n",
    "        self.age += 1\n",
    "\n",
    "\n",
    "p = Policy(\"P001\", 34, 250_000, 310.50)\n",
    "print(p.age, p.premium_per_mille())\n",
    "p.birthday()\n",
    "print(p.age)\n",
    "p          # without __repr__, not very helpful"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-004",
   "metadata": {},
   "source": [
    "## 2. Encapsulation and `@property`\n",
    "A leading underscore marks internal state; changes go through methods that validate\n",
    "them. A property looks like an attribute but is computed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-005",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Account:\n",
    "    def __init__(self, balance: float):\n",
    "        self._balance = balance\n",
    "\n",
    "    @property\n",
    "    def balance(self) -> float:\n",
    "        return self._balance\n",
    "\n",
    "    def deposit(self, amount: float) -> None:\n",
    "        if amount <= 0:\n",
    "            raise ValueError(\"deposit must be positive\")\n",
    "        self._balance += amount\n",
    "\n",
    "\n",
    "a = Account(100)\n",
    "a.deposit(50)\n",
    "print(a.balance)\n",
    "try:\n",
    "    a.balance = 0\n",
    "except AttributeError as err:\n",
    "    print(\"AttributeError:\", err)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-006",
   "metadata": {},
   "source": [
    "Derived values as properties can never go stale:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-007",
   "metadata": {},
   "outputs": [],
   "source": [
    "class CouponBond:\n",
    "    def __init__(self, face, coupon_rate, years, freq=2):\n",
    "        self.face, self.coupon_rate, self.years, self.freq = face, coupon_rate, years, freq\n",
    "\n",
    "    @property\n",
    "    def coupon(self) -> float:\n",
    "        return self.face * self.coupon_rate / self.freq\n",
    "\n",
    "    @property\n",
    "    def n_periods(self) -> int:\n",
    "        return round(self.years * self.freq)\n",
    "\n",
    "\n",
    "b = CouponBond(1000, 0.06, 10)\n",
    "print(b.coupon, b.n_periods)\n",
    "b.face = 2000\n",
    "print(b.coupon)          # updated automatically"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-008",
   "metadata": {},
   "source": [
    "## 3. Special methods: `__repr__`, `__str__`, `__len__`, `__add__`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-009",
   "metadata": {},
   "outputs": [],
   "source": [
    "class CashFlows:\n",
    "    def __init__(self, amounts: list[float]):\n",
    "        self.amounts = list(amounts)\n",
    "\n",
    "    def __repr__(self) -> str:\n",
    "        return f\"CashFlows({self.amounts!r})\"\n",
    "\n",
    "    def __str__(self) -> str:\n",
    "        return f\"{len(self)} cash flows totalling {sum(self.amounts):,.2f}\"\n",
    "\n",
    "    def __len__(self) -> int:\n",
    "        return len(self.amounts)\n",
    "\n",
    "    def __add__(self, other: \"CashFlows\") -> \"CashFlows\":\n",
    "        n = max(len(self), len(other))\n",
    "        a = self.amounts + [0.0] * (n - len(self))\n",
    "        b = other.amounts + [0.0] * (n - len(other))\n",
    "        return CashFlows([x + y for x, y in zip(a, b)])\n",
    "\n",
    "    def pv(self, i: float) -> float:\n",
    "        return sum(cf * (1 + i) ** -t for t, cf in enumerate(self.amounts, 1))\n",
    "\n",
    "\n",
    "net = CashFlows([60, 60, 1060]) + CashFlows([-500, -500])\n",
    "print(repr(net))\n",
    "print(net)\n",
    "print(len(net), round(net.pv(0.05), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-010",
   "metadata": {},
   "source": [
    "---\n",
    "## Your turn — give `Policy` a proper interface\n",
    "\n",
    "1. Add a `__repr__` returning exactly `Policy('P001', 34, 250000, 310.5)`.\n",
    "2. Add a read-only property `annual_premium_rate` = premium ÷ sum assured.\n",
    "3. Add `__eq__` so two policies are equal when all four attributes match."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-011",
   "metadata": {},
   "outputs": [],
   "source": [
    "class BetterPolicy(Policy):\n",
    "    \"\"\"Policy with a useful repr, a derived rate, and equality.\"\"\"\n",
    "    ...  # your code here\n",
    "\n",
    "\n",
    "bp = BetterPolicy(\"P001\", 34, 250_000, 310.50)\n",
    "check(\"repr\", repr(bp), \"Policy('P001', 34, 250000, 310.5)\")\n",
    "check(\"premium rate\", getattr(bp, \"annual_premium_rate\", None), 0.001242)\n",
    "check(\"equality\", bp == BetterPolicy(\"P001\", 34, 250_000, 310.50), True)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
