diff --git a/09.modules.ipynb b/09.modules.ipynb new file mode 100644 index 0000000..e035c1d --- /dev/null +++ b/09.modules.ipynb @@ -0,0 +1,297 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c13b000e", + "metadata": {}, + "source": [ + "## modules\n", + "\n", + "Why do we use modules?\n", + "\n", + "- Code reuse: allows code to be shared & reused.\n", + "- Namespace partitioning: Avoid namespace clashes among different parts of your program.\n", + "\n", + "e.g.\n", + "```\n", + "math.isclose(a, b) # compares two floats (math.isclose(0.1+0.2, 0.3) == True)\n", + "directions.isclose(point1, location) \n", + "```\n", + "\n", + "### Terminology\n", + "\n", + "Python files can either be:\n", + "\n", + "**Top Level Files**\n", + "\n", + "Sometimes called a \"script\", consists of main control flow of program. Will typically use modules.\n", + "\n", + "**Modules**\n", + "\n", + "Define set of variables, functions, classes, etc. that can be used by other programs/modules.\n", + "\n", + "**Application**\n", + "\n", + "Top-level file that uses other modules.\n", + "\n", + "**Library**\n", + "\n", + "Collection of one or more modules with no top level file.\n", + "\n", + "\n", + "### Import Syntax\n", + "\n", + "```python\n", + "# bring `modulename` into current scope\n", + "import modulename \n", + "\n", + "# brings `thing1`, `thing2` into current scope\n", + "from math import sin, cos \n", + "\n", + "# bring `thing1` into current scope, but with `new_name`\n", + "from modulename import thing1 as new_name \n", + "\n", + "# import everything from `modulename` into scope (DO NOT USE)\n", + "from modulename import *\n", + "```\n", + "\n", + "When an `import` statement is run (either form), the following happens:\n", + "\n", + "- Python searches on disk for the module. (order determined by PYTHONPATH)\n", + "- Once found, the file is executed until the end of the file is reached.\n", + "- If `import modname`, then all top-level definitions are assigned to the module namespace.\n", + "- If `from modname`, then the imported definitions are added to the global namespace.\n", + "\n", + "Note: `print` statements & other top-level code will run." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de09d942", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4d5f6e18", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import statistics\n", + "#help(statistics)" + ] + }, + { + "cell_type": "markdown", + "id": "ce952a10", + "metadata": {}, + "source": [ + "### import `modulename`" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "09c27388", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "-1.0" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import math\n", + "\n", + "math.cos(math.pi)" + ] + }, + { + "cell_type": "markdown", + "id": "9d5f6a9f", + "metadata": {}, + "source": [ + "### `help` and `dir`\n", + "\n", + "`help` can be called on functions or modules and returns their docstring\n", + "\n", + "`dir` can be called on any object and returns all properties" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "164caf5b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on built-in function cos in module math:\n", + "\n", + "cos(x, /)\n", + " Return the cosine of x (measured in radians).\n", + "\n" + ] + } + ], + "source": [ + "#help(math)\n", + "help(math.cos)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "58c6b06f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "#dir(math)" + ] + }, + { + "cell_type": "markdown", + "id": "0128b8f4", + "metadata": {}, + "source": [ + "### from `modulename` import `thing`" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "87bef5f0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "39.4" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from statistics import mean\n", + "\n", + "mean([34, 44, 16, 21, 82])" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1604f2d7", + "metadata": {}, + "outputs": [], + "source": [ + "#help(statistics.mode)" + ] + }, + { + "cell_type": "markdown", + "id": "47280629-2406-4730-b04e-b2df88c37b14", + "metadata": {}, + "source": [ + "## Ed Post on importing your code from IPython: \n", + "\n", + "https://edstem.org/us/courses/68016/discussion/5533114\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1b80b290", + "metadata": {}, + "outputs": [], + "source": [ + "#dir(__builtins__)" + ] + }, + { + "cell_type": "markdown", + "id": "361acaa7", + "metadata": {}, + "source": [ + "## module conventions\n", + "\n", + "Named in snake_case, typically concise.\n", + "\n", + "Convention is to use underscore prefix for modules intended to be internal:\n", + "\n", + "`import _util`\n", + "\n", + "Avoid built-in module names, `fast_math` not `math`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57d95a79", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29519f78", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.10.15" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": false, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": true + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/10.OOP.ipynb b/10.OOP.ipynb new file mode 100644 index 0000000..90d5e67 --- /dev/null +++ b/10.OOP.ipynb @@ -0,0 +1,1453 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8d96c164-300f-4b01-907c-d645b5338b42", + "metadata": { + "tags": [] + }, + "source": [ + "# Object Oriented Programming\n", + "\n", + "\n", + "A programming paradigm that utilizes the concepts of \"objects\" which represent related bits of data & code.\n", + "\n", + "A common misconception is that a language needs classes to be object-oriented. While classes are the most common feature provided in OO-focused languages, one can write code in any language that fits this paradigm.\n", + "\n", + "In a hypothetical language that only had data structures and functions, we might write code like:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4ae30b4-bd82-46ac-90bd-f6d885018f52", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "person_a = {\"name\": \"Andy\", \"costume\": \"Cowboy\", \"candy\": []}\n", + "person_b = {\"name\": \"Gil\", \"costume\": \"Robot\", \"candy\": []}\n", + "person_c = {\"name\": \"Lisa\", \"costume\": \"Ghost\", \"candy\": []}\n", + "\n", + "candy_bag = [\"Kit Kat\", \"Kit Kat\", \"Lollipop\", \"M&Ms\"]\n", + "\n", + "def costume_is_scary(person):\n", + " return person[\"costume\"] in (\"Ghost\", \"Wolfman\", \"Mummy\")\n", + "\n", + "def do_trick(person):\n", + " print(f\"{person['name']} did a trick\")\n", + "\n", + "def trick_or_treat(person):\n", + " success = give_candy(candy_bag, person)\n", + " # extra candy for scary costumes!\n", + " if costume_is_scary(person):\n", + " give_candy(candy_bag, person)\n", + " if not success:\n", + " do_trick(person)\n", + "\n", + "def give_candy(candy_bag, person):\n", + " if candy_bag:\n", + " candy = random.choice(candy_bag)\n", + " candy_bag.remove(candy)\n", + " person[\"candy\"].append(candy)\n", + " return True\n", + " else:\n", + " return False" + ] + }, + { + "cell_type": "markdown", + "id": "35e5ef1d-5ac3-4771-8ec7-cfeb00ee2fa9", + "metadata": {}, + "source": [ + "A common pattern to see is a lot of functions that need a particular data structure as a parameter. Objects give us a way to connect that code, making it more clear what should be passed in, and reducing the chance of errors.\n", + "\n", + "The same code might be rewritten as:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94ddd4b7-3229-4307-b097-c0d03f5c5a73", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self, name, costume):\n", + " self.name = name\n", + " self.costume = costume\n", + " self.candy = []\n", + "\n", + " def is_scary(self):\n", + " return self.costume in (\"Ghost\", \"Wolfman\", \"Mummy\")\n", + " \n", + " def do_trick(self):\n", + " self.tricks = True\n", + " print(f\"{self.name} did a trick\")\n", + " \n", + " def accept_candy(self, candy):\n", + " self.candy.append(candy)\n", + " \n", + "class NoCandy(Exception):\n", + " pass\n", + "\n", + "class House:\n", + " def __init__(self, initial_candy):\n", + " self.candy = initial_candy\n", + " \n", + " def get_candy(self):\n", + " if not self.candy:\n", + " raise NoCandy(\"no more candy!\")\n", + " candy = random.choice(self.candy)\n", + " self.candy.remove(candy)\n", + " return candy\n", + " \n", + "\n", + "def trick_or_treat(person, house):\n", + " try:\n", + " candy = house.get_candy()\n", + " person.accept_candy(candy)\n", + " if person.is_scary():\n", + " person.accept_candy(house.get_candy())\n", + " except NoCandy:\n", + " do_trick(person, house)" + ] + }, + { + "cell_type": "markdown", + "id": "50cfecf9-db6a-46a3-8441-4b4765e97324", + "metadata": {}, + "source": [ + "This code provides blueprints for what data & actions a \"person\" has. We also take our \"candy_bag\" list and turn it into a full-fledged object as well, since presumably we'd have multiple copies of it in our real-world application." + ] + }, + { + "cell_type": "markdown", + "id": "78fb4860-13c9-4c90-951c-c9b19ff6f47d", + "metadata": {}, + "source": [ + "## Terminology\n", + "\n", + "- **Object** - An encapsulation of data & related operations.\n", + "- **Class** - A blueprint for an object, providing methods that will act on instances of the data.\n", + "- **Instance** - An object created from a class \"blueprint\".\n", + "- **Method** - A function that is tied to a specific class.\n", + "- **Attribute** - Data that is tied to a specific instance.\n", + "- **Constructor** - A special method that creates & populates an instance of a class.\n" + ] + }, + { + "cell_type": "markdown", + "id": "cd8d1021-89d5-4572-8fc8-7083f0414ce6", + "metadata": { + "tags": [] + }, + "source": [ + "## Everything in Python is an Object\n", + "\n", + "`isinstance` is the preferred way to check if an item is of a particular type.\n", + "\n", + "It can return true for multiple types, we'll see why this is the case shortly." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "eb098ee4-813d-4ede-9fba-04caf9922a76", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance([1, 2, 3], list)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "8f904840-e5f1-4f44-a11a-dcb568914b98", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance([1, 2, 3], tuple)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "b7c087da-0213-41e4-a298-873d2e677ab9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance([1, 2, 3], object)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "1ad4e49c-dbc2-406b-9148-6f708c01f4b9", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['', 0, 0.0, {1, 2, 3}, ]\n" + ] + } + ], + "source": [ + "s = set([1,2,3])\n", + "\n", + "# using constructors here for demo purposes, generally would use a literal (e.g. [], 0, \"\") for these\n", + "ll = list() \n", + "ll.append(str())\n", + "ll.append(int())\n", + "ll.append(float())\n", + "ll.append(s)\n", + "ll.append(print)\n", + "\n", + "print(ll)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "ea08451b-862a-446f-beb4-13bd8fcaa869", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[True, True, True, True, True]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[isinstance(item, object) for item in ll]" + ] + }, + { + "cell_type": "markdown", + "id": "e4c2a53e-9070-4446-b72b-d54f63fe8ead", + "metadata": {}, + "source": [ + "Keeping this in mind can help keep things straight when we delve deeper into making our own objects.\n", + "\n", + "Let's revisit a few things that we already know:\n", + "\n", + "- each `list` is independent of all others, when you create a new via `list()` (or `[]`) that is an **instance**\n", + "- calling things like `.append` operate on the instance they are called from. \n", + "- Some methods modify the underlying object (`.append`) while others just provide a return value like any other function. (What are some non-modifying methods?)" + ] + }, + { + "cell_type": "markdown", + "id": "e870d8fe-786d-4499-86fc-4808a89981a3", + "metadata": {}, + "source": [ + "## Classes in Python" + ] + }, + { + "cell_type": "markdown", + "id": "350fc7e7-ded5-4d4c-bab5-59de74d23764", + "metadata": {}, + "source": [ + "### Instances, Classes, and Instantiation\n", + "\n", + "One way to think of classes are as blueprints for creating specific realizations.\n", + "\n", + "The blueprint can specify features that vary from car to car (color, transmission type, etc.). We can create multiple car **instances** with different values for a given attribute." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "1fce0f7d-7065-432d-8672-8fefd2b11f67", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Honda Civic 2019\n", + "Chevy Volt 2022\n" + ] + } + ], + "source": [ + "class Car:\n", + " # __init__ is a special method\n", + " # known as a double-underscore or dunder method\n", + " # in Python it represents our constructor\n", + " def __init__(self, make, model, year=2000):\n", + " self.make = make\n", + " self.model = model\n", + " self.year = year\n", + " self.mileage = 0\n", + " self.hybrid = False\n", + " \n", + "# to actually create Cars, we need to call this constructor\n", + "car1 = Car(\"Honda\", \"Civic\", 2019)\n", + "car2 = Car(\"Chevy\", \"Volt\", 2022)\n", + "print(car1.make, car1.model, car1.year)\n", + "print(car2.make, car2.model, car2.year)\n", + "car3 = car2" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8cb58e57-c4ba-4066-ab43-2c8469757918", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "car3 is car2" + ] + }, + { + "cell_type": "markdown", + "id": "07f8b1f1-c2cc-4e93-98a4-0f5cbdee72f4", + "metadata": {}, + "source": [ + "This is known as *instantiation*, making an instance of the class.\n", + "\n", + "### `self` & methods\n", + "\n", + "The first parameter of methods is always `self`. \n", + "\n", + "This parameter is never passed directly, but is a local reference to the object the instance is being called upon.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "68e49035-d0cc-4f24-b19b-c84d568a1233", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "class Car:\n", + " def __init__(self, make, model, year):\n", + " self.make = make\n", + " self.model = model\n", + " self.year = year\n", + " self.mileage = 0\n", + " self.hybrid = False\n", + " \n", + " def print_report(self):\n", + " print(f\"{self.year} {self.make} {self.model} with {self.mileage} miles\")\n", + " \n", + " def drive(self, miles):\n", + " self.mileage += miles\n", + " \n", + "car1 = Car(\"Honda\", \"Civic\", 2019)\n", + "car2 = Car(\"Chevy\", \"Volt\", 2022)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "0e2162f2-7e5a-451b-96e9-b3fd4c5bcfaa", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2019 Honda Civic with 0 miles\n" + ] + } + ], + "source": [ + "car1.print_report()" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "dbb352b0-7cc4-476f-9d58-e7fff9c3ae35", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2022 Chevy Volt with 500 miles\n" + ] + } + ], + "source": [ + "car2.drive(500)\n", + "car2.print_report()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "756e9562-79bc-4e38-9eb5-e5c0e8622124", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2019 Honda Civic with 0 miles\n" + ] + } + ], + "source": [ + "car1.print_report()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "4afa851b-562e-4b81-a92a-017275f7d246", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "500\n" + ] + } + ], + "source": [ + "print(car2.mileage)" + ] + }, + { + "cell_type": "markdown", + "id": "fec57402-3e07-482a-b51d-0dda7e25e031", + "metadata": {}, + "source": [ + "Because of `self`, methods can know which instance they are operating upon." + ] + }, + { + "cell_type": "markdown", + "id": "8fec9ea6-1e09-42c8-808d-0eb5952fb8e2", + "metadata": {}, + "source": [ + "#### How does this work?\n", + "\n", + "This is confusing at first glance, where does `self` come from? \n", + "\n", + "It is actually the \"parameter before the dot\".\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "0c0e0e3b-308d-411f-842d-e422d8dc7968", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2022 Chevy Volt with 500 miles\n" + ] + } + ], + "source": [ + "# explicitly call Car.print_report and pass self\n", + "Car.print_report(car2) \n", + "# this is not how we call class methods! (but it works)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "6f0599d6-df4f-413d-9c87-ac7ca479b35f", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[4]" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ll = []\n", + "list.append(ll, 4) # list is class, ll is self here\n", + "ll" + ] + }, + { + "cell_type": "markdown", + "id": "41a91dbb-868b-4cf9-92fc-a417da548e4f", + "metadata": {}, + "source": [ + "#### What happens if `self` is omitted?\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "184e8e69-0eb8-4068-bb3c-a2aec2433fec", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "class Mistake:\n", + " def __init__(self):\n", + " print(\"constructor!\")\n", + " \n", + " def method_no_self(self):\n", + " print(\"method!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "42fb46df-2abb-4387-9e49-5ddb27d0c23a", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "constructor!\n", + "method!\n" + ] + } + ], + "source": [ + "m = Mistake()\n", + "m.method_no_self()\n", + "# rewritten as Mistake.method_no_self(m)" + ] + }, + { + "cell_type": "markdown", + "id": "d9c92e9f-65cd-4a7a-847a-6f5651552672", + "metadata": {}, + "source": [ + "### Attributes\n", + "\n", + "- Created on assignment, like other variables.\n", + "- `self.name = value`\n", + "- All attributes are accessible from inside the class and outside:\n", + " - `self.name` from inside.\n", + " - `instance_name.name` from outside.\n", + " \n", + "**Best practice: create all attributes inside constructor!**\n", + "\n", + "Why?" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "31f1a926-7099-4740-9f86-769967fa41a9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "my_car = Car(\"DMC\", \"DeLorean\", 1982)\n", + "#print(my_car.miles_driven)\n", + "my_car.driver = \"Marty\" # allowed, but to be avoided" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c0afa4e-0219-4b45-a527-b63263a04d5d", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "a2082903-236a-4b8b-b47f-a491ca3c71d6", + "metadata": {}, + "source": [ + "#### Exception to the rule: function objects\n", + "\n", + "Functions are objects, and can have attributes assigned to them as well.\n", + "\n", + "We sometimes do this since there's no opportunity to assign them before. (Because functions do not have constructors we can modify.)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "f0e0212d-363e-4bde-bfa4-3e717b800f5b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def f():\n", + " print(f\"called f()\")\n", + " #f.call_count = 0 # NO\n", + "f.call_count = 0" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "b15355ff-359d-4fc8-b47a-04e4c0d4afd5", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "called f()\n", + "1\n" + ] + } + ], + "source": [ + "f.call_count += 1\n", + "f()\n", + "print(f.call_count)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "1cbc7aa8-19b6-43eb-91bb-69b889bd2a59", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# using a decorator to add call_count to any function\n", + "def counter(func):\n", + " #inner.call_count\n", + " def inner(*args, **kwargs):\n", + " inner.call_count += 1\n", + " print(f\"call count {inner.call_count}\")\n", + " return func(*args, **kwargs)\n", + " inner.call_count = 0\n", + " return inner" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "ad71f5ff-e97d-4559-8c37-c4b1af0c0ec2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "@counter\n", + "def f():\n", + " print(\"called f()\")" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "4654848c-b793-4847-a18c-072da3188868", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "@counter\n", + "def g():\n", + " print(f\"called g()\")" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "d2e7192b-bb8c-4b55-9a38-1f9d00e650c7", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "call count 1\n", + "called f()\n", + "call count 2\n", + "called f()\n", + "call count 3\n", + "called f()\n" + ] + } + ], + "source": [ + "f()\n", + "f()\n", + "f()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa824ffd-8301-41e3-89f3-ef474dcc3025", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "g()" + ] + }, + { + "cell_type": "markdown", + "id": "b9ca97f1-7d64-4d27-a685-fa17251acf3b", + "metadata": {}, + "source": [ + "## Encapsulation\n", + "\n", + "Why might it be a bad idea to allow users to change attributes?\n" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "68b9d90d-17ed-40c2-a3f2-736d7d6357ca", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# oops!\n", + "car2.mileage -= 100\n", + "car2.hybrid = \"no\"" + ] + }, + { + "cell_type": "markdown", + "id": "28b09877-bd45-4d57-a671-9927cc02f1aa", + "metadata": {}, + "source": [ + "Furthermore, imagine we've noticed sometimes `year` is an `int` and other times a `str`. We could decide to remedy this in our constructor like:\n", + "\n", + "```python\n", + "class Car:\n", + " def __init__(self, make, model, year):\n", + " self.make = make\n", + " self.model = model\n", + " self.year = int(year)\n", + " self.mileage = 0\n", + " \n", + " def drive(self, miles):\n", + " if miles < 0:\n", + " return # maybe an error instead? \n", + " self.mileage += miles\n", + "```\n", + "\n", + "We can also protect against trying to roll back the odometer by driving in reverse.\n", + "\n", + "If other code is assigning to internal variables, we need to make these checks/changes in dozens of places.\n", + "\n", + "**Encapsulation** therefore allows the implementation of a class interface to be changed with *minimal impact* upon users of the class.\n", + "\n", + "Good object-oriented design involves thinking through what **interface** you're providing to code making use of your objects.\n", + "\n", + "Python has changed how (e.g.) `dict` works internally several times over the decades. Imagine if each time they did so, methods like `.keys()` and `pop()` stopped working." + ] + }, + { + "cell_type": "markdown", + "id": "638405f2-8e72-4450-95e6-973ffcbee224", + "metadata": { + "tags": [] + }, + "source": [ + "### \"private\" in Python\n", + "\n", + "Some languages use access specifiers like \"private\", \"public\", \"protected\" to handle this. Python instead relies on convention.\n", + "\n", + "A name with a single underscore at the front is meant to be \"internal\" to the class, and should not be modified except from methods of that class.\n", + "\n", + "A name with a double underscore at the front is actually modified internally by Python to avoid people assigning to it accidentally.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "41292841-0aa9-4bfd-9cfe-c12f860bbdd9", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2022 Chevy Volt with 500 miles\n" + ] + } + ], + "source": [ + "class Car: \n", + " def __init__(self, make, model, year):\n", + " self._make = make \n", + " self._model = model \n", + " self._year = year\n", + " self.__mileage = 0\n", + " \n", + " def drive(self, miles):\n", + " if miles > 0:\n", + " self.__mileage += miles\n", + " else:\n", + " ...\n", + " \n", + " def print_report(self):\n", + " print(f\"{self._year} {self._make} {self._model} with {self.__mileage} miles\")\n", + " \n", + "car1 = Car(\"Honda\", \"Civic\", 2019)\n", + "car2 = Car(\"Chevy\", \"Volt\", 2022)\n", + "\n", + "car2.drive(500)\n", + "car2.print_report()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "ef88e138-13c2-4174-b51f-03d1c3aad5aa", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'Car' object has no attribute '__mileage'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[20], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[43mcar2\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m__mileage\u001b[49m)\n", + "\u001b[0;31mAttributeError\u001b[0m: 'Car' object has no attribute '__mileage'" + ] + } + ], + "source": [ + "print(car2.__mileage)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "cb2ba9e0-a5af-45bd-bc21-c25bc9414637", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "???\n" + ] + } + ], + "source": [ + "car2._make = \"???\"\n", + "print(car2._make) \n", + "# soft protection, can still access but \"at your own risk\"" + ] + }, + { + "cell_type": "markdown", + "id": "5320f144-0164-4f09-b191-7e5eae6c020c", + "metadata": {}, + "source": [ + "### Dunder Methods\n", + "\n", + "Methods that begin and end with double-underscore are called special methods or dunder methods. These allow us to define classes that implement existing protocols.\n", + "\n", + "* `__repr__`\n", + "* `__str__`\n", + "* `__eq__`" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "9647e5ad-86a4-486f-bf7e-44668d25a472", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "class Car: \n", + " def __init__(self, make, model, year):\n", + " self._make = make \n", + " self._model = model \n", + " self._year = year\n", + " self.__mileage = 0\n", + "\n", + " def drive(self, miles):\n", + " if miles > 0:\n", + " self.__mileage += miles\n", + " else:\n", + " ...\n", + " \n", + " def __eq__(self, other):\n", + " # we can decide equality does/doesn't include mileage\n", + " return (self._make == other._make \n", + " and self._model == other._model \n", + " and self._year == other._year)\n", + " \n", + " def __repr__(self):\n", + " return f\"Car({self._make}, {self._model}, {self._year})\"\n", + "\n", + " def __str__(self):\n", + " return f\"{self._year} {self._make} {self._model} with {self.__mileage} miles\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14e8fbb4-0453-425d-804e-ee2a4aa44976", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "eb77364b-b30d-4a10-b0fb-355fa83e003d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "truck = Car(\"Ford\", \"F-150\", 1985)\n", + "truck2 = Car(\"Ford\", \"F-150\", 1985)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "5c124af8-9d7d-4909-9124-1ba2bb170f16", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Car(Ford, F-150, 1985)" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "truck" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "e5b3f849-83b8-4625-b160-1141e39a1a99", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'1985 Ford F-150 with 0 miles'" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "str(truck) # implicit conversion to string, uses repr(truck)" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "8f4919d6-bab6-49d4-9c73-dbf0790b118c", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1985 Ford F-150 with 0 miles\n" + ] + } + ], + "source": [ + "print(str(truck)) # uses str(truck)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "74bf6a50-b18d-443d-a0ef-e2f1b8004898", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "truck == car1 # eq" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "01bdde7f-c4a8-4014-9f75-ff041370892d", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "truck.__eq__(car1)" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "b29e3c72-a392-4991-aeb5-fee9463495f2", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "truck == truck2 # eq" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26edc5e9-84ad-4e6f-9e9f-c7a546f7f7b7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "truck is truck2" + ] + }, + { + "cell_type": "markdown", + "id": "d08d5dbf-7662-4534-8a3a-d44a79ee56f0", + "metadata": {}, + "source": [ + "### `str` vs `repr`\n", + "\n", + "`repr` is supposed to be a programmatic interpretation, used in debugging output. In jupyter/ipython if a function returns a value we see the repr by default.\n", + "\n", + "`str` is used when print is called, or an explicit conversion to string as shown above.\n", + "\n", + "If only `__repr__` is defined, then `str(obj)` will use `__repr__`, so if you don't have a need for them to differ, then define `__repr__`." + ] + }, + { + "cell_type": "markdown", + "id": "3f21be66-4b7a-4ef7-97e0-ff1c382993f6", + "metadata": { + "tags": [] + }, + "source": [ + "## Protocols, Duck-Typing, and Polymorphism\n", + "\n", + "In a language like C++, functions can be created with one name but different argument lists.\n", + "\n", + "```c++\n", + "void foo(int x)\n", + "void foo(double x)\n", + "void foo(int x, double y)\n", + "```\n", + "\n", + "The compiler can decide which function to call at compile time based on the types given.\n", + "\n", + "This is called \"polymorphism\".\n", + "\n", + "We've seen one way to achieve similar results via variadic arguments.\n", + "\n", + "In Python, polymorphism stems from the idea **\"the meaning of an operation depends on the objects being operated on\"**.\n", + "\n", + "```python\n", + "1 + 5 # addition\n", + "\"1\" + \"5\" # string concatenation\n", + "[1,2,3] + [4,5] # list concatenation\n", + "```\n", + "\n", + "Remember, we mentioned that everything in Python is an `object` and `object`s have operations associated with them. \n", + "\n", + "```python\n", + "def times(x, y):\n", + " return x * y\n", + "```\n", + "\n", + "As long as our objects `x` and `y` support the `*` protocol, it is safe to call `times(x, y)`.\n", + "\n", + "### Duck Typing\n", + "\n", + "In Python, instead of forcing our arguments to be specific types, we use something known as \"Duck Typing.\" This comes from the expression:\n", + "\"If it looks like a duck, and it quacks like a duck, it might as well be a duck.\"\n", + "\n", + "## Protocols\n", + "\n", + "Another way of thinking about this is that objects of a given type follow a certain protocol.\n", + "\n", + "- \"addable\"\n", + "- \"comparable\"\n", + "- \"iterable\"\n", + "- \"callable\"\n", + "\n", + "These are typically implemented via dunder methods. To be \"addable\" an object needs a `__add__` method at minimum. To be comparable it needs `__eq__` and `__lt__` or `__gt__` at least.\n", + "\n", + "Let's look at iterable now through this new lens:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a161eedc-520a-4827-a0ba-701f1cba3376", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1\n", + "2\n", + "3\n", + "4\n", + "5\n" + ] + } + ], + "source": [ + "l = [1, 2, 3, 4, 5]\n", + "g = (x**2 for x in l)\n", + "r = range(8)\n", + "\n", + "# iterable: we can use it in a for loop\n", + "for x in l: # or g, or r\n", + " print(x)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "9f73f6a7-b97d-457d-92bc-f96ee9f45246", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + " at 0x1085c0860>" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g" + ] + }, + { + "cell_type": "markdown", + "id": "2caf44fa-74e1-4e42-95d3-f0a070d34dc7", + "metadata": {}, + "source": [ + "What actually happens here?\n", + "\n", + "The iteration protocol has a few requirements:\n", + "\n", + "- When an iterable is passed to an iteration context (for loop, comprehension, `map`, etc.) The context will call `iter(iterable)` to obtain the object's iterator. \n", + "- `iter(obj)` calls `obj.__iter__`\n", + "- Iterator objects return values one at a time, we can call `next(iterator)` to obtain the next value. \n", + "`next(it)` calls `it.__next__`\n", + "- `StopIteration` is raised when there are no more values." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a71a043a-1c1b-4c88-b489-b5e47b9f2d4f", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + " at 0x1085c2c20>\n", + "\n", + "\n" + ] + } + ], + "source": [ + "l = [1, 2, 3]\n", + "g = (x**2 for x in l)\n", + "r = range(8)\n", + "\n", + "li = iter(l)\n", + "gi = iter(g)\n", + "ri = iter(r)\n", + "li2 = iter(l)\n", + "\n", + "print(li)\n", + "print(gi)\n", + "print(ri)\n", + "print(li2)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "6aec1be0-9094-450c-95f7-91ebec9c3c76", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "ename": "StopIteration", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mStopIteration\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[13], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43mnext\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mli\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mStopIteration\u001b[0m: " + ] + } + ], + "source": [ + "next(li)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "032cfb1f-25f6-4359-8190-53129ec1b67b", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(li2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dbb7148c-af4a-434b-9863-1ff7083fab05", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "print(next(li))\n", + "print(next(gi))\n", + "print(next(ri))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4260dadd-9476-4103-bf7f-496812904a1e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "print(next(li))\n", + "print(next(gi))\n", + "print(next(ri))" + ] + }, + { + "cell_type": "markdown", + "id": "599aa900-9cc4-4579-b94a-8c0474e4ef06", + "metadata": { + "tags": [] + }, + "source": [ + "### Discussion\n", + "\n", + "- What else is iterable?\n", + "- What are other protocols we've seen?\n", + "- Do all iterables eventually raise `StopIteration`?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b4cb301-3152-4cd2-bba7-6f0cdd258afe", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.10.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}