{ "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", "\n", "import working_in_python\n", "# apcsp:begin type=\"note\" chapter=\"14\"\n", "working_in_python.enable_docstring_reminders()\n", "# apcsp:end" ] }, { "cell_type": "markdown", "id": "e826e661", "metadata": {}, "source": [ "# Classes and Functions\n", "\n", "At this point you know how to use functions to organize code and how to use built-in types to organize data.\n", "The next step is **object-oriented programming**, which uses programmer-defined types to organize both code and data.\n", "\n", "Object-oriented programming is a big topic, so we will proceed gradually.\n", "In this chapter, we'll start with code that is not idiomatic -- that is, it is not the kind of code experienced programmers write -- but it is a good place to start.\n", "In the next two chapters, we will use additional features to write more idiomatic code." ] }, { "cell_type": "markdown", "id": "6b414d4a", "metadata": {}, "source": [ "## Programmer-defined types\n", "\n", "We have used many of Python's built-in types -- now we will define a new type.\n", "As a first example, we'll create a type called `Time` that represents a time of day.\n", "A programmer-defined type is also called a **class**.\n", "A class definition looks like this:" ] }, { "cell_type": "code", "execution_count": 2, "id": "c9c99d2c", "metadata": {}, "outputs": [], "source": [ "class Time:\n", " \"\"\"Represents a time of day.\"\"\"" ] }, { "cell_type": "markdown", "id": "e2414cd2", "metadata": {}, "source": [ "The header indicates that the new class is called `Time`.\n", "The body is a docstring that explains what the class is for.\n", "Defining a class creates a **class object**.\n", "\n", "The class object is like a factory for creating objects.\n", "To create a `Time` object, you call `Time` as if it were a function." ] }, { "cell_type": "code", "execution_count": 3, "id": "d318001a", "metadata": {}, "outputs": [], "source": [ "lunch = Time()" ] }, { "cell_type": "markdown", "id": "f63247d4", "metadata": {}, "source": [ "The result is a new object whose type is `__main__.Time`, where `__main__` is the name of the module where `Time` is defined." ] }, { "cell_type": "code", "execution_count": 4, "id": "f37d67fd", "metadata": {}, "outputs": [], "source": [ "type(lunch)" ] }, { "cell_type": "markdown", "id": "14d0c96a", "metadata": {}, "source": [ "When you print an object, Python tells you what type it is and where it is stored in memory (the prefix `0x` means that the following number is in hexadecimal)." ] }, { "cell_type": "code", "execution_count": 5, "id": "e2bd114a", "metadata": {}, "outputs": [], "source": [ "print(lunch)" ] }, { "cell_type": "markdown", "id": "b6445414", "metadata": {}, "source": [ "Creating a new object is called **instantiation**, and the object is an **instance** of the class." ] }, { "cell_type": "markdown", "id": "4c3768ec", "metadata": {}, "source": [ "## Attributes\n", "\n", "An object can contain variables, which are called **attributes** and pronounced with the emphasis on the first syllable, like \"AT-trib-ute\", rather than the second syllable, like \"a-TRIB-ute\".\n", "We can create attributes using dot notation." ] }, { "cell_type": "code", "execution_count": 6, "id": "e166701a", "metadata": {}, "outputs": [], "source": [ "lunch.hour = 11\n", "lunch.minute = 59\n", "lunch.second = 1" ] }, { "cell_type": "markdown", "id": "b3fd8858", "metadata": {}, "source": [ "This example creates attributes called `hour`, `minute`, and `second`, which contain the hours, minutes, and seconds of the time `11:59:01`, which is lunch time as far as I am concerned.\n", "\n", "The following diagram shows the state of `lunch` and its attributes after these assignments. " ] }, { "cell_type": "code", "execution_count": 7, "id": "3eb47826", "metadata": { "tags": [] }, "outputs": [], "source": [ "from diagram import make_frame, make_binding\n", "\n", "d1 = dict(hour=11, minute=59, second=1)\n", "frame = make_frame(d1, name='Time', dy=-0.3, offsetx=0.48)\n", "binding = make_binding('lunch', frame)" ] }, { "cell_type": "code", "execution_count": 8, "id": "6702a353", "metadata": { "tags": [] }, "outputs": [], "source": [ "from diagram import diagram, adjust\n", "\n", "width, height, x, y = [1.77, 1.24, 0.25, 0.86]\n", "ax = diagram(width, height)\n", "bbox = binding.draw(ax, x, y)\n", "#adjust(x, y, bbox)" ] }, { "cell_type": "markdown", "id": "d9df5b48", "metadata": {}, "source": [ "The variable `lunch` refers to a `Time` object, which contains three attributes. \n", "Each attribute refers to an integer.\n", "A state diagram like this -- which shows an object and its attributes -- is called an **object diagram**.\n", "\n", "You can read the value of an attribute using the dot operator." ] }, { "cell_type": "code", "execution_count": 9, "id": "4c4eff2b", "metadata": {}, "outputs": [], "source": [ "lunch.hour" ] }, { "cell_type": "markdown", "id": "5ccfaea0", "metadata": {}, "source": [ "You can use an attribute as part of any expression." ] }, { "cell_type": "code", "execution_count": 10, "id": "7ac6db21", "metadata": {}, "outputs": [], "source": [ "total_minutes = lunch.hour * 60 + lunch.minute\n", "total_minutes" ] }, { "cell_type": "markdown", "id": "c5e6725b", "metadata": {}, "source": [ "And you can use the dot operator in an expression in an f-string." ] }, { "cell_type": "code", "execution_count": 11, "id": "1ecdc091", "metadata": {}, "outputs": [], "source": [ "f'{lunch.hour}:{lunch.minute}:{lunch.second}'" ] }, { "cell_type": "markdown", "id": "e70671d2", "metadata": {}, "source": [ "But notice that the previous example is not in the standard format.\n", "To fix it, we have to print the `minute` and `second` attributes with a leading zero.\n", "We can do that by extending the expressions in curly braces with a **format specifier**.\n", "In the following example, the format specifiers indicate that `minute` and `second` should be displayed with at least two digits and a leading zero if needed." ] }, { "cell_type": "code", "execution_count": 12, "id": "a8a45573", "metadata": {}, "outputs": [], "source": [ "f'{lunch.hour}:{lunch.minute:02d}:{lunch.second:02d}'" ] }, { "cell_type": "markdown", "id": "bcbea13a", "metadata": {}, "source": [ "We'll use this f-string to write a function that displays the value of a `Time`object.\n", "You can pass an object as an argument in the usual way.\n", "For example, the following function takes a `Time` object as an argument. " ] }, { "cell_type": "code", "execution_count": 13, "id": "fc77feb2", "metadata": {}, "outputs": [], "source": [ "def print_time(time):\n", " s = f'{time.hour:02d}:{time.minute:02d}:{time.second:02d}'\n", " print(s)" ] }, { "cell_type": "markdown", "id": "3b8ccbed", "metadata": {}, "source": [ "When we call it, we can pass `lunch` as an argument." ] }, { "cell_type": "code", "execution_count": 14, "id": "59b7f4f4", "metadata": {}, "outputs": [], "source": [ "print_time(lunch)" ] }, { "cell_type": "markdown", "id": "18826e53", "metadata": {}, "source": [ "## Objects as return values\n", "\n", "Functions can return objects. For example, `make_time` takes parameters called `hour`, `minute`, and `second`, stores them as attributes in a `Time` object, and returns the new object." ] }, { "cell_type": "code", "execution_count": 15, "id": "fde15b59", "metadata": {}, "outputs": [], "source": [ "def make_time(hour, minute, second):\n", " time = Time()\n", " time.hour = hour\n", " time.minute = minute\n", " time.second = second\n", " return time" ] }, { "cell_type": "markdown", "id": "d8a6acca", "metadata": {}, "source": [ "It might be surprising that the parameters have the same names as the attributes, but that's a common way to write a function like this.\n", "Here's how we use `make_time` to create a `Time` object." ] }, { "cell_type": "code", "execution_count": 16, "id": "f4199d7f", "metadata": {}, "outputs": [], "source": [ "time = make_time(11, 59, 1)\n", "print_time(time)" ] }, { "cell_type": "markdown", "id": "05720bcb", "metadata": {}, "source": [ "## Objects are mutable\n", "\n", "Suppose you are going to a screening of a movie, like *Monty Python and the Holy Grail*, which starts at `9:20 PM` and runs for `92` minutes, which is one hour `32` minutes.\n", "What time will the movie end?\n", "\n", "First, we'll create a `Time` object that represents the start time." ] }, { "cell_type": "code", "execution_count": 17, "id": "57847af3", "metadata": {}, "outputs": [], "source": [ "start = make_time(9, 20, 0)\n", "print_time(start)" ] }, { "cell_type": "markdown", "id": "001bcda9", "metadata": {}, "source": [ "To find the end time, we can modify the attributes of the `Time` object, adding the duration of the movie." ] }, { "cell_type": "code", "execution_count": 18, "id": "f3637b10", "metadata": {}, "outputs": [], "source": [ "start.hour += 1\n", "start.minute += 32\n", "print_time(start)" ] }, { "cell_type": "markdown", "id": "7007ab61", "metadata": {}, "source": [ "The movie will be over at 10:52 PM.\n", "\n", "Let's encapsulate this computation in a function and generalize it to take the duration of the movie in three parameters: `hours`, `minutes`, and `seconds`." ] }, { "cell_type": "code", "execution_count": 19, "id": "3468f4d0", "metadata": {}, "outputs": [], "source": [ "def increment_time(time, hours, minutes, seconds):\n", " time.hour += hours\n", " time.minute += minutes\n", " time.second += seconds" ] }, { "cell_type": "markdown", "id": "a51913e2", "metadata": {}, "source": [ "Here is an example that demonstrates the effect." ] }, { "cell_type": "code", "execution_count": 20, "id": "ad8177ad", "metadata": {}, "outputs": [], "source": [ "start = make_time(9, 20, 0)\n", "increment_time(start, 1, 32, 0)\n", "print_time(start)" ] }, { "cell_type": "markdown", "id": "42d7de02", "metadata": {}, "source": [ "The following stack diagram shows the state of the program just before `increment_time` modifies the object." ] }, { "cell_type": "code", "execution_count": 21, "id": "6f90c060", "metadata": { "tags": [] }, "outputs": [], "source": [ "from diagram import Frame, Binding, Value, Stack\n", "\n", "d1 = dict(hour=9, minute=20, second=0)\n", "obj1 = make_frame(d1, name='Time', dy=-0.25, offsetx=0.78)\n", "\n", "binding1 = make_binding('start', frame, draw_value=False, dx=0.7)\n", "frame1 = Frame([binding1], name='__main__', loc='left', offsetx=-0.2)\n", "\n", "binding2 = Binding(Value('time'), draw_value=False, dx=0.7, dy=0.35)\n", "binding3 = make_binding('hours', 1)\n", "binding4 = make_binding('minutes',32)\n", "binding5 = make_binding('seconds', 0)\n", "frame2 = Frame([binding2, binding3, binding4, binding5], name='increment_time', \n", " loc='left', dy=-0.25, offsetx=0.08)\n", "\n", "stack = Stack([frame1, frame2], dx=-0.3, dy=-0.5)" ] }, { "cell_type": "code", "execution_count": 22, "id": "93a1db71", "metadata": { "tags": [] }, "outputs": [], "source": [ "from diagram import Bbox\n", "\n", "width, height, x, y = [3.4, 1.89, 1.75, 1.5]\n", "ax = diagram(width, height)\n", "bbox1 = stack.draw(ax, x, y)\n", "bbox2 = obj1.draw(ax, x+0.23, y)\n", "bbox = Bbox.union([bbox1, bbox2])\n", "# adjust(x, y, bbox)" ] }, { "cell_type": "markdown", "id": "d1e27667", "metadata": {}, "source": [ "Inside the function, `time` is an alias for `start`, so when `time` is modified, `start` changes.\n", "\n", "This function works, but after it runs, we're left with a variable named `start` that refers to an object that represents the *end* time, and we no longer have an object that represents the start time.\n", "It would be better to leave `start` unchanged and make a new object to represent the end time.\n", "We can do that by copying `start` and modifying the copy." ] }, { "cell_type": "markdown", "id": "0128f850", "metadata": {}, "source": [ "## Copying\n", "\n", "The `copy` module provides a function called `copy` that can duplicate any object.\n", "We can import it like this." ] }, { "cell_type": "code", "execution_count": 23, "id": "9f74834d", "metadata": {}, "outputs": [], "source": [ "from copy import copy" ] }, { "cell_type": "markdown", "id": "940adbeb", "metadata": {}, "source": [ "To see how it works, let's start with a new `Time` object that represents the start time of the movie." ] }, { "cell_type": "code", "execution_count": 24, "id": "770c077a", "metadata": {}, "outputs": [], "source": [ "start = make_time(9, 20, 0)" ] }, { "cell_type": "markdown", "id": "514f05b9", "metadata": {}, "source": [ "And make a copy." ] }, { "cell_type": "code", "execution_count": 25, "id": "edced6e5", "metadata": {}, "outputs": [], "source": [ "end = copy(start)" ] }, { "cell_type": "markdown", "id": "87d8956b", "metadata": {}, "source": [ "Now `start` and `end` contain the same data." ] }, { "cell_type": "code", "execution_count": 26, "id": "509c3640", "metadata": {}, "outputs": [], "source": [ "print_time(start)\n", "print_time(end)" ] }, { "cell_type": "markdown", "id": "e75c1e09", "metadata": {}, "source": [ "But the `is` operator confirms that they are not the same object." ] }, { "cell_type": "code", "execution_count": 27, "id": "60d812f7", "metadata": {}, "outputs": [], "source": [ "start is end" ] }, { "cell_type": "markdown", "id": "22b68a3f", "metadata": {}, "source": [ "Let's see what the `==` operator does." ] }, { "cell_type": "code", "execution_count": 28, "id": "4d504362", "metadata": {}, "outputs": [], "source": [ "start == end" ] }, { "cell_type": "markdown", "id": "78ebf931", "metadata": {}, "source": [ "You might expect `==` to yield `True` because the objects contain the same data.\n", "But for programmer-defined classes, the default behavior of the `==` operator is the same as the `is` operator -- it checks identity, not equivalence." ] }, { "cell_type": "markdown", "id": "a3934fdd-d4cd-41e0-86e6-5bb78d0886a7", "metadata": {}, "source": [ "## Pure functions\n", "\n", "We can use `copy` to write pure functions that don't modify their parameters.\n", "For example, here's a function that takes a `Time` object and a duration in hours, minutes and seconds.\n", "It makes a copy of the original object, uses `increment_time` to modify the copy, and returns it." ] }, { "cell_type": "code", "execution_count": 29, "id": "85090d3e", "metadata": {}, "outputs": [], "source": [ "def add_time(time, hours, minutes, seconds):\n", " total = copy(time)\n", " increment_time(total, hours, minutes, seconds)\n", " return total" ] }, { "cell_type": "markdown", "id": "c181af12", "metadata": {}, "source": [ "Here's how we use it." ] }, { "cell_type": "code", "execution_count": 30, "id": "1d9cf4da", "metadata": {}, "outputs": [], "source": [ "end = add_time(start, 1, 32, 0)\n", "print_time(end)" ] }, { "cell_type": "markdown", "id": "54b1ca4a", "metadata": {}, "source": [ "The return value is a new object representing the end time of the movie.\n", "And we can confirm that `start` is unchanged." ] }, { "cell_type": "code", "execution_count": 31, "id": "9fe30d71", "metadata": {}, "outputs": [], "source": [ "print_time(start)" ] }, { "cell_type": "markdown", "id": "1233b2db", "metadata": {}, "source": [ "`add_time` is a **pure function** because it does not modify any of the objects passed to it as arguments and its only effect is to return a value.\n", "\n", "Anything that can be done with impure functions can also be done with pure functions.\n", "In fact, some programming languages only allow pure functions.\n", "Programs that use pure functions might be less error-prone, but impure functions are sometimes convenient and can be more efficient.\n", "\n", "In general, I suggest you write pure functions whenever it is reasonable and resort to impure functions only if there is a compelling advantage.\n", "This approach might be called a **functional programming style**." ] }, { "cell_type": "markdown", "id": "9d9fabbc", "metadata": {}, "source": [ "## Prototype and patch\n", "\n", "In the previous example, `increment_time` and `add_time` seem to work, but if we try another example, we'll see that they are not quite correct.\n", "\n", "Suppose you arrive at the theater and discover that the movie starts at `9:40`, not `9:20`.\n", "Here's what happens when we compute the updated end time." ] }, { "cell_type": "code", "execution_count": 32, "id": "57a96bf9-7d7b-4715-a4b3-2dfad1beb670", "metadata": {}, "outputs": [], "source": [ "start = make_time(9, 40, 0)\n", "end = add_time(start, 1, 32, 0)\n", "print_time(end)" ] }, { "cell_type": "markdown", "id": "c712ebf7-7e52-490e-91d7-5f1c83334de0", "metadata": {}, "source": [ "The result is not a valid time.\n", "The problem is that `increment_time` does not deal with cases where the number of seconds or minutes adds up to more than `60`.\n", "\n", "Here's an improved version that checks whether `second` exceeds or equals `60` -- if so, it increments `minute` -- then checks whether `minute` exceeds or equals `60` -- if so, it increments `hour`." ] }, { "cell_type": "code", "execution_count": 33, "id": "5bada1df-be0a-4f6a-8fe3-d92bd937dc70", "metadata": {}, "outputs": [], "source": [ "def increment_time(time, hours, minutes, seconds):\n", " time.hour += hours\n", " time.minute += minutes\n", " time.second += seconds\n", "\n", " if time.second >= 60:\n", " time.second -= 60\n", " time.minute += 1\n", "\n", " if time.minute >= 60:\n", " time.minute -= 60\n", " time.hour += 1" ] }, { "cell_type": "markdown", "id": "c133c5d8", "metadata": {}, "source": [ "Fixing `increment_time` also fixes `add_time`, which uses it.\n", "So now the previous example works correctly." ] }, { "cell_type": "code", "execution_count": 34, "id": "a139b64b", "metadata": {}, "outputs": [], "source": [ "end = add_time(start, 1, 32, 0)\n", "print_time(end)" ] }, { "cell_type": "markdown", "id": "a2f644a6-ca43-494e-af14-6e845b3d7973", "metadata": {}, "source": [ "But this function is still not correct, because the arguments might be bigger than `60`.\n", "For example, suppose we are given the run time as `92` minutes, rather than `1` hours and `32` minutes.\n", "We might call `add_time` like this." ] }, { "cell_type": "code", "execution_count": 35, "id": "8c9384cb", "metadata": {}, "outputs": [], "source": [ "end = add_time(start, 0, 92, 0)\n", "print_time(end)" ] }, { "cell_type": "markdown", "id": "72e0a08b", "metadata": {}, "source": [ "The result is not a valid time.\n", "So let's try a different approach, using the `divmod` function.\n", "We'll make a copy of `start` and modify it by incrementing the `minute` attribute." ] }, { "cell_type": "code", "execution_count": 36, "id": "47b04507", "metadata": {}, "outputs": [], "source": [ "end = copy(start)\n", "end.minute = start.minute + 92\n", "end.minute" ] }, { "cell_type": "markdown", "id": "c56355bc", "metadata": {}, "source": [ "Now `minute` is `132`, which is `2` hours and `12` minutes.\n", "We can use `divmod` to divide by `60` and return the number of whole hours and the number of minutes left over." ] }, { "cell_type": "code", "execution_count": 37, "id": "8ce8f8bc", "metadata": {}, "outputs": [], "source": [ "carry, end.minute = divmod(end.minute, 60)\n", "carry, end.minute" ] }, { "cell_type": "markdown", "id": "43204703", "metadata": {}, "source": [ "Now `minute` is correct, and we can add the hours to `hour`." ] }, { "cell_type": "code", "execution_count": 38, "id": "90445645", "metadata": {}, "outputs": [], "source": [ "end.hour += carry\n", "print_time(end)" ] }, { "cell_type": "markdown", "id": "a68ae1cd", "metadata": {}, "source": [ "The result is a valid time.\n", "We can do the same thing with `hour` and `second`, and encapsulate the whole process in a function." ] }, { "cell_type": "code", "execution_count": 39, "id": "0a9653a2", "metadata": {}, "outputs": [], "source": [ "def increment_time(time, hours, minutes, seconds):\n", " time.hour += hours\n", " time.minute += minutes\n", " time.second += seconds\n", " \n", " carry, time.second = divmod(time.second, 60)\n", " carry, time.minute = divmod(time.minute + carry, 60)\n", " carry, time.hour = divmod(time.hour + carry, 24)" ] }, { "cell_type": "markdown", "id": "7437113a", "metadata": {}, "source": [ "With this version of `increment_time`, `add_time` works correctly, even if the arguments exceed `60`." ] }, { "cell_type": "code", "execution_count": 40, "id": "694cfdd1", "metadata": {}, "outputs": [], "source": [ "end = add_time(start, 0, 90, 120)\n", "print_time(end)" ] }, { "cell_type": "markdown", "id": "7c6329b2", "metadata": {}, "source": [ "This section demonstrates a program development plan I call **prototype and patch**.\n", "We started with a simple prototype that worked correctly for the first example.\n", "Then we tested it with more difficult examples -- when we found an error, we modified the program to fix it, like putting a patch on tire with a puncture.\n", "\n", "This approach can be effective, especially if you don't yet have a deep understanding of the problem.\n", "But incremental corrections can generate code that is unnecessarily complicated -- since it deals with many special cases -- and unreliable -- since it is hard to know if you have\n", "found all the errors." ] }, { "cell_type": "markdown", "id": "39031461-49a9-4eba-a075-ef49a6f5552b", "metadata": {}, "source": [ "## Design-first development\n", "\n", "An alternative plan is **design-first development**, which involves more planning before prototyping. In a design-first process, sometimes a high-level insight into the problem makes the programming much easier.\n", "\n", "In this case, the insight is that we can think of a `Time` object as a three-digit number in base 60 -- also known as sexagesimal.\n", "The `second` attribute is the \"ones column\", the `minute` attribute is the \"sixties column\",\n", "and the `hour` attribute is the \"thirty-six hundreds column\".\n", "When we wrote `increment_time`, we were effectively doing addition in base 60, which is why we had to carry from one column to the next.\n", "\n", "This observation suggests another approach to the whole problem -- we can convert `Time` objects to integers and take advantage of the fact that Python knows how to do integer arithmetic.\n", "\n", "Here is a function that converts from a `Time` to an integer." ] }, { "cell_type": "code", "execution_count": 41, "id": "a4427f06-10f3-478f-af4b-888297ee59ac", "metadata": {}, "outputs": [], "source": [ "def time_to_int(time):\n", " minutes = time.hour * 60 + time.minute\n", " seconds = minutes * 60 + time.second\n", " return seconds" ] }, { "cell_type": "markdown", "id": "c7e7789e", "metadata": {}, "source": [ "The result is the number of seconds since the beginning of the day.\n", "For example, `01:01:01` is `1` hour, `1` minute and `1` second from the beginning of the day, which is the sum of `3600` seconds, `60` seconds, and `1` second." ] }, { "cell_type": "code", "execution_count": 42, "id": "e71f9661", "metadata": {}, "outputs": [], "source": [ "time = make_time(1, 1, 1)\n", "print_time(time)\n", "time_to_int(time)" ] }, { "cell_type": "markdown", "id": "6ea525c8-4547-4bde-91c3-17f45add1bf8", "metadata": {}, "source": [ "And here's a function that goes in the other direction -- converting an integer to a `Time` object -- using the `divmod` function." ] }, { "cell_type": "code", "execution_count": 43, "id": "7a93edcc-de21-43b1-b0a9-1bcbc7f6125c", "metadata": {}, "outputs": [], "source": [ "def int_to_time(seconds):\n", " minute, second = divmod(seconds, 60)\n", " hour, minute = divmod(minute, 60)\n", " return make_time(hour, minute, second)" ] }, { "cell_type": "markdown", "id": "4706b5df", "metadata": {}, "source": [ "We can test it by converting the previous example back to a `Time`." ] }, { "cell_type": "code", "execution_count": 44, "id": "967fc3c2", "metadata": {}, "outputs": [], "source": [ "time = int_to_time(3661)\n", "print_time(time)" ] }, { "cell_type": "markdown", "id": "0c2b8469-d4a7-46f9-a0a1-f2a6c1595183", "metadata": {}, "source": [ "Using these functions, we can write a more concise version of `add_time`." ] }, { "cell_type": "code", "execution_count": 45, "id": "0278a042-9e5b-460f-bd26-2fa319e7193a", "metadata": {}, "outputs": [], "source": [ "def add_time(time, hours, minutes, seconds):\n", " duration = make_time(hours, minutes, seconds)\n", " seconds = time_to_int(time) + time_to_int(duration)\n", " return int_to_time(seconds)" ] }, { "cell_type": "markdown", "id": "cb560257", "metadata": {}, "source": [ "The first line converts the arguments to a `Time` object called `duration`.\n", "The second line converts `time` and `duration` to seconds and adds them.\n", "The third line converts the sum to a `Time` object and returns it.\n", "\n", "Here's how it works." ] }, { "cell_type": "code", "execution_count": 46, "id": "ee78ffbc", "metadata": {}, "outputs": [], "source": [ "start = make_time(9, 40, 0)\n", "end = add_time(start, 1, 32, 0)\n", "print_time(end)" ] }, { "cell_type": "markdown", "id": "db762aa8-4aab-4c17-a88d-72c5048f18c0", "metadata": {}, "source": [ "In some ways, converting from base 60 to base 10 and back is harder than\n", "just dealing with times. Base conversion is more abstract; our intuition\n", "for dealing with time values is better.\n", "\n", "But if we have the insight to treat times as base 60 numbers -- and invest the effort to write the conversion functions `time_to_int` and `int_to_time` -- we get a program that is shorter, easier to read and debug, and more reliable.\n", "\n", "It is also easier to add features later. For example, imagine subtracting two `Time` objects to find the duration between them.\n", "The naive approach is to implement subtraction with borrowing.\n", "Using the conversion functions is easier and more likely to be correct.\n", "\n", "Ironically, sometimes making a problem harder -- or more general -- makes it easier, because there are fewer special cases and fewer opportunities for error." ] }, { "cell_type": "markdown", "id": "a0d23d08", "metadata": { "tags": [] }, "source": [ "## Debugging\n", "\n", "Python provides several built-in functions that are useful for testing and debugging programs that work with objects.\n", "For example, if you are not sure what type an object is, you can ask." ] }, { "cell_type": "code", "execution_count": 47, "id": "652bee8f", "metadata": {}, "outputs": [], "source": [ "type(start)" ] }, { "cell_type": "markdown", "id": "7ec0eabf", "metadata": {}, "source": [ "You can also use `isinstance` to check whether an object is an instance of a particular class." ] }, { "cell_type": "code", "execution_count": 48, "id": "3ab974e4", "metadata": {}, "outputs": [], "source": [ "isinstance(end, Time)" ] }, { "cell_type": "markdown", "id": "4f453fe9", "metadata": {}, "source": [ "If you are not sure whether an object has a particular attribute, you\n", "can use the built-in function `hasattr`." ] }, { "cell_type": "code", "execution_count": 49, "id": "5f80e5ad", "metadata": {}, "outputs": [], "source": [ "hasattr(start, 'hour')" ] }, { "cell_type": "markdown", "id": "a0131d84", "metadata": {}, "source": [ "To get all of the attributes, and their values, in a dictionary, you can use `vars`." ] }, { "cell_type": "code", "execution_count": 50, "id": "2a102f0f", "metadata": {}, "outputs": [], "source": [ "vars(start)" ] }, { "cell_type": "markdown", "id": "f1a443c8", "metadata": {}, "source": [ "The `structshape` module, which we saw in [Chapter 11](section_debugging_11), also works with programmer-defined types." ] }, { "cell_type": "code", "execution_count": 51, "id": "b71f46d8", "metadata": { "tags": [] }, "outputs": [], "source": [ "download('https://raw.githubusercontent.com/AllenDowney/ThinkPython/v3/structshape.py');" ] }, { "cell_type": "code", "execution_count": 52, "id": "1e6498a8", "metadata": {}, "outputs": [], "source": [ "from structshape import structshape\n", "\n", "t = start, end\n", "structshape(t)" ] }, { "cell_type": "markdown", "id": "501436c0-6634-415f-be84-2d130232b2b8", "metadata": {}, "source": [ "## Glossary\n", "\n", "**object-oriented programming:**\n", "A style of programming that uses objects to organize code and data.\n", "\n", "**class:**\n", " A programmer-defined type. A class definition creates a new class object.\n", "\n", "**class object:**\n", "An object that represents a class -- it is the result of a class definition.\n", "\n", "**instantiation:**\n", "The process of creating an object that belongs to a class.\n", "\n", "**instance:**\n", " An object that belongs to a class.\n", "\n", "**attribute:**\n", " A variable associated with an object, also called an instance variable.\n", "\n", "**object diagram:**\n", "A graphical representation of an object, its attributes, and their values.\n", "\n", "**format specifier:**\n", "In an f-string, a format specifier determines how a value is converted to a string.\n", "\n", "**pure function:**\n", "A function that does not modify its parameters or have any effect other than returning a value.\n", "\n", "**functional programming style:**\n", "A way of programming that uses pure functions whenever possible.\n", "\n", "**prototype and patch:**\n", "A way of developing programs by starting with a rough draft and gradually adding features and fixing bugs.\n", "\n", "**design-first development:**\n", "A way of developing programs with more careful planning that prototype and patch." ] }, { "cell_type": "markdown", "id": "10bccde6", "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": 53, "id": "ab3d0104", "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", "There is a lot of new vocabulary in this chapter.\n", "A conversation with a virtual assistant can help solidify your understanding.\n", "Consider asking:\n", "\n", "* \"What is the difference between a class and a type?\"\n", "\n", "* \"What is the difference between an object and an instance?\"\n", "\n", "* \"What is the difference between a variable and an attribute?\"\n", "\n", "* \"What are the pros and cons of pure functions compared to impure functions?\"\n", "\n", "Because we are just getting started with object oriented programming, the code in this chapter is not idiomatic -- it is not the kind of code experienced programmers write.\n", "If you ask a virtual assistant for help with the exercises, you will probably see features we have not covered yet.\n", "In particular, you are likely to see a method called `__init__` used to initialize the attributes of an instance.\n", "\n", "If these features make sense to you, go ahead and use them.\n", "But if not, be patient -- we will get there soon.\n", "In the meantime, see if you can solve the following exercises using only the features we have covered so far.\n", "\n", "Also, in this chapter we saw one example of a format specifier. For more information, ask \"What format specifiers can be used in a Python f-string?\"" ] }, { "cell_type": "markdown", "id": "bcdab7d6", "metadata": {}, "source": [ "### Exercise\n", "\n", "Write a function called `subtract_time` that takes two `Time` objects and returns the interval between them in seconds -- assuming that they are two times during the same day." ] }, { "cell_type": "markdown", "id": "5033ee5f", "metadata": { "tags": [] }, "source": [ "Here's an outline of the function to get you started." ] }, { "cell_type": "code", "execution_count": 54, "id": "7d898f43", "metadata": { "tags": [] }, "outputs": [], "source": [ "def subtract_time(t1, t2):\n", " \"\"\"Compute the difference between two times in seconds.\n", " \n", " >>> subtract_time(make_time(3, 2, 1), make_time(3, 2, 0))\n", " 1\n", " >>> subtract_time(make_time(3, 2, 1), make_time(3, 0, 0))\n", " 121\n", " >>> subtract_time(make_time(11, 12, 0), make_time(9, 40, 0))\n", " 5520\n", " \"\"\"\n", " return None" ] }, { "cell_type": "code", "execution_count": 55, "id": "f1b54959", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "73334265", "metadata": { "tags": [] }, "source": [ "You can use `doctest` to test your function." ] }, { "cell_type": "code", "execution_count": 56, "id": "5a25a3de", "metadata": { "tags": [] }, "outputs": [], "source": [ "from doctest import run_docstring_examples\n", "\n", "def run_doctests(func):\n", " run_docstring_examples(func, globals(), name=func.__name__)\n", "\n", "run_doctests(subtract_time)" ] }, { "cell_type": "markdown", "id": "c3189549", "metadata": {}, "source": [ "### Exercise\n", "\n", "Write a function called `is_after` that takes two `Time` objects and returns `True` if the first time is later in the day than the second, and `False` otherwise." ] }, { "cell_type": "markdown", "id": "fd4ac340", "metadata": { "tags": [] }, "source": [ "Here's an outline of the function to get you started." ] }, { "cell_type": "code", "execution_count": 57, "id": "05499ffa", "metadata": {}, "outputs": [], "source": [ "def is_after(t1, t2):\n", " \"\"\"Checks whether `t1` is after `t2`.\n", " \n", " >>> is_after(make_time(3, 2, 1), make_time(3, 2, 0))\n", " True\n", " >>> is_after(make_time(3, 2, 1), make_time(3, 2, 1))\n", " False\n", " >>> is_after(make_time(11, 12, 0), make_time(9, 40, 0))\n", " True\n", " \"\"\"\n", " return None" ] }, { "cell_type": "code", "execution_count": 58, "id": "12b4ad17", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "f9da8ede", "metadata": { "tags": [] }, "source": [ "You can use `doctest` to test your function." ] }, { "cell_type": "code", "execution_count": 59, "id": "4e580404", "metadata": { "tags": [] }, "outputs": [], "source": [ "run_doctests(is_after)" ] }, { "cell_type": "markdown", "id": "16dff862", "metadata": {}, "source": [ "### Exercise\n", "\n", "Here's a definition for a `Date` class that represents a date -- that is, a year, month, and day of the month." ] }, { "cell_type": "code", "execution_count": 60, "id": "c5de60ed", "metadata": {}, "outputs": [], "source": [ "class Date:\n", " \"\"\"Represents a year, month, and day\"\"\"" ] }, { "cell_type": "markdown", "id": "3311fa97", "metadata": {}, "source": [ "1. Write a function called `make_date` that takes `year`, `month`, and `day` as parameters, makes a `Date` object, assigns the parameters to attributes, and returns the result the new object. Create an object that represents June 22, 1933.\n", "\n", "2. Write a function called `print_date` that takes a `Date` object, uses an f-string to format the attributes, and prints the result. If you test it with the `Date` you created, the result should be `1933-06-22`.\n", "\n", "3. Write a function called `is_after` that takes two `Date` objects as parameters and returns `True` if the first comes after the second. Create a second object that represents September 17, 1933, and check whether it comes after the first object.\n", "\n", "Hint: You might find it useful to write a function called `date_to_tuple` that takes a `Date` object and returns a tuple that contains its attributes in year, month, day order." ] }, { "cell_type": "markdown", "id": "90b10ca5", "metadata": { "tags": [] }, "source": [ "You can use this function outline to get started." ] }, { "cell_type": "code", "execution_count": 61, "id": "9e16bcde", "metadata": { "tags": [] }, "outputs": [], "source": [ "def make_date(year, month, day):\n", " return None" ] }, { "cell_type": "code", "execution_count": 62, "id": "ff95300b", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "20c5edf8", "metadata": { "tags": [] }, "source": [ "You can use these examples to test `make_date`." ] }, { "cell_type": "code", "execution_count": 63, "id": "62180007", "metadata": { "tags": [] }, "outputs": [], "source": [ "birthday1 = make_date(1933, 6, 22)" ] }, { "cell_type": "code", "execution_count": 64, "id": "2d70104e", "metadata": { "tags": [] }, "outputs": [], "source": [ "birthday2 = make_date(1933, 9, 17)" ] }, { "cell_type": "markdown", "id": "16ff5bef", "metadata": { "tags": [] }, "source": [ "You can use this function outline to get started." ] }, { "cell_type": "code", "execution_count": 65, "id": "2cc0653e", "metadata": { "tags": [] }, "outputs": [], "source": [ "def print_date(date):\n", " print('')" ] }, { "cell_type": "code", "execution_count": 66, "id": "0b8f63df", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "c36c432e", "metadata": { "tags": [] }, "source": [ "You can use this example to test `print_date`." ] }, { "cell_type": "code", "execution_count": 67, "id": "2d0a026d", "metadata": { "tags": [] }, "outputs": [], "source": [ "print_date(birthday1)" ] }, { "cell_type": "markdown", "id": "17565b1e", "metadata": { "tags": [] }, "source": [ "You can use this function outline to get started." ] }, { "cell_type": "code", "execution_count": 68, "id": "70413f48", "metadata": { "tags": [] }, "outputs": [], "source": [ "def is_after(date1, date2):\n", " return None" ] }, { "cell_type": "code", "execution_count": 69, "id": "b244a057", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": 70, "id": "5fab04bd", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "id": "d9b5dd67", "metadata": { "tags": [] }, "source": [ "You can use these examples to test `is_after`." ] }, { "cell_type": "code", "execution_count": 71, "id": "59166d30", "metadata": { "tags": [] }, "outputs": [], "source": [ "is_after(birthday1, birthday2) # should be False" ] }, { "cell_type": "code", "execution_count": 72, "id": "c33706ee", "metadata": { "tags": [] }, "outputs": [], "source": [ "is_after(birthday2, birthday1) # should be True" ] }, { "cell_type": "markdown", "id": "d6f1cc2f", "metadata": {}, "source": [] }, { "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 }