{ "cells": [ { "cell_type": "raw", "metadata": {}, "source": [ "---\n", "title: \"Notebook 6: Modules & Object-Oriented Design\"\n", "subtitle: \"COMP 1150 โ€” Computer Science Concepts\"\n", "author: \"Brendan Shea, PhD\"\n", "date: last-modified\n", "---" ] }, { "cell_type": "markdown", "metadata": { "colab_header": true }, "source": [ "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/brendanpshea/computing_concepts_python/blob/main/v2/notebooks/COMP1150_NB06_ModulesOOP.ipynb) \n", "[Download .ipynb](https://raw.githubusercontent.com/brendanpshea/computing_concepts_python/main/v2/notebooks/COMP1150_NB06_ModulesOOP.ipynb) ยท [View on GitHub](https://github.com/brendanpshea/computing_concepts_python/blob/main/v2/notebooks/COMP1150_NB06_ModulesOOP.ipynb)\n" ] }, { "cell_type": "markdown", "id": "f39fa464", "metadata": {}, "source": [ "๐Ÿ“บ **Lecture video:** *(link coming soon)*" ] }, { "cell_type": "markdown", "id": "63790928", "metadata": {}, "source": [ "## Learning Outcomes\n", "\n", "By the end of this notebook, you will be able to:\n", "\n", "- **Import** code from Python's standard library and from your own files, and explain what a **module** and a **namespace** are.\n", "- **Define** a class with `__init__`, create objects from it, and explain the difference between a **class** (the blueprint) and an **object** (a thing built from it).\n", "- **Write** methods that use `self` to act on an object's own data, and explain what `self` refers to.\n", "- **Use** inheritance and `super()` to build a specialized class on top of a general one, and **override** a method.\n", "- **Explain** encapsulation and abstraction, and implement a small **abstract data type** as a class โ€” picking up the ADT idea from Notebook 5.\n", "- **Spot** two classic OOP bugs: forgetting `self.`, and a mutable class attribute shared across every object.\n", "\n", "*Maps to course LOs: 6, 8*" ] }, { "cell_type": "markdown", "id": "743fb585", "metadata": {}, "source": [ "## The Problem: Too Much Copy-Paste\n", "\n", "Merlin runs **Merlin Advisory**, a small consulting shop just off the main square in Camelot. His team has a habit: every time they start a new client report, somebody copies the last report's code, pastes it into a fresh file, and changes the numbers. By the third month there are nine slightly different copies of the same \"calculate the fee\" code, and three of them have different bugs.\n", "\n", "Down the street, **Guinevere** at **Camelot Civic Systems** has the opposite problem. She isn't repeating code โ€” she's drowning in *loose variables*. Every citizen in her system is tracked by a tangle of separate lists: `names`, `ages`, `wards`, `balances`. To update one person she has to touch four lists and pray the positions still line up." ] }, { "cell_type": "markdown", "id": "4dca80db", "metadata": {}, "source": [ "Two problems, two fixes โ€” and they're the two halves of this notebook.\n", "\n", "- **Modules** solve Merlin's problem. Write code *once*, in one file, and **import** it everywhere. Fix a bug in one place; it's fixed everywhere.\n", "- **Objects** solve Guinevere's problem. Instead of four lists that have to stay in sync, bundle each citizen's data โ€” *and the things you can do to it* โ€” into one tidy package called an **object**.\n", "\n", "Object-oriented design (OOP) is the second idea taken seriously: programs built out of objects that each know their own data and their own behavior. By the end you'll build classes for a security firm, a research lab, and a delivery service โ€” and you'll see why Guinevere sleeps better at night." ] }, { "cell_type": "markdown", "id": "9397a939", "metadata": {}, "source": [ "### The Roadmap\n", "\n", "1. **Modules & imports** โ€” borrow code from Python, and split your own code into reusable files.\n", "2. **Classes & objects** โ€” the blueprint and the things you stamp out from it.\n", "3. **Methods & `self`** โ€” giving objects something to *do*.\n", "4. **Inheritance** โ€” build a general class once, then specialize it.\n", "5. **Abstraction** โ€” hide the storage behind a clean interface (the Notebook 5 ADT idea, now in code).\n", "\n", "A quick reminder before we start: in Notebook 4 you learned to write **functions** โ€” reusable blocks of behavior. Everything here builds on that. A method is just a function that lives inside an object." ] }, { "cell_type": "markdown", "id": "d34d823d", "metadata": {}, "source": [ "## Modules: Code You Don't Have to Rewrite\n", "\n", "A **module** is just a file of Python code that you can pull into another file. Python ships with hundreds of them โ€” together they're called the **standard library** โ€” covering math, dates, randomness, files, and more. You don't install anything; you just `import` what you need.\n", "\n", "Think of it as Merlin borrowing a colleague's finished work instead of redoing it. The `import` statement is how you say \"I'd like to use the tools in that file, please.\"" ] }, { "cell_type": "markdown", "id": "b82291ce", "metadata": {}, "source": [ "Merlin needs a square root for a risk calculation. Square roots aren't built into plain Python โ€” they live in the **`math`** module.\n", "\n", "**Importing a whole module.** You name the module, then reach inside it with a dot:\n", "\n", "```python\n", "import module_name # bring the whole module into reach\n", "module_name.tool(...) # use something from inside it โ€” the dot means \"look inside\"\n", "```\n", "\n", "The next cell imports `math` and uses it." ] }, { "cell_type": "code", "execution_count": 1, "id": "ee1553e8", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:10.786515Z", "iopub.status.busy": "2026-06-02T14:22:10.785868Z", "iopub.status.idle": "2026-06-02T14:22:10.802039Z", "shell.execute_reply": "2026-06-02T14:22:10.798228Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Risk score: 12.0\n", "Pi, for good measure: 3.141592653589793\n" ] } ], "source": [ "import math\n", "\n", "risk_score = math.sqrt(144)\n", "print(\"Risk score:\", risk_score)\n", "print(\"Pi, for good measure:\", math.pi)" ] }, { "cell_type": "markdown", "id": "3d00dcd9", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- `import math` brings the whole `math` module into reach.\n", "- `math.sqrt(144)` reads as \"the `sqrt` tool *that lives inside* `math`.\" The dot means \"look inside.\"\n", "- `math.pi` isn't a function โ€” it's a value the module hands you. Modules can offer both." ] }, { "cell_type": "markdown", "id": "85929db0", "metadata": {}, "source": [ "Writing `math.` in front of everything gets tiring. The `from ... import ...` form lets you pull *specific* names out of a module and use them bare.\n", "\n", "**Importing specific names.** Reach into a module and pull out just the names you want:\n", "\n", "```python\n", "from module_name import tool # grab only `tool` out of the module\n", "tool(...) # now call it directly โ€” no module_name. prefix\n", "```\n", "\n", "Merlin's assistant wants a random client to spot-check and today's date for the report header." ] }, { "cell_type": "code", "execution_count": 2, "id": "d69fccd5", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:10.809502Z", "iopub.status.busy": "2026-06-02T14:22:10.808925Z", "iopub.status.idle": "2026-06-02T14:22:10.817616Z", "shell.execute_reply": "2026-06-02T14:22:10.815535Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Audit this one today: Astolat Mills\n", "Report date: 2026-06-02\n" ] } ], "source": [ "from random import choice\n", "from datetime import date\n", "\n", "clients = [\"Pellinore Estates\", \"Astolat Mills\", \"Lyonesse Foods\"]\n", "print(\"Audit this one today:\", choice(clients))\n", "print(\"Report date:\", date.today())" ] }, { "cell_type": "markdown", "id": "ab022065", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- `from random import choice` reaches *into* `random` and pulls out just `choice`. Now you call `choice(...)` directly โ€” no `random.` prefix.\n", "- `choice(clients)` picks one item from the list at random.\n", "- `from datetime import date` does the same for the `date` tool; `date.today()` returns today's date.\n", "- The trade-off: `from` imports are shorter to type, but `import math` keeps it obvious *where* a name came from." ] }, { "cell_type": "markdown", "id": "b49ad5a8", "metadata": {}, "source": [ "The real payoff is writing your **own** module. The cell below uses Jupyter's `%%writefile` to save a small file, `camelot_tools.py`, holding one function. In a normal project you'd just create that file in your editor." ] }, { "cell_type": "code", "execution_count": 3, "id": "ea7a0bdb", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:10.823646Z", "iopub.status.busy": "2026-06-02T14:22:10.822997Z", "iopub.status.idle": "2026-06-02T14:22:10.836725Z", "shell.execute_reply": "2026-06-02T14:22:10.834629Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Writing camelot_tools.py\n" ] } ], "source": [ "%%writefile camelot_tools.py\n", "\n", "def advisory_fee(hours, rate=150):\n", " \"\"\"Merlin's fee: hours x rate, plus a 10% surcharge over 40 hours.\"\"\"\n", " base = hours * rate\n", " if hours > 40:\n", " base = base * 1.10\n", " return round(base, 2)" ] }, { "cell_type": "markdown", "id": "51bea978", "metadata": {}, "source": [ "Now any file โ€” including this notebook โ€” can `import` that function. Merlin writes the fee rule **once**; every report shares the same copy. Fix it here, and it's fixed for the whole firm." ] }, { "cell_type": "code", "execution_count": 4, "id": "857d0d48", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:10.841427Z", "iopub.status.busy": "2026-06-02T14:22:10.841020Z", "iopub.status.idle": "2026-06-02T14:22:10.898910Z", "shell.execute_reply": "2026-06-02T14:22:10.896088Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Small job: 1500\n", "Big job: 8250.0\n" ] } ], "source": [ "from camelot_tools import advisory_fee\n", "\n", "print(\"Small job:\", advisory_fee(10))\n", "print(\"Big job:\", advisory_fee(50))" ] }, { "cell_type": "markdown", "id": "eb7d5adb", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- `%%writefile camelot_tools.py` saved everything below it as a real file named `camelot_tools.py`.\n", "- `from camelot_tools import advisory_fee` imports *your* function exactly like importing from the standard library โ€” Python doesn't care that you wrote it.\n", "- That's the cure for copy-paste: **one definition, many imports.** No more nine drifting copies." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### โœ๏ธ Your Turn โ€” Your Own Toolkit\n", "\n", "Python's standard library is full of ready-made tools. Import one you haven't used yet and try it โ€” for example `import statistics` then `statistics.mean([...])`, or `from datetime import date` then `date.today()`. Print the result." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "#| eval: false\n", "# TODO: import a tool from the standard library and use it.\n", "# import statistics\n", "# print(statistics.mean([10, 20, 30]))" ] }, { "cell_type": "markdown", "id": "c4faf44d", "metadata": {}, "source": [ "## What OOP Is: A Way of Thinking, Not Just Syntax\n", "\n", "Before we build a single class, step back. The code you wrote in Notebooks 1โ€“5 all followed one style โ€” what computer scientists call a **paradigm**, a basic way of organizing a program. That style was **procedural**: data sits in variables, and functions reach out, grab the data, do something to it, and put it back. The data is passive. The functions are in charge.\n", "\n", "**Object-oriented programming (OOP)** turns that around. Instead of passive data and bossy functions, you build **objects** that own their data *and* the behavior that goes with it. A `Citizen` object won't wait for some far-away function to change its balance โ€” it has its own `pay` method and changes its own balance. Data and behavior travel together.\n", "\n", "Why care? In Guinevere's old setup, the rule \"how do I update a citizen?\" lived somewhere far from the citizen's data, easy to get out of sync. In OOP, that rule lives *with* the citizen. Big programs stay sane because each object minds its own business." ] }, { "cell_type": "markdown", "id": "d04267fe", "metadata": {}, "source": [ "This idea isn't new. It runs back to the **Simula** language in the 1960s and **Smalltalk** in the 1970s, where Alan Kay pictured objects as little machines sending each other messages. (Whether OOP was a *good* idea is a real debate โ€” that's the subject of this notebook's case study. Here we focus on learning to *think and design* in objects.)\n", "\n", "The contrast is below: procedural code keeps data and functions in separate piles; object-oriented code bundles each thing's data and behavior into one object." ] }, { "cell_type": "code", "execution_count": 5, "id": "10441405", "metadata": { "cellView": "form", "execution": { "iopub.execute_input": "2026-06-02T14:22:10.905916Z", "iopub.status.busy": "2026-06-02T14:22:10.905407Z", "iopub.status.idle": "2026-06-02T14:22:12.953273Z", "shell.execute_reply": "2026-06-02T14:22:12.950024Z" }, "jupyter": { "source_hidden": true } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "cluster_proc\n", "\n", "Procedural\n", "\n", "\n", "cluster_oo\n", "\n", "Object-Oriented\n", "\n", "\n", "\n", "data\n", "\n", "Data\n", "(passive)\n", "\n", "\n", "\n", "func\n", "\n", "Functions\n", "(in charge)\n", "\n", "\n", "\n", "func->data\n", "\n", "\n", "acts on\n", "\n", "\n", "\n", "o1\n", "\n", "Citizen\n", "\n", "name, balance\n", "\n", "pay()\n", "\n", "\n", "\n", "o2\n", "\n", "Guard\n", "\n", "name\n", "\n", "patrol()\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#| echo: false\n", "#@title ๐Ÿงญ Two paradigms โ€” diagram code (click to show)\n", "from graphviz import Digraph\n", "\n", "g = Digraph()\n", "g.attr(rankdir=\"LR\")\n", "with g.subgraph(name=\"cluster_proc\") as s:\n", " s.attr(label=\"Procedural\", style=\"filled\", fillcolor=\"#fbe9e7\")\n", " s.node(\"data\", \"Data\\n(passive)\", shape=\"box\", style=\"filled\", fillcolor=\"white\")\n", " s.node(\"func\", \"Functions\\n(in charge)\", shape=\"box\", style=\"filled\", fillcolor=\"white\")\n", " s.edge(\"func\", \"data\", label=\"acts on\")\n", "with g.subgraph(name=\"cluster_oo\") as s:\n", " s.attr(label=\"Object-Oriented\", style=\"filled\", fillcolor=\"#e8f5e9\")\n", " s.node(\"o1\", \"{Citizen|name, balance|pay()}\", shape=\"record\", style=\"filled\", fillcolor=\"white\")\n", " s.node(\"o2\", \"{Guard|name|patrol()}\", shape=\"record\", style=\"filled\", fillcolor=\"white\")\n", "g" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ๐Ÿ’ญ Think About It โ€” Seeing the World as Objects\n", "\n", "OOP is a way of thinking: model a problem as objects that bundle data together with the things they can do.\n", "\n", "- Look around the room and pick one thing. Describe it as an \"object\": what does it *know* (its data) and what can it *do* (its actions)?\n", "- Bundling data with behavior is just one way to organize a program โ€” the earlier notebooks used separate functions and variables. What might be appealing about keeping related data and actions together?\n", "- Some people say OOP matches how humans naturally think about the world. Do you agree? Is \"everything is an object\" always a helpful lens, or can it be forced?\n", "\n", "There are no single right answers here โ€” share a sentence or two on each." ] }, { "cell_type": "markdown", "id": "2ac03129", "metadata": {}, "source": [ "**Reading it:** On the left, data and functions are separate boxes โ€” the functions reach over to act on the data. On the right, each object carries its own data *and* its own methods in one bundle. That bundling is the whole idea of OOP." ] }, { "cell_type": "markdown", "id": "6b5a7981", "metadata": {}, "source": [ "## Classes & Objects: Blueprints and the Things You Build From Them\n", "\n", "Modules organize *code*. Now we organize *data*. Back to Guinevere's tangle of parallel lists โ€” let's first see exactly how bad it gets, so the fix feels earned.\n", "\n", "The cell below tracks two citizens the \"loose variables\" way: separate lists, lined up by position." ] }, { "cell_type": "code", "execution_count": 6, "id": "0e487285", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:12.961584Z", "iopub.status.busy": "2026-06-02T14:22:12.960197Z", "iopub.status.idle": "2026-06-02T14:22:12.975220Z", "shell.execute_reply": "2026-06-02T14:22:12.971758Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Elaine Astolat owes 145.0 in North ward\n" ] } ], "source": [ "names = [\"Elaine Astolat\", \"Percival Wales\"]\n", "ages = [34, 41]\n", "wards = [\"North\", \"Harbor\"]\n", "balances = [120.00, 0.00]\n", "\n", "# To raise Elaine's balance, you must remember she's at index 0 in EVERY list:\n", "balances[0] = balances[0] + 25\n", "print(names[0], \"owes\", balances[0], \"in\", wards[0], \"ward\")" ] }, { "cell_type": "markdown", "id": "2c703ae7", "metadata": {}, "source": [ "See the problem? Elaine's facts are scattered across four lists, held together only by the fragile promise that index `0` means \"Elaine\" in all of them. Sort one list, or insert a citizen in the middle, and everything silently misaligns. There has to be a better way โ€” and there is." ] }, { "cell_type": "markdown", "id": "2a1eb484", "metadata": {}, "source": [ "A **class** is a blueprint. It describes what *every* citizen has. From one blueprint you stamp out as many **objects** as you like, and each object keeps all of its own facts together in one place.\n", "\n", "**Defining a class and building an object.** Use the `class` keyword and a special `__init__` setup method; `self` stands for the object being built:\n", "\n", "```python\n", "class ClassName:\n", " def __init__(self, value): # runs automatically when you build one\n", " self.attribute = value # store a fact ON the object itself\n", "\n", "thing = ClassName(\"some value\") # build an object โ€” __init__ runs right now\n", "thing.attribute # read a fact back with a dot\n", "```\n", "\n", "Guinevere defines a `Citizen` class once." ] }, { "cell_type": "code", "execution_count": 7, "id": "143efd27", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:12.983671Z", "iopub.status.busy": "2026-06-02T14:22:12.982295Z", "iopub.status.idle": "2026-06-02T14:22:12.996583Z", "shell.execute_reply": "2026-06-02T14:22:12.993272Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Elaine Astolat is in North ward\n" ] } ], "source": [ "class Citizen:\n", " def __init__(self, name, ward, balance):\n", " self.name = name\n", " self.ward = ward\n", " self.balance = balance\n", "\n", "elaine = Citizen(\"Elaine Astolat\", \"North\", 120.00)\n", "print(elaine.name, \"is in\", elaine.ward, \"ward\")" ] }, { "cell_type": "markdown", "id": "010e0aaf", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- `class Citizen:` starts the blueprint. By convention class names use `CapWords`.\n", "- `__init__` is a special method that runs **automatically** every time you create a Citizen โ€” it's the setup routine. You never call it by name.\n", "- `self.name = name` stores the value *on the object itself*, so it travels with that citizen forever.\n", "- `elaine = Citizen(\"Elaine Astolat\", \"North\", 120.00)` builds one object. Those three arguments line up with `__init__`'s parameters (after `self`).\n", "- `elaine.name` reads a fact back. One object, all its facts in one bundle โ€” no parallel lists." ] }, { "cell_type": "markdown", "id": "2981dbf0", "metadata": {}, "source": [ "A class earns its keep when you make *many* objects from it. Each call to `Citizen(...)` produces a brand-new, independent object with its own data." ] }, { "cell_type": "code", "execution_count": 8, "id": "e2d95036", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:13.005563Z", "iopub.status.busy": "2026-06-02T14:22:13.004674Z", "iopub.status.idle": "2026-06-02T14:22:13.017188Z", "shell.execute_reply": "2026-06-02T14:22:13.014477Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Elaine Astolat โ€” North ward, balance 120.0\n", "Percival Wales โ€” Harbor ward, balance 0.0\n" ] } ], "source": [ "percival = Citizen(\"Percival Wales\", \"Harbor\", 0.00)\n", "roster = [elaine, percival]\n", "\n", "for person in roster:\n", " print(person.name, \"โ€”\", person.ward, \"ward, balance\", person.balance)" ] }, { "cell_type": "markdown", "id": "a79bf68c", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- `percival` is a second object built from the same blueprint, with its own values.\n", "- A list of objects (`roster`) replaces Guinevere's four parallel lists with **one** list where each entry carries everything about one person.\n", "- Looping over objects and reading `person.name`, `person.ward` is far safer than juggling matched indexes." ] }, { "cell_type": "markdown", "id": "52745a0d", "metadata": {}, "source": [ "### ๐Ÿ”ฎ Predict Before You Run\n", "\n", "`elaine` and `percival` were built from the same class. Before running the next cell, predict: if we change **elaine's** balance, does percival's balance change too?" ] }, { "cell_type": "code", "execution_count": 9, "id": "b746766c", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:13.026020Z", "iopub.status.busy": "2026-06-02T14:22:13.024837Z", "iopub.status.idle": "2026-06-02T14:22:13.037448Z", "shell.execute_reply": "2026-06-02T14:22:13.034539Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Elaine: 170.0\n", "Percival: 0.0\n" ] } ], "source": [ "elaine.balance = elaine.balance + 50\n", "print(\"Elaine:\", elaine.balance)\n", "print(\"Percival:\", percival.balance)" ] }, { "cell_type": "markdown", "id": "89b1eb1a", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- Changing `elaine.balance` touched **only** elaine. Percival is untouched.\n", "- Each object has its **own** copy of the data set up in `__init__`. The class is the shared blueprint; the objects are independent.\n", "- This is the opposite of the aliasing trap from Notebook 5 โ€” separate objects don't share their instance data." ] }, { "cell_type": "markdown", "id": "ae2ba038", "metadata": {}, "source": [ "## Methods: Giving Objects Something to Do\n", "\n", "So far our objects just *hold* data. The real power of OOP is bundling data **with the behavior that acts on it**. A function that lives inside a class is called a **method**.\n", "\n", "Here's the idea everyone trips over first, so we'll go slow: every method's first parameter is `self`, and `self` means *\"the particular object this method was called on.\"*" ] }, { "cell_type": "markdown", "id": "ffb0b58c", "metadata": {}, "source": [ "Guinevere adds a `pay` method to `Citizen` so a citizen can reduce their own balance.\n", "\n", "**Defining a method.** A method is a function written inside the class, and its first parameter is always `self`:\n", "\n", "```python\n", "class ClassName:\n", " def method_name(self, argument): # self first, then the real arguments\n", " self.attribute = argument # act on THIS object's own data\n", "\n", "thing.method_name(value) # you pass `value`; Python fills in `self`\n", "```\n", "\n", "Watch the `self` in the cell below." ] }, { "cell_type": "code", "execution_count": 10, "id": "583870af", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:13.044641Z", "iopub.status.busy": "2026-06-02T14:22:13.044011Z", "iopub.status.idle": "2026-06-02T14:22:13.057053Z", "shell.execute_reply": "2026-06-02T14:22:13.053987Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "After paying 20: 100.0\n" ] } ], "source": [ "class Citizen:\n", " def __init__(self, name, ward, balance):\n", " self.name = name\n", " self.ward = ward\n", " self.balance = balance\n", "\n", " def pay(self, amount):\n", " self.balance = self.balance - amount\n", " return self.balance\n", "\n", "elaine = Citizen(\"Elaine Astolat\", \"North\", 120.00)\n", "print(\"After paying 20:\", elaine.pay(20))" ] }, { "cell_type": "markdown", "id": "e4f9ab73", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- `def pay(self, amount):` โ€” `self` is first, `amount` is the real argument.\n", "- When you write `elaine.pay(20)`, Python quietly fills `self` with `elaine` and `amount` with `20`. **You don't pass `self` yourself** โ€” that's why the call has one argument but the definition lists two.\n", "- Inside the method, `self.balance` means \"*this object's* balance.\" For this call, that's elaine's balance." ] }, { "cell_type": "markdown", "id": "0fd5adf5", "metadata": {}, "source": [ "Why does `self` matter so much? Because the **same method** does different things depending on **which object** you call it on. `self` is how the method knows whose data to touch." ] }, { "cell_type": "code", "execution_count": 11, "id": "ea6c81ba", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:13.065890Z", "iopub.status.busy": "2026-06-02T14:22:13.064997Z", "iopub.status.idle": "2026-06-02T14:22:13.078994Z", "shell.execute_reply": "2026-06-02T14:22:13.075605Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Elaine: 80.0\n", "Percival: 225.0\n" ] } ], "source": [ "percival = Citizen(\"Percival Wales\", \"Harbor\", 300.00)\n", "\n", "elaine.pay(20) # self = elaine\n", "percival.pay(75) # self = percival\n", "print(\"Elaine:\", elaine.balance)\n", "print(\"Percival:\", percival.balance)" ] }, { "cell_type": "markdown", "id": "8b51d246", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- One method definition, two objects, two different results.\n", "- In the first call `self` *is* elaine; in the second, `self` *is* percival. The code is identical โ€” the object differs.\n", "- That's the whole trick: methods are written once, but each object brings its own `self`." ] }, { "cell_type": "markdown", "id": "8cc0ebea", "metadata": {}, "source": [ "### A Trap: Forgetting `self.`\n", "\n", "Inside a method, you must write `self.balance`, not bare `balance`, to touch the object's data. A bare name just makes a throwaway local variable that vanishes when the method ends. The next cell shows the bug." ] }, { "cell_type": "code", "execution_count": 12, "id": "fa07383e", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:13.087437Z", "iopub.status.busy": "2026-06-02T14:22:13.086449Z", "iopub.status.idle": "2026-06-02T14:22:13.100175Z", "shell.execute_reply": "2026-06-02T14:22:13.096775Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Balance after paying 40: 100\n" ] } ], "source": [ "class BrokenCitizen:\n", " def __init__(self, name, balance):\n", " self.name = name\n", " self.balance = balance\n", "\n", " def pay(self, amount):\n", " balance = self.balance - amount # BUG: missing self. (writes a local)\n", " return \"done\"\n", "\n", "p = BrokenCitizen(\"Test\", 100)\n", "p.pay(40)\n", "print(\"Balance after paying 40:\", p.balance) # still 100 โ€” the payment vanished" ] }, { "cell_type": "markdown", "id": "21ff2644", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- `balance = self.balance - amount` created a *local* variable called `balance` and threw it away when `pay` returned.\n", "- The object's real `self.balance` was never touched โ€” so it's still `100`.\n", "- The fix is one word: `self.balance = self.balance - amount`. Missing `self.` is the single most common beginner OOP bug. When an update \"doesn't stick,\" check for it first." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### โœ๏ธ Your Turn โ€” Lancelot's Athlete Class\n", "\n", "Write a class `Athlete` with an `__init__` that stores `name` and `wins` (default `0`), plus a method `record_win(self)` that adds one to `wins`. Make **two** athletes, record a few wins for each, print both โ€” and confirm that recording a win for one does **not** change the other's total." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "#| eval: false\n", "# TODO: define class Athlete with __init__(self, name, wins=0)\n", "# and a method record_win(self) that does self.wins += 1.\n", "\n", "# class Athlete:\n", "# ...\n", "\n", "# Make two athletes, record some wins, then print both totals." ] }, { "cell_type": "markdown", "id": "79d62b1c", "metadata": {}, "source": [ "Some data isn't meant to be poked at from outside. Python has no hard \"private\" lock, but there's a strong convention: a leading underscore (`_pin`) means *\"internal โ€” please don't touch directly.\"* This is **encapsulation**: the object guards its own data and exposes safe methods instead. We'll also add `__str__`, a special method that controls how the object prints.\n", "\n", "**The underscore convention and a `__str__` method.**\n", "\n", "```python\n", "class ClassName:\n", " def __init__(self, value):\n", " self._hidden = value # leading _ : \"internal โ€” don't touch from outside\"\n", "\n", " def __str__(self): # special method: controls what print() shows\n", " return f\"ClassName(...)\" # return the text you want printed\n", "```" ] }, { "cell_type": "code", "execution_count": 13, "id": "0ecff5fd", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:13.108348Z", "iopub.status.busy": "2026-06-02T14:22:13.107407Z", "iopub.status.idle": "2026-06-02T14:22:13.123583Z", "shell.execute_reply": "2026-06-02T14:22:13.119806Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Account(Guinevere)\n", "True\n" ] } ], "source": [ "class Account:\n", " def __init__(self, owner, pin):\n", " self.owner = owner\n", " self._pin = pin # leading _ : internal, don't touch from outside\n", "\n", " def check_pin(self, guess):\n", " return guess == self._pin\n", "\n", " def __str__(self):\n", " return f\"Account({self.owner})\" # note: no PIN leaked\n", "\n", "a = Account(\"Guinevere\", 4821)\n", "print(a) # uses __str__\n", "print(a.check_pin(4821)) # the safe, public way to test the PIN" ] }, { "cell_type": "markdown", "id": "c8fac043", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- `self._pin` still works exactly like any attribute โ€” the underscore is a *message to humans*, not a lock. It says \"this is internal.\"\n", "- Other code is meant to call `check_pin(...)` rather than read `_pin` directly. The object controls access โ€” that's **encapsulation**.\n", "- `__str__` runs whenever the object is printed, giving a clean, readable result instead of `<__main__.Account object at 0x...>` โ€” and notice it deliberately doesn't print the PIN." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### โœ๏ธ Your Turn โ€” Guinevere's `__str__`\n", "\n", "Guinevere wants her `Citizen` objects to print nicely instead of as `<...object at 0x...>`. Add a `__str__` method to `Citizen` that returns something like `\"Elaine Astolat (North ward): owes 120.0\"`. Then create a citizen and `print` it to confirm your `__str__` runs." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "#| eval: false\n", "class Citizen:\n", " def __init__(self, name, ward, balance):\n", " self.name = name\n", " self.ward = ward\n", " self.balance = balance\n", "\n", " # TODO: add __str__(self) returning e.g.\n", " # \"Elaine Astolat (North ward): owes 120.0\"\n", "\n", "# c = Citizen(\"Elaine Astolat\", \"North\", 120.0)\n", "# print(c)" ] }, { "cell_type": "markdown", "id": "c669e796", "metadata": {}, "source": [ "## Inheritance: Build Once, Specialize Later\n", "\n", "**Arthur Pendragon** runs **Pendragon Security**. He has plain *guards* and elite *knights*. Both have a name and clock in for patrol โ€” but knights also carry a rank and lead a squad. He could write two totally separate classes and duplicate the shared parts... but duplication is exactly what we're trying to kill.\n", "\n", "**Inheritance** lets a class build *on top of* another. The general class is the **parent** (or base); the specialized one is the **child** (or subclass). The child gets everything the parent has, for free, plus its own extras." ] }, { "cell_type": "markdown", "id": "b3172b06", "metadata": {}, "source": [ "First the parent. A `Guard` has a name and can report for patrol." ] }, { "cell_type": "code", "execution_count": 14, "id": "ee1dbcf6", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:13.132841Z", "iopub.status.busy": "2026-06-02T14:22:13.131422Z", "iopub.status.idle": "2026-06-02T14:22:13.144988Z", "shell.execute_reply": "2026-06-02T14:22:13.141954Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Gareth is on patrol.\n" ] } ], "source": [ "class Guard:\n", " def __init__(self, name):\n", " self.name = name\n", "\n", " def patrol(self):\n", " return f\"{self.name} is on patrol.\"\n", "\n", "gareth = Guard(\"Gareth\")\n", "print(gareth.patrol())" ] }, { "cell_type": "markdown", "id": "afc61942", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- Nothing new yet โ€” `Guard` is an ordinary class with one attribute and one method.\n", "- The point is what comes next: we won't rewrite any of this for knights." ] }, { "cell_type": "markdown", "id": "ddcd1f8d", "metadata": {}, "source": [ "Now the child. `class Knight(Guard):` means \"a Knight **is a** Guard, with more.\" Even with an empty body, a Knight already has `name` and `patrol` โ€” inherited from Guard.\n", "\n", "**Inheriting from a parent.** Put the parent class in parentheses after the child's name:\n", "\n", "```python\n", "class Child(Parent): # Child gets all of Parent's attributes and methods for free\n", " ...\n", "```" ] }, { "cell_type": "code", "execution_count": 15, "id": "56952291", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:13.153961Z", "iopub.status.busy": "2026-06-02T14:22:13.153044Z", "iopub.status.idle": "2026-06-02T14:22:13.164552Z", "shell.execute_reply": "2026-06-02T14:22:13.162181Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Lancelot is on patrol.\n" ] } ], "source": [ "class Knight(Guard):\n", " pass # empty body โ€” but Knight already inherits everything from Guard\n", "\n", "lancelot = Knight(\"Lancelot\")\n", "print(lancelot.patrol()) # works, even though we never defined patrol in Knight" ] }, { "cell_type": "markdown", "id": "f650643d", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- `Knight(Guard)` puts `Guard` in parentheses โ€” that's the inheritance link.\n", "- `pass` means \"no extra code,\" yet `lancelot.patrol()` still works. Knight **inherited** `__init__` and `patrol`.\n", "- This is the anti-duplication payoff: shared behavior is written once, in the parent." ] }, { "cell_type": "markdown", "id": "ccf9bdb9", "metadata": {}, "source": [ "Knights need *more* than guards: a rank. So Knight gets its own `__init__`. But we don't want to retype the name-setup โ€” we ask the parent to do its part with **`super().__init__()`**, then add the new piece.\n", "\n", "**Reusing the parent's setup.** `super()` refers to the parent class; call its `__init__` first, then add what's new:\n", "\n", "```python\n", "class Child(Parent):\n", " def __init__(self, a, b):\n", " super().__init__(a) # let Parent run its own setup for the shared part\n", " self.b = b # then add the data unique to Child\n", "```" ] }, { "cell_type": "code", "execution_count": 16, "id": "3a47c6fa", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:13.176272Z", "iopub.status.busy": "2026-06-02T14:22:13.174887Z", "iopub.status.idle": "2026-06-02T14:22:13.189860Z", "shell.execute_reply": "2026-06-02T14:22:13.186221Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Lancelot is on patrol.\n", "Rank: First Knight\n" ] } ], "source": [ "class Knight(Guard):\n", " def __init__(self, name, rank):\n", " super().__init__(name) # let Guard set up self.name\n", " self.rank = rank # then add what's new to Knight\n", "\n", "lancelot = Knight(\"Lancelot\", \"First Knight\")\n", "print(lancelot.patrol())\n", "print(\"Rank:\", lancelot.rank)" ] }, { "cell_type": "markdown", "id": "68470c14", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- `super()` means \"the parent class, `Guard`.\" `super().__init__(name)` runs Guard's setup, so `self.name` is handled by the code that already exists.\n", "- `self.rank = rank` adds the part that's unique to knights.\n", "- Two ideas share one keyword: inheritance *gives* you the parent's methods; `super()` lets you *reuse* the parent's setup instead of copying it." ] }, { "cell_type": "markdown", "id": "88293961", "metadata": {}, "source": [ "A child can also **override** a method โ€” define one with the same name as the parent's, and the child's version wins for child objects. Knights announce their rank on patrol; guards don't." ] }, { "cell_type": "code", "execution_count": 17, "id": "f3c58943", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:13.198085Z", "iopub.status.busy": "2026-06-02T14:22:13.197212Z", "iopub.status.idle": "2026-06-02T14:22:13.210679Z", "shell.execute_reply": "2026-06-02T14:22:13.207425Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Gareth is on patrol.\n", "First Knight Lancelot leads the patrol.\n" ] } ], "source": [ "class Knight(Guard):\n", " def __init__(self, name, rank):\n", " super().__init__(name)\n", " self.rank = rank\n", "\n", " def patrol(self): # same name as Guard.patrol โ€” this version wins for Knights\n", " return f\"{self.rank} {self.name} leads the patrol.\"\n", "\n", "print(Guard(\"Gareth\").patrol())\n", "print(Knight(\"Lancelot\", \"First Knight\").patrol())" ] }, { "cell_type": "markdown", "id": "1b4c7d0f", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- Both classes have a `patrol` method. Python uses the one belonging to the object's *own* class โ€” Guard's for guards, Knight's for knights.\n", "- The Knight version **overrides** the inherited one. The Guard version is untouched and still used by guards.\n", "- Same method name, different behavior per class โ€” this is how one line of calling code (`x.patrol()`) adapts to whatever object `x` is." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### โœ๏ธ Your Turn โ€” Morgan's Researcher Hierarchy\n", "\n", "Write a base class `Researcher` that stores `name` and has a method `describe()` returning `\" runs experiments.\"`. Then write a subclass `LeadResearcher(Researcher)` that adds a `team_size` (using `super().__init__()`) and **overrides** `describe()` to also mention the team size. Make one of each and print both descriptions." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "#| eval: false\n", "# TODO: class Researcher with __init__(self, name) and describe(self).\n", "# TODO: class LeadResearcher(Researcher) that adds team_size via super().__init__()\n", "# and overrides describe().\n", "\n", "# r = Researcher(\"Morgan\")\n", "# lead = LeadResearcher(\"Nimue\", team_size=4)\n", "# print(r.describe())\n", "# print(lead.describe())" ] }, { "cell_type": "markdown", "id": "67767c69", "metadata": {}, "source": [ "### Polymorphism: One Call, Many Behaviors\n", "\n", "Look again at what just happened. We called `.patrol()` on a guard and on a knight, and each did the *right thing for its own type* โ€” and we never checked which was which. That ability has a name: **polymorphism** (Greek for \"many shapes\"). One method name, many behaviors, chosen automatically by the object.\n", "\n", "It pays off when you have a *mix* of objects. Arthur's roster holds guards and knights together. He loops once and calls `.patrol()` on each โ€” Python runs the correct version every time. The next cell builds a mixed roster and patrols it." ] }, { "cell_type": "code", "execution_count": 18, "id": "b4d65fcb", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:13.217908Z", "iopub.status.busy": "2026-06-02T14:22:13.217106Z", "iopub.status.idle": "2026-06-02T14:22:13.228536Z", "shell.execute_reply": "2026-06-02T14:22:13.226391Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Gareth is on patrol.\n", "First Knight Lancelot leads the patrol.\n", "Kay is on patrol.\n" ] } ], "source": [ "roster = [Guard(\"Gareth\"), Knight(\"Lancelot\", \"First Knight\"), Guard(\"Kay\")]\n", "\n", "for officer in roster:\n", " print(officer.patrol()) # each object runs its OWN patrol()" ] }, { "cell_type": "markdown", "id": "a4144bc1", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- The list mixes two different classes, yet the loop treats every item the same way.\n", "- Each `officer.patrol()` runs the version belonging to *that* object's class โ€” Guard's for guards, Knight's for knights.\n", "- We never wrote an `if` to check the type. That's the power of **polymorphism**: add a brand-new kind of officer tomorrow, and this loop keeps working untouched." ] }, { "cell_type": "markdown", "id": "2a167a3d", "metadata": {}, "source": [ "### Picture It: The Class Family Tree\n", "\n", "The diagram below shows the relationship: `Knight` inherits from `Guard`, adding `rank` and its own `patrol`. The next cell draws it." ] }, { "cell_type": "code", "execution_count": 19, "id": "24713226", "metadata": { "cellView": "form", "execution": { "iopub.execute_input": "2026-06-02T14:22:13.237212Z", "iopub.status.busy": "2026-06-02T14:22:13.236191Z", "iopub.status.idle": "2026-06-02T14:22:16.360800Z", "shell.execute_reply": "2026-06-02T14:22:16.358230Z" }, "jupyter": { "source_hidden": true } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "<>:8: SyntaxWarning: invalid escape sequence '\\l'\n", "<>:8: SyntaxWarning: invalid escape sequence '\\l'\n", "C:\\Users\\brend\\AppData\\Local\\Temp\\claude\\ipykernel_75116\\1007185676.py:8: SyntaxWarning: invalid escape sequence '\\l'\n", " g.node(\"Knight\", \"{Knight|name (inherited)\\lrank (new)|patrol() (overridden)\\l}\")\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "Guard\n", "\n", "Guard\n", "\n", "name\n", "\n", "patrol()\n", "\n", "\n", "\n", "Knight\n", "\n", "Knight\n", "\n", "name (inherited)\n", "rank (new)\n", "\n", "patrol() (overridden)\n", "\n", "\n", "\n", "Knight->Guard\n", "\n", "\n", "  is a\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#| echo: false\n", "#@title ๐ŸŒณ Class family tree โ€” diagram code (click to show)\n", "from graphviz import Digraph\n", "\n", "g = Digraph()\n", "g.attr(\"node\", shape=\"record\", style=\"filled\", fillcolor=\"#eef4ff\", fontname=\"Helvetica\")\n", "g.node(\"Guard\", \"{Guard|name|patrol()}\")\n", "g.node(\"Knight\", \"{Knight|name (inherited)\\lrank (new)|patrol() (overridden)\\l}\")\n", "g.edge(\"Knight\", \"Guard\", label=\" is a\", arrowhead=\"onormal\")\n", "g" ] }, { "cell_type": "markdown", "id": "149b6876", "metadata": {}, "source": [ "**Reading it:** The hollow arrow points from child to parent and reads \"Knight *is a* Guard.\" Knight keeps `name` from Guard, adds `rank`, and replaces `patrol` with its own version." ] }, { "cell_type": "markdown", "id": "15c013d0", "metadata": {}, "source": [ "### A Trap: A Shared Mutable Class Attribute\n", "\n", "If you put a **mutable** value (like a list) directly in the class body instead of inside `__init__`, *every* object shares the one same list. Change it through one object and it changes for all of them. The next cell shows the surprise." ] }, { "cell_type": "code", "execution_count": 20, "id": "3a88c2cf", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:16.367719Z", "iopub.status.busy": "2026-06-02T14:22:16.366881Z", "iopub.status.idle": "2026-06-02T14:22:16.380460Z", "shell.execute_reply": "2026-06-02T14:22:16.378054Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Red squad: ['Gareth']\n", "Blue squad: ['Gareth']\n" ] } ], "source": [ "class Squad:\n", " members = [] # BUG: one list shared by ALL squads\n", "\n", " def add(self, name):\n", " self.members.append(name)\n", "\n", "red = Squad()\n", "blue = Squad()\n", "red.add(\"Gareth\")\n", "print(\"Red squad:\", red.members)\n", "print(\"Blue squad:\", blue.members) # Gareth leaked into Blue!" ] }, { "cell_type": "markdown", "id": "6acce78a", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- `members = []` sits in the *class body*, so there is exactly one list, owned by the class and shared by every object.\n", "- Adding to `red.members` changed the one shared list, so `blue.members` shows it too.\n", "- **The fix:** create the list per-object inside `__init__` (`self.members = []`). Anything that should differ between objects belongs in `__init__`, not the class body. This is the OOP cousin of Notebook 5's aliasing trap." ] }, { "cell_type": "markdown", "id": "a1e65094", "metadata": {}, "source": [ "## \"Is-a\" vs. \"Has-a\": Inheritance Isn't Always the Answer\n", "\n", "Inheritance is powerful, and beginners reach for it too often. There's a simple test. Ask: is this *a kind of* that, or does this *contain* that?\n", "\n", "- **Is-a โ†’ inheritance.** A Knight *is a* kind of Guard, so `Knight(Guard)` is right.\n", "- **Has-a โ†’ composition.** A Squad is *not* a kind of guard โ€” a Squad *has* guards. You don't inherit; you store them inside.\n", "\n", "That second pattern โ€” building an object out of other objects โ€” is called **composition**. Arthur's `Squad` below *has a* list of guards and delegates the work to them." ] }, { "cell_type": "code", "execution_count": 21, "id": "bd0d5d20", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:16.387567Z", "iopub.status.busy": "2026-06-02T14:22:16.386521Z", "iopub.status.idle": "2026-06-02T14:22:16.399329Z", "shell.execute_reply": "2026-06-02T14:22:16.397001Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['Gareth is on patrol.', 'First Knight Lancelot leads the patrol.']\n" ] } ], "source": [ "class Squad:\n", " def __init__(self, name):\n", " self.name = name\n", " self.members = [] # a Squad HAS guards (composition)\n", "\n", " def recruit(self, guard):\n", " self.members.append(guard)\n", "\n", " def patrol_all(self):\n", " return [g.patrol() for g in self.members]\n", "\n", "watch = Squad(\"Night Watch\")\n", "watch.recruit(Guard(\"Gareth\"))\n", "watch.recruit(Knight(\"Lancelot\", \"First Knight\"))\n", "print(watch.patrol_all())" ] }, { "cell_type": "markdown", "id": "fb5b8fd0", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- `Squad` does **not** inherit from `Guard` โ€” a squad isn't a guard. It *holds* guards in `self.members`. That's **composition**.\n", "- `patrol_all` does its job by asking each member to `patrol()` โ€” the Squad delegates to the objects it contains.\n", "- A famous design guideline says **\"favor composition over inheritance.\"** Composition is more flexible: you can change a squad's members while the program runs, but you can't change a class's parent. Overusing inheritance is one of the main complaints in this notebook's case study." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ๐Ÿ’ญ Think About It โ€” Drawing the Family Tree\n", "\n", "Inheritance models an \"is-a\" relationship (a dog *is an* animal); composition models \"has-a\" (a car *has an* engine). Choosing wrong leads to awkward designs.\n", "\n", "- Real-world categories are messy: is a tomato a fruit or a vegetable? Why might forcing the world into clean \"is-a\" hierarchies cause trouble for a programmer?\n", "- Give an everyday example of an \"is-a\" relationship and a \"has-a\" relationship. How did you decide which was which?\n", "- \"Prefer composition over inheritance\" is common advice. Without the jargon, why might building things by *combining parts* be more flexible than building them by *specializing a parent*?\n", "\n", "There are no single right answers here โ€” share a sentence or two on each." ] }, { "cell_type": "markdown", "id": "0df681f9", "metadata": {}, "source": [ "## Designing with Objects: Finding the Classes\n", "\n", "Knowing the syntax is not the same as knowing *what classes to build*. Deciding that is **design** โ€” and it's the part that makes OOP a computer-science skill, not just a Python trick. So where do classes come from?\n", "\n", "A reliable way to start is the **noun/verb heuristic**. Write the problem in plain English, then:\n", "\n", "- **Nouns** (the things) become candidate **classes** and **attributes**.\n", "- **Verbs** (the actions) become candidate **methods**.\n", "\n", "It won't hand you a perfect design, but it gets you off the blank page." ] }, { "cell_type": "markdown", "id": "0750b69a", "metadata": {}, "source": [ "Here is Nimue describing Lake Logistics in plain words:\n", "\n", "> \"A **dispatch desk** holds **packages** waiting to go out. Each **package** has a **destination** and a **weight**. A **courier** *picks up* the next package and *delivers* it.\"\n", "\n", "Underline the nouns and verbs and the design almost writes itself:\n", "\n", "- Nouns โ†’ classes: `DispatchDesk`, `Package`, `Courier`. Attributes: a package's `destination` and `weight`.\n", "- Verbs โ†’ methods: `pick_up`, `deliver`.\n", "\n", "One more pair of ideas tells you whether you carved the classes *well*." ] }, { "cell_type": "markdown", "id": "ca520bc2", "metadata": {}, "source": [ "### Two Words for Judging a Design: Cohesion and Coupling\n", "\n", "Designers judge a set of classes with two ideas worth knowing by name:\n", "\n", "- **Cohesion** โ€” how focused one class is. A `Package` that only knows package things has *high* cohesion. A class that tracks packages *and* couriers *and* billing *and* the weather is doing too much โ€” low cohesion. **Each class should have one clear responsibility.**\n", "- **Coupling** โ€” how tangled classes are with each other. If changing `Package` forces you to rewrite `Courier`, they are *tightly* coupled, and fragile. Good designs keep coupling *low*, so you can change one class without breaking the others.\n", "\n", "The goal in one line: **high cohesion, low coupling.** Each object minds one job and talks to the others only through clean methods โ€” exactly the interfaces you have been building." ] }, { "cell_type": "code", "execution_count": 22, "id": "78c57c46", "metadata": { "cellView": "form", "execution": { "iopub.execute_input": "2026-06-02T14:22:16.408556Z", "iopub.status.busy": "2026-06-02T14:22:16.407261Z", "iopub.status.idle": "2026-06-02T14:22:17.721432Z", "shell.execute_reply": "2026-06-02T14:22:17.718700Z" }, "jupyter": { "source_hidden": true } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "desk\n", "\n", "DispatchDesk\n", "\n", "queue of packages\n", "\n", "next_package()\n", "\n", "\n", "\n", "pkg\n", "\n", "Package\n", "\n", "destination, weight\n", "\n", " \n", "\n", "\n", "\n", "desk->pkg\n", "\n", "\n", "  holds\n", "\n", "\n", "\n", "cour\n", "\n", "Courier\n", "\n", "name\n", "\n", "pick_up(), deliver()\n", "\n", "\n", "\n", "cour->pkg\n", "\n", "\n", "  delivers\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#| echo: false\n", "#@title ๐Ÿงฉ Noun/verb design โ€” diagram code (click to show)\n", "from graphviz import Digraph\n", "\n", "g = Digraph()\n", "g.attr(\"node\", shape=\"record\", style=\"filled\", fillcolor=\"#eef4ff\", fontname=\"Helvetica\")\n", "g.node(\"desk\", \"{DispatchDesk|queue of packages|next_package()}\")\n", "g.node(\"pkg\", \"{Package|destination, weight|}\")\n", "g.node(\"cour\", \"{Courier|name|pick_up(), deliver()}\")\n", "g.edge(\"desk\", \"pkg\", label=\" holds\")\n", "g.edge(\"cour\", \"pkg\", label=\" delivers\")\n", "g" ] }, { "cell_type": "markdown", "id": "83b3558b", "metadata": {}, "source": [ "**Reading it:** Each box is one class with high cohesion โ€” it owns just its own data and jobs. The arrows are the only links between them (low coupling): a Courier touches a Package only by delivering it, never by reaching inside the DispatchDesk. Change one box and the others barely notice." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### โœ๏ธ Your Turn โ€” Morgan's Lab, On Paper\n", "\n", "**No code for this one โ€” design first.** Morgan le Fay describes Avalon Health Sciences:\n", "\n", "> \"A **study** has a **title** and a list of **participants**. Each **participant** has an **ID** and an **age**. A **researcher** can *enroll* a participant and *count* how many are signed up.\"\n", "\n", "Using the **noun/verb heuristic**, in a markdown cell list (a) the **classes** you'd create, (b) the **attributes** each holds, and (c) the **methods**. Then write one sentence on which class should own the `enroll` method, and why. (Hint: think about cohesion โ€” which class is *responsible* for the participant list?)" ] }, { "cell_type": "markdown", "id": "a1f0db45", "metadata": {}, "source": [ "## Abstraction: One Interface, Many Implementations\n", "\n", "In Notebook 5 we met **abstract data types (ADTs)**: the idea that a collection's *promise* (what it does) is separate from its *storage* (how it does it). We even used a `CustomerQueue` without caring how it stored things. Now we build one on purpose โ€” that's what classes are *for*.\n", "\n", "**Nimue** runs **Lake Logistics**. Her dispatch desk needs a **queue**: packages are handled first-come, first-served. The promise is simple: *add a package to the back; take the next one from the front.* How it's stored is her business, not her users' business." ] }, { "cell_type": "markdown", "id": "4b8512ec", "metadata": {}, "source": [ "### Abstraction: The Big Idea Behind All of Computer Science\n", "\n", "Abstraction sounds like a fancy word, but you have leaned on it all semester. **Abstraction means hiding complicated details behind a simple surface, so you can use a thing without knowing how it works inside.**\n", "\n", "Trace it through the whole course. The transistors from Notebook 2 hide under **logic gates**. Logic gates hide under the CPU's **machine code**. Machine code hides under **Python**. And now your **classes** hide their own details under a few method names. Each layer lets the layer above it ignore the mess below.\n", "\n", "That is not a minor convenience โ€” it is *the* reason software can grow large at all. No human can hold a whole system in mind at the transistor level. Abstraction is how a small mind builds a large machine, one layer resting on the next. A well-designed class is just one more layer in that tower โ€” which is exactly what Nimue's delivery queue, below, will be." ] }, { "cell_type": "code", "execution_count": 23, "id": "c34330b0", "metadata": { "cellView": "form", "execution": { "iopub.execute_input": "2026-06-02T14:22:17.726926Z", "iopub.status.busy": "2026-06-02T14:22:17.726358Z", "iopub.status.idle": "2026-06-02T14:22:18.911059Z", "shell.execute_reply": "2026-06-02T14:22:18.908596Z" }, "jupyter": { "source_hidden": true } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "Your classes (DeliveryQueue)\n", "\n", "Your classes (DeliveryQueue)\n", "\n", "\n", "\n", "Python\n", "\n", "Python\n", "\n", "\n", "\n", "Your classes (DeliveryQueue)->Python\n", "\n", "\n", "  hides\n", "\n", "\n", "\n", "Machine code\n", "\n", "Machine code\n", "\n", "\n", "\n", "Python->Machine code\n", "\n", "\n", "  hides\n", "\n", "\n", "\n", "Logic gates\n", "\n", "Logic gates\n", "\n", "\n", "\n", "Machine code->Logic gates\n", "\n", "\n", "  hides\n", "\n", "\n", "\n", "Transistors\n", "\n", "Transistors\n", "\n", "\n", "\n", "Logic gates->Transistors\n", "\n", "\n", "  hides\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#| echo: false\n", "#@title ๐Ÿ›๏ธ Abstraction layers โ€” diagram code (click to show)\n", "from graphviz import Digraph\n", "\n", "g = Digraph()\n", "g.attr(\"node\", shape=\"box\", style=\"filled\", fillcolor=\"#e3f2fd\", fontname=\"Helvetica\")\n", "layers = [\"Your classes (DeliveryQueue)\", \"Python\", \"Machine code\", \"Logic gates\", \"Transistors\"]\n", "for name in layers:\n", " g.node(name, name)\n", "for upper, lower in zip(layers, layers[1:]):\n", " g.edge(upper, lower, label=\" hides\")\n", "g" ] }, { "cell_type": "markdown", "id": "a1b39f58", "metadata": {}, "source": [ "**Reading it:** Read top to bottom. Each layer rests on the one beneath it and hides that layer's detail. You write `DeliveryQueue` without ever thinking about transistors โ€” every layer below is abstracted away." ] }, { "cell_type": "markdown", "id": "8e815c21", "metadata": {}, "source": [ "The `DeliveryQueue` class below stores packages in a list, but hides that behind two methods: `enqueue` and `dequeue`. Code that uses the queue never touches the list directly." ] }, { "cell_type": "code", "execution_count": 24, "id": "8081cb60", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:18.917507Z", "iopub.status.busy": "2026-06-02T14:22:18.916973Z", "iopub.status.idle": "2026-06-02T14:22:18.926883Z", "shell.execute_reply": "2026-06-02T14:22:18.924413Z" } }, "outputs": [], "source": [ "class DeliveryQueue:\n", " def __init__(self):\n", " self._items = [] # storage โ€” hidden behind the methods\n", "\n", " def enqueue(self, package): # add to the back\n", " self._items.append(package)\n", "\n", " def dequeue(self): # take from the front\n", " return self._items.pop(0)\n", "\n", " def is_empty(self):\n", " return len(self._items) == 0" ] }, { "cell_type": "markdown", "id": "e08e7eaf", "metadata": {}, "source": [ "Nimue's dispatcher uses the queue through its **interface** only โ€” `enqueue`, `dequeue`, `is_empty`. They never see the list inside." ] }, { "cell_type": "code", "execution_count": 25, "id": "bbb5c4d9", "metadata": { "execution": { "iopub.execute_input": "2026-06-02T14:22:18.933184Z", "iopub.status.busy": "2026-06-02T14:22:18.932625Z", "iopub.status.idle": "2026-06-02T14:22:18.940976Z", "shell.execute_reply": "2026-06-02T14:22:18.939293Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Now delivering: Camelot #1\n", "Now delivering: Astolat #2\n", "Now delivering: Lyonesse #3\n" ] } ], "source": [ "dispatch = DeliveryQueue()\n", "dispatch.enqueue(\"Camelot #1\")\n", "dispatch.enqueue(\"Astolat #2\")\n", "dispatch.enqueue(\"Lyonesse #3\")\n", "\n", "while not dispatch.is_empty():\n", " print(\"Now delivering:\", dispatch.dequeue())" ] }, { "cell_type": "markdown", "id": "b550433e", "metadata": {}, "source": [ "### Understanding the Code\n", "\n", "- `enqueue` adds to the back; `dequeue` removes from the front โ€” first in, first out.\n", "- The dispatcher's loop talks only to the **interface** (the method names). It has no idea a Python list is doing the work.\n", "- That's **abstraction**: Nimue could swap the list for something faster tomorrow, and as long as the four method names behave the same, the dispatcher code doesn't change at all." ] }, { "cell_type": "markdown", "id": "c324ff1e", "metadata": {}, "source": [ "### Picture It: One Interface, Two Implementations\n", "\n", "The diagram makes the point: two completely different storage choices can sit behind the *same* promise. Callers only ever see the top box." ] }, { "cell_type": "code", "execution_count": 26, "id": "0ad5d85f", "metadata": { "cellView": "form", "execution": { "iopub.execute_input": "2026-06-02T14:22:18.945821Z", "iopub.status.busy": "2026-06-02T14:22:18.945258Z", "iopub.status.idle": "2026-06-02T14:22:20.044752Z", "shell.execute_reply": "2026-06-02T14:22:20.042348Z" }, "jupyter": { "source_hidden": true } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "api\n", "\n", "DeliveryQueue interface\n", "enqueue ยท dequeue ยท is_empty\n", "\n", "\n", "\n", "a\n", "\n", "Implementation A\n", "Python list\n", "\n", "\n", "\n", "api->a\n", "\n", "\n", "  could use\n", "\n", "\n", "\n", "b\n", "\n", "Implementation B\n", "linked nodes\n", "\n", "\n", "\n", "api->b\n", "\n", "\n", "  could use\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#| echo: false\n", "#@title ๐Ÿ”Œ Interface vs. implementation โ€” diagram code (click to show)\n", "from graphviz import Digraph\n", "\n", "g = Digraph()\n", "g.attr(\"node\", shape=\"box\", style=\"filled\", fontname=\"Helvetica\")\n", "g.node(\"api\", \"DeliveryQueue interface\\nenqueue ยท dequeue ยท is_empty\", fillcolor=\"#fde8c8\")\n", "g.node(\"a\", \"Implementation A\\nPython list\", fillcolor=\"#dff0d8\")\n", "g.node(\"b\", \"Implementation B\\nlinked nodes\", fillcolor=\"#dff0d8\")\n", "g.edge(\"api\", \"a\", label=\" could use\")\n", "g.edge(\"api\", \"b\", label=\" could use\")\n", "g" ] }, { "cell_type": "markdown", "id": "7eefe15d", "metadata": {}, "source": [ "**Reading it:** Callers depend only on the top box (the promise). Either storage box below can fulfill it, and swapping them never reaches the caller. Promise on top, storage on the bottom โ€” that's the ADT idea from Notebook 5, now built by hand." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### โœ๏ธ Your Turn โ€” Nimue's Queue, Extended\n", "\n", "Add a method `peek(self)` to the `DeliveryQueue` class that returns the next package **without** removing it (the front item is `self._items[0]`). Build a queue, add three packages, call `peek()`, then `dequeue()` once, and show that `peek` reported the right one without consuming it." ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "#| eval: false\n", "class DeliveryQueue:\n", " def __init__(self):\n", " self._items = []\n", "\n", " def enqueue(self, package):\n", " self._items.append(package)\n", "\n", " def dequeue(self):\n", " return self._items.pop(0)\n", "\n", " def is_empty(self):\n", " return len(self._items) == 0\n", "\n", " # TODO: add peek(self) -> return the front item WITHOUT removing it\n", "\n", "# Build a queue, enqueue three packages, peek, dequeue once, peek again." ] }, { "cell_type": "markdown", "id": "87bdd13a", "metadata": {}, "source": [ "## The Four Pillars of OOP\n", "\n", "People often boil object-oriented design down to four ideas. You have now met all four in action โ€” here they are by name, so you recognize them on a test, in a textbook, or in a job interview.\n", "\n", "1. **Encapsulation** โ€” bundle data with the methods that guard it. *(Guinevere's `Account` kept its `_pin` private and exposed `check_pin`.)*\n", "2. **Abstraction** โ€” hide the *how* behind a simple interface. *(Nimue's `DeliveryQueue` hid its list behind `enqueue`/`dequeue`.)*\n", "3. **Inheritance** โ€” let a child class build on a parent. *(A `Knight` extended a `Guard`.)*\n", "4. **Polymorphism** โ€” one method name behaving correctly for each object. *(The mixed patrol roster.)*\n", "\n", "If you can name these four and give an example of each, you understand what object-oriented design *is*. Everything after this is practice and judgment โ€” knowing not just *how* to use each one, but *when*." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ๐Ÿ’ญ Think About It โ€” When Is OOP the Right Choice?\n", "\n", "You've now seen the four pillars โ€” encapsulation, inheritance, polymorphism, abstraction โ€” working together.\n", "\n", "- OOP is powerful but not free: it adds structure and ceremony. For what kinds of problems do you think that structure pays off, and where might it just be overhead?\n", "- Of the four pillars, which felt most genuinely useful to you, and which felt most abstract or hard to grasp? Why?\n", "- A big program can be organized many ways. Why does *how* code is organized matter so much, even when two versions produce the exact same output?\n", "\n", "There are no single right answers here โ€” share a sentence or two on each." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## โœ๏ธ Capstone โ€” Build a Battle Game (OOP)\n", "\n", "This is where object-oriented design proves its worth. You'll build a small **turn-based battle game**: a hero faces a band of monsters, they trade blows in a loop, and the last one standing wins. The default is a Camelot knight against a horde, but **pick your own**: a space marine versus aliens, a wizard versus slimes, a robot versus viruses.\n", "\n", "To earn its keep, your game must be built the OOP way:\n", "\n", "- a **base class** โ€” call it `Combatant` (or `Character`) โ€” that holds shared data like `name` and `hp` and has methods such as `attack(self, other)`, `take_damage(self, amount)`, and `is_alive(self)`;\n", "- a hero built from it, and **at least two kinds of monster** made by **inheritance** (for example `Goblin` and `Dragon` extending a `Monster` base), where each monster **overrides** a method so it fights differently โ€” that's polymorphism;\n", "- a **list of monsters** the hero battles, fought in a **turn loop** until the hero falls or every monster is defeated.\n", "\n", "**Step 1 โ€” Design it first (before the AI).** In a markdown cell, sketch your classes the noun/verb way: what is the base `Combatant` (its attributes and methods), what two monster types extend it, and how does each one fight differently? Decide the starting `hp` and attack power for the hero and the monsters.\n", "\n", "**Step 2 โ€” Turn your design into a prompt.** Fill the blanks and send it to Gemini (or Claude / ChatGPT):\n", "\n", "> *Write a small turn-based battle game in Python for Google Colab. Theme: **[your theme]**. Define a base class `Combatant` with `name` and `hp` and methods `attack(other)`, `take_damage(amount)`, and `is_alive()`. Make a hero and at least two monster subclasses (**[your two monsters]**) that inherit from a `Monster` base and **override** how they attack. Put the monsters in a **list** and loop through a battle, turn by turn, printing what happens, until the hero dies or all monsters are defeated. Keep it short and beginner-readable.*\n", "\n", "**Step 3 โ€” Get the bones working, then test.** Paste it into the cell below and **run it now**. Get the simplest fight working first โ€” one hero, one monster, hp going down, someone wins โ€” and check it by hand before adding more.\n", "\n", "**Step 4 โ€” Add the bells and whistles.** Once a basic fight runs, add **one or two**, testing after each: a third monster type with a special attack, a healing potion the hero can use, a `Boss` that overrides `take_damage` to be tougher, or a `Party` class that *has* a list of heroes (composition).\n", "\n", "**Step 5 โ€” Test it like Smee.** Try to break it: hp driven below zero, an empty monster list, a monster attacking after it's already been defeated. Fix one thing it gets wrong.\n", "\n", "**Step 6 โ€” Reflect.** In a markdown cell, write 2โ€“3 sentences: where did inheritance save you repetition, and what did the AI get wrong that you had to fix?\n", "\n", "Remember the course rule: *AI is a fast first draft. You verify.*" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "#| eval: false\n", "# โœ๏ธ Paste your AI-built battle game here, then run it and fix what's broken." ] }, { "cell_type": "markdown", "id": "e9437385", "metadata": {}, "source": [ "## Key Terms\n", "\n", "- **Abstraction** โ€” Hiding complicated details behind a simple surface, so you can use something without knowing how it works inside.\n", "- **Abstraction layer** โ€” One level in a stack where each layer hides the details of the layer below it (transistors โ†’ gates โ†’ machine code โ†’ Python โ†’ your classes).\n", "- **Attribute** โ€” A piece of data stored on an object, e.g. `self.name`. Read with `object.name`.\n", "- **Class** โ€” A blueprint that describes the data and methods every object of that type will have.\n", "- **Cohesion** โ€” How focused a class is on a single responsibility. High cohesion is the goal.\n", "- **Composition** โ€” Building an object out of other objects (a \"has-a\" relationship), as an alternative to inheritance.\n", "- **Coupling** โ€” How dependent classes are on one another. Low coupling is the goal.\n", "- **Encapsulation** โ€” Bundling data with the methods that act on it, and guarding the data so it is changed only through those methods.\n", "- **`__init__`** โ€” The setup method that runs automatically when an object is created. You never call it by name.\n", "- **Inheritance** โ€” Building a class (child) on top of another (parent) so the child gets the parent's attributes and methods for free.\n", "- **Instance** โ€” Another word for an object: one specific thing built from a class.\n", "- **Method** โ€” A function defined inside a class; it acts on an object's data and takes `self` as its first parameter.\n", "- **Module** โ€” A file of Python code you can `import` into another file. The standard library is a big collection of them.\n", "- **Namespace** โ€” The \"where a name lives\" idea: `math.pi` says `pi` lives inside the `math` namespace." ] }, { "cell_type": "markdown", "id": "7dcf5b8a", "metadata": {}, "source": [ "- **Noun/verb heuristic** โ€” A design starter: nouns in a problem become candidate classes and attributes; verbs become candidate methods.\n", "- **Object** โ€” A thing built from a class, holding its own data. Also called an instance.\n", "- **Object-oriented programming (OOP)** โ€” A paradigm built from objects that own their data *and* their behavior.\n", "- **Override** โ€” Defining a method in a child class with the same name as the parent's; the child's version wins for child objects.\n", "- **Paradigm** โ€” A basic style of organizing a program, such as procedural or object-oriented.\n", "- **Polymorphism** โ€” One method name producing different behavior depending on the object it is called on.\n", "- **Procedural programming** โ€” A paradigm where functions act on passive data, rather than objects owning their own behavior.\n", "- **Responsibility** โ€” The one job a well-designed class is meant to handle.\n", "- **`self`** โ€” The first parameter of every method; refers to the particular object the method was called on.\n", "- **Standard library** โ€” The modules that ship with Python (`math`, `random`, `datetime`, โ€ฆ) โ€” no install needed.\n", "- **`super()`** โ€” A reference to the parent class, used (e.g. `super().__init__(...)`) to reuse the parent's code instead of retyping it." ] }, { "cell_type": "markdown", "id": "2a5e46a2", "metadata": {}, "source": [ "## Summary\n", "\n", "- **OOP is a paradigm, not just syntax.** Objects own their data *and* their behavior โ€” unlike procedural code, where functions act on passive data.\n", "- **Modules kill copy-paste.** Write code once in a file, `import` it everywhere, fix it in one place.\n", "- **Classes are blueprints; objects are what you stamp out from them.** Each object keeps its own data together, replacing tangles of parallel lists.\n", "- **`__init__` runs automatically** to set an object up; **methods** act on the object's data and always take `self` first. `self` is *this* object โ€” that's how one method serves many objects.\n", "- **The four pillars:** encapsulation, abstraction, inheritance, and polymorphism โ€” the four ideas that define object-oriented design.\n", "- **Design before syntax.** Find classes with the noun/verb heuristic, give each one a single responsibility (**high cohesion**), keep them untangled (**low coupling**), and prefer **composition** (\"has-a\") over **inheritance** (\"is-a\") when one thing merely contains another.\n", "- **Abstraction is the big idea behind all of CS** โ€” each layer hides the one below, which is how small minds build large machines.\n", "- **Two bugs to remember:** forgetting `self.` (your update vanishes) and a mutable class attribute (one list shared by every object)." ] }, { "cell_type": "markdown", "id": "8d51133b", "metadata": {}, "source": [ "## What's Next\n", "\n", "You can now organize *code* into modules and *data + behavior* into objects. **Notebook 7** asks a harder question: once you've built something, how *good* is it? We'll measure algorithms with **Big O**, compare searching and sorting strategies, and then step to the edge of the map โ€” the problems that **no** program, however cleverly written, can ever solve." ] }, { "cell_type": "markdown", "id": "9df64a5e", "metadata": {}, "source": [ "*COMP 1150 โ€” Computer Science Concepts ยท Brendan Shea, PhD*\n", "*Content licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).*" ] } ], "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.13.9" } }, "nbformat": 4, "nbformat_minor": 5 }