{ "cells": [ { "cell_type": "markdown", "id": "1331faa1", "metadata": {}, "source": [ "You can order print and ebook versions of *Think Python 3e* from\n", "[Bookshop.org](https://bookshop.org/a/98697/9781098155438) and\n", "[Amazon](https://www.amazon.com/_/dp/1098155432?smid=ATVPDKIKX0DER&_encoding=UTF8&tag=oreilly20-20&_encoding=UTF8&tag=greenteapre01-20&linkCode=ur2&linkId=e2a529f94920295d27ec8a06e757dc7c&camp=1789&creative=9325)." ] }, { "cell_type": "code", "execution_count": 1, "id": "ae5a86f8", "metadata": { "tags": [] }, "outputs": [], "source": [ "from os.path import basename, exists\n", "\n", "def download(url):\n", " \"\"\"Download a file if it isn't already here, and return its filename.\"\"\"\n", " filename = basename(url)\n", " if not exists(filename):\n", " from urllib.request import urlretrieve\n", "\n", " local, _ = urlretrieve(url, filename)\n", " print(\"Downloaded \" + str(local))\n", " return filename\n", "\n", "download('https://github.com/porttack/working-in-python/raw/v3/working_in_python.py');\n", "download('https://github.com/AllenDowney/ThinkPython/raw/v3/diagram.py');\n", "download('https://github.com/ramalho/jupyturtle/releases/download/2024-03/jupyturtle.py');\n", "\n", "import working_in_python\n", "# apcsp:begin type=\"note\" chapter=\"16\"\n", "working_in_python.enable_docstring_reminders()\n", "# apcsp:end" ] }, { "cell_type": "markdown", "id": "e826e661", "metadata": {}, "source": [ "# Classes and Objects\n", "\n", "At this point we have defined classes and created objects that represent the time of day and the day of the year.\n", "And we've defined methods that create, modify, and perform computations with these objects.\n", "\n", "In this chapter we'll continue our tour of object-oriented programming (OOP) by defining classes that represent geometric objects, including points, lines, rectangles, and circles.\n", "We'll write methods that create and modify these objects, and we'll use the `jupyturtle` module to draw them.\n", "\n", "I'll use these classes to demonstrate OOP topics including object identity and equivalence, shallow and deep copying, and polymorphism." ] }, { "cell_type": "markdown", "id": "6b414d4a", "metadata": { "tags": [] }, "source": [ "## Creating a Point\n", "\n", "In computer graphics a location on the screen is often represented using a pair of coordinates in an `x`-`y` plane.\n", "By convention, the point `(0, 0)` usually represents the upper-left corner of the screen, and `(x, y)` represents the point `x` units to the right and `y` units down from the origin.\n", "Compared to the Cartesian coordinate system you might have seen in a math class, the `y` axis is upside-down.\n", "\n", "There are several ways we might represent a point in Python:\n", "\n", "- We can store the coordinates separately in two variables, `x` and `y`.\n", "\n", "- We can store the coordinates as elements in a list or tuple.\n", "\n", "- We can create a new type to represent points as objects.\n", "\n", "In object-oriented programming, it would be most idiomatic to create a new type.\n", "To do that, we'll start with a class definition for `Point`." ] }, { "cell_type": "code", "execution_count": 2, "id": "c9c99d2c", "metadata": {}, "outputs": [], "source": [ "class Point:\n", " \"\"\"Represents a point in 2-D space.\"\"\"\n", " \n", " def __init__(self, x, y):\n", " self.x = x\n", " self.y = y\n", " \n", " def __str__(self):\n", " return f'Point({self.x}, {self.y})'" ] }, { "cell_type": "markdown", "id": "3d35a095", "metadata": {}, "source": [ "The `__init__` method takes the coordinates as parameters and assigns them to attributes `x` and `y`.\n", "The `__str__` method returns a string representation of the `Point`.\n", "\n", "Now we can instantiate and display a `Point` object like this." ] }, { "cell_type": "code", "execution_count": 3, "id": "d318001a", "metadata": {}, "outputs": [], "source": [ "start = Point(0, 0)\n", "print(start)" ] }, { "cell_type": "markdown", "id": "b3fd8858", "metadata": {}, "source": [ "The following diagram shows the state of the new object. " ] }, { "cell_type": "code", "execution_count": 4, "id": "3eb47826", "metadata": { "tags": [] }, "outputs": [], "source": [ "from diagram import make_frame, make_binding\n", "\n", "d1 = vars(start)\n", "frame = make_frame(d1, name='Point', dy=-0.25, offsetx=0.18)\n", "binding = make_binding('start', frame)" ] }, { "cell_type": "code", "execution_count": 5, "id": "6702a353", "metadata": { "tags": [] }, "outputs": [], "source": [ "from diagram import diagram, adjust\n", "\n", "width, height, x, y = [1.41, 0.89, 0.26, 0.5]\n", "ax = diagram(width, height)\n", "bbox = binding.draw(ax, x, y)\n", "#adjust(x, y, bbox)" ] }, { "cell_type": "markdown", "id": "713b7410", "metadata": {}, "source": [ "As usual, a programmer-defined type is represented by a box with the name of the type outside and the attributes inside.\n", "\n", "In general, programmer-defined types are mutable, so we can write a method like `translate` that takes two numbers, `dx` and `dy`, and adds them to the attributes `x` and `y`." ] }, { "cell_type": "code", "execution_count": 6, "id": "0f710c4f", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Point\n", "\n", " def translate(self, dx, dy):\n", " self.x += dx\n", " self.y += dy" ] }, { "cell_type": "markdown", "id": "4d183292", "metadata": {}, "source": [ "This function translates the `Point` from one location in the plane to another.\n", "If we don't want to modify an existing `Point`, we can use `copy` to copy the original object and then modify the copy." ] }, { "cell_type": "code", "execution_count": 7, "id": "6e37126b", "metadata": {}, "outputs": [], "source": [ "from copy import copy\n", "\n", "end1 = copy(start)\n", "end1.translate(300, 0)\n", "print(end1)" ] }, { "cell_type": "markdown", "id": "562567c2", "metadata": {}, "source": [ "We can encapsulate those steps in another method called `translated`." ] }, { "cell_type": "code", "execution_count": 8, "id": "f38bfaaa", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Point\n", "\n", " def translated(self, dx=0, dy=0):\n", " point = copy(self)\n", " point.translate(dx, dy)\n", " return point" ] }, { "cell_type": "markdown", "id": "a7a635ee", "metadata": {}, "source": [ "In the same way that the `sort` method modifies a list and the `sorted` function creates a new list, now we have a `translate` method that modifies a `Point` and a `translated` method that creates a new one.\n", "\n", "Here's an example:" ] }, { "cell_type": "code", "execution_count": 9, "id": "cef864a6", "metadata": {}, "outputs": [], "source": [ "end2 = start.translated(0, 150)\n", "print(end2)" ] }, { "cell_type": "markdown", "id": "923362d2", "metadata": {}, "source": [ "In the next section, we'll use these points to define and draw a line." ] }, { "cell_type": "markdown", "id": "837f98fd", "metadata": {}, "source": [ "## Creating a Line\n", "\n", "Now let's define a class that represents the line segment between two points.\n", "As usual, we'll start with an `__init__` method and a `__str__` method." ] }, { "cell_type": "code", "execution_count": 10, "id": "6ae012a1", "metadata": {}, "outputs": [], "source": [ "class Line:\n", " def __init__(self, p1, p2):\n", " self.p1 = p1\n", " self.p2 = p2\n", " \n", " def __str__(self):\n", " return f'Line({self.p1}, {self.p2})'" ] }, { "cell_type": "markdown", "id": "d7dad30e", "metadata": {}, "source": [ "With those two methods, we can instantiate and display a `Line` object we'll use to represent the `x` axis." ] }, { "cell_type": "code", "execution_count": 11, "id": "39b2ae2a", "metadata": {}, "outputs": [], "source": [ "line1 = Line(start, end1)\n", "print(line1)" ] }, { "cell_type": "markdown", "id": "e7b5fd9a", "metadata": {}, "source": [ "When we call `print` and pass `line` as a parameter, `print` invokes `__str__` on `line`.\n", "The `__str__` method uses an f-string to create a string representation of the `line`. \n", "\n", "The f-string contains two expressions in curly braces, `self.p1` and `self.p2`.\n", "When those expressions are evaluated, the results are `Point` objects.\n", "Then, when they are converted to strings, the `__str__` method from the `Point` class gets invoked.\n", "\n", "That's why, when we display a `Line`, the result contains the string representations of the `Point` objects.\n", "\n", "The following object diagram shows the state of this `Line` object." ] }, { "cell_type": "code", "execution_count": 12, "id": "dee93202", "metadata": { "tags": [] }, "outputs": [], "source": [ "from diagram import Binding, Value, Frame\n", "\n", "d1 = vars(line1.p1)\n", "frame1 = make_frame(d1, name='Point', dy=-0.25, offsetx=0.17)\n", "\n", "d2 = vars(line1.p2)\n", "frame2 = make_frame(d2, name='Point', dy=-0.25, offsetx=0.17)\n", "\n", "binding1 = Binding(Value('start'), frame1, dx=0.4)\n", "binding2 = Binding(Value('end'), frame2, dx=0.4)\n", "frame3 = Frame([binding1, binding2], name='Line', dy=-0.9, offsetx=0.4, offsety=-0.25)\n", "\n", "binding = make_binding('line1', frame3)" ] }, { "cell_type": "code", "execution_count": 13, "id": "43682c57", "metadata": { "tags": [] }, "outputs": [], "source": [ "width, height, x, y = [2.45, 2.12, 0.27, 1.76]\n", "ax = diagram(width, height)\n", "bbox = binding.draw(ax, x, y)\n", "#adjust(x, y, bbox)" ] }, { "cell_type": "markdown", "id": "079859b5", "metadata": {}, "source": [ "String representations and object diagrams are useful for debugging, but the point of this example is to generate graphics, not text!\n", "So we'll use the `jupyturtle` module to draw lines on the screen.\n", "\n", "As we did in [Chapter 4](section_turtle_module), we'll use `make_turtle` to create a `Turtle` object and a small canvas where it can draw.\n", "To draw lines, we'll use two new functions from the `jupyturtle` module:\n", "\n", "* `jumpto`, which takes two coordinates and moves the `Turtle` to the given location without drawing a line, and \n", "\n", "* `moveto`, which moves the `Turtle` from its current location to the given location, and draws a line segment between them.\n", "\n", "Here's how we import them." ] }, { "cell_type": "code", "execution_count": 14, "id": "5225d870", "metadata": {}, "outputs": [], "source": [ "from jupyturtle import make_turtle, jumpto, moveto" ] }, { "cell_type": "markdown", "id": "9d2dd88f", "metadata": {}, "source": [ "And here's a method that draws a `Line`." ] }, { "cell_type": "code", "execution_count": 15, "id": "999843cb", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Line\n", "\n", " def draw(self):\n", " jumpto(self.p1.x, self.p1.y)\n", " moveto(self.p2.x, self.p2.y)" ] }, { "cell_type": "markdown", "id": "2341f0e0", "metadata": {}, "source": [ "To show how it's used, I'll create a second line that represents the `y` axis." ] }, { "cell_type": "code", "execution_count": 16, "id": "6ac10ec7", "metadata": {}, "outputs": [], "source": [ "line2 = Line(start, end2)\n", "print(line2)" ] }, { "cell_type": "markdown", "id": "d7450736", "metadata": {}, "source": [ "And then draw the axes." ] }, { "cell_type": "code", "execution_count": 17, "id": "b15b70dd", "metadata": {}, "outputs": [], "source": [ "make_turtle()\n", "line1.draw()\n", "line2.draw()" ] }, { "cell_type": "markdown", "id": "473c156f", "metadata": {}, "source": [ "As we define and draw more objects, we'll use these lines again.\n", "But first let's talk about object equivalence and identity." ] }, { "cell_type": "markdown", "id": "950da673", "metadata": {}, "source": [ "## Equivalence and identity\n", "\n", "Suppose we create two points with the same coordinates." ] }, { "cell_type": "code", "execution_count": 18, "id": "62414805", "metadata": {}, "outputs": [], "source": [ "p1 = Point(200, 100)\n", "p2 = Point(200, 100)" ] }, { "cell_type": "markdown", "id": "82b14526", "metadata": {}, "source": [ "If we use the `==` operator to compare them, we get the default behavior for programmer-defined types -- the result is `True` only if they are the same object, which they are not." ] }, { "cell_type": "code", "execution_count": 19, "id": "7c611eb2", "metadata": {}, "outputs": [], "source": [ "p1 == p2" ] }, { "cell_type": "markdown", "id": "96be0ff8", "metadata": {}, "source": [ "If we want to change that behavior, we can provide a special method called `__eq__` that defines what it means for two `Point` objects to be equal." ] }, { "cell_type": "code", "execution_count": 20, "id": "3a976072", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Point\n", "\n", "def __eq__(self, other):\n", " return (self.x == other.x) and (self.y == other.y)" ] }, { "cell_type": "markdown", "id": "7f4409de", "metadata": {}, "source": [ "This definition considers two `Points` to be equal if their attributes are equal.\n", "Now when we use the `==` operator, it invokes the `__eq__` method, which indicates that `p1` and `p2` are considered equal." ] }, { "cell_type": "code", "execution_count": 21, "id": "7660d756", "metadata": {}, "outputs": [], "source": [ "p1 == p2" ] }, { "cell_type": "markdown", "id": "52662e6a", "metadata": {}, "source": [ "But the `is` operator still indicates that they are different objects." ] }, { "cell_type": "code", "execution_count": 22, "id": "e76ff9ef", "metadata": {}, "outputs": [], "source": [ "p1 is p2" ] }, { "cell_type": "markdown", "id": "c008d3dd", "metadata": {}, "source": [ "It's not possible to override the `is` operator -- it always checks whether the objects are identical.\n", "But for programmer-defined types, you can override the `==` operator so it checks whether the objects are equivalent.\n", "And you can define what equivalent means." ] }, { "cell_type": "markdown", "id": "893a8cab", "metadata": {}, "source": [ "## Creating a Rectangle\n", "\n", "Now let's define a class that represents and draws rectangles.\n", "To keep things simple, we'll assume that the rectangles are either vertical or horizontal, not at an angle.\n", "What attributes do you think we should use to specify the location and size of a rectangle?\n", "\n", "There are at least two possibilities:\n", "\n", "- You could specify the width and height of the rectangle and the location of one corner.\n", "\n", "- You could specify two opposing corners.\n", "\n", "At this point it's hard to say whether either is better than the other, so let's implement the first one.\n", "Here is the class definition." ] }, { "cell_type": "code", "execution_count": 23, "id": "c2a2aa29", "metadata": {}, "outputs": [], "source": [ "class Rectangle:\n", " \"\"\"Represents a rectangle. \n", "\n", " attributes: width, height, corner.\n", " \"\"\"\n", " def __init__(self, width, height, corner):\n", " self.width = width\n", " self.height = height\n", " self.corner = corner\n", " \n", " def __str__(self):\n", " return f'Rectangle({self.width}, {self.height}, {self.corner})'" ] }, { "cell_type": "markdown", "id": "df2852f3", "metadata": {}, "source": [ "As usual, the `__init__` method assigns the parameters to attributes and the `__str__` returns a string representation of the object.\n", "Now we can instantiate a `Rectangle` object, using a `Point` as the location of the upper-left corner." ] }, { "cell_type": "code", "execution_count": 24, "id": "5d36d561", "metadata": {}, "outputs": [], "source": [ "corner = Point(30, 20)\n", "box1 = Rectangle(100, 50, corner)\n", "print(box1)" ] }, { "cell_type": "markdown", "id": "a9e0b5ec", "metadata": {}, "source": [ "The following diagram shows the state of this object." ] }, { "cell_type": "code", "execution_count": 25, "id": "6e47d99a", "metadata": { "tags": [] }, "outputs": [], "source": [ "from diagram import Binding, Value\n", "\n", "def make_rectangle_binding(name, box, **options):\n", " d1 = vars(box.corner)\n", " frame_corner = make_frame(d1, name='Point', dy=-0.25, offsetx=0.07)\n", "\n", " d2 = dict(width=box.width, height=box.height)\n", " frame = make_frame(d2, name='Rectangle', dy=-0.25, offsetx=0.45)\n", " binding = Binding(Value('corner'), frame1, dx=0.92, draw_value=False, **options)\n", " frame.bindings.append(binding)\n", "\n", " binding = Binding(Value(name), frame)\n", " return binding, frame_corner\n", "\n", "binding_box1, frame_corner1 = make_rectangle_binding('box1', box1)" ] }, { "cell_type": "code", "execution_count": 26, "id": "1e7e4a72", "metadata": { "tags": [] }, "outputs": [], "source": [ "from diagram import Bbox\n", "\n", "width, height, x, y = [2.83, 1.49, 0.27, 1.1]\n", "ax = diagram(width, height)\n", "bbox1 = binding_box1.draw(ax, x, y)\n", "bbox2 = frame_corner1.draw(ax, x+1.85, y-0.6)\n", "bbox = Bbox.union([bbox1, bbox2])\n", "#adjust(x, y, bbox)" ] }, { "cell_type": "markdown", "id": "bb54e6b5", "metadata": {}, "source": [ "To draw a rectangle, we'll use the following method to make four `Point` objects to represent the corners." ] }, { "cell_type": "code", "execution_count": 27, "id": "8fb74cd8", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Rectangle\n", "\n", " def make_points(self):\n", " p1 = self.corner\n", " p2 = p1.translated(self.width, 0)\n", " p3 = p2.translated(0, self.height)\n", " p4 = p3.translated(-self.width, 0)\n", " return p1, p2, p3, p4" ] }, { "cell_type": "markdown", "id": "20dbe0cb", "metadata": {}, "source": [ "Then we'll make four `Line` objects to represent the sides." ] }, { "cell_type": "code", "execution_count": 28, "id": "1c419c73", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Rectangle\n", "\n", " def make_lines(self):\n", " p1, p2, p3, p4 = self.make_points()\n", " return Line(p1, p2), Line(p2, p3), Line(p3, p4), Line(p4, p1)" ] }, { "cell_type": "markdown", "id": "30fe41cc", "metadata": {}, "source": [ "Then we'll draw the sides." ] }, { "cell_type": "code", "execution_count": 29, "id": "5ceccb7d", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Rectangle\n", "\n", " def draw(self):\n", " lines = self.make_lines()\n", " for line in lines:\n", " line.draw()" ] }, { "cell_type": "markdown", "id": "390ba3e7", "metadata": {}, "source": [ "Here's an example." ] }, { "cell_type": "code", "execution_count": 30, "id": "2870c782", "metadata": {}, "outputs": [], "source": [ "make_turtle()\n", "line1.draw()\n", "line2.draw()\n", "box1.draw()" ] }, { "cell_type": "markdown", "id": "532a4f69", "metadata": {}, "source": [ "The figure includes two lines to represent the axes." ] }, { "cell_type": "markdown", "id": "0e713a90", "metadata": {}, "source": [ "## Changing rectangles\n", "\n", "Now let's consider two methods that modify rectangles, `grow` and `translate`.\n", "We'll see that `grow` works as expected, but `translate` has a subtle bug.\n", "See if you can figure it out before I explain.\n", "\n", "`grow` takes two numbers, `dwidth` and `dheight`, and adds them to the `width` and `height` attributes of the rectangle." ] }, { "cell_type": "code", "execution_count": 31, "id": "21537ed7", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Rectangle\n", "\n", " def grow(self, dwidth, dheight):\n", " self.width += dwidth\n", " self.height += dheight" ] }, { "cell_type": "markdown", "id": "a51913e2", "metadata": {}, "source": [ "Here's an example that demonstrates the effect by making a copy of `box1` and invoking `grow` on the copy." ] }, { "cell_type": "code", "execution_count": 32, "id": "243ca804", "metadata": {}, "outputs": [], "source": [ "box2 = copy(box1)\n", "box2.grow(60, 40)\n", "print(box2)" ] }, { "cell_type": "markdown", "id": "6d74da62", "metadata": {}, "source": [ "If we draw `box1` and `box2`, we can confirm that `grow` works as expected." ] }, { "cell_type": "code", "execution_count": 33, "id": "110f995f", "metadata": {}, "outputs": [], "source": [ "make_turtle()\n", "line1.draw()\n", "line2.draw()\n", "box1.draw()\n", "box2.draw()" ] }, { "cell_type": "markdown", "id": "0c940008", "metadata": {}, "source": [ "Now let's see about `translate`.\n", "It takes two numbers, `dx` and `dy`, and moves the rectangle the given distances in the `x` and `y` directions. " ] }, { "cell_type": "code", "execution_count": 34, "id": "8a5e7e73", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Rectangle\n", "\n", " def translate(self, dx, dy):\n", " self.corner.translate(dx, dy)" ] }, { "cell_type": "markdown", "id": "c27fe91d", "metadata": {}, "source": [ "To demonstrate the effect, we'll translate `box2` to the right and down." ] }, { "cell_type": "code", "execution_count": 35, "id": "78b9e344", "metadata": {}, "outputs": [], "source": [ "box2.translate(30, 20)\n", "print(box2)" ] }, { "cell_type": "markdown", "id": "e01badbc", "metadata": {}, "source": [ "Now let's see what happens if we draw `box1` and `box2` again." ] }, { "cell_type": "code", "execution_count": 36, "id": "b70bcad9", "metadata": {}, "outputs": [], "source": [ "make_turtle()\n", "line1.draw()\n", "line2.draw()\n", "box1.draw()\n", "box2.draw()" ] }, { "cell_type": "markdown", "id": "5310bdd7", "metadata": {}, "source": [ "It looks like both rectangles moved, which is not what we intended!\n", "The next section explains what went wrong." ] }, { "cell_type": "markdown", "id": "940adbeb", "metadata": {}, "source": [ "## Deep copy\n", "\n", "When we use `copy` to duplicate `box1`, it copies the `Rectangle` object but not the `Point` object it contains.\n", "So `box1` and `box2` are different objects, as intended." ] }, { "cell_type": "code", "execution_count": 37, "id": "b67ce168", "metadata": {}, "outputs": [], "source": [ "box1 is box2" ] }, { "cell_type": "markdown", "id": "eac5309b", "metadata": {}, "source": [ "But their `corner` attributes refer to the same object." ] }, { "cell_type": "code", "execution_count": 38, "id": "1d9cf4da", "metadata": {}, "outputs": [], "source": [ "box1.corner is box2.corner" ] }, { "cell_type": "markdown", "id": "f0cc51b5", "metadata": {}, "source": [ "The following diagram shows the state of these objects." ] }, { "cell_type": "code", "execution_count": 39, "id": "6d883459", "metadata": { "tags": [] }, "outputs": [], "source": [ "from diagram import Stack\n", "from copy import deepcopy\n", "\n", "binding_box1, frame_corner1 = make_rectangle_binding('box1', box1)\n", "binding_box2, frame_corner2 = make_rectangle_binding('box2', box2, dy=0.4)\n", "binding_box2.value.bindings.reverse()\n", "\n", "stack = Stack([binding_box1, binding_box2], dy=-1.3)" ] }, { "cell_type": "code", "execution_count": 40, "id": "637042fb", "metadata": { "tags": [] }, "outputs": [], "source": [ "from diagram import Bbox\n", "\n", "width, height, x, y = [2.76, 2.54, 0.27, 2.16]\n", "ax = diagram(width, height)\n", "bbox1 = stack.draw(ax, x, y)\n", "bbox2 = frame_corner1.draw(ax, x+1.85, y-0.6)\n", "bbox = Bbox.union([bbox1, bbox2])\n", "# adjust(x, y, bbox)" ] }, { "cell_type": "markdown", "id": "35f3e7e1", "metadata": {}, "source": [ "What `copy` does is called a **shallow copy** because it copies the object but not the objects it contains.\n", "As a result, changing the `width` or `height` of one `Rectangle` does not affect the other, but changing the attributes of the shared `Point` affects both!\n", "This behavior is confusing and error-prone.\n", "\n", "Fortunately, the `copy` module provides another function, called `deepcopy`, that copies not only the object but also the objects it refers to, and the objects *they* refer to, and so on. \n", "This operation is called a **deep copy**.\n", "\n", "To demonstrate, let's start with a new `Rectangle` that contains a new `Point`." ] }, { "cell_type": "code", "execution_count": 41, "id": "168277a9", "metadata": {}, "outputs": [], "source": [ "corner = Point(20, 20)\n", "box3 = Rectangle(100, 50, corner)\n", "print(box3)" ] }, { "cell_type": "markdown", "id": "ff9ee872", "metadata": {}, "source": [ "And we'll make a deep copy." ] }, { "cell_type": "code", "execution_count": 42, "id": "75219ef6", "metadata": {}, "outputs": [], "source": [ "from copy import deepcopy\n", "\n", "box4 = deepcopy(box3)" ] }, { "cell_type": "markdown", "id": "7efd0e6a", "metadata": {}, "source": [ "We can confirm that the two `Rectangle` objects refer to different `Point` objects." ] }, { "cell_type": "code", "execution_count": 43, "id": "fde2486c", "metadata": {}, "outputs": [], "source": [ "box3.corner is box4.corner" ] }, { "cell_type": "markdown", "id": "ca925206", "metadata": {}, "source": [ "Because `box3` and `box4` are completely separate objects, we can modify one without affecting the other.\n", "To demonstrate, we'll move `box3` and grow `box4`." ] }, { "cell_type": "code", "execution_count": 44, "id": "3f6d1d6b", "metadata": {}, "outputs": [], "source": [ "box3.translate(50, 30)\n", "box4.grow(100, 60)" ] }, { "cell_type": "markdown", "id": "3ff31c7c", "metadata": {}, "source": [ "And we can confirm that the effect is as expected." ] }, { "cell_type": "code", "execution_count": 45, "id": "092ded2c", "metadata": {}, "outputs": [], "source": [ "make_turtle()\n", "line1.draw()\n", "line2.draw()\n", "box3.draw()\n", "box4.draw()" ] }, { "cell_type": "markdown", "id": "67051d62", "metadata": {}, "source": [ "## Polymorphism\n", "\n", "In the previous example, we invoked the `draw` method on two `Line` objects and two `Rectangle` objects.\n", "We can do the same thing more concisely by making a list of objects." ] }, { "cell_type": "code", "execution_count": 46, "id": "c846343c", "metadata": {}, "outputs": [], "source": [ "shapes = [line1, line2, box3, box4]" ] }, { "cell_type": "markdown", "id": "773955dd", "metadata": {}, "source": [ "The elements of this list are different types, but they all provide a `draw` method, so we can loop through the list and invoke `draw` on each one." ] }, { "cell_type": "code", "execution_count": 47, "id": "10912b62", "metadata": {}, "outputs": [], "source": [ "make_turtle()\n", "\n", "for shape in shapes:\n", " shape.draw()" ] }, { "cell_type": "markdown", "id": "a1ae190c", "metadata": {}, "source": [ "The first and second time through the loop, `shape` refers to a `Line` object, so when `draw` is invoked, the method that runs is the one defined in the `Line` class.\n", "\n", "The third and fourth time through the loop, `shape` refers to a `Rectangle` object, so when `draw` is invoked, the method that runs is the one defined in the `Rectangle` class.\n", "\n", "In a sense, each object knows how to draw itself.\n", "This feature is called **polymorphism**.\n", "The word comes from Greek roots that mean \"many shaped\".\n", "In object-oriented programming, polymorphism is the ability of different types to provide the same methods, which makes it possible to perform many computations -- like drawing shapes -- by invoking the same method on different types of objects.\n", "\n", "As an exercise at the end of this chapter, you'll define a new class that represents a circle and provides a `draw` method.\n", "Then you can use polymorphism to draw lines, rectangles, and circles." ] }, { "cell_type": "markdown", "id": "74d1b48f", "metadata": {}, "source": [ "## Debugging\n", "\n", "In this chapter, we ran into a subtle bug that happened because we created a `Point` that was shared by two `Rectangle` objects, and then we modified the `Point`.\n", "In general, there are two ways to avoid problems like this: you can avoid sharing objects or you can avoid modifying them.\n", "\n", "To avoid sharing objects, you can use deep copy, as we did in this chapter.\n", "\n", "To avoid modifying objects, consider replacing impure functions like `translate` with pure functions like `translated`.\n", "For example, here's a version of `translated` that creates a new `Point` and never modifies its attributes." ] }, { "cell_type": "code", "execution_count": 48, "id": "c803c91a", "metadata": {}, "outputs": [], "source": [ " def translated(self, dx=0, dy=0):\n", " x = self.x + dx\n", " y = self.y + dy\n", " return Point(x, y)" ] }, { "cell_type": "markdown", "id": "76972167", "metadata": {}, "source": [ "Python provides features that make it easier to avoid modifying objects.\n", "They are beyond the scope of this book, but if you are curious, ask a virtual assistant, \"How do I make a Python object immutable?\"\n", "\n", "Creating a new object takes more time than modifying an existing one, but the difference seldom matters in practice.\n", "Programs that avoid shared objects and impure functions are often easier to develop, test, and debug -- and the best kind of debugging is the kind you don't have to do." ] }, { "cell_type": "markdown", "id": "02106995", "metadata": {}, "source": [ "## Glossary\n", "\n", "**shallow copy:**\n", "A copy operation that does not copy nested objects.\n", "\n", "**deep copy:**\n", "A copy operation that also copies nested objects.\n", "\n", "**polymorphism:**\n", "The ability of a method or operator to work with multiple types of objects." ] }, { "cell_type": "markdown", "id": "ebfa0463", "metadata": {}, "source": [ "\n", "**See also:** the [full vocabulary glossary](https://python.porttack.com/alignment/ap-vocabulary-glossary.html) collects every term in this book, alphabetized, alongside the complete AP CSP exam vocabulary list.\n", "" ] }, { "cell_type": "markdown", "id": "09dd41c1", "metadata": {}, "source": [ "## Exercises" ] }, { "cell_type": "code", "execution_count": null, "id": "32b151a5", "metadata": { "tags": [] }, "outputs": [], "source": [ "# This cell tells Jupyter to provide detailed debugging information\n", "# when a runtime error occurs. Run it before working on the exercises.\n", "\n", "%xmode Verbose" ] }, { "cell_type": "markdown", "id": "da0aea86", "metadata": {}, "source": [ "### Ask a virtual assistant\n", "\n", "For all of the following exercises, consider asking a virtual assistant for help.\n", "If you do, you'll want include as part of the prompt the class definitions for `Point`, `Line`, and `Rectangle` -- otherwise the VA will make a guess about their attributes and functions, and the code it generates won't work." ] }, { "cell_type": "markdown", "id": "7721e47b", "metadata": {}, "source": [ "### Exercise\n", "\n", "Write an `__eq__` method for the `Line` class that returns `True` if the `Line` objects refer to `Point` objects that are equivalent, in either order." ] }, { "cell_type": "markdown", "id": "2e488e0f", "metadata": {}, "source": [ "You can use the following outline to get started." ] }, { "cell_type": "code", "execution_count": 49, "id": "92c07380", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Line\n", "\n", "def __eq__(self, other):\n", " return None" ] }, { "cell_type": "code", "execution_count": 50, "id": "a99446c4", "metadata": { "tags": [] }, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "3a44e45a", "metadata": {}, "source": [ "You can use these examples to test your code." ] }, { "cell_type": "code", "execution_count": 51, "id": "aa086dd1", "metadata": {}, "outputs": [], "source": [ "start1 = Point(0, 0)\n", "start2 = Point(0, 0)\n", "end = Point(200, 100)" ] }, { "cell_type": "markdown", "id": "e825f049", "metadata": {}, "source": [ "This example should be `True` because the `Line` objects refer to `Point` objects that are equivalent, in the same order." ] }, { "cell_type": "code", "execution_count": 52, "id": "857cba26", "metadata": {}, "outputs": [], "source": [ "line_a = Line(start1, end)\n", "line_b = Line(start2, end)\n", "line_a == line_b # should be True" ] }, { "cell_type": "markdown", "id": "c3d8fb2b", "metadata": { "tags": [] }, "source": [ "This example should be `True` because the `Line` objects refer to `Point` objects that are equivalent, in reverse order." ] }, { "cell_type": "code", "execution_count": 53, "id": "b45def0a", "metadata": {}, "outputs": [], "source": [ "line_c = Line(end, start1)\n", "line_a == line_c # should be True" ] }, { "cell_type": "markdown", "id": "8c9c787b", "metadata": {}, "source": [ "Equivalence should always be transitive -- that is, if `line_a` and `line_b` are equivalent, and `line_a` and `line_c` are equivalent, then `line_b` and `line_c` should also be equivalent." ] }, { "cell_type": "code", "execution_count": 54, "id": "9784300c", "metadata": {}, "outputs": [], "source": [ "line_b == line_c # should be True" ] }, { "cell_type": "markdown", "id": "d4f385fa", "metadata": {}, "source": [ "This example should be `False` because the `Line` objects refer to `Point` objects that are not equivalent." ] }, { "cell_type": "code", "execution_count": 55, "id": "5435c8e4", "metadata": {}, "outputs": [], "source": [ "line_d = Line(start1, start2)\n", "line_a == line_d # should be False" ] }, { "cell_type": "markdown", "id": "0e629491", "metadata": {}, "source": [ "### Exercise\n", "\n", "Write a `Line` method called `midpoint` that computes the midpoint of a line segment and returns the result as a `Point` object." ] }, { "cell_type": "markdown", "id": "b8c52d19", "metadata": {}, "source": [ "You can use the following outline to get started." ] }, { "cell_type": "code", "execution_count": 56, "id": "f377afbb", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Line\n", "\n", " def midpoint(self):\n", " return Point(0, 0)" ] }, { "cell_type": "code", "execution_count": 57, "id": "96d81b90", "metadata": { "tags": [] }, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "4df69a9f", "metadata": {}, "source": [ "You can use the following examples to test your code and draw the result." ] }, { "cell_type": "code", "execution_count": 58, "id": "0d603aa3", "metadata": {}, "outputs": [], "source": [ "start = Point(0, 0)\n", "end1 = Point(300, 0)\n", "end2 = Point(0, 150)\n", "line1 = Line(start, end1)\n", "line2 = Line(start, end2)" ] }, { "cell_type": "code", "execution_count": 59, "id": "647d0982", "metadata": {}, "outputs": [], "source": [ "mid1 = line1.midpoint()\n", "print(mid1)" ] }, { "cell_type": "code", "execution_count": 60, "id": "e351bea3", "metadata": {}, "outputs": [], "source": [ "mid2 = line2.midpoint()\n", "print(mid2)" ] }, { "cell_type": "code", "execution_count": 61, "id": "5ad5a076", "metadata": {}, "outputs": [], "source": [ "line3 = Line(mid1, mid2)" ] }, { "cell_type": "code", "execution_count": 62, "id": "8effaff0", "metadata": {}, "outputs": [], "source": [ "make_turtle()\n", "\n", "for shape in [line1, line2, line3]:\n", " shape.draw()" ] }, { "cell_type": "markdown", "id": "0518c200", "metadata": {}, "source": [ "### Exercise\n", "\n", "Write a `Rectangle` method called `midpoint` that find the point in the center of a rectangle and returns the result as a `Point` object." ] }, { "cell_type": "markdown", "id": "c586a3ed", "metadata": {}, "source": [ "You can use the following outline to get started." ] }, { "cell_type": "code", "execution_count": 63, "id": "d94a6350", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Rectangle\n", "\n", " def midpoint(self):\n", " return Point(0, 0)" ] }, { "cell_type": "code", "execution_count": 64, "id": "de6756da", "metadata": { "tags": [] }, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "d186c84b", "metadata": {}, "source": [ "You can use the following example to test your code." ] }, { "cell_type": "code", "execution_count": 65, "id": "4aec759c", "metadata": {}, "outputs": [], "source": [ "corner = Point(30, 20)\n", "rectangle = Rectangle(100, 80, corner)" ] }, { "cell_type": "code", "execution_count": 66, "id": "7ec3339d", "metadata": {}, "outputs": [], "source": [ "mid = rectangle.midpoint()\n", "print(mid)" ] }, { "cell_type": "code", "execution_count": 67, "id": "326dbf24", "metadata": {}, "outputs": [], "source": [ "diagonal = Line(corner, mid)" ] }, { "cell_type": "code", "execution_count": 68, "id": "4da710d4", "metadata": {}, "outputs": [], "source": [ "make_turtle()\n", "\n", "for shape in [line1, line2, rectangle, diagonal]:\n", " shape.draw()" ] }, { "cell_type": "markdown", "id": "00cbc4d9", "metadata": {}, "source": [ "### Exercise\n", "\n", "Write a `Rectangle` method called `make_cross` that:\n", "\n", "1. Uses `make_lines` to get a list of `Line` objects that represent the four sides of the rectangle.\n", "\n", "2. Computes the midpoints of the four lines.\n", "\n", "3. Makes and returns a list of two `Line` objects that represent lines connecting opposite midpoints, forming a cross through the middle of the rectangle." ] }, { "cell_type": "markdown", "id": "29e994c6", "metadata": {}, "source": [ "You can use this outline to get started." ] }, { "cell_type": "code", "execution_count": 69, "id": "30cc0726", "metadata": {}, "outputs": [], "source": [ "%%add_method_to Rectangle\n", "\n", " def make_diagonals(self):\n", " return []" ] }, { "cell_type": "code", "execution_count": 70, "id": "6cdde16e", "metadata": { "tags": [] }, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "970fcbca", "metadata": {}, "source": [ "You can use the following example to test your code." ] }, { "cell_type": "code", "execution_count": 71, "id": "2afd718c", "metadata": {}, "outputs": [], "source": [ "corner = Point(30, 20)\n", "rectangle = Rectangle(100, 80, corner)" ] }, { "cell_type": "code", "execution_count": 72, "id": "b7bdb467", "metadata": {}, "outputs": [], "source": [ "lines = rectangle.make_cross()" ] }, { "cell_type": "code", "execution_count": 73, "id": "9d09b2c3", "metadata": {}, "outputs": [], "source": [ "make_turtle()\n", "\n", "rectangle.draw()\n", "for line in lines:\n", " line.draw()" ] }, { "cell_type": "markdown", "id": "0f707fe3", "metadata": {}, "source": [ "### Exercise\n", "\n", "Write a definition for a class named `Circle` with attributes `center` and `radius`, where `center` is a Point object and `radius` is a number.\n", "Include special methods `__init__` and a `__str__`, and a method called `draw` that uses `jupyturtle` functions to draw the circle." ] }, { "cell_type": "markdown", "id": "cb1b24a3", "metadata": {}, "source": [ "You can use the following function, which is a version of the `circle` function we wrote in Chapter 4." ] }, { "cell_type": "code", "execution_count": 74, "id": "b3d2328f", "metadata": {}, "outputs": [], "source": [ "from jupyturtle import make_turtle, forward, left, right\n", "import math\n", " \n", "def draw_circle(radius):\n", " circumference = 2 * math.pi * radius\n", " n = 30\n", " length = circumference / n\n", " angle = 360 / n\n", " left(angle / 2)\n", " for i in range(n):\n", " forward(length)\n", " left(angle)" ] }, { "cell_type": "code", "execution_count": 75, "id": "189c30d4", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "b4325143", "metadata": {}, "source": [ "You can use the following example to test your code.\n", "We'll start with a square `Rectangle` with width and height `100`." ] }, { "cell_type": "code", "execution_count": 76, "id": "49074ed5", "metadata": {}, "outputs": [], "source": [ "corner = Point(20, 20)\n", "rectangle = Rectangle(100, 100, corner)" ] }, { "cell_type": "markdown", "id": "2cdecfa9", "metadata": {}, "source": [ "The following code should create a `Circle` that fits inside the square." ] }, { "cell_type": "code", "execution_count": 77, "id": "d65a9163", "metadata": {}, "outputs": [], "source": [ "center = rectangle.midpoint()\n", "radius = rectangle.height / 2\n", "\n", "circle = Circle(center, radius)\n", "print(circle)" ] }, { "cell_type": "markdown", "id": "37e94d98", "metadata": {}, "source": [ "If everything worked correctly, the following code should draw the circle inside the square (touching on all four sides)." ] }, { "cell_type": "code", "execution_count": 78, "id": "e3b23b4d", "metadata": {}, "outputs": [], "source": [ "make_turtle(delay=0.01)\n", "\n", "rectangle.draw()\n", "circle.draw()" ] }, { "cell_type": "markdown", "id": "a7f4edf8", "metadata": { "tags": [] }, "source": [ "\n", "**Working in Python** — modified by Eric Brown for a high school Computer Science Principles class. Source and modifications: [github.com/porttack/working-in-python](https://github.com/porttack/working-in-python)\n", "\n", "---\n", "\n", "\n", "[Think Python: 3rd Edition](https://allendowney.github.io/ThinkPython/index.html)\n", "\n", "Copyright 2024 [Allen B. Downey](https://allendowney.com)\n", "\n", "Code license: [MIT License](https://mit-license.org/)\n", "\n", "Text license: [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/)" ] } ], "metadata": { "celltoolbar": "Tags", "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.11" } }, "nbformat": 4, "nbformat_minor": 5 }