From 13f9b4e5f9f0d170770998ad5e4299d1836b1bfa Mon Sep 17 00:00:00 2001 From: Robert Lanzafame Date: Fri, 11 Oct 2024 15:26:44 +0200 Subject: [PATCH] move classes files to learn-programming from learn-python --- book/year2/classes.ipynb | 970 ++++++++++++++++++++++++++++++ book/year2/classes_solution.ipynb | 969 +++++++++++++++++++++++++++++ 2 files changed, 1939 insertions(+) create mode 100644 book/year2/classes.ipynb create mode 100644 book/year2/classes_solution.ipynb diff --git a/book/year2/classes.ipynb b/book/year2/classes.ipynb new file mode 100644 index 0000000..83082d4 --- /dev/null +++ b/book/year2/classes.ipynb @@ -0,0 +1,970 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Classes and Object-Oriented Programming in Python" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Object-Oriented Programming (OOP) is a way of programming that organizes code into reusable blocks of code called *classes*, which bundle data (*attributes*) and behaviors (*methods*) together. 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", + "\n", + "These concepts may seem abstract for now, but don't be concerned because you have already been using them quite a lot: everything you do in Python uses objects! This should become obvious as you go through this page, after which you should be able to:\n", + "\n", + "1. Understand the fundamental concepts of classes and object-oriented programming (OOP) in Python.\n", + "2. Comprehend the key principles of encapsulation, inheritance, and polymorphism in OOP.\n", + "3. Define and create classes in Python.\n", + "4. Create objects from classes and understand the relationship between classes and objects.\n", + "\n", + "Click {fa}`rocket` --> {guilabel}`Live Code` in the top-right corner of this screen and the live code feature will be enabled.\n", + "\n", + "**There is an interactive exercise at the end of this notebook.**\n", + "\n", + "```{admonition} Note to MUDE Students\n", + "\n", + "This page contains a lot of information that is useful for improving your programming skills, however you are not required to learn all of it. You should be able to cover the first two objectives listed above, which will enable you to better understand and use the classes that are everywhere in Python packages. For example, the class `rv_continuous` in `scipy.stats`, which is used for defining probability distributions.\n", + "\n", + "Use the this page to complete the relevant programming assignments and to gauge how much of it you should read and understand. \n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\"header\"\n" + ] + }, + { + "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", + "\"Rocket\"\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", + "\"Fleet" + ] + }, + { + "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": [ + "\"Distance" + ] + }, + { + "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", + "\"Shuttle" + ] + }, + { + "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", + "\"ImprovedShuttle" + ] + }, + { + "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\n", + "\n", + "```{admonition} Exercise\n", + "\n", + "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.\n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "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": [ + "\"\"\"Use this code cell to type your answer.\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "````{admonition} Solution\n", + ":class: tip\n", + "\n", + "**If you have done the exercise correctly, the following output should be printed when you run the cell above.**\n", + "\n", + "```\n", + "Showing how class Student is defined:\n", + "New GPA grade of student is 7.2.\n", + "\n", + "Showing how class Teacher is defined:\n", + "Hello, my name is Joe, I am 42 and I am from Germany.\n", + "The classroom consists of the following students:\n", + "Hello, my name is Mike, I am 22 and I am from Italy.\n", + "My GPA grade is 7.2 and I started studying in year 2021.\n", + "```\n", + "````" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Revisiting PEP 8\n", + "\n", + "If you recall from the [Golden Rules](https://mude.citg.tudelft.nl/book/programming/golden_rules.html), [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": [ + "\n", + "\n", + "" + ] + } + ], + "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/year2/classes_solution.ipynb b/book/year2/classes_solution.ipynb new file mode 100644 index 0000000..edd565b --- /dev/null +++ b/book/year2/classes_solution.ipynb @@ -0,0 +1,969 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Classes and Object-Oriented Programming in Python\n", + "\n", + "```{note}\n", + "This page was not shared with MUDE students in 2023-2024 (year 2).\n", + "\n", + "It may have been a new page, or a modified page from year 1.\n", + "\n", + "There may be pages in year 1 and year 2 that are nearly identical, or have significant modifications. Modifications usually were to reformat the notebooks to fit in a jupyter book framework better.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\"header\"" + ] + }, + { + "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", + "\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", + "\"Rocket\"\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", + "\"Fleet" + ] + }, + { + "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": [ + "\"Distance" + ] + }, + { + "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", + "\"Shuttle" + ] + }, + { + "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", + "\"ImprovedShuttle" + ] + }, + { + "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 +}