diff --git a/book/_toc.yml b/book/_toc.yml index ac11658f24b5b0672116b0f8c66cfeedb597f66d..c9d156bcebfaff3691df5d81c7b4e43445bf28b3 100644 --- a/book/_toc.yml +++ b/book/_toc.yml @@ -432,12 +432,19 @@ parts: sections: - file: programming/python_topics/classes title: Classes +# START REMOVE-FROM-PUBLISH + - file: programming/python_topics/classes_solution + title: Classes Solution +# END REMOVE-FROM-PUBLISH - file: programming/python_topics/visualization title: Advanced Visualization - file: programming/sympy/sympy.ipynb # START REMOVE-FROM-PUBLISH - file: programming/python_topics/modules title: Modules + - file: programming/python_topics/modules_solution + title: Modules Solution +# END REMOVE-FROM-PUBLISH # THIS SECTION WAS DRAFTED BUT NEVER RELEASED FROM OLD BRANCH - file: programming/code/overview @@ -463,7 +470,6 @@ parts: # sections: # - file: programming/code/packages # - file: programming/code/environments -# END REMOVE-FROM-PUBLISH - caption: Fundamental Concepts numbered: 2 chapters: diff --git a/book/intro.md b/book/intro.md index 1540220652cf8ce65499c73ad2bf33a977e755a8..d7b923ca1b404e87f5c8a88ce158441e3c58d301 100644 --- a/book/intro.md +++ b/book/intro.md @@ -4,7 +4,7 @@ Welcome to the MUDE textbook for the 2023-24 academic year. This is where assign ````{admonition} Interactive Pages---Use Python in your Browser! -This online textbook has a number of pages that are set up to be used interactively. On such pages you can use the "Live Code" button under the Rocket Ship icon in the top right to activate the interactive features and use Python interactively! +This online textbook has a number of pages that are set up to be used interactively. You can use the "Live Code" button under the Rocket Ship icon in the top right to activate the interactive features and use Python interactively! Sometimes the interactivity will involve completing an exercise, wheras on other pages it might simply provide the opportunity to edit the contents of code cells and execute it to explore the page contents interactively. Other pages may provide interactive figures (e.g., widgets). diff --git a/book/programming/python_topics/classes_solution.ipynb b/book/programming/python_topics/classes_solution.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..ae1082c174bf60f3eb8690bfa539d16e70b6e133 --- /dev/null +++ b/book/programming/python_topics/classes_solution.ipynb @@ -0,0 +1,961 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Classes and Object-Oriented Programming in Python" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<img src=\"https://surfdrive.surf.nl/files/index.php/apps/files_sharing/ajax/publicpreview.php?x=1920&y=452&a=true&file=header.png&t=meVX4gGMjLbKX1z&scalingup=0\" alt=\"header\" border=\"0\"/>" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Learning Objectives:\n", + "By the end of this class, students should be able to:\n", + "\n", + "- Understand the fundamental concepts of classes and object-oriented programming (OOP) in Python.\n", + "- Comprehend the key principles of encapsulation, inheritance, and polymorphism in OOP.\n", + "- Define and create classes in Python.\n", + "- Create objects from classes and understand the relationship between classes and objects." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Contents\n", + "===\n", + "- [Introduction to Object-Oriented Programming](#Introduction-to-Object-Oriented-Programming)\n", + "- [What are classes?](#What-are-classes?)\n", + "- [Adding parameters to the class](#Adding-parameters-to-the-class)\n", + " - [Accepting parameters for the \\_\\_init\\_\\_() method](#Accepting-parameters-for-the-__init__%28%29-method)\n", + " - [Accepting parameters in a method](#Accepting-parameters-in-a-method)\n", + "- [Encapsulation](#Encapsulation)\n", + "- [Inheritance](#Inheritance)\n", + "- [Polymorphism](#Polymorphism)\n", + "- [Exercises](#Exercises)\n", + "- [Additional Information (optional)](#Additional-Information-(optional))\n", + "- [References and used resources](#References-and-used-resources)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Introduction to Object-Oriented Programming\n", + "===\n", + "Object-Oriented Programming (OOP) is a programming paradigm that organizes code into reusable blocks of code called *classes*, which bundle data (*attributes*) and behaviors (*methods*) together.\n", + "When you want to use a class in one of your programs, you make an **object** from that class, which is where the phrase \"object-oriented\" comes from.\n", + "\n", + "OOP promotes:\n", + "- modularity: making multiple modules or functions and then combining them to form a complete system\n", + "- reusability: shared attributes and functions can be reused without the need of rewriting code\n", + "- better code organization: less code and more reuse\n", + "<!-- \n", + "OOP is defined by 4 main concepts:\n", + "- Encapsulation: concept of bundling data and methods that operate on that data into a single unit called a class. Its purpose is to help in organizing and protecting an object's internal state and behavior;\n", + "- Inheritance: mechanism that allows a new class to inherit attributes and methods from an existing class. Its purpose is to promote code reusability, extensibility, and the creation of class hierarchies, where subclasses can build upon the functionality of their parent classes;\n", + "- Polymorphism: ability of different objects to respond to the same method name in a way that is specific to their class. It simplifies code by enabling flexibility and dynamic behavior;\n", + "- Abstraction: process of simplifying complex systems by modeling classes based on their essential properties. It reduces complexity, making code more understandable and maintainable. -->\n", + "\n", + "These concepts may seem abstract for now but they will start to make more sense throughout the rest of the notebook." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What are classes?\n", + "===\n", + "Classes can be thought of as blueprints or templates for creating objects. \n", + "\n", + "A **class** is a body of code that defines the **attributes** and **behaviors** required to accurately model something you need for your program. Each class encapsulates both properties (attributes) and functions (methods) related to that type of object.\n", + "\n", + "An **attribute** is a piece of information. In code, an attribute is just a variable that is part of a class.\n", + "\n", + "A **method** is an action that is defined within a class, i.e., just a function that is defined for the class.\n", + "\n", + "An **object** is a particular instance of a class. An object has a certain set of values for all of the attributes (variables) in the class. You can have as many objects as you want for any one class." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's consider an example using a rocket ship in a game.\n", + "When defining the Rocket class, we have to imagine what are properties and actions that are common to all rockets in this hypotethic game.\n", + "For example, the a very simple rocket will have some x and y coordinates and will be able to move up.\n", + "Here is what the rocket class can look like in code:\n", + "<img src=\"https://surfdrive.surf.nl/files/index.php/apps/files_sharing/ajax/publicpreview.php?x=1920&y=452&a=true&file=Cartoon_space_rocket.png&t=KEEPyzZeDZPfcLj&scalingup=0\" alt=\"Rocket\" border=\"0\" width=\"200\" align=\"right\"/>\n", + "\n", + "```python\n", + "class Rocket:\n", + " # Rocket simulates a rocket ship for a game.\n", + " \n", + " def __init__(self):\n", + " # Each rocket has an (x,y) position.\n", + " self.x = 0\n", + " self.y = 0\n", + "\n", + " def move_up(self):\n", + " # Each rocket can move along the y-axis.\n", + " self.y += 1\n", + "```\n", + "\n", + "Now let's examine how we created this class.\n", + "\n", + "The first line, with the keyword **class**, tells Python that you are about to define a class. The naming convention for classes is the CamelCase, a convention where each letter that starts a word is capitalized, with no underscores in the name. \n", + "It is recommended that the class name does not have any parentheses after it, unless it inherits from another class (we'll see this better later).\n", + "\n", + "It is good practice to write a comment at the beginning of your class, describing the class. There is a [more formal syntax](http://www.python.org/dev/peps/pep-0257/) for documenting your classes, but you can wait a little bit to get that formal. For now, just write a comment at the beginning of your class summarizing what you intend the class to do. Writing more formal documentation for your classes will be easy later if you start by writing simple comments now.\n", + "\n", + "One of the first things you do when creating a class is to define the `__init__()` method. \n", + "The `__init__()` method is called automatically when you create an object from your class and sets the values for any parameters that need to be defined when an object is first created. 0\n", + "We call this method a ***constructor***. \n", + "In this case, The `__init__()` method initializes the x and y values of the Rocket to 0.\n", + "\n", + "The ***self*** part is a syntax that allows you to access variables and methods from anywhere else in the class.\n", + "The word \"self\" refers to the current object that you are working with. Basically, all methods in a class need the *self* object as their first argument, so they can access any attribute that is part of the class.\n", + "\n", + "Methods define a specific action that the class can do.\n", + "A method is just a function that is part of a class. Since it is just a function, you can do anything with a method that you learned about with functions.\n", + "Each method generally accepts at least one argument, the value **self**. This is a reference to the particular object that is calling the method." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With this code, we just defined the class, which is a blueprint.\n", + "To define the actual object, we have to create an *instance* of the class.\n", + "In other words, you create a variable that takes all of the attributes and methods as the Rocket class.\n", + "\n", + "This can be done simply as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Rocket:\n", + " # Rocket simulates a rocket ship for a game,\n", + " \n", + " def __init__(self):\n", + " # Each rocket has an (x,y) position.\n", + " self.x = 0\n", + " self.y = 0\n", + " \n", + " def move_up(self):\n", + " # Increment the y-position of the rocket.\n", + " self.y += 1\n", + "\n", + "# Create a Rocket object.\n", + "my_rocket = Rocket()\n", + "print(my_rocket)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once you have a class, you can define an object and use its methods. Here is how you might define a rocket and have it start to move up:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a Rocket object, and have it start to move up.\n", + "my_rocket = Rocket()\n", + "print(f\"Rocket altitude: {my_rocket.y}\", )\n", + "\n", + "my_rocket.move_up()\n", + "print(f\"Rocket altitude: {my_rocket.y}\")\n", + "\n", + "my_rocket.move_up()\n", + "print(f\"Rocket altitude: {my_rocket.y}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To access an object's variables or methods, you give the name of the object and then use *dot notation* to access the variables and methods. \n", + "So to get the y-value of `my_rocket`, you use `my_rocket.y`. \n", + "\n", + "To use the `move_up()` method on my_rocket, you write `my_rocket.move_up()`.\n", + "This tells Python to apply the method `move_up()` to the object `my_rocket`.\n", + "Python finds the y-value associated with `my_rocket` and adds 1 to that value. \n", + "This process is repeated several times, and you can see from the output that the y-value is in fact increasing." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Making multiple objects from a class\n", + "---\n", + "One of the goals of object-oriented programming is to create reusable code. Once you have written the code for a class, you can create as many objects from that class as you need. It is worth mentioning at this point that classes are usually saved in a separate file, and then imported into the program you are working on. So you can build a library of classes, and use those classes over and over again in different programs. Once you know a class works well, you can leave it alone and know that the objects you create in a new program are going to work as they always have.\n", + "\n", + "You can see this \"code reusability\" already when the Rocket class is used to make more than one Rocket object. Each object is its own instance of that class, with its own separate variables. All of the objects are capable of the same behavior, but each object's particular actions do not affect any of the other objects.\n", + "Here is the code that made a fleet of Rocket objects:\n", + "\n", + "<img src=\"https://surfdrive.surf.nl/files/index.php/apps/files_sharing/ajax/publicpreview.php?x=1920&y=452&a=true&file=many.png&t=XnqII6NO70dgxjw&scalingup=0\" alt=\"Fleet of rockets\" border=\"0\" width=\"800\"/>" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Rocket:\n", + " # Rocket simulates a rocket ship for a game,\n", + " # or a physics simulation.\n", + " \n", + " def __init__(self):\n", + " # Each rocket has an (x,y) position.\n", + " self.x = 0\n", + " self.y = 0\n", + " \n", + " def move_up(self):\n", + " # Increment the y-position of the rocket.\n", + " self.y += 1\n", + " \n", + "# Create a fleet of 3 rockets, and store them in a list.\n", + "rocket_1 = Rocket()\n", + "rocket_2 = Rocket()\n", + "rocket_3 = Rocket()\n", + "\n", + "# Show that each rocket is a separate object.\n", + "print(rocket_1, rocket_2, rocket_3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can see that each rocket is at a separate place in memory and therefore printed in a different way." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can show that each rocket has its own x and y values by moving just one of the rockets:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Move the third rocket up.\n", + "rocket_3.move_up()\n", + "\n", + "# Show that only the third rocket has moved.\n", + "print(f\"Rocket altitudes: {rocket_1.y}, {rocket_2.y}, {rocket_3.y}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A quick check-in\n", + "---\n", + "If all of this makes sense, then the rest of your work with classes will involve learning a lot of details about how classes can be used in more flexible and powerful ways. If this does not make any sense, you could try a few different things:\n", + "\n", + "- Reread the previous sections, and see if things start to make any more sense.\n", + "- Type out these examples in your own editor, and run them. Try making some changes, and see what happens.\n", + "- Read on. The next sections are going to add more functionality to the Rocket class. These steps will involve rehashing some of what has already been covered, in a slightly different way.\n", + "\n", + "Classes are a huge topic, and once you understand them you will probably use them for the rest of your life as a programmer. If you are brand new to this, be patient and trust that things will start to sink in." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Adding parameters to the class\n", + "===\n", + "The Rocket class so far is very simple. It can be made a little more interesting with some refinements to the `__init__()` method, and by the addition of some methods." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Accepting parameters for the \\_\\_init\\_\\_() method\n", + "---\n", + "The `__init__()` method is run automatically one time when you create a new object from a class. The `__init__()` method for the Rocket class so far is pretty simple.\n", + "But we can easily add keyword arguments so that new rockets can be initialized at any position:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Rocket:\n", + " # Rocket simulates a rocket ship for a game,\n", + " # or a physics simulation.\n", + " \n", + " def __init__(self, x=0, y=0):\n", + " # Each rocket has an (x,y) position.\n", + " self.x = x\n", + " self.y = y\n", + " \n", + " def move_up(self):\n", + " # Increment the y-position of the rocket.\n", + " self.y += 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now when you create a new Rocket object you have the choice of passing in arbitrary initial values for x and y:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Make a series of rockets at different starting places.\n", + "rockets = []\n", + "rockets.append(Rocket())\n", + "rockets.append(Rocket(0,10))\n", + "rockets.append(Rocket(100,0))\n", + "\n", + "# Show where each rocket is.\n", + "for index, rocket in enumerate(rockets):\n", + " print(f\"Rocket {index} is at (x,y)=({rocket.x}, {rocket.y}).\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Accepting parameters in a method\n", + "---\n", + "The `__init__()` method is just a special method that serves a particular purpose, which is to help create new objects from a class. Any method in a class can accept parameters of any kind. With this in mind, the `move_up()` method can be made much more flexible. By accepting keyword arguments, the `move_up()` method can be rewritten as a more general `move_rocket()` method. This new method will allow the rocket to be moved any amount, in any direction:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Rocket():\n", + " # Rocket simulates a rocket ship for a game,\n", + " # or a physics simulation.\n", + " \n", + " def __init__(self, x=0, y=0):\n", + " # Each rocket has an (x,y) position.\n", + " self.x = x\n", + " self.y = y\n", + " \n", + " def move_rocket(self, x_increment=0, y_increment=1):\n", + " # Move the rocket according to the paremeters given.\n", + " # Default behavior is to move the rocket up one unit.\n", + " self.x += x_increment\n", + " self.y += y_increment" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The paremeters for the move() method are named x_increment and y_increment rather than x and y. It's good to emphasize that these are changes in the x and y position, not new values for the actual position of the rocket. By carefully choosing the right default values, we can define a meaningful default behavior. If someone calls the method `move_rocket()` with no parameters, the rocket will simply move up one unit in the y-direciton. Note that this method can be given negative values to move the rocket left or right:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Rocket():\n", + " # Rocket simulates a rocket ship for a game,\n", + " # or a physics simulation.\n", + " \n", + " def __init__(self, x=0, y=0):\n", + " # Each rocket has an (x,y) position.\n", + " self.x = x\n", + " self.y = y\n", + " \n", + " def move_rocket(self, x_increment=0, y_increment=1):\n", + " # Move the rocket according to the paremeters given.\n", + " # Default behavior is to move the rocket up one unit.\n", + " self.x += x_increment\n", + " self.y += y_increment\n", + "\n", + " \n", + "# Create three rockets.\n", + "rockets = [Rocket() for x in range(0,3)]\n", + "\n", + "# Move each rocket a different amount.\n", + "rockets[0].move_rocket()\n", + "rockets[1].move_rocket(10,10)\n", + "rockets[2].move_rocket(-10,0)\n", + " \n", + "# Show where each rocket is.\n", + "for index, rocket in enumerate(rockets):\n", + " print(f\"Rocket {index} is at ({rocket.x}, {rocket.y}).\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Adding a new method\n", + "---\n", + "One of the strengths of object-oriented programming is the ability to closely model real-world phenomena by adding appropriate attributes and behaviors to classes. One of the jobs of a team piloting a rocket is to make sure the rocket does not get too close to any other rockets. Let's add a method that will report the distance from one rocket to any other rocket. Note how this method uses another instance of the same class as one of its arguments!\n", + "\n", + "If you are not familiar with distance calculations, there is a fairly simple formula to tell the distance between two points if you know the x and y values of each point. This new method performs that calculation, and then returns the resulting distance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from math import sqrt\n", + "\n", + "class Rocket:\n", + " # Rocket simulates a rocket ship for a game,\n", + " # or a physics simulation.\n", + " \n", + " def __init__(self, x=0, y=0):\n", + " # Each rocket has an (x,y) position.\n", + " self.x = x\n", + " self.y = y\n", + " \n", + " def move_rocket(self, x_increment=0, y_increment=1):\n", + " # Move the rocket according to the paremeters given.\n", + " # Default behavior is to move the rocket up one unit.\n", + " self.x += x_increment\n", + " self.y += y_increment\n", + " \n", + " def get_distance(self, other_rocket):\n", + " # Calculates the distance from this rocket to another rocket,\n", + " # and returns that value.\n", + " distance = sqrt((self.x-other_rocket.x)**2+(self.y-other_rocket.y)**2)\n", + " return distance\n", + " \n", + "# Make two rockets, at different places.\n", + "rocket_0 = Rocket()\n", + "rocket_1 = Rocket(10,5)\n", + "\n", + "# Show the distance between them.\n", + "distance = rocket_0.get_distance(rocket_1)\n", + "print(f\"The rockets are {distance:.6f} units apart.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<img src=\"https://surfdrive.surf.nl/files/index.php/apps/files_sharing/ajax/publicpreview.php?x=1920&y=452&a=true&file=distance.png&t=DVIenuADtGb249E&scalingup=0\" alt=\"Distance between rockets\" border=\"0\" width=\"600\" align=\"left\"/>" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Hopefully these short refinements show that you can extend a class' attributes and behavior to model the phenomena you are interested in as closely as you want. The rocket could have a name, a crew capacity, a payload, a certain amount of fuel, and any number of other attributes. You could define any behavior you want for the rocket, including interactions with other rockets and launch facilities, gravitational fields, and whatever you need it to! There are techniques for managing these more complex interactions, but what you have just seen is the core of object-oriented programming." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Encapsulation\n", + "===\n", + "As we have mentioned before using dictionaries is not useful to create objects that consist of several attributes. Encapsulation entails the wrapping of attributes and methods within one unit. This is beneficial as it enables engineers to write code that is easy to maintain. For example, if you add a new attribute or a method to a class, you will not need to update all your objects, but only the class itself.\n", + "\n", + "A nice feature that encapsulation provides is private attributes and private methods. Those are units, which are meant to only be used internally in a class and not accessed outside of it. By convention programmers should not access them via the object. Furthermore, in order to create a private attribute or a private method, you need to put 2 leading underscores (`_`) before their name.\n", + "\n", + "You can think of the `__init__` method for an example. You are not supposed to call the method, as it is automatically called when you create a new object. Furthermore, note that it has 2 leading and 2 trailing underscores. This is meant to show that this method is resereved in Python. **Therefore, you should not make attributes or methods that have both leading and trailing underscores, because you may mess up how Python works**. Besides the `__init__` method, there are more built-in methods that start and end with 2 underscores. These are called **magic methods**, and determine the behaviour when certain operations (for example: +, - or `print()`) are performed on an object.\n", + "\n", + "Have a look at the Rocket class below, which contains a private attribute `creation_date`. We will call this attribute `__creation_date` to tell Python that we want it to be private. This attribute is set inside the `__init__` method and should not be accessed outside the class unless we create a method, which returns it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from math import sqrt\n", + "import datetime\n", + "\n", + "class Rocket:\n", + " # Rocket simulates a rocket ship for a game,\n", + " # or a physics simulation.\n", + " \n", + " def __init__(self, x=0, y=0):\n", + " # Each rocket has an (x,y) position.\n", + " self.x = x\n", + " self.y = y\n", + " self.__creation_time = datetime.datetime.now()\n", + " \n", + " def move_rocket(self, x_increment=0, y_increment=1):\n", + " # Move the rocket according to the paremeters given.\n", + " # Default behavior is to move the rocket up one unit.\n", + " self.x += x_increment\n", + " self.y += y_increment\n", + " \n", + " def get_distance(self, other_rocket):\n", + " # Calculates the distance from this rocket to another rocket,\n", + " # and returns that value.\n", + " distance = sqrt((self.x-other_rocket.x)**2+(self.y-other_rocket.y)**2)\n", + " return distance\n", + " \n", + " def get_creation_time(self):\n", + " # Returns the time the Rocket was made.\n", + " return self.__creation_time\n", + " \n", + "# Make a rocket.\n", + "rocket_0 = Rocket()\n", + "\n", + "# Try to get the creation time via a method.\n", + "date = rocket_0.get_creation_time()\n", + "print(f\"Rocket was made in: {date}\")\n", + "\n", + "# Try to get the creation time directly.\n", + "date = rocket_0.__creation_time\n", + "print(f\"Rocket was made in: {date}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As seen in the example above, we can only access `__creation_time` via the method `get_creation_time` and get an `AttributeError` if we attempt to directly use the *dot notation* on the object `rocket_0`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Inheritance\n", + "===\n", + "Another of the most important goals of the object-oriented approach to programming is the creation of stable, reliable, reusable code. If you had to create a new class for every kind of object you wanted to model, you would hardly have any reusable code. In Python and any other language that supports OOP, one class can **inherit** from another class. This means you can base a new class on an existing class; the new class *inherits* all of the attributes and behavior of the class it is based on. A new class can override any undesirable attributes or behavior of the class it inherits from, and it can add any new attributes or behavior that are appropriate. The original class is called the **parent** class or **superclass**, and the new class is a **child** or **subclass** of the parent class.\n", + "\n", + "The child class inherits all attributes and behavior from the parent class, but any attributes that are defined in the child class are not available to the parent class. This may be obvious to many people, but it is worth stating. This also means a child class can ***override*** behavior of the parent class. If a child class defines a method that also appears in the parent class, objects of the child class will use the new method rather than the parent class method.\n", + "\n", + "To better understand inheritance, let's look at an example of a class that can be based on the Rocket class." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Shuttle class\n", + "---\n", + "If you wanted to model a space shuttle, you could write an entirely new class. But a space shuttle is just a special kind of rocket. Instead of writing an entirely new class, you can inherit all of the attributes and behavior of a Rocket, and then add a few appropriate attributes and behavior for a Shuttle.\n", + "\n", + "One of the most significant characteristics of a space shuttle is that it can be reused. So the only difference we will add at this point is to record the number of flights the shutttle has completed. Everything else you need to know about a shuttle has already been coded into the Rocket class.\n", + "\n", + "Here is what the Shuttle class looks like:\n", + "\n", + "<img src=\"https://surfdrive.surf.nl/files/index.php/apps/files_sharing/ajax/publicpreview.php?x=1920&y=452&a=true&file=shuttle-boosters-colour.png&t=O5AhNTVFPoYB0Iv&scalingup=0\" alt=\"Shuttle image\" border=\"0\" width=\"200\" align=\"left\"/>" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Shuttle(Rocket):\n", + " # Shuttle simulates a space shuttle, which is really\n", + " # just a reusable rocket.\n", + " \n", + " def __init__(self, x=0, y=0, flights_completed=0):\n", + " super().__init__(x, y)\n", + " self.flights_completed = flights_completed\n", + " \n", + "shuttle = Shuttle(10,0,3)\n", + "print(shuttle.x)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When a new class is based on an existing class, you write the name of the parent class in parentheses when you define the new class:\n", + "```python\n", + "class NewClass(ParentClass):\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `__init__()` function of the new class needs to call the `__init__()` function of the parent class. The `__init__()` function of the new class needs to accept all of the parameters required to build an object from the parent class, and these parameters need to be passed to the `__init__()` function of the parent class. The `super().__init__()` function takes care of this:\n", + "\n", + "```python\n", + "class NewClass(ParentClass):\n", + " \n", + " def __init__(self, arguments_parent_class, arguments_new_class):\n", + " super().__init__(arguments_parent_class)\n", + " # Code for initializing an object of the new class.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `super()` function passes the *self* argument to the parent class automatically." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Inheritance is a powerful feature of object-oriented programming. Using just what you have seen so far about classes, you can model an incredible variety of real-world and virtual phenomena with a high degree of accuracy. The code you write has the potential to be stable and reusable in a variety of applications." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Polymorphism\n", + "===\n", + "Another important goal of the object-oriented approach to programming is to provide flexibility of your code. This can be achived by Polymorphism, which entails that an entity is able to take multiple forms. In Python polymorphism allows us to create methods in a child class with the same name as a method in a parent class. This would mean that a method can serve one purpose in a parent class and different one in a child class.\n", + "\n", + "Child classes inherit all the methods of their parent classes, however, sometimes those methods need to be modified to fit the function of the child. This is achieved by reimplementing the parent methods in the child class.\n", + "\n", + "To better understand polymorphism, let's look at an example of a class that can be based on the Shuttle class and transitively on the Rocket class as well." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ImprovedShuttle class\n", + "---\n", + "Our Shuttle class already improves the basic Rocket class, however, the information we receive from the Rocket class such as `get_distance` is very limited. This is because we currently only get information about the absolute distance, but we do not know the direction, which we need to face to get to that place the fastest.\n", + "\n", + "Therefore, we will create an improved Shuttle, which will be based on the initial Shuttle and will provide better distance information such as angle in which we need to rotate. The formula used is based on taking arctangent of the 2-dimension distances and transforming from radians to degrees.\n", + "\n", + "Here is what the ImprovedShuttle class looks like:\n", + "\n", + "<img src=\"https://surfdrive.surf.nl/files/index.php/apps/files_sharing/ajax/publicpreview.php?x=1920&y=452&a=true&file=space-shuttle.png&t=Y8Vupg9FQNud80j&scalingup=0\" alt=\"ImprovedShuttle image\" border=\"0\" width=\"150\" align=\"left\"/>" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from math import atan, pi, sqrt\n", + "\n", + "class ImprovedShuttle(Shuttle):\n", + " # Improved Shuttle that provides better distance information\n", + " # such as angle.\n", + " \n", + " def __init__(self, x=0, y=0, flights_completed=0):\n", + " super().__init__(x, y)\n", + " self.flights_completed = flights_completed\n", + " \n", + " def get_distance(self, other_rocket):\n", + " # Calculates the distance from this rocket to another rocket,\n", + " # the angle to rotate to face the other rocket,\n", + " # and returns those values.\n", + " distance = super().get_distance(other_rocket)\n", + " angle = atan((other_rocket.y - self.y) / (other_rocket.x - self.x)) * (180 / pi)\n", + " return distance, angle\n", + " \n", + "improvedShuttle = ImprovedShuttle(10,0,3)\n", + "otherShuttle = ImprovedShuttle(13, 3)\n", + "\n", + "# Show the distance between them.\n", + "distance, angle = improvedShuttle.get_distance(otherShuttle)\n", + "print(f\"The shuttles are {distance:.6f} units apart.\")\n", + "print(f\"The angle the initial shuttle needs to rotate in case it needs to go to the other shuttle is {angle:.2f} degrees.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see in the example above, since ImprovedShuttle inherits Shuttle and Shuttle inherits Rocket, then transitively ImprovedShuttle is a child of Rocket class and has access to the parent `get_distance` method. It is possible to access that parent method by making a `super().get_distance()` call.\n", + "\n", + "As a result, class ImprovedShuttle has ***overridden*** Rocket's get_distance. This means that it has reimplemented the parent's method.\n", + "\n", + "It is important to mention that it is not necessary to override (reimplement) every method in the parent class when using inheritance, but if needed, it is possible. \n", + "\n", + "> Note: ImprovedShuttle's get_distance() now returns two outputs, while the parent class only returns one. Imagine you are looping a list containing a mix of Rockets and ImprovedShuttles to store their distance in an array (with as many elements as the length of the lists); this difference in the output may require some extra lines of code to handle potential problems." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Exercise:** It is time to practice what you have learned so far about classes, inheritance, and polymorphism.\n", + "Given the predefined Person class, create a Student and Teacher classes that inherit from People and have the additional following requirements\n", + "\n", + "1. Create a class Student:\n", + " 1. Make class Student inherit class Person;\n", + " 2. Add parameters `start year` and `GPA grade` in the `__init__` method and reuse the parent constructor for the other attributes;\n", + " 3. Create a method `change_grade`, which sets a new grade for the student;\n", + " 4. Override the `introduce_yourself` method to account for the 2 new fields (start year and GPA grade). In your overriding try to reuse the parent `introduce_yourself` method implementation by calling `super()`;\n", + "\n", + "2. Create a class Teacher:\n", + " 1. Make class Teacher inherit class Person;\n", + " 2. Add an attribute called `students` of type `set` in the `__init__` method and reuse the parent constructor for the other attributes. Remember that a set is a collection of elements that contains no duplicates. A set can be initialised in multiple ways. For example, `my_set = set()` or `my_set = {}`;\n", + " 3. Create a method `add_student`, which adds a student to the student set of a teacher. Elements to a set can be added using the method `add()`. For example, `my_set.add(3)`;\n", + " 4. Create a method `print_classroom`, which prints the introductions of all the students in the classroom of the teacher. (Hint: call `introduce_yourself` on every Student object in the set).\n", + " \n", + "3. Similarly to the previous exercise, show your new classes are working properly by creating objects for each of them and calling their respective methods. Furthermore, add the necessary documentation for the classes and methods." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Person():\n", + " def __init__(self, name, age, country_of_origin):\n", + " \"\"\"Constructor to initialise a Person object.\n", + " \n", + " Keyword arguments:\n", + " name -- the name of the person\n", + " age -- the age of the person\n", + " country_of_origin -- the country the person was born\n", + " \"\"\"\n", + " self.__name = name\n", + " self.age = age\n", + " self.country_of_origin = country_of_origin\n", + " \n", + " def introduce_yourself(self):\n", + " \"\"\"Prints a brief introduction of the person.\"\"\"\n", + " print(f\"Hello, my name is {self.__name}, I am {self.age} and I am from {self.country_of_origin}.\")\n", + " \n", + " def get_name(self):\n", + " \"\"\"Gets the name of a person.\"\"\"\n", + " return self.__name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"Start of student exercise\"\"\"\n", + "class Student(Person):\n", + " def __init__(self, name, age, country_of_origin, start_year, gpa_grade):\n", + " \"\"\"Constructor to initialise a Student object.\n", + " \n", + " Keyword arguments:\n", + " name -- the name of the student\n", + " age -- the age of the student\n", + " country_of_origin -- the country the student was born\n", + " start_year -- the year the education of the student began\n", + " gpa_grade -- current gpa_grade of a student\n", + " \"\"\"\n", + " super().__init__(name, age, country_of_origin)\n", + " self.start_year = start_year\n", + " self.gpa_grade = gpa_grade\n", + " \n", + " def introduce_yourself(self):\n", + " \"\"\"Prints a brief introduction of the student.\"\"\"\n", + " super().introduce_yourself()\n", + " print(f\"My GPA grade is {self.gpa_grade} and I started studying in year {self.start_year}.\")\n", + " \n", + " def change_grade(self, new_grade):\n", + " \"\"\"Modifies current GPA grade of the student.\"\"\"\n", + " self.gpa_grade = new_grade\n", + "\n", + "class Teacher(Person):\n", + " def __init__(self, name, age, country_of_origin):\n", + " \"\"\"Constructor to initialise a Teacher object.\n", + " \n", + " Keyword arguments:\n", + " name -- the name of the teacher\n", + " age -- the age of the teacher\n", + " country_of_origin -- the country the teacher was born\n", + " \"\"\"\n", + " super().__init__(name, age, country_of_origin)\n", + " self.students = set()\n", + " \n", + " def add_student(self, student):\n", + " \"\"\"Adds a student to the classroom of a teacher.\n", + " \n", + " Keyword arguments:\n", + " studnet -- student to add to the classroom\n", + " \"\"\"\n", + " self.students.add(student)\n", + " \n", + " def print_classroom(self):\n", + " \"\"\"Prints the classroom of a teacher.\"\"\"\n", + " print(\"The classroom consists of the following students:\")\n", + " for student in self.students:\n", + " student.introduce_yourself()\n", + "\n", + "\"\"\"End of student exercise\"\"\"\n", + "\n", + "print(\"Showing how class Student is defined:\")\n", + "student = Student(\"Mike\", 22, \"Italy\", \"2021\", 7.3)\n", + "\n", + "student.change_grade(7.2)\n", + "print(f\"New GPA grade of student is {student.gpa_grade}.\")\n", + "print()\n", + "\n", + "print(\"Showing how class Teacher is defined:\")\n", + "teacher = Teacher(\"Joe\", 42, \"Germany\")\n", + "teacher.introduce_yourself()\n", + "teacher.add_student(student)\n", + "\n", + "teacher.print_classroom()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Additional information (optional)\n", + "===" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Revisiting PEP 8\n", + "===\n", + "If you recall, [PEP 8](http://www.python.org/dev/peps/pep-0008) is the style guide for writing Python code. Another document, [PEP 257](https://peps.python.org/pep-0257/), covers conventions for writing docstrings. As PEP 8 does not have as many rules as PEP 257 related to documentation of classes and methods, we will briefly cover the regulations on documenting your classes:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Naming conventions\n", + "---\n", + "[Class names](http://www.python.org/dev/peps/pep-0008/#class-names) should be written in *CamelCase*, with an initial capital letter and any new word capitalized. There should be no underscores in your class names. For example, if you have a super cool class, you should name it `ASuperCoolClass`.\n", + "\n", + "[Method names](https://peps.python.org/pep-0008/#function-and-variable-names) should always have an initial lowercase letter, similar to the regulations on naming functions. Furthermore, the first argument of every class method should be the keyword `self`. For example, the method signature (the same as function signature - the function name and function arguments) of a method `move_up()` should be `move_up(self):`. Nevertheless, if a method name contains more than 1 word, then the words should be separated by underscore `_`. For instance, if you have an important method, you should name it `important_method`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Docstrings\n", + "---\n", + "A docstring is a string literal that appears at the start of classes/methods.\n", + "\n", + "By convention, docstrings begin and end with 3 quotation marks: `\"\"\"docstring\"\"\"` and should be placed right below the signature of a method or the class signature. A rule of thumb is to have 1 line explaning what a method does, followed by 1 blank line, followed by zero/one/multiple lines explaning what each of the parameter does:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Rocket():\n", + " \"\"\"Rocket simulates a rocket ship for a game,\n", + " or a physics simulation.\n", + " \"\"\"\n", + " \n", + " def __init__(self, x=0, y=0):\n", + " \"\"\"Constructor to initialise a Rocket object.\n", + " \n", + " Keyword arguments:\n", + " x -- x coordinate (default 0)\n", + " y -- y coordinate (default 0)\n", + " \"\"\"\n", + " self.x = x\n", + " self.y = y\n", + "\n", + " def move_rocket(self, x_increment=0, y_increment=1):\n", + " \"\"\"Moves the rocket according to the paremeters given.\n", + " \n", + " Keyword arguments:\n", + " x_increment -- units to move in x dimension (default 0)\n", + " y_increment -- units to move in y dimension (default 1)\n", + " \"\"\"\n", + " self.x += x_increment\n", + " self.y += y_increment\n", + " \n", + " def get_distance(self, other_rocket):\n", + " \"\"\"Calculates the distance from this rocket to another rocket\n", + " and returns that value.\n", + " \n", + " Keyword arguments:\n", + " other_rocket -- the other rocket, which distance to compare to\n", + " \"\"\"\n", + " distance = sqrt((self.x-other_rocket.x)**2+(self.y-other_rocket.y)**2)\n", + " return distance\n", + " \n", + "# Check the documentation of Rocket class\n", + "help(Rocket)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the *self* argument is not explained the docstrings, because its use is implicitly known." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## References and used resources\n", + "- http://introtopython.org/" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/book/programming/python_topics/modules_solution.ipynb b/book/programming/python_topics/modules_solution.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..d562070de864d9c37c71e8a3826e87467e6f7f2d --- /dev/null +++ b/book/programming/python_topics/modules_solution.ipynb @@ -0,0 +1,565 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Modules" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Contents\n", + "===\n", + "- [Introduction](#Introduction)\n", + "- [Modules and classes](#Modules-and-classes)\n", + " - [Storing a single class in a module](#Storing-a-single-class-in-a-module)\n", + " - [Storing multiple classes in a module](#Storing-multiple-classes-in-a-module)\n", + " - [A number of ways to import modules and classes, functions and variables](#A-number-of-ways-to-import-modules-of-classes,-functions-and-variables)\n", + " - [A module of functions and variables](#A-module-of-functions-and-variables)\n", + "- [How does module importing work](#How-does-module-importing-work)\n", + "- [Modules and PEP8](#Modules-and-PEP8)\n", + " - [Multiple imports](#Multiple-imports)\n", + " - [Ordering imports](#Ordering-imports)\n", + "- [Exercises](#Exercises)\n", + "- [Optional material](#Optional-material)\n", + " - [Command line arguments with `sys.argv`](#Command-line-arguments-with-sys.argv)\n", + " - [Parsing command line arguments with `argparse`](#Parsing-command-line-arguments-with-argparse)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Introduction\n", + "Programming tasks usually require writing several lines of code which are much better organized in a **modular** fashion, rather than in single, extremely long Jupyter notebooks (or .py script files). Modularization refers to splitting such large programming tasks into smaller, separate, and more manageable subtasks. Python scripts are modularized through **functions**, **classes**, **modules**, and **packages**.\n", + "\n", + "While you should be already familiar with *functions* and *classes*, you should think of *modules* as a `.py` files containing Python functions, classes, definitions and statements. On the other hand, a package is a set of modules, i.e., a collection of `.py` files organized in folders and subfolders. Python accesses the modules in a package by referencing the package name.\n", + "\n", + "Modularity has the added advantage of isolating your code blocks into files that can be used in any number of different programs. Futhermore, if you want to extend their functionality, you would not need to modify multiple files, but only the file they reside in.\n", + "\n", + "You have been using Pyhon modularization all along, maybe without even realizing it.\n", + "\n", + "Here is a quick example:\n", + "\n", + "```python\n", + "from matplotlib.pyplot import subplots\n", + "``` \n", + "\n", + "which follows the format:\n", + "\n", + "```python\n", + "from package_name.module_name import function_name\n", + "``` \n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Modules and classes\n", + "===\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Storing classes in a module\n", + "---\n", + "\n", + "A module is simply a file that contains one or more classes or functions, so the Shuttle and Rocket classes can also be in the same file. \n", + "\n", + "Now you can import the Rocket and the Shuttle class, and use them both in a clean uncluttered program file:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "from space import Rocket, Shuttle\n", + "\n", + "rocket = Rocket()\n", + "print(f\"The rocket is at ({rocket.x}, {rocket.y}).\")\n", + "\n", + "shuttle = Shuttle()\n", + "shuttle.move_rocket()\n", + "print(f\"The shuttle is at ({shuttle.x}, {shuttle.y}).\")\n", + "print(f\"The shuttle has completed {shuttle.flights_completed} flights.\")\n", + "\n", + "print(f\"The distance between the rocket and the shuttle is ({rocket.get_distance(shuttle)}).\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The first line tells Python to import both the *Rocket* and the *Shuttle* classes from the *rocket* module. You don't have to import every class in a module; you can pick and choose the classes you care to use, and Python will only spend time processing those particular classes." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A number of ways to import modules of classes, functions and variables\n", + "---\n", + "There are several ways to import modules, and each has its own merits. We illustrate mainly how you can import classes, however, you can import functions and variables in the exact same way:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### from *module_name* import *ClassName*\n", + "\n", + "The syntax for importing classes that was just shown:\n", + "```python\n", + "from module_name import ClassName\n", + "```\n", + "is straightforward, and is used quite commonly. It allows you to use the class names directly in your program, so you have very clean and readable code. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### import *module_name*\n", + "\n", + "Directly using the class names from a module can be a problem if the names of the classes you are importing conflict with names that have already been used in the program you are working on. For example, if a module contains a function or a class with the same name as one you have defined in your notebook. Have a look at the code cell below, where we have a Rocket class in the current cell and a Rocket class in module `space`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "from space import Rocket\n", + "\n", + "class Rocket:\n", + " def __init__(self, name):\n", + " self.name = name\n", + "\n", + "# Instatiate a class from the current file\n", + "rocket = Rocket(\"Ariance\")\n", + "print(f\"The rocket is called {rocket.name}.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Rocket defined in the cell is taking precedence before the Rocket class in module `space`. For instance, the Rocket class in the module has no field `name`. Thus, it is not possible to directly use that class. In order to mitigate this, we can make use of the dot notation:\n", + "\n", + "The general syntax for this kind of import is:\n", + "```python\n", + "import module_name\n", + "```\n", + "\n", + "After this, classes are accessed using dot notation:\n", + "```python\n", + "module_name.ClassName\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import space\n", + "\n", + "class Rocket:\n", + " def __init__(self, name):\n", + " self.name = name\n", + "\n", + "# Instatiate a class from the current file\n", + "new_rocket = Rocket(\"Ariance\")\n", + "print(f\"The rocket is called {new_rocket.name}.\")\n", + "\n", + "# Instatiate a class from module rocket\n", + "module_rocket = space.Rocket()\n", + "print(f\"\\nThe rocket is at ({module_rocket.x}, {module_rocket.y}).\")\n", + "print(f\"The distance between the same rocket is ({module_rocket.get_distance(module_rocket)}).\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This prevents some name conflicts. If you were reading carefully however, you might have noticed that the variable name *rocket* in the previous example had to be changed because it has the same name as the module itself. This is not good, because in a longer program that could mean a lot of renaming." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### import *module_name* as *local_module_name*\n", + "\n", + "There is another syntax for imports that is quite useful:\n", + "```python\n", + "import module_name as local_module_name\n", + "```\n", + "When you are importing a module into one of your projects, you are free to choose any name you want for the module in your project. So the last example could be rewritten in a way that the variable name *rocket* would not need to be changed:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "import space as space_module\n", + "\n", + "rocket = space_module.Rocket()\n", + "print(f\"The rocket is at ({rocket.x}, {rocket.y}).\")\n", + "\n", + "shuttle = space_module.Shuttle()\n", + "shuttle.move_rocket()\n", + "print(f\"The shuttle is at ({shuttle.x}, {shuttle.y}).\")\n", + "print(f\"The shuttle has completed {shuttle.flights_completed} flights.\")\n", + "\n", + "print(f\"The distance between the rocket and the shuttle is ({rocket.get_distance(shuttle)}).\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This approach is often used to shorten the name of the module, so you don't have to type a long module name before each class name that you want to use. But it is easy to shorten a name so much that you force people reading your code to scroll to the top of your file and see what the shortened name stands for. In this example, you can abbreviate space to something like:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import space as s" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Of course there are well known shortening examples, which you might have already seen:\n", + "```python\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### from *module_name* import *\n", + "There is one more import syntax that you should be aware of, but *you should probably avoid using*. This syntax imports **all of the available classes, all functions in a module and all variables in a module**. Note that functions or variables, which have leading underscore `_` in their name are excluded from this rule. Similarly to encapsulation in OOP, they are considered private:\n", + "\n", + "```python\n", + "from module_name import *\n", + "```\n", + "\n", + "This is not recommended, for a couple reasons. First of all, you may have no idea what all the names of the classes and functions in a module are. If you accidentally give one of your variables the same name as a name from the module, you will have naming conflicts. Also, you may be importing way more code into your program than you need.\n", + "\n", + "If you really need all the functions and classes from a module, just import the module and use the `module_name.ClassName` syntax in your program." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You will get a sense of how to write your imports as you read more Python code, and as you write and share some of your own code." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "How does module importing work\n", + "===\n", + "Python has built-in modules, which are accessible anywhere. Examples of those are `sys`, `math`, `random`. For a full list, check the following link: https://docs.python.org/3/py-modindex.html\n", + "\n", + "When importing modules, Python searches for modueles in the following order:\n", + "1. Looks if the module name matches any of the names in the index above. \n", + "2. Searches for a python file in the same working directory as the file, which imports it\n", + "3. Looks at PYTHONPATH - we will not cover this, but you can think of it as the default search path for modules\n", + "4. Goes over installed packages if any match\n", + "\n", + "As a result, if you have the `numpy` package installed, but you also have a file `numpy.py` in the same working directory, Python will import the local file instead of the installed package. You should avoid naming modules after standard built-in modules or standard packages such as `numpy` or `matplotlib`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Modules and PEP8\n", + "===\n", + "Modules also have their own list of rules in PEP8 Style guide:\n", + "\n", + "Multiple imports\n", + "---\n", + "In the case that we have multiple modules that we wish to import simultaneously, there are some requirements to follow. **Module imports should be done in multiple lines**. For instance, if you have the modules `os` and `sys`, it is recommended to use multiple `import` statements. In addition, modules within every group should be ordered **alphabetically**:\n", + "```python\n", + "import os\n", + "import sys\n", + "```\n", + "The wrong way to do this is to import both of the modules on the same line:\n", + "```python\n", + "import os, sys\n", + "```\n", + "\n", + "Ordering imports\n", + "---\n", + "Apart from separating every import in a new line, it is also important to group modules depending on their type. The correct order to import modules is the following:\n", + "1. Standard library imports\n", + "2. Related third party imports\n", + "3. Local application/library specific imports\n", + "\n", + "Blank lines should be placed between each group.\n", + "\n", + "Here is an example of correct order of module imports:\n", + "```python\n", + "import os\n", + "import sys\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "import rocket\n", + "import shuttle\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Exercises\n", + "===" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from jupyterquiz import display_quiz\n", + "\n", + "display_quiz(\"https://surfdrive.surf.nl/files/index.php/s/wHKH0oP3SmbZHLP/download\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Optional material\n", + "===\n", + "The material in the subsections below is considered optional. Therefore, it is not mandatory to study and if you wish, you may skip it." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Command line arguments with `sys.argv`\n", + "---\n", + "Python files can also be used as scripts, which can run speicific tasks. For example, it is possible to create a python file, which when run executes pieces of code. For example, the code below will run a python script(file) and create an image `weather.png`, which displays temperatures over a period of time:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Currently, we are reading from an old dataset, however, imagine that we were getting the data from a server, which adds more data everyday or even every hour to the dataset. Then it would be very convenient to regularly run this script and observe the changes via a graph. \n", + "\n", + "Although this looks easy to use, it is not very flexible, because we need to modify the script every time we run it if we want different periods of time or different plot file names. Hence, there is a solution to this setback in the `sys` module. More specifically by command line arguments in `sys.argv`. Command line arguments can be thought of as arguments you pass to a function.\n", + "\n", + "For example, suppose we could pick different start and end date of observations simply by passing those two values as arguments to the python file:\n", + "```bash\n", + "python weather_script.py \"01/03/2021\" \"31/05/2021\"\n", + "```\n", + "Here `weather_script.py` can be thought of as a function, which takes 2 arguments - start and end date.\n", + "\n", + "To achieve this, we the code uses as dates `sys.argv[index]` statements:\n", + "```python\n", + "import sys\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "...\n", + "\n", + "# set start and end time\n", + "start_date = pd.to_datetime(sys.argv[1],dayfirst=True)\n", + "end_date = pd.to_datetime(sys.argv[2],dayfirst=True)\n", + "\n", + "# preprocess the data\n", + "\n", + "...\n", + "\n", + "fig.savefig('weather.png')\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "! python weather_script.py \"01/03/2021\" \"31/05/2021\"\n", + "\n", + "# Display the generated image\n", + "from IPython import display\n", + "display.Image(\"./weather.png\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Parsing command line arguments with `argparse`\n", + "---\n", + "We can take command line arguements a step further via the module `argparse`, which provides even more flexibility. For instance, you can have optional arguments with default values, arguments help, argument types and much more as described here: https://docs.python.org/3/library/argparse.html. We will briefly cover the basics of this module in this section." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Remember how Git has the command `git help` and also how we pass arguments using dash (`-`)? For example, when we are making a commit, we can pass the message in multiple ways:\n", + "\n", + "```bash\n", + "git commit -m \"My first commit\"\n", + "git commit --message \"My first commit\"\n", + "```\n", + "`argparse` module adds the same quirks of getting help and also having long and short arguments. To add `argparse` to our script, we need to rewrite the way we are getting the `start_date` and `end_date` as follows:\n", + "```python\n", + "import argparse\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "...\n", + "\n", + "parser = argparse.ArgumentParser()\n", + "# set start and end time\n", + "parser.add_argument('-s', '--start', type=str, default=\"1/1/2019\", help=\"Start time\")\n", + "parser.add_argument('-e', '--end', type=str, default=\"1/1/2021\", help=\"End time\")\n", + "\n", + "args = parser.parse_args()\n", + "\n", + "start_date = pd.to_datetime(args.start,dayfirst=True)\n", + "end_date = pd.to_datetime(args.end,dayfirst=True)\n", + "\n", + "# preprocess the data\n", + "\n", + "...\n", + "\n", + "fig.savefig('weather.png')\n", + "```\n", + "Note that if an argument is not passed to a script, it has a default value, which will be used" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Exercise:** Copy `weather_script` into a new python file called `weather_script_improved` (note you need to create that file yourself). Then, modify the new file as described above to make use of `argparse`. Finally, run the code cell below to verify your code is working. The expected output is a graph, which contains temperatures only in the range 01/03/2021 - 31/05/2021. Note that we can use both long and short versions of arguments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "! python weather_script_improved.py -s \"01/03/2021\" --end \"31/05/2021\" --output \"weather.png\"\n", + "\n", + "# Display the generated image\n", + "from IPython import display\n", + "display.Image(\"./weather.png\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run the next cell to check what arguments can be passed to the script:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "! python weather_script_argparse.py --help" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It looks similar to how git shows its help, right?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "! git --help" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# References and used resources\n", + "- http://introtopython.org/\n", + "- https://aaltoscicomp.github.io/python-for-scicomp/scripts/#scripts\n", + "- https://www.learnpython.org/en/Modules_and_Packages\n", + "- https://docs.python.org/3/tutorial/modules.html" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}