{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 4. Object-Oriented Programming I\n", "\n", "This is the first in a series of sessions concerning Object-Oriented Programming (OOP).\n", "So far we've been talking about the functional programming paradigm where everything is about functions, whereas with OOP everything is now about objects." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Why OOP?\n", "\n", "- encourages more well-structured and modular code \n", "- code is easier to reuse\n", "- more intuitive code / high readability\n", "- hide away unessesary complexity by raising the abstraction level\n", "- helps you better understand the Python programming language \n", "\n", "*Note that Python is a multi-paradigm programming language meaning you're free to choose the paradigm best suited for the task at hand or even mix them as you see fit.*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## What is meant by an `Object`\n", "The OOP programming paradigm is a modeling approach where the code is structured such that properties and behaviors are contained in individual objects.\n", "\n", "Simple examples of objects we have already been using:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "(-11, 2)\n", "False\n" ] } ], "source": [ "# 'float' object\n", "my_float = -5.5\n", "print( type(my_float) )\n", "print( my_float.as_integer_ratio() )\n", "print( my_float.is_integer() )" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Uppercase: HELLO\n", "Lowercase: hello\n" ] } ], "source": [ "# 'str' object\n", "my_string = 'Hello'\n", "print( type(my_string) )\n", "print( 'Uppercase:', my_string.upper() )\n", "print( 'Lowercase:', my_string.lower() )" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Index of 'k': 2\n" ] } ], "source": [ "# 'list' object\n", "my_list = [1, 2, 'k', 'h']\n", "print( type(my_list) )\n", "print( \"Index of 'k':\", my_list.index('k') )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So when we define a string, float, integer, boolean, list etc. in Python we're actually **initiating objects** (instances of the respective classes).\n", "Beside storing information, these objects also contains a set of **behavioral operations** (methods) relevant for the specific object. \n", "\n", "With functional programming you would have had to define all these methods as individual functions (polluting the namespace) and also name them so it's clear what type of variables they're intended for. With OOP you can integrate these into each of the specific object types, making them only accessible right where you may need them.\n", "\n", "The method names can also be simpler as it is obvious which data type they're intended for and it can be made more readable (e.g. `my_float.as_integer_ratio()`). \n", "\n", "**But most importantly, using OOP encourages you to split your problem into logical entities/objects and bind any behavioral function-like features to these objects which results in an intuitive structure that is easy to use and build upon by others.**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Basic `Class` and `Instances`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When dealing with OOP there are four terms one needs to understand:\n", "- class\n", "- instance\n", "- method\n", "- attribute\n", "\n", "A `class` is at its essence a user-defined data structure with fully customizable behaviors. It's the blueprint describing the information structure and any corresponding behavioral operations for any objects/instances created from it.\n", "The `float class` e.g. describes the data structure for being able to store a signed decimal number together with definition of any behavioral operations/method calls relevant for floats.\n", "The class itself will never contain a specific floating number!\n", "\n", "From any class an infinite number of `instances` can be initiated. While the class contains the blueprint, instances are the actual objects created from a class.\n", "'my_float', 'my_string', 'my_list' are all instances/actual objects made using the blueprint defined by the corresponding classes.\n", "So while the `float class` from above will never contain a specific floating number, `instances` of the class can be created to represent specific floating point numbers. \n", "\n", "A `method` is a function-like operation defined by and tied to the class. Methods are available to be called from any instances initiated from the class. Just like with functions, methods are also executed by having a bracket after the method name containing possible input arguments.\n", "\n", "Finally, an `attribute` refers to some information/value (a property) stored in an object. To access an attribute of an object you do not have a bracket after the attribute name as it does not need to be executed." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Naming notation\n", "Naming convention recommended by [PEP8](https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles):\n", "\n", "- `joined_lower` for functions, methods, attributes, variables\n", "\n", "- `joined_lower` or `ALL_CAPS` for constants\n", "\n", "- `CamelCase` for classes\n", "\n", "- `mixedCase` only to conform to pre-existing conventions" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Class example - Rectangle" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "How to define a new class:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class Rectangle:\n", " '''Defining a 'Rectangle' class.'''\n", " \n", " def __init__(self, height, width):\n", " '''This is a special constructor method that initializes the object.'''\n", " self.height = height\n", " self.width = width\n", " \n", " def calculate_area(self):\n", " '''Return the area of the rectangle.'''\n", " area = self.height * self.width\n", " return area" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `__init__` method is a special method that is executed when a new instance/object is initialized.\n", "\n", "The `self` is a placeholder object for the instance object not yet created during the class definition." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "# initiate rectangle instance \n", "rect = Rectangle(height=5, width=3)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "<__main__.Rectangle object at 0x000002DF2C2F6708>\n" ] } ], "source": [ "# this is an object\n", "print(rect)" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "reactangle area: 15\n" ] } ], "source": [ "# you can get the area by executing the area method\n", "print( 'reactangle area:', rect.calculate_area() )" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "height attribute: 5\n", "height attribute: 6\n" ] } ], "source": [ "# you can get (read) and set (assign) attributes in an object\n", "print( 'height attribute:', rect.height )\n", "rect.height = 6 # attributes modify an attribute\n", "print( 'height attribute:', rect.height )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice how you don't have to input the `self` argument when the method is executed from an instance.\n", "However, same result can also be obtained by executing the method directly from thee class definition, but this time giving the rectangle instance *itself* to be considered as the `self` input." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "18" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Rectangle.calculate_area(rect)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Rectangle - version 2 (lazy area calculation)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's try to store the area found so it doesn't have to be recomputed everytime someone ask for the area. This also means we should try to prevent users from changing the height and width as the area is no longer recalculated." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "class Rectangle:\n", " ''' Defining a 'Rectangle' class.'''\n", " \n", " num_corners = 4 # this is a class variable that applies to all rectangles\n", " \n", " def __init__(self, height, width):\n", " self._height = height # this is a protected attribute\n", " self._width = width # this is a protected attribute\n", " self.__area = None # this is a private attribute\n", " \n", " def get_area(self):\n", " '''Return the area of the rectangle if present, if not compute it and return it.'''\n", " # check if __area have been updated\n", " if not self.__area:\n", " self.__area = Rectangle.calculate_area(self._height, self._width)\n", " return self.__area\n", " \n", " # a static method is independent of an instance and is defined by the corresponding decorator\n", " @staticmethod\n", " def calculate_area(height, width):\n", " '''Compute and return the area of the rectangle.'''\n", " area = height * width\n", " return area" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice the *class variable* `num_corners`, which is defined *before* the `__init__` method. \n", "Recall that the `__init__` method is used to initialize each specific instance, i.e. all types of rectangles that might be created. By defining the class variable before the initialization of each unique instance, it will be assigned to all instances of a class that are created.\n", "This makes sense since all rectangles are bound to have four corners." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "# Initialize new rectangle instance\n", "rect2 = Rectangle(height=10, width=3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Java-style getters and setters are not nesessary in Python:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Number of corners: 4\n", "the area is: 30\n" ] } ], "source": [ "print('Number of corners:', rect2.num_corners) # ask for number of corners\n", "print('the area is: ', rect2.get_area()) # get area" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A static method can be used, directly from the class, without having to initialize an instance.\n", "This is useful if one wants to use a class also as a container for related functions which may even be used by the methods like here." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "the area is: 36\n" ] } ], "source": [ "print('the area is: ', Rectangle.calculate_area(height=12, width=3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Protected attibutes are not actually protected but just a way to communicate to the users that the code don't support direct changes and/or reads from this attribute." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "100" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# protected attibutes (single underscore prefix)\n", "rect2._height = 100\n", "rect2._height" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "the area is: 30\n" ] } ], "source": [ "# the area do not support this height change\n", "print('the area is: ', rect2.get_area())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Private (or hidden) attributes are a stronger way of saying \"Don't mess with me!\"" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "ename": "AttributeError", "evalue": "'Rectangle' object has no attribute '__area'", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;31m# private attibutes (dunder prefix) are more defficult to mess with\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'__area attribute:'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mrect2\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m__area\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;31mAttributeError\u001b[0m: 'Rectangle' object has no attribute '__area'" ] } ], "source": [ "# private attibutes (dunder prefix) are more defficult to mess with\n", "print('__area attribute:', rect2.__area)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As seen, they cannot be accessed directly from outside the class definition." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "However, if you really want there's always a way:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "available attributes: ['_Rectangle__area', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_height', '_width', 'calculate_area', 'get_area', 'num_corners']\n" ] } ], "source": [ "# list all available attributes\n", "print('available attributes:', dir(rect2))" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "_Rectangle__area: 999\n" ] } ], "source": [ "rect2._Rectangle__area = 999\n", "print('_Rectangle__area:', rect2._Rectangle__area)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you're not sure if an attribute is available or not the `getattr` (for \"get attribute\") allows for a default return on error:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "_Rectangle__private: my default value\n" ] } ], "source": [ "# getattr allows for providing a default return when attribute does not exist\n", "print('_Rectangle__private:', getattr(rect2, '__private', 'my default value'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Rectangle - version 3 (area as a property)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's now try to build a rectangle class that supports get(s) and set(s) of all properties: height, width and area." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "class Rectangle:\n", " '''Defining a 'Rectangle' class.'''\n", " \n", " __slots__ = ('height', 'width') # predefine allowable/possible attributes\n", " \n", " def __init__(self, height, width):\n", " self.height = height\n", " self.width = width\n", "\n", " # disguise a method as a property (getter method)\n", " @property\n", " def area(self):\n", " return Rectangle.calculate_area(self.height, self.width)\n", "\n", " # method for when someone tries to set the area (setter method)\n", " @area.setter\n", " def area(self, *args, **kwargs):\n", " print('You cannot change the area! you must change the width or height instead.')\n", " \n", " # a static method is independent of an instance and is defined by the corresponding decorator\n", " @staticmethod\n", " def calculate_area(height, width):\n", " area = height * width\n", " return area" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "# Initialize new rectangle instance\n", "rect3 = Rectangle(height=12, width=6)\n", "rect4 = Rectangle(height=20, width=4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The area property is now available as if it were an actual attribute:" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "the area is: 72\n", "the area is: 24\n" ] } ], "source": [ "print('the area is: ', rect3.area)\n", "rect3.width = 2\n", "print('the area is: ', rect3.area)" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "You cannot change the area! you must change the width or height instead.\n" ] } ], "source": [ "rect3.area = 100" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# compare rectangle areas with high readability\n", "rect3.area < rect4.area" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Best Practice\n", "\n", "**Separation of Concerns** [read more](https://en.wikipedia.org/wiki/Separation_of_concerns)\n", "Ideally, you should try to split any programming problem into a series of principal concerns for which you each design a cohesive and loosely coupled class:\n", "- Cohesive (the class content should belong together)\n", "- Loosely coupled (minimise dependency on other classes for better reuseability and testing in isolation)\n", "When done successful the program is considered modular. Modular coding requires a bit extra coding for all the interfacing but typically pays off through simplification and maintenance of code.\n", "\n", "**Four Tenets of OOP** [read more](https://www.jasoncoffin.com/)\n", "- Abstraction (model a simplified version of a complex real life entity)\n", "- Encapsulation (group together data and the operations on the data behind a well-defined interface)\n", "- Inheritance (avoid repeating common functionalities by enheriting from shared base class)\n", "- Polymorphism (reuse and extensibility)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 1\n", "Build your own Triangle class that takes a *height* and *width* argument at initialisation and that is capable of providing the user with its area. Expose the area method as a property of the object.\n", "\n", "Initiate two instances and compare their area." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 2\n", "Construct a list of 10 random triangle objects using a list comprehension and the build-in random libary. Then sort the list in-place according to area using a lamda function.\n", "\n", "#### Hint 1\n", "Seek inspiration from the Session 3 exercise." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 3-1\n", "Build a simple Counter class. When initiated it should default to zero but also allow for an optional starting value. Equip the class with both a `count_up` and a `count_down` method. Also, provide it with a `get_value` method and make the attribute containing the value private, thus encouraging users to use the get_value method." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 3-2\n", "Now you realize that you need to be able to count up by an arbitrary value.\n", "Reimplement the Counter class now also with a `count_up_by(value)` method." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# End of exercises\n", "*The cell below is for setting the style of this document. It's not part of the exercises.*" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Apply css theme to notebook\n", "from IPython.display import HTML\n", "HTML(''.format(open('../css/cowi.css').read()))" ] } ], "metadata": { "hide_input": false, "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.7.4" }, "latex_envs": { "LaTeX_envs_menu_present": true, "autoclose": false, "autocomplete": true, "bibliofile": "biblio.bib", "cite_by": "apalike", "current_citInitial": 1, "eqLabelWithNumbers": true, "eqNumInitial": 1, "hotkeys": { "equation": "Ctrl-E", "itemize": "Ctrl-I" }, "labels_anchors": false, "latex_user_defs": false, "report_style_numbering": false, "user_envs_cfg": false }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": false, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Table of Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 2 }