From b45632df7e802fcc951742a33979bb118763e4b8 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Thu, 27 Jul 2023 22:53:29 +0000 Subject: [PATCH] Add concepts and examples --- lessons/python/7_oop.ipynb | 461 +++++++++++++++++++++++++++++++++---- 1 file changed, 422 insertions(+), 39 deletions(-) diff --git a/lessons/python/7_oop.ipynb b/lessons/python/7_oop.ipynb index de49515..71694c5 100644 --- a/lessons/python/7_oop.ipynb +++ b/lessons/python/7_oop.ipynb @@ -48,41 +48,50 @@ "source": [ "## Concepts\n", "\n", - "Let's explore OOP concepts through bicycles.\n", + "
\n", + " \"A\n", + "
Figure 1: A Flying Pigeon bicycle.
\n", + "
\n", "\n", - "\"A bike\" (class) \n", - "Bikes have attributes: (brakes, handle bars, wheels\n", - "Bikes have behaviors : move, steer, brake \n", + "Let's explore some OOP concepts through bicycles.\n", "\n", - "Objects or instances of the bike class:\n", + "Bikes share many features.\n", + "They have wheels, handlebars, pedals, and brakes.\n", + "On a bicycle,\n", + "you can move, steer, and stop.\n", "\n", - "- Mark's Specialized Stumpjumper FS\n", - "- Benjamin's Bianchi Sprint\n", + "As a thought experiment,\n", + "consider a *class* called `Bike`.\n", + "Classes are used to organize information.\n", + "They are made up of data, called *attributes*,\n", + "and functions that act on these data, called *methods*.\n", + "In our `Bike` class,\n", + "wheels, handlebars, pedals, and brakes are attributes (what makes a `Bike`),\n", + "and the behaviors of move, steer, and stop are methods (what a `Bike` does).\n", "\n", + "Classes are generalizations.\n", + "An *object* is a particular instance of a class.\n", + "For example,\n", + "The Flying Pigeon in Figure 1 in an instance of a bike.\n", + "We can see that it has wheels, handlebars, pedals, and brakes,\n", + "and, in our thought experiment,\n", + "these attributes could be used in the behaviors of move, steer, and stop.\n", "\n", + "Let's go further.\n", + "Consider different kinds of bikes, such as mountain bikes versus road bikes.\n", + "Both mountain bikes and road bikes have all the attributes and methods of bikes,\n", + "but there are differences; for example,\n", + "mountain bikes tend to have wider tires and flat handlebars,\n", + "while road bikes have thinner tires and drop bars.\n", + "In OOP, we can handle this speciation through *inheritance*:\n", + "we can *subclass* `Bike` into new `MountainBike` and `RoadBike` classes.\n", + "These new classes have all the aspects of a `Bike`,\n", + "and they can define new ones,\n", + "such as a suspension attribute for a `MountainBike`.\n", + "An instance of a `MountainBike` is Mark's Specialized Stumpjumper FS.\n", + "An instance of a `RoadBike` is Benjamin's Bianchi Sprint.\n", "\n", - "In OOP, code is organized into a class.\n", - "\n", - "Terms:\n", - "\n", - "* class\n", - "* object\n", - "* attribute (data)\n", - "* method (behavior)\n", - "* instance\n", - "* subclass\n", - "* inheritance\n", - "* abstract\n", - "* constructor\n", - "\n", - "The variable *self* is used to access the attributes and methods of an object.\n", - "\n", - "Access attributes and methods from an object with the method invocation operator \".\".\n", - "\n", - "\n", - "OOP requires some time to write and develop good classes.\n", - "\n", - "The following notebook is based on a recent [webinar](https://youtu.be/dLrahDArm4w) we recorded at CSDMS. You can find the [Slideshow](https://github.com/csdms/level-up/blob/master/info/Level-3-Object-oriented-Programming.pdf) and a more elaborate example here [python_corona_simulation](https://github.com/BCampforts/python_corona_simulation),which is forked from [paulvangentcom](https://github.com/paulvangentcom/python_corona_simulation)." + "Let's move from concepts to a programmatic example next." ] }, { @@ -96,7 +105,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A textbook example of polygons." + "For the rest of this lesson,\n", + "we'll use a textbook example--polygons--to see how OOP works in Python." ] }, { @@ -107,7 +117,32 @@ "source": [ "![Shapes UML diagram](./media/shapes-uml-diagram.png \"UML class diagrams for Shape, Circle, Rectangle, and Square.\")\n", "\n", - "*Figure 1. Class diagrams for Shape, Circle, Rectangle, and Square.*" + "*Figure 2: Class diagrams for Shape, Circle, Rectangle, and Square.*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We'll use NumPy several times in the example." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Shape" ] }, { @@ -120,8 +155,6 @@ "source": [ "from abc import ABC, abstractmethod\n", "\n", - "import numpy as np\n", - "\n", "\n", "class Shape(ABC):\n", " @abstractmethod\n", @@ -140,7 +173,21 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let's examine this code." + "Let's examine this code.\n", + "\n", + "Choosing to use type hints.\n", + "Not needed, but useful.\n", + "\n", + "The variable *self* is used to access the attributes and methods of an object.\n", + "\n", + "Access attributes and methods from an object with `.` the method invocation operator." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Try to make an instance of `Shape`." ] }, { @@ -152,6 +199,21 @@ "#x = Shape()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Circle" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A circle is a shape.\n", + "Here's one way to define a `Circle` class." + ] + }, { "cell_type": "code", "execution_count": null, @@ -173,6 +235,151 @@ " self.area = np.pi * self.r**2" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`Circle` is subclassed from `Shape`,\n", + "so it automatically inherits all of the `Shape`\n", + "attributes (*x*, *y*, *n_sides*, and *area*)\n", + "and\n", + "methods (*__init__* and *calculate_area*).\n", + "\n", + "We can choose how to create a `Circle`.\n", + "Here, we use a center and a radius.\n", + "We then use the parametric equation for a circle to translate these\n", + "into the *(x,y)* coordinate pairs expected by `Shape`.\n", + "The inherited `Shape` constructor is called with the Python builtin function *super*.\n", + "\n", + "\n", + "\n", + "constructor and *calculate_area* method now concrete.\n", + "\n", + "We actually invoke `Shape` ctr because it does stuff. We don't bother to invoke the *calculate_area* method of the superclass. It has to be overridden in the subclass, though, or Python will raise an exception." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Make an instance of `Circle`, specifying a center and radius." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "circle = Circle(center=(1.0,1.0), radius=2.0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Show the values of the circle's center and radius attributes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "circle.c" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "circle.r" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What's the area of the circle?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "circle.calculate_area()\n", + "circle.area" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Rectangle" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are many ways to define a rectangle.\n", + "We'll specify the lower left corner,\n", + "the width, and the height to make a `Rectangle` class.\n", + "Like `Circle`,\n", + "`Rectangle` inherits from `Shape`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "class Rectangle(Shape):\n", + " def __init__(\n", + " self, lower_left: tuple = (1.0, 1.0), width: float = 3.0, height: float = 2.0\n", + " ) -> None:\n", + " self.ll = lower_left\n", + " self.w = width\n", + " self.h = height\n", + " x = [self.ll[0], self.ll[0] + self.w, self.ll[0] + self.w, self.ll[0]]\n", + " y = [self.ll[1], self.ll[1], self.ll[1] + self.h, self.ll[1] + self.h]\n", + " super().__init__(x, y)\n", + "\n", + " def calculate_area(self) -> None:\n", + " self.area = self.w * self.h" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that most of the work in the contructor goes into forming arguments appropriate for `Shape`,\n", + "although creating *w* and *h* attributes for `Rectangle` helps with calculating the area." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Make a rectangle using the default argument values." + ] + }, { "cell_type": "code", "execution_count": null, @@ -181,7 +388,14 @@ }, "outputs": [], "source": [ - "s = Circle(center=(1.0,1.0), radius=2.0)" + "rectangle = Rectangle()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Show the coordinates of the rectangle's vertices." ] }, { @@ -192,7 +406,7 @@ }, "outputs": [], "source": [ - "s.c" + "rectangle.x" ] }, { @@ -203,7 +417,14 @@ }, "outputs": [], "source": [ - "s.r" + "rectangle.y" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "How many sides does the rectangle have?" ] }, { @@ -214,7 +435,69 @@ }, "outputs": [], "source": [ - "s.calculate_area()" + "rectangle.n_sides" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What's the area of the rectangle?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "rectangle.calculate_area()\n", + "rectangle.area" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Square" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A square is a special case of a rectangle,\n", + "so it makes sense to subclass `Square` from `Rectangle`.\n", + "Inheritance makes the code really straightforward." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "class Square(Rectangle):\n", + " def __init__(self, lower_left: tuple = (1.0, 1.0), width: float = 2.0) -> None:\n", + " super().__init__(lower_left, width, width)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the `Square` constructor simply calls the `Rectangle` constructor with the *width* argument repeated. `Square` also doesn't need to define its own *calculate_area* method as it can use `Rectangle`'s." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Make a square:" ] }, { @@ -225,7 +508,107 @@ }, "outputs": [], "source": [ - "s.area" + "square = Square()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What are the coordinates of the square's vertices?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "for coordinate in zip(square.x, square.y):\n", + " print(coordinate)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What's the area of the square?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "square.calculate_area()\n", + "square.area" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exercises" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Two suggestions:\n", + "\n", + "1. Write a `Triangle` class. (How about a `Hexagon`?!)\n", + "1. Write code that uses Matplotlib to display an arbitrary `Shape`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "OOP requires some time to write and develop good classes.\n", + "\n", + "Concepts in this notebook are explored further in a recent CSDMS [webinar](https://youtu.be/dLrahDArm4w)\n", + "by Benjamin Campforts and Mark Piper.\n", + "You can find the slides used for the webinar [here](https://github.com/csdms/level-up/blob/master/info/Level-3-Object-oriented-Programming.pdf),\n", + "and a more elaborate example [here](https://github.com/BCampforts/python_corona_simulation)\n", + "(forked from [@paulvangentcom](https://github.com/paulvangentcom/python_corona_simulation))." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The table below summarizes the OOP terms used in this lesson.\n", + "\n", + "| Term | Description |\n", + "| ---- | ----------- |\n", + "| file system | the part of operating system that organizes how information is stored and accessed |\n", + "| path | a string that gives the location of a file or directory on the file system |\n", + "| absolute path | a path that starts at the root of the file system |\n", + "\n", + "\n", + "Terms:\n", + "* class\n", + "* object\n", + "* attribute (data)\n", + "* method (behavior)\n", + "* instance\n", + "* subclass\n", + "* inheritance\n", + "* abstract vs concrete\n", + "* constructor" ] }, {