From d9456ce2cd60999131608d2d6dcc4be8b8249b23 Mon Sep 17 00:00:00 2001 From: James Turk Date: Mon, 4 Nov 2024 14:17:06 -0600 Subject: [PATCH] 12 --- 12.inheritance.ipynb | 929 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 929 insertions(+) create mode 100644 12.inheritance.ipynb diff --git a/12.inheritance.ipynb b/12.inheritance.ipynb new file mode 100644 index 0000000..968aafd --- /dev/null +++ b/12.inheritance.ipynb @@ -0,0 +1,929 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "44ca09f7", + "metadata": {}, + "source": [ + "## Inheritance\n", + "\n", + "### Motivations\n", + "\n", + "Let's say we're building an application that tracks students." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "feaa69a2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "class Student:\n", + "\n", + " # this is a class-level variable\n", + " # instead of each instance having its own copy\n", + " # the variable is shared among all `Student`\n", + " next_id_counter = 1\n", + " \n", + " def __init__(self, name):\n", + " # assign each student a unique id\n", + " # note use of Student. not self.\n", + " self.id = Student.next_id_counter\n", + " Student.next_id_counter += 1\n", + " \n", + " self.name = name\n", + " self.year = 1\n", + " self.major = \"Undeclared\"\n", + " self.course_grades = {}\n", + " \n", + " def add_grade(self, course_name, grade):\n", + " self.course_grades[course_name] = grade\n", + " \n", + " @property\n", + " def gpa(self):\n", + " grade_pts = {\"A\":4.0, \"A-\":3.7, \"B+\":3.3, \"B\":3.0, \"B-\":2.7, \"C+\":2.3, \"C\":2.0, \"C-\":1.7, \"D+\":1.3, \"D\":1.0, \"F\":0.0} \n", + " if len(self.course_grades) == 0:\n", + " return 0\n", + " return sum(grade_pts[g] for g in self.course_grades.values()) / len(self.course_grades)\n", + " \n", + " def __repr__(self):\n", + " return f\"Student(name={self.name}, id={self.id}, gpa={self.gpa})\"" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "c410fb1d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "s1 = Student(\"Adam\")\n", + "s2 = Student(\"Beth\")\n", + "s2.add_grade(\"Programming Python\", \"A\")\n", + "s2.add_grade(\"Discrete Math\", \"B+\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "f9deab26", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Student(name=Adam, id=1, gpa=0)\n", + "Student(name=Beth, id=2, gpa=3.65)\n" + ] + } + ], + "source": [ + "print(s1)\n", + "print(s2)" + ] + }, + { + "cell_type": "markdown", + "id": "6aa3358d", + "metadata": {}, + "source": [ + "Perhaps we want to add `Alumni` to our application.\n", + "\n", + "An alum will have some things in common with students:\n", + "\n", + "- They still have a name.\n", + "- We want to remember their major.\n", + "- We'll still want to keep track of their grades/GPA.\n", + "\n", + "We now also:\n", + "\n", + "- Want to record their year of graduation.\n", + "- No longer want to allow grades to be recorded.\n", + "- Want to be able to calculate how long ago they graduated.\n", + "- When displaying them, we want to display their graduation year.\n", + "\n", + "**How to implement?**\n", + "\n", + "We *could* copy `student.py` and rename to `alum.py` and rename the class as needed.\n", + "\n", + "**But copying & pasting is generally a bad idea!**\n", + "\n", + "We'd need to fix bugs & add features in both classes separately.\n", + "\n", + "A new feature in `Student` would need to be copied over to `Alum`, this will quickly get messy.\n" + ] + }, + { + "cell_type": "markdown", + "id": "d0d61727", + "metadata": {}, + "source": [ + "### Implementation\n", + "\n", + "Instead we will use **inheritance**, which allows us to create a new class from an existing one. The new class inherits the attributes and methods from the parent.\n", + "\n", + "- **superclass**, **parent**, or **base** class: The pre-existing class.\n", + "- **subclass**, **child**, or **derived** class: The new class that inherits the code (attributes & methods) of another class.\n", + "\n", + "Subclasses can extend/modify the functionality of superclasses.\n", + "\n", + "Syntax:\n", + "\n", + "```python\n", + "class Subclass(Superclass):\n", + " pass\n", + "```\n", + "\n", + "For example:\n", + "\n", + "```python\n", + "class Alum(Student):\n", + " pass\n", + "```\n", + "\n", + "At this point, `Alum` is a new class with the exact same implementation as `Student`.\n", + "\n", + "Typically we'll want to add new instance & class variables, methods, etc.\n", + "\n", + "Newly defined features will only apply to instances of `Alum`\n", + "\n", + "It is possible to override parent class behavior, or rely on parent behavior, whichever is needed." + ] + }, + { + "cell_type": "markdown", + "id": "e4d45c9f", + "metadata": {}, + "source": [ + "### Adding & Overriding Behavior" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "47f7dfc9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "class Alum(Student):\n", + " def __init__(self, name, grad_year):\n", + " # call Student's constructor, which contains id logic\n", + " super().__init__(name)\n", + " self.graduation_year = grad_year\n", + " \n", + " # new behavior\n", + " def years_since_graduation(self, now):\n", + " return now - self.graduation_year\n", + " \n", + " # overrides parent's add_grade\n", + " def add_grade(self, course_name, grade):\n", + " print(\"Sorry, you cannot add grades to Alums\")\n", + " # we choose not call super().add_grade here\n", + " \n", + " # overrides parent's __repr__\n", + " def __repr__(self):\n", + " #return f\"Alum(name={self.name}, id={self.id}, gpa={self.gpa}, graduated={self.graduation_year})\"\n", + " string = super().__repr__()\n", + " string += \" is an alum\"\n", + " return string" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "4811b0fd", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Student(name=Charlie, id=3, gpa=0) is an alum\n", + "6 years since graduation\n", + "Sorry, you cannot add grades to Alums\n" + ] + }, + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "alum1 = Alum(\"Charlie\", 2016)\n", + "print(alum1)\n", + "print(alum1.years_since_graduation(2022), \"years since graduation\")\n", + "alum1.add_grade(\"Python\", \"B\")\n", + "alum1.gpa" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "18a75666-9986-4e2b-9d55-ec0e0b9e930d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "alum2 = Alum(\"Charlie\", 2016)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6719a36-b827-4373-87e0-23714438b83f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "3cd672f1", + "metadata": {}, + "source": [ + "### super()\n", + "\n", + "Allows direct access to parent class(es).\n", + "\n", + "Many different ways to be called, but for our purposes we will stick to `super().method_name()` to access parent implementation of `method_name()`" + ] + }, + { + "cell_type": "markdown", + "id": "f6d2c035", + "metadata": {}, + "source": [ + "### issubclass & isinstance" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "a46d5b02", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(7, int)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3596a45-2c00-4541-910d-08251d3f9c5f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# same as?\n", + "type(7) == int" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "eaa8b6db-e7df-4137-b4c8-67dfa0be06d5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(7) == object" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "0e50f7bc-541e-4fec-9cd6-ad708f312831", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# isinstance checks the inheritance hierarchy \n", + "isinstance(7, object)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd55fd8d", + "metadata": {}, + "outputs": [], + "source": [ + "isinstance([1, 2, 3], list)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "feb9f2ac", + "metadata": {}, + "outputs": [], + "source": [ + "s1 = Student(\"Sarah\")\n", + "isinstance(s1, Student)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6a2f3b1", + "metadata": {}, + "outputs": [], + "source": [ + "# child classes are instances of parent types\n", + "alum1 = Alum(\"Charlie\", 2016)\n", + "isinstance(alum1, Student)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f759efb", + "metadata": {}, + "outputs": [], + "source": [ + "# but not vice-versa\n", + "isinstance(s1, Alum)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "1d52b927", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "issubclass() arg 1 must be a class", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[15], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# takes class names\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;43missubclass\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43malum1\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mStudent\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: issubclass() arg 1 must be a class" + ] + } + ], + "source": [ + "# takes class names\n", + "issubclass(alum1, Student)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "a71ee1cb", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "issubclass(Student, Alum)" + ] + }, + { + "cell_type": "markdown", + "id": "32e11764", + "metadata": {}, + "source": [ + "### `object`\n", + "\n", + "Every object derives from a base class named `object`.\n", + "\n", + "```python\n", + "class Point:\n", + " def __init__(self, x, y):\n", + " self.x = y\n", + "\n", + "# Same as: \n", + "\n", + "class Point(object):\n", + " def __init__(self, x, y):\n", + " self.x = y\n", + " self.y = y\n", + "```\n", + "\n", + "### MRO\n", + "\n", + "When we call a function, Python walks up the chain of parent classes to determine the first one that has the method defined.\n", + "\n", + "This is called the **method resolution order**.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "1d4e00ca", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on Alum in module __main__ object:\n", + "\n", + "class Alum(Student)\n", + " | Alum(name, grad_year)\n", + " | \n", + " | Method resolution order:\n", + " | Alum\n", + " | Student\n", + " | builtins.object\n", + " | \n", + " | Methods defined here:\n", + " | \n", + " | __init__(self, name, grad_year)\n", + " | Initialize self. See help(type(self)) for accurate signature.\n", + " | \n", + " | __repr__(self)\n", + " | Return repr(self).\n", + " | \n", + " | add_grade(self, course_name, grade)\n", + " | # overrides parent's add_grade\n", + " | \n", + " | years_since_graduation(self, now)\n", + " | # new behavior\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Readonly properties inherited from Student:\n", + " | \n", + " | gpa\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Data descriptors inherited from Student:\n", + " | \n", + " | __dict__\n", + " | dictionary for instance variables (if defined)\n", + " | \n", + " | __weakref__\n", + " | list of weak references to the object (if defined)\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Data and other attributes inherited from Student:\n", + " | \n", + " | next_id_counter = 5\n", + "\n" + ] + } + ], + "source": [ + "help(alum1)" + ] + }, + { + "cell_type": "markdown", + "id": "d2519b22", + "metadata": {}, + "source": [ + "## Abstract Base Classes\n", + "\n", + "Sometimes we want to define a class that can't be instantiated directly, but is intended to be inherited from.\n", + "\n", + "These are known as **abstract classes**. This helps us define an interface, which contains a collection of methods that the **concrete class** must implement.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "9f705ba6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def print_dot_prod(v1, v2):\n", + " \"\"\" prints dot product between two vectors \"\"\"\n", + " print(v1.dot_product(v2))" + ] + }, + { + "cell_type": "markdown", + "id": "ec8087af", + "metadata": {}, + "source": [ + "If we want this method to be polymorphic for vectors of multiple dimensions, such as:" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "4b20d7b3", + "metadata": {}, + "outputs": [], + "source": [ + "class Vec2:\n", + " def __init__(self,x,y):\n", + " self.x = x\n", + " self.y = y \n", + " \n", + "class Vec3:\n", + " def __init__(self,x,y,z):\n", + " self.x = x\n", + " self.y = y \n", + " self.z = z " + ] + }, + { + "cell_type": "markdown", + "id": "86d216a7", + "metadata": {}, + "source": [ + "We can force that these types implement an interface (i.e., an abstract base class) such that we can guarantee that objects we pass to ``print_dot_prod`` will always work by forcing them to implement a ``dot_product`` method. \n", + "\n", + "We will define an abstract class called ``Vector`` that has only the required method: \n", + "\n", + "`` def dot_product(self, other) `` " + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "a2cddb6d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from abc import ABC, abstractmethod\n", + "\n", + "class Vector(ABC): \n", + " @abstractmethod\n", + " def dot_product(self, other):\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "a32d26a8", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "Can't instantiate abstract class Vector with abstract method dot_product", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[44], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# we can't instantiate abstract classes\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m v \u001b[38;5;241m=\u001b[39m \u001b[43mVector\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: Can't instantiate abstract class Vector with abstract method dot_product" + ] + } + ], + "source": [ + "# we can't instantiate abstract classes\n", + "v = Vector()" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "5a9ae047", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "class Vec2(Vector):\n", + " def __init__(self, x, y):\n", + " self.x = x\n", + " self.y = y \n", + " \n", + " def dot_product(self, other): \n", + " return self.x * other.x + self.y * other.y\n", + " \n", + "class Vec3(Vector):\n", + " def __init__(self, x, y, z):\n", + " self.x = x\n", + " self.y = y \n", + " self.z = z \n", + " \n", + " def dot_product(self, other): \n", + " return self.x * other.x + self.y * other.y + self.z * other.z" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "e25536d1", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "True\n", + "----\n", + "True\n", + "True\n" + ] + } + ], + "source": [ + "# now print_dot_prod works\n", + "\n", + "# Vec2 and Vec3 objects are instances of Vector since their classes \n", + "# inherit from the Vector ABC.\n", + "v2a = Vec2(1,2)\n", + "v2b = Vec2(3,4)\n", + "v3a = Vec3(6,7,3)\n", + "v3b = Vec3(1,2,3)\n", + "\n", + "print(isinstance(v2a, Vec2)) \n", + "print(isinstance(v2a, Vector)) \n", + "print(\"----\")\n", + "print(isinstance(v3a, Vec3)) \n", + "print(isinstance(v3a, Vector))" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "96d41011", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "11\n" + ] + } + ], + "source": [ + "print_dot_prod(v2a, v2b)" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "7cf62b14", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "29\n" + ] + } + ], + "source": [ + "print_dot_prod(v3a, v3b)" + ] + }, + { + "cell_type": "markdown", + "id": "ebbd1b47-601b-4a65-b493-213ccbedf373", + "metadata": {}, + "source": [ + "## Dataclasses\n", + "\n", + "Python 3.7 added `dataclasses` as a handy way to create classes that are mostly responsible for representing data. These classes often have few or no methods defined." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "4f98cf62-52da-4072-bf5a-da7de3ad9a98", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "\n", + "@dataclass\n", + "class InventoryItem:\n", + " \"\"\"Class for keeping track of an item in inventory.\"\"\"\n", + " name: str\n", + " unit_price: float\n", + " quantity_on_hand: int = 0\n", + "\n", + " def total_cost(self) -> float:\n", + " return self.unit_price * self.quantity_on_hand\n", + "#InventoryItem = dataclass(InventoryItem)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af1f9445-29c7-4fd8-8648-596fa3a12006", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "8faf9c31-33e4-42f1-a8e8-8b72a7f1f96f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "wrench = InventoryItem(\"Wrench\", 12.95, 10)\n", + "hammer = InventoryItem(\"Hammer\", 16, 8)\n", + "nails = InventoryItem(\"Nails\", 0.03, 1000)\n", + "saw = InventoryItem(\"Saw\", 99)" + ] + }, + { + "cell_type": "markdown", + "id": "4a088329-2f4d-4548-a750-b0f02df5dc9b", + "metadata": {}, + "source": [ + "Dataclasses get an automatic `__init__`, `__repr__`, `__eq__`, and several other helpful options. (Even more is possible via the decorator: https://docs.python.org/3/library/dataclasses.html)" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "e3a8dd5f-55bc-49d2-af83-79c977393b0d", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "30.0" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nails.total_cost()" + ] + }, + { + "cell_type": "markdown", + "id": "d8f9a214-3277-40a9-b50c-2aabf5794079", + "metadata": { + "tags": [] + }, + "source": [ + "Beyond this, additional methods/staticmethods/etc. can be defined in the usual way.\n", + "\n", + "This syntax uses Python's type-hinting, and if you're looking to use it you'll want to get familiar with the rules\n", + "around complex types: https://docs.python.org/3/library/typing.html\n", + "\n", + "**Note: This syntax has been evolving rapidly from Python 3.6->now. This is one are where making sure you have a current (>=3.10) version of Python will matter.**" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "3c1a8baa-3840-437d-b6ad-d880a4676423", + "metadata": {}, + "outputs": [], + "source": [ + "# instead of returning tuples and \n", + "# remembering the positional order, can instead\n", + "@dataclass\n", + "class RetType:\n", + " data: list[int]\n", + " counter: int\n", + "\n", + "def fn():\n", + " return RetType([], counter)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7097097d-b7a6-437a-8f60-f12d38df6a1b", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73195ac4-c0cc-4f8b-8852-2b63168e1713", + "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": { + "height": "calc(100% - 180px)", + "left": "10px", + "top": "150px", + "width": "256.933px" + }, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}