{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Module 2, Practical 1\n", "\n", "The practicals of the second teaching module are a refinement of those prepared by Massimiliano Luca and Erik Dassi. Many thanks for their help.\n", "\n", "## Object Oriented Programming\n", "\n", "As seen in the lecture, Python is a multi-paradigm language and it supports in fact the **imperative/procedural** paradigm (programs are sequences of statements that change the state of the system), the **functional** paradigm (programs are seen as mathematical functions, e.g. list comprehensions), some libraries are **declarative** (they define the logic without specifying the control-flow e.g. Matplotlib) but is also **Object Oriented**. In fact **everything in Python is an object.** Moreover, as we will see, new data-types can be defined in Python.\n", "\n", "In Object Oriented Programming (OOP) objects are **data structures that contain data, which is attributes and functions to work with them**. In OOP, programs are made by a set of objects that interact with each other.\n", "\n", "OOP allows to create a distinction (*abstraction*) between the way objects are **implemented** and how objects are **used** (i.e. what we can do with them).\n", "\n", "### Classes, Methods and Objects\n", "\n", "The three key players of OOP are: **classes**, **objects** and **methods**. \n", "\n", "**Classes** (types) are an abstraction that captures: \n", "\n", "1. the internal data representation (i.e. data attributes that are called **fields**)\n", "2. the interface to interact with the class (i.e. functions that can be used to manipulate the the **methods**). \n", "\n", "\n", "**Objects** are **instances** of classes. Classes define the structure and are used to create objects. Objects are a *concrete* realization of the class, a real instance based on the footprint of the class. Programs are interactions among different objects. \n", "\n", "\n", "**Methods** are functions that can be applied to manipulate objects.\n", "\n", "Attributes and methods within an instantiated object can be accessed by using the ```. ``` (dot) operator.\n", "\n", "\n", "### Self\n", "Within a class method, we can refer to that very same instance of the object being created by using a special argument that is called ```self```. **self** is always the first argument of each method.\n", "\n", "\n", "
\n", "\n", "**Important note:** All data types seen so far are in fact classes and every time that we used a data type (e.g. defining a list, a string etc.) we were in fact instantiating an object of that type (class).\n", "\n", "
\n", "\n", "\n", "### Definition of a class\n", "\n", "The syntax to define a class is the following:\n", "```\n", "class class_name:\n", " #the initializer method\n", " def __init__(self, val1,...,valn):\n", " self.att1 = val1\n", " ...\n", " self.attn = valn\n", " \n", " #definition of a method returning something\n", " def method1(self, par1,...,parn):\n", " ...\n", " return value\n", " \n", " #definition of a method returning None\n", " def method2(self, par1,...,parn):\n", " ...\n", "``` \n", "In this case we defined a **class** ```class_name``` that has ```att1,..., attn``` (attributes) **fields** and two methods ```method1``` with parameters ```par1,...,parn``` returning a **value** ```value``` and a method ```method2``` with parameters ```par1,...,parn``` that does not return anything. \n", "\n", "The values of the fields are **initialized when the object is instantiated** at the beginning by calling the **\\_\\_init\\_\\_** method, which does not return anything. Note also the use of ```self``` in the **initializer**, which is used to specify that each field **of this instance** has to be given the corresponding value and must always be the first argument of the initializer. \n", "\n", "The object is instantiated with:\n", "```\n", "my_class = class_name(p1,...,pn)\n", "```\n", "which attributes the values ```p1,...,pn``` to the fields ```field1,...,fieldn```.\n", "\n", "\n", "**Example:**\n", "Let's define a simple class rectangle with two fields (length and width) and two methods (perimeter and area)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class class_name:\n", " #the initilizer method\n", " def __init__(self, val1,...,valn):\n", " self.att1 = val1\n", " ...\n", " self.attn = valn\n", " \n", " #definition of a method returning something\n", " def method1(self, par1,...,parn):\n", " ...\n", " return value\n", " \n", " #definition of a method returning None\n", " def method2(self, par1,...,parn):\n", " ...\n", " \n", " \n", "my_class = class_name(p1,...,pn)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "R == R1? False id R:140436373140688 id R1:140436373142480\n", "\n", "R:\n", "Length: 5 Width: 10\n", "Perimeter: 30\n", "Area:50\n", "R's diagonal: 11.18\n", "\n", "R2:\n", "Length: 72 Width: 13\n", "Perimeter: 170\n", "Area:936\n", "R's diagonal: 73.16\n" ] } ], "source": [ "import math\n", "\n", "class Rectangle:\n", " def __init__(self, l,w):\n", " self.length = l\n", " self.width = w\n", " \n", " def perimeter(self):\n", " return 2*(self.length + self.width)\n", " \n", " def area(self):\n", " return self.length * self.width\n", " \n", " def diagonal(self):\n", " return math.sqrt(self.length**2 + self.width**2)\n", "\n", "R = Rectangle(5,10)\n", "print(type(R))\n", "R1 = Rectangle(5,10)\n", "print(type(R1))\n", "print(\"R == R1? {} id R:{} id R1:{}\".format(R == R1, \n", " id(R),\n", " id(R1)))\n", "p = R.perimeter()\n", "a = R.area()\n", "d = R.diagonal()\n", "print(\"\\nR:\\nLength: {} Width: {}\\nPerimeter: {}\\nArea:{}\".format(R.length,\n", " R.width,\n", " p,\n", " a))\n", "print(\"R's diagonal: {:.2f}\".format(d))\n", "R2 = Rectangle(72,13)\n", "p = R2.perimeter()\n", "a = R2.area()\n", "d = R2.diagonal()\n", "print(\"\\nR2:\\nLength: {} Width: {}\\nPerimeter: {}\\nArea:{}\".format(R2.length,\n", " R2.width,\n", " p,\n", " a))\n", "print(\"R's diagonal: {:.2f}\".format(d))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the type of the two objects ```R``` and ```R2``` are of type ```Rectangle``` and that they have different identifiers. Instantiating objects automatically calls the initializer methods (```__init__```) passing the correct parameters to it. The dot ```.``` operator is used to access methods of the objects. Through the dot operator we can also access the fields of an object, even though this is normally not the best practice and implementing methods to ```get``` and ```set``` the values of fields are recommended. \n", "\n", "\n", "### The life-cycle of classes and objects in a program:\n", "\n", "The usual life-cycle of classes and objects is the following:\n", "\n", "1. Classes are defined with the specification of class attributes (fields) and methods;\n", "2. Objects are instantiated based on the definition of the corresponding classes;\n", "3. Objects interact with each other to implement the logic of the program and modify their state;\n", "4. Objects are destroyed (explicitly with del) or implicitly when there are no more references to them.\n", "\n", "\n", "## Encapsulation\n", "\n", "When defining classes, it is possible to hide some of the details that must be kept private to the object itself and not accessed directly. This can be done by setting **methods** and **attributes** (fields) as **private** to the object (i.e. accessible only internally to the object itself).\n", "\n", "Private attributes and methods can be defined using the ```__``` notation (i.e. the name of the attribute or method is **preceded** by two underscores ```__```). \n", "\n", "**Example** \n", "Let's see what happens to the rectangle class with encapsulation." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "ename": "AttributeError", "evalue": "'Rectangle' object has no attribute '__length'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[3], line 22\u001b[0m\n\u001b[1;32m 20\u001b[0m d \u001b[38;5;241m=\u001b[39m R\u001b[38;5;241m.\u001b[39mdiagonal()\n\u001b[1;32m 21\u001b[0m \u001b[38;5;66;03m#we might be tempted to access the encapsulated values:\u001b[39;00m\n\u001b[0;32m---> 22\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124mR:\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124mLength: \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m Width: \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124mPerimeter: \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124mArea:\u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(R\u001b[38;5;241m.\u001b[39m__length,\n\u001b[1;32m 23\u001b[0m R\u001b[38;5;241m.\u001b[39m__width,\n\u001b[1;32m 24\u001b[0m p,\n\u001b[1;32m 25\u001b[0m a))\n", "\u001b[0;31mAttributeError\u001b[0m: 'Rectangle' object has no attribute '__length'" ] } ], "source": [ "import math\n", "\n", "class Rectangle:\n", " def __init__(self, l,w):\n", " self.__length = l\n", " self.__width = w\n", " \n", " def perimeter(self):\n", " return 2*(self.__length + self.__width)\n", " \n", " def area(self):\n", " return self.__length * self.__width\n", " \n", " def diagonal(self):\n", " return math.sqrt(self.__length**2 + self.__width**2)\n", "\n", "R = Rectangle(10,6)\n", "p = R.perimeter()\n", "a = R.area()\n", "d = R.diagonal()\n", "#we might be tempted to access the encapsulated values:\n", "print(\"\\nR:\\nLength: {} Width: {}\\nPerimeter: {}\\nArea:{}\".format(R.__length,\n", " R.__width,\n", " p,\n", " a))\n", "#The following is going to fail alike.\n", "#print(\"\\nR:\\nLength: {} Width: {}\\nPerimeter: {}\\nArea:{}\".format(R.length,\n", "# R.width,\n", "# p,\n", "# a))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since the ```length``` and ```width``` attributes are private to the class, it is not possible to get access from the outside. The code above will fail with the ***object has no attribute*** error message\n", "\n", "To work around this, we can define a specific interface to access the values (these are normally called **getter** methods as they get and return the value).\n", "\n", "**Example** \n", "Let's see what happens to the rectangle class with encapsulation and getter methods." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "R:\n", "Length: 10 Width: 6\n", "Perimeter: 32\n", "Area:60\n" ] } ], "source": [ "import math\n", "\n", "class Rectangle:\n", " def __init__(self, l,w):\n", " self.__length = l\n", " self.__width = w\n", " \n", " def getLength(self):\n", " return self.__length\n", " \n", " def getWidth(self):\n", " return self.__width\n", " \n", " def perimeter(self):\n", " return 2*(self.__length + self.__width)\n", " \n", " def area(self):\n", " return self.__length * self.__width\n", " \n", " def diagonal(self):\n", " return math.sqrt(self.__length**2 + self.__width**2)\n", "\n", "R = Rectangle(10,6)\n", "p = R.perimeter()\n", "a = R.area()\n", "d = R.diagonal()\n", "print(\"\\nR:\\nLength: {} Width: {}\\nPerimeter: {}\\nArea:{}\".format(R.getLength(),\n", " R.getWidth(),\n", " p,\n", " a))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Setter** methods can be used to change the values of attributes ***after initialization***. \n", "\n", "**Example:**\n", "Let's define a Person class with the following attributes: name, surname, telephone number and address. All attributes are private. The address and phone numbers might change, so we need a method to change them. " ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Joe Page\n", "Date of Birth: 20/5/1980\n", "Phone: unknown\n", "Address: Somerset Rd.,Los Angeles, CA 90016\n", "\n", "New address: via Sommarive, Povo, Trento\n" ] } ], "source": [ "class Person:\n", " def __init__(self, name, surname, birthdate):\n", " self.__n = name\n", " self.__s = surname\n", " self.__dob = birthdate\n", " self.__a = \"unknown\"\n", " self.__t = \"unknown\"\n", " \n", " def setAddress(self, address):\n", " self.__a = address\n", " \n", " def setTelephone(self, telephone):\n", " self.__t = telephone\n", " \n", " def getName(self):\n", " return self.__n\n", " \n", " def getSurname(self):\n", " return self.__s\n", " \n", " def getDoB(self):\n", " return self.__dob\n", " \n", " def getAddress(self):\n", " return self.__a\n", " \n", " def getTel(self):\n", " return self.__t\n", "\n", "Joe = Person(\"Joe\", \"Page\", \"20/5/1980\")\n", "Joe.setAddress(\"Somerset Rd.,Los Angeles, CA 90016\")\n", "print(\"{} {}\\nDate of Birth: {}\\nPhone: {}\\nAddress: {}\".format(Joe.getName(),\n", " Joe.getSurname(),\n", " Joe.getDoB(),\n", " Joe.getTel(),\n", " Joe.getAddress()\n", " ))\n", "#Joe moves to Trento\n", "Joe.setAddress(\"via Sommarive, Povo, Trento\")\n", "print(\"\\nNew address: {}\".format(Joe.getAddress()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The ```setAddress``` method is a **setter method** that is used to change the value of the attribute ```__a```. \n", "\n", "## Special methods\n", "\n", "As seen in the lecture, it is possible to redefine some operators by redefining the corresponding **special methods** through a process called **overriding**. \n", "\n", "These are useful to define things like the sum of two objects (```__add__```), the equality (```__eq__```), which one is the smallest (```__lt__``` that is less than) or the way the object should be translated into a string (```__str__```), for example for printing purposes.\n", "\n", "More information on these special methods can be found [here](https://docs.python.org/3/reference/datamodel.html?#special-method-names).\n", "\n", "\n", "**Example**\n", "A decimal number $T$ can be expressed in base $X$ (for simplicity we will consider $X \\in [1,9]$) as: $T = aX^{N} + bX^{N-1}+...+ (n+1) X^{0}$. Such a number can be represented as two elements: $(X, (a,b,...,n+1))$ (the base and the tuple of all the values). \n", "Let's define a class MyNumber that can represent numbers in any base (from 1 to 9). The class has two attributes, the ```base``` (which is a number representing the base) and the ```values``` a tuple of numbers. Let's redefine some operators (i.e. __add__, __lt__, __str__) and implement some methods to covert these numbers into decimals.\n" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Base: 10\n", "Values:(1, 2, 3)\n", "123 = 1*100 + 2*10 + 3*1\n", "\n", "Base: 4\n", "Values:(1, 2, 3)\n", "27 = 1*16 + 2*4 + 3*1\n", "\n", "mn+mn2:150\n", "\n", "\n", "Error.Values must be lower than base\n", "Can't create n. with base 3 and values (7, 1, 1)\n", "\n", "\n", "123 < 27? False\n", "123 == 27? False\n", "123 > 27? True\n" ] } ], "source": [ "class MyNumber:\n", " \n", " def __init__(self, base, values):\n", " self.base = base\n", " warn = False\n", " for v in values:\n", " if(v >= base):\n", " print(\"Error.Values must be lower than base\")\n", " print(\"Can't create n. with base {} and values {}\".format(base,values))\n", " warn = True\n", " if(not warn):\n", " self.values = values\n", " else:\n", " self.values = None\n", " \n", " def toDecimal(self):\n", " res = 0 \n", " L = len(self.values)\n", " for i in range(L):\n", " res += self.values[i] * self.base**(L-1 - i) \n", " return res\n", "\n", " \n", " def __str__(self):\n", " return \"Base: {}\\nValues:{}\".format(self.base, self.values)\n", " \n", " def __add__(self, other):\n", " return self.toDecimal() + other.toDecimal()\n", " \n", " def __lt__(self, other):\n", " return self.toDecimal() < other.toDecimal()\n", " \n", " def toDecimalString(self):\n", " L = len(self.values)\n", " res = str(self.values[0]) + \"*\" +str(self.base ** (L-1))\n", " for i in range(1,L):\n", " res += \" + \" + str(self.values[i]) + \"*\" + str(self.base**(L-1 - i))\n", " return res \n", "\n", "mn = MyNumber(10,(1,2,3))\n", "print(mn)\n", "print(\"{} = {}\".format(mn.toDecimal(), mn.toDecimalString()))\n", "mn2 = MyNumber(4, (1,2,3))\n", "print(\"\\n{}\".format(mn2))\n", "print(\"{} = {}\".format(mn2.toDecimal(), mn2.toDecimalString()))\n", "mn3 = mn + mn2\n", "print(\"\\nmn+mn2:{}\".format(mn3))\n", "print(\"\\n\")\n", "mn4 = MyNumber(3,(7,1,1))\n", "print(\"\\n\")\n", "print(\"{} < {}? {}\".format(mn.toDecimal(),mn2.toDecimal(),mn < mn2))\n", "print(\"{} == {}? {}\".format(mn.toDecimal(),mn2.toDecimal(),mn == mn2))\n", "print(\"{} > {}? {}\".format(mn.toDecimal(),mn2.toDecimal(),mn > mn2))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Inheritance and overriding\n", "\n", "One object can **inherit** the attributes and methods from another object. This establishes a \"Is-a\" relationship between the two objects. The first object is called **subclass** of the original class. A subclass inherits all the methods and attributes of the **superclass**, but it can also redefine some methods through a process called **overriding**.\n", "\n", "The syntax to define a subclass is the following:\n", "```\n", "class MySuperClass:\n", " ...\n", " def myMethod(self,...):\n", " ...\n", " \n", "class MySubClass(MySuperClass):\n", " ...\n", " def myMethod(self,...):\n", " ...\n", "```\n", "basically, we just specify the superclass after the name of the subclass we are defining.\n", "\n", "Consider the following example:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Jade Smith is aged 5\n", "Dan Smith is aged 45\n", " - Dan's child is Jade Smith\n", " - Dan's child is John Smith\n", " - Dan's child is Tim Smith\n", "\n" ] } ], "source": [ "class Person:\n", " \n", " def __init__(self, name, surname, age):\n", " self.name = name\n", " self.surname = surname\n", " self.age = age\n", " \n", " def getInfo(self):\n", " return \"{} {} is aged {}\".format(self.name,\n", " self.surname,\n", " self.age)\n", " \n", "class Dad(Person):\n", " \n", " children = []\n", " \n", " def addChild(self,child):\n", " self.children.append(child)\n", "\n", " def getChildren(self):\n", " return self.children\n", " \n", " def getInfo(self):\n", " personalInfo = \"{} {} is aged {}\".format(self.name,\n", " self.surname,\n", " self.age)\n", " childrInfo = \"\"\n", " for son in self.getChildren():\n", " childrInfo += \" - {}'s child is {} {}\".format(\n", " self.name, son.name, son.surname) +\"\\n\"\n", " \n", " return personalInfo + \"\\n\" + childrInfo\n", " \n", "jade = Person(\"Jade\", \"Smith\",5)\n", "print(jade.getInfo())\n", "john = Person(\"John\", \"Smith\",4)\n", "tim = Person(\"Tim\", \"Smith\",1)\n", "dan = Dad(\"Dan\", \"Smith\", 45)\n", "dan.addChild(jade)\n", "dan.addChild(john)\n", "dan.addChild(tim)\n", "print(dan.getInfo())\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the object ```Dad``` (subclass) is-a ```Person``` (superclass) but has a further attribute that is the list of children, each of which are of type ```Person```. The ```getInfo``` method of the subclass ```Dad``` overrides the corresponding method of the superclass ```Person``` and prints some information on the children.\n" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Jade Smith is aged 5\n", "Dan Smith is aged 45\n", " - Dan's child is Jade Smith\n", " - Dan's child is John Smith\n", " - Dan's child is Tim Smith\n", "\n" ] } ], "source": [ "class Person:\n", " \n", " def __init__(self, name, surname, age):\n", " self.name = name\n", " self.surname = surname\n", " self.age = age\n", " \n", " def __str__(self):\n", " return \"{} {} is aged {}\".format(self.name,\n", " self.surname,\n", " self.age) \n", "\n", "class Dad(Person):\n", " \n", " children = []\n", " \n", " def addChild(self,child):\n", " self.children.append(child)\n", "\n", " def setChildren(self, children):\n", " self.children = children\n", "\n", " def getChildren(self):\n", " return self.children\n", " \n", " def __str__(self):\n", " personalInfo = \"{} {} is aged {}\".format(self.name,\n", " self.surname,\n", " self.age)\n", " childrInfo = \"\"\n", " for son in self.getChildren():\n", " childrInfo += \" - {}'s child is {} {}\".format(\n", " self.name, son.name, son.surname) +\"\\n\"\n", " \n", " return personalInfo + \"\\n\" + childrInfo\n", " \n", "jade = Person(\"Jade\", \"Smith\",5)\n", "#print(jade.getInfo())\n", "john = Person(\"John\", \"Smith\",4)\n", "tim = Person(\"Tim\", \"Smith\",1)\n", "dan = Dad(\"Dan\", \"Smith\", 45)\n", "dan.setChildren([jade,john])\n", "#dan.addChild(john)\n", "dan.addChild(tim)\n", "\n", "print(jade)\n", "print(dan)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Exercise\n", "\n", "Implement a superclass ```Polygon``` with two attributes:\n", "\n", "- shape\n", "- sides' length.\n", "\n", "Add a method to return the perimeter and a method to return the number of edges of the polygon. Implement a ```__str__``` method to print an object of the class with its shape. Then create two subclasses of poylgon: Rectangle and Triangle. For each subclass modify the initializer accordingly and also override the ```__str__``` to print the object id.\n", "\n", "\n", "
Show/Hide solution" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "I am a polygon\n", "I am a rectangle with id: 140436237036112. \n", "4\n", "8\n", "I am a triangle with id: 140436237125072. \n", "3\n", "12\n" ] } ], "source": [ "class Polygon:\n", " \n", " def __init__(self):\n", " self.shape = 'polygon'\n", " self.side_len = None\n", " \n", " def get_perimeter(self):\n", " return sum(self.side_len)\n", " \n", " def get_number_of_edges(self):\n", " return len(self.side_len)\n", "\n", " def __str__(self):\n", " return \"I am a {}\".format(self.shape)\n", "\n", "\n", "class Rectangle(Polygon):\n", " \n", " def __init__(self):\n", " self.shape = 'rectangle'\n", " self.side_len = [2, 2,2, 2]\n", "\n", " def __str__(self):\n", " return super().__str__() + \" with id: {}. \".format(id(self))\n", "\n", "class Triangle(Polygon):\n", " \n", " def __init__(self):\n", " self.shape = 'triangle'\n", " self.side_len = [4, 4, 4] \n", "\n", " def __str__(self):\n", " return super().__str__() + \" with id: {}. \".format(id(self))\n", "\n", "\n", "p = Polygon()\n", "\n", "print(p)\n", "\n", "\n", "rect = Rectangle()\n", "print(rect)\n", "\n", "print(rect.get_number_of_edges())\n", "print(rect.get_perimeter())\n", "\n", "\n", "tr = Triangle()\n", "\n", "print(tr)\n", "\n", "print(tr.get_number_of_edges())\n", "print(tr.get_perimeter())\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Lambda functions\n", "\n", "Computing in the **functional programming** paradigm is obtained by applying functions to a set of inputs. \n", "To have an idea about the differences with OOP, see this link:\n", "https://dev.to/documatic/when-to-use-functional-programming-vs-oop-122n\n", "\n", "Three mechanisms are available in python:\n", "\n", "* map : ```map(f, input_list)``` applies the function ```f``` to all the elements of ```input_list```;\n", "* filter : ```filter(f, input_list)``` filters ```input_list``` based on a function ```f``` that returns true or false for each of the input elements;\n", "* reduce : ```reduce(f, input_list)``` applies the function ```f``` to the first two elements of the input list, then it applies it to the result and to the third element and so on until the end of the list is reached and one value only is returned.\n", "\n", "Note that the ```reduce``` function is part of the ```functools``` module and needs to be imported with: \n", "```\n", "from functools import reduce\n", "```\n", "\n", "Let's see some examples of map and filter.\n", "\n", "**Example:**\n", "Let's define two functions to convert temperatures from fahrenheit to celsius and vice-versa. Let's finally use them to work out the temperatures that are above and below freezing." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "27.50°F --> -2.50°C\n", "29.00°F --> -1.67°C\n", "37.50°F --> 3.06°C\n", "12.00°F --> -11.11°C\n", "44.00°F --> 6.67°C\n", "72.00°F --> 22.22°C\n", "100.00°F --> 37.78°C\n", "\n", "\n", "Freezing temperatures:\n", "\n", "27.50°F --> -2.50°C\n", "29.00°F --> -1.67°C\n", "12.00°F --> -11.11°C\n" ] } ], "source": [ "def fahrenheit(T):\n", " return ((float(9)/5)*T + 32)\n", "def celsius(T):\n", " return (float(5)/9)*(T-32)\n", "\n", "def freezingFiltF(T):\n", " return celsius(T) < 0\n", "def freezingFiltC(T):\n", " return T < 0\n", "\n", "farTmp = (27.5, 29, 37.5,12, 44, 72,100)\n", "\n", "C = map(celsius, farTmp)\n", "print(type(C)) \n", "C = tuple(C)\n", "for i in range(len(C)):\n", " print(\"{:.2f}°F --> {:.2f}°C\".format(farTmp[i],C[i]))\n", "\n", "print(\"\\n\")\n", "print(\"Freezing temperatures:\")\n", "print(type(filter(freezingFiltF, farTmp)))\n", "ftF = tuple(filter(freezingFiltF, farTmp))\n", "ftC = tuple(filter(freezingFiltC, C))\n", "for i in range(len(ftF)):\n", " print(\"{:.2f}°F --> {:.2f}°C\".format(ftF[i],ftC[i]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that ```map``` and ```filter``` respectively return an object ```map``` and an object ```filter``` which we need to convert into lists or tuples if we want to work with the results.\n", "\n", "**Examples**\n", "\n", "Use ```reduce``` to:\n", "\n", "1. sum the elements of a list\n", "2. count how many **non-space** characters a string has\n" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "22\n" ] } ], "source": [ "from functools import reduce\n", "\n", "lst = [4,5,6,7]\n", "\n", "reducedLst = reduce(int.__add__, lst)\n", "print(reducedLst)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Word sizes: [7, 5, 3, 9, 3, 3, 7, 2, 4]\n", "Dijkstra's quote has 43 characters\n" ] } ], "source": [ "from functools import reduce\n", "\n", "myText = \"Testing shows the presence, not the absence of bugs\"\n", "words = myText.split()\n", "\n", "print(\"Word sizes: {}\".format(list(map(len, words))))\n", "cnt = reduce(int.__add__, list(map(len, words)))\n", "\n", "print(\"Dijkstra's quote has {} characters\".format(cnt))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "All the above examples required the specification of a function. We do not necessarily need to specify a function name in Python thanks to the **anonimous functions** also known as **lambda functions**.\n", "\n", "Their basic syntax is: \n", "```\n", "lambda input-parameters: expression\n", "```\n", "where the keyword ```lambda``` proceeds the comma separated list of input parameters and a colon ```:``` marks the beginning of the expression. We can anyway give a name to the lambda function with:\n", "```\n", "myfunct = lambda input-parameters: expression\n", "```\n", "\n", "Some examples of lambda functions follow:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "30\n", "Hi there!\n", "\n", "\n", "200\n", "Hi! Hi! Hi! \n", "\n", "\n", "Hi there from luca!\n", "Hi There From Luca!\n" ] } ], "source": [ "sum_lambda = lambda x, y : x+y\n", "mult_lambda = lambda x,y : x*y\n", "cap_lambda = lambda x : x.capitalize()\n", "\n", "print(sum_lambda(10,20))\n", "print(sum_lambda(\"Hi \", \"there!\"))\n", "print(\"\\n\")\n", "print(mult_lambda(10,20))\n", "print(mult_lambda(\"Hi! \", 3))\n", "print(\"\\n\")\n", "txt = \"hi there from luca!\"\n", "print(cap_lambda(txt))\n", "print(\" \".join(map(cap_lambda, txt.split())))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "More interesting uses of lambda involve sorting of tuples through the ```key``` parameter:\n", "\n", "**Example:**\n", "Let's count how many occurrences we have for each non-space character in the string \"Testing shows the presence, not the absence of bugs\". And print the least and most frequent character, as well as the first and last in alphabetic order." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[('t', 5), ('e', 8), ('s', 6), ('i', 1), ('n', 4), ('g', 2), ('h', 3), ('o', 3), ('w', 1), ('p', 1), ('r', 1), ('c', 2), ('a', 1), ('b', 2), ('f', 1), ('u', 1)]\n", "All least frequent: [('i', 1), ('w', 1), ('p', 1), ('r', 1), ('a', 1), ('f', 1), ('u', 1)]\n", "All most frequent: [('e', 8)]\n", "The first in alphabetic order: ('a', 1)\n", "The last in alphabetic order: ('w', 1)\n" ] } ], "source": [ "myText = \"testing shows the presence, not the absence of bugs\"\n", "\n", "cnts = [(x,myText.count(x)) for x in myText if x != \" \" and x != \",\"]\n", "uniqueCnts = []\n", "for e in cnts:\n", " if(e not in uniqueCnts):\n", " uniqueCnts.append(e)\n", "print(uniqueCnts)\n", "\n", "get_first = lambda x : x[0]\n", "get_second = lambda x : x[1]\n", "\n", "#Get the most and least frequent character(s).\n", "uniqueCnts.sort(key = get_second)\n", "all_least_f = list(filter(lambda x: x[1] == uniqueCnts[0][1], uniqueCnts))\n", "all_most_f = list(filter(lambda x: x[1] == uniqueCnts[-1][1], uniqueCnts))\n", "print(\"All least frequent: {}\".format(all_least_f))\n", "print(\"All most frequent: {}\".format(all_most_f))\n", "\n", "#Get the most and least frequent character.\n", "uniqueCnts.sort(key = get_first)\n", "print(\"The first in alphabetic order: {}\".format(uniqueCnts[0]))\n", "print(\"The last in alphabetic order: {}\".format(uniqueCnts[-1]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercises\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. Create a Consumer class with:\n", " - wealth as attribute (amount of money)\n", " - earn method, where earn(m) increments the consumer’s wealth by m\n", " - a spend method, where spend(m) either decreases wealth by m or returns an error if there are not enough money\n", " - a ```__str__``` will print the wealth at the current time\n", "\n", "\n", "\n", "
Show/Hide solution" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Current wealth of consumer: 100.\n", "Current wealth of consumer: 77.\n", "Current wealth of consumer: 177.\n", "Not enough money!\n" ] } ], "source": [ "class Consumer:\n", "\n", " def __init__(self, w):\n", " self.wealth = w\n", "\n", " def earn(self, m):\n", " self.wealth += m\n", "\n", " def spend(self, m):\n", " new_wealth = self.wealth - m\n", " if new_wealth < 0:\n", " print(\"Not enough money!\")\n", " else:\n", " self.wealth = new_wealth\n", "\n", " def __str__(self):\n", " return \"Current wealth of consumer: {}.\".format(self.wealth)\n", "\n", "c1 = Consumer(100) # Create instance with initial wealth 100\n", "print(c1)\n", "c1.spend(23)\n", "print(c1)\n", "\n", "\n", "c1.earn(100)\n", "print(c1)\n", "\n", "c2= Consumer(20)\n", "c2.spend(100)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "2. Define a 3D point class (```Point3D```) which contains three attributes that are the (x,y,z) coordinates in the 3D space and a string (label). Implement a ```computeDistance``` method that computes the distance between the point and another point. Remember that if $a = (x_a,y_a,z_a)$ and $b = (x_b,y_b,z_b)$, $distance(a,b) = \\sqrt{{(x_a-x_b)}^2 + {(y_a-y_b)}^2 + {(z_a-z_b)}^2}$.\n", "\n", "Given the following points:\n", "```\n", "p = Point3D(0,10,0, \"alfa\")\n", "p1 = Point3D(10,20,10, \"point\")\n", "p2 = Point3D(4,9, 10, \"other\")\n", "p3 = Point3D(8,9,11, \"zebra\")\n", "p4 = Point3D(0,10,10, \"label\")\n", "p5 = Point3D(0,10,10, \"last\")\n", "p6 = Point3D(42,102,10, \"fifth\")\n", "```\n", "a. Write some code to find out the closest pair of 3D points;\n", "\n", "b. Sort the points by distance to the origin (0,0,0) and print them in this order;\n", "\n", "c. Sort the points alphabetically by label (and print them).\n", "\n", "\n", "
Show/Hide solution" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Closest pair (d:0.0): (label:(0,10,10), last:(0,10,10))\n", "alfa:(0,10,0) distance from origin:10.00\n", "other:(4,9,10) distance from origin:14.04\n", "label:(0,10,10) distance from origin:14.14\n", "last:(0,10,10) distance from origin:14.14\n", "zebra:(8,9,11) distance from origin:16.31\n", "point:(10,20,10) distance from origin:24.49\n", "fifth:(42,102,10) distance from origin:110.76\n", "\n", "Sort by label\n", "alfa:(0,10,0) distance from origin:10.00\n", "fifth:(42,102,10) distance from origin:110.76\n", "label:(0,10,10) distance from origin:14.14\n", "last:(0,10,10) distance from origin:14.14\n", "other:(4,9,10) distance from origin:14.04\n", "point:(10,20,10) distance from origin:24.49\n", "zebra:(8,9,11) distance from origin:16.31\n" ] } ], "source": [ "#from previous edition of the class \n", "\n", "import math\n", "\n", "class Point3D:\n", " def __init__(self,x,y,z, lab):\n", " self.x = x \n", " self.y = y\n", " self.z = z\n", " self.label = lab\n", " \n", " def computeDistance(self, otherP):\n", " dx = (self.x - otherP.x)**2\n", " dy = (self.y - otherP.y)**2\n", " dz = (self.z - otherP.z)**2\n", " return math.sqrt(dx + dy + dz)\n", " \n", " def __str__(self):\n", " return \"{}:({},{},{})\".format(self.label, self.x,self.y,self.z)\n", " \n", "p = Point3D(0,10,0, \"alfa\")\n", "p1 = Point3D(10,20,10, \"point\")\n", "p2 = Point3D(4,9, 10, \"other\")\n", "p3 = Point3D(8,9,11, \"zebra\")\n", "p4 = Point3D(0,10,10, \"label\")\n", "p5 = Point3D(0,10,10, \"last\")\n", "p6 = Point3D(42,102,10, \"fifth\")\n", "\n", "allPoints = [p, p1, p2, p3, p4,p5, p6]\n", "\n", "minDist = 10000000\n", "closest = None\n", "for i in range(len(allPoints)):\n", " for j in range(i+1, len(allPoints)):\n", " d = allPoints[i].computeDistance(allPoints[j])\n", " if(d < minDist):\n", " closest = (allPoints[i],allPoints[j])\n", " minDist = d\n", "\n", "print(\"Closest pair (d:{}): ({}, {})\".format(minDist,closest[0],closest[1])) \n", "\n", "#sort the points by distance from origin.\n", "lambda_dist = lambda x : x.computeDistance(Point3D(0,0,0,\"origin\"))\n", "\n", "allPoints.sort(key = lambda_dist)\n", "for el in allPoints:\n", " print(\"{} distance from origin:{:.2f}\".format(el,lambda_dist(el)))\n", " \n", "#sort by label\n", "lambda_label = lambda x : x.label\n", "allPoints.sort(key = lambda_label)\n", "print(\"\\nSort by label\")\n", "for el in allPoints:\n", " print(\"{} distance from origin:{:.2f}\".format(el,lambda_dist(el)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
\n", "\n", "3. Write a Person class with the following attributes: name, surname and mailbox. A mailbox is a list that contains string messages sent to the person. Each message entry should be a tuple ```(name, surname, message)``` where ```name``` and ```surname``` are the name and surname of the sender of the message, while ```message``` is a string with the text of the message. Implement the following methods:\n", "\n", " a. ```getName``` : gets the name of the Person;\n", "\n", " b. ```getSurname``` : gets the surname of the Person;\n", "\n", " c. ```sendMessage``` : that has a ```Person``` and the string with the message in input and sends the message to the specified Person;\n", "\n", " d. ```checkMailbox``` : returns the number of messages in the mailbox;\n", "\n", " e. ```readMessages``` : returns all the messages in the mailbox as a list;\n", "\n", " f. ```checkMessagesFrom``` : checks and prints any messages coming from a specific Person (in input);\n", "\n", " e. ```clearMailbox``` : clears the mailbox (does not return anything);\n", "\n", "Test the class with the following conversation:\n", "```\n", "luca = Person(\"Luca\", \"Bianco\")\n", "alberto = Person(\"Alberto\", \"Montresor\")\n", "david = Person(\"David\", \"Leoni\")\n", "stranger = \"\"\n", "\n", "luca.sendMessage(alberto, \"Hi Alberto, hope things are fine.\")\n", "alberto.sendMessage(luca, \"I am fine, thanks. Yourself?\")\n", "luca.sendMessage(alberto, \"Great. Cheers. How about David?\")\n", "alberto.sendMessage(david, \"You OK?\")\n", "david.sendMessage(alberto, \"Yep. Thanks\")\n", "alberto.sendMessage(luca, \"All OK\")\n", "luca.sendMessage(stranger, \"Who are you?\")\n", "```\n", "and check the mailbox of all the Persons in ```[luca,alberto,david]```.\n", "\n", "\n", "
Show/Hide solution" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Unable to send message. Person not found\n", "\n", "Luca Bianco has 2 new message(s):\n", " - from Alberto Montresor: I am fine, thanks. Yourself?\n", " - from Alberto Montresor: All OK\n", "\n", "Alberto Montresor has 3 new message(s):\n", " - from Luca Bianco: Hi Alberto, hope things are fine.\n", " - from Luca Bianco: Great. Cheers. How about David?\n", " - from David Leoni: Yep. Thanks\n", "\n", "David Leoni has 1 new message(s):\n", " - from Alberto Montresor: You OK?\n", "\n", "\n", "Alberto Montresor --> Luca Bianco: I am fine, thanks. Yourself?\n", "Alberto Montresor --> Luca Bianco: All OK\n", "\n", "Luca Bianco does not have messages from David Leoni\n", "\n", "\n", "Luca Bianco --> Alberto Montresor: Hi Alberto, hope things are fine.\n", "Luca Bianco --> Alberto Montresor: Great. Cheers. How about David?\n", "\n", "\n", "David Leoni --> Alberto Montresor: Yep. Thanks\n", "\n", "David Leoni does not have messages from Luca Bianco\n", "\n", "\n", "Alberto Montresor --> David Leoni: You OK?\n", "\n", "Luca Bianco has 0 new message(s).\n" ] } ], "source": [ "\n", "class Person:\n", " def __init__(self, name, surname):\n", " self.__n = name\n", " self.__s = surname\n", " self.mailbox = []\n", " \n", " \n", " def getName(self):\n", " return self.__n\n", " \n", " def getSurname(self):\n", " return self.__s\n", " \n", " def sendMessage(self, otherPerson, msg):\n", " if(type(otherPerson) == Person):\n", " otherPerson.mailbox.append((self.__n, self.__s, msg))\n", " else:\n", " print(\"Unable to send message. Person not found\")\n", " \n", " def checkMailbox(self):\n", " if(len(self.mailbox) == 0 ):\n", " return 0\n", " else:\n", " return len(self.mailbox)\n", " \n", " def readMessages(self):\n", " return self.mailbox\n", " \n", " def checkMessagesFrom(self, otherPerson):\n", " if(type(otherPerson) == Person):\n", " msgs = [x for x in self.mailbox \n", " if x[0] == otherPerson.getName() \n", " and x[1] == otherPerson.getSurname()\n", " ]\n", " return msgs\n", " else:\n", " return []\n", " \n", " def clearMailbox(self):\n", " self.mailbox.clear()\n", " \n", " def __str__(self):\n", " return str(self.__n) + \" \" + str(self.__s)\n", " \n", "\n", "luca = Person(\"Luca\", \"Bianco\")\n", "alberto = Person(\"Alberto\", \"Montresor\")\n", "david = Person(\"David\", \"Leoni\")\n", "stranger = \"\"\n", "\n", "luca.sendMessage(alberto, \"Hi Alberto, hope things are fine.\")\n", "alberto.sendMessage(luca, \"I am fine, thanks. Yourself?\")\n", "luca.sendMessage(alberto, \"Great. Cheers. How about David?\")\n", "alberto.sendMessage(david, \"You OK?\")\n", "david.sendMessage(alberto, \"Yep. Thanks\")\n", "alberto.sendMessage(luca, \"All OK\")\n", "luca.sendMessage(stranger, \"Who are you?\")\n", "\n", "for p in [luca, alberto,david]:\n", " print(\"\\n{} has {} new message(s):\".format(p,p.checkMailbox()))\n", " for m in p.readMessages():\n", " print(\" - from {} {}: {}\".format(m[0],\n", " m[1],\n", " m[2]))\n", "\n", "for p in [luca, alberto, david]:\n", " for p1 in [luca, alberto, david]:\n", " if(p != p1):\n", " msgs = p.checkMessagesFrom(p1)\n", " if(len(msgs) > 0):\n", " print(\"\\n\")\n", " for m in msgs:\n", " print(\"{} --> {}: {}\".format(p1,p,m[2]))\n", " else:\n", " print(\"\\n{} does not have messages from {}\".format(p, p1))\n", "\n", "luca.clearMailbox()\n", "print(\"\\n{} has {} new message(s).\".format(luca,luca.checkMailbox()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
" ] } ], "metadata": { "celltoolbar": "Edit Metadata", "kernelspec": { "display_name": "base", "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.11.7" } }, "nbformat": 4, "nbformat_minor": 2 }