{ "cells": [ { "cell_type": "markdown", "id": "2612fc97-1e84-4cc1-bd55-4cc58dbd5074", "metadata": {}, "source": [ "# Introduction to object-oriented programming in Python\n", "\n", "Simeon Simeonov @ Statnett" ] }, { "cell_type": "markdown", "id": "84139fa9-733e-4871-a2f9-920f6e7c4911", "metadata": {}, "source": [ "# Goals\n", "\n", "- present the Python programming language in a different way than [https://docs.python.org](https://docs.python.org)\n", "\n", "- avoid information overload\n", "\n", "- use examples and interaction rather than documents and slides\n", "\n", "\n", "## Target audience\n", "\n", "- beginner Python programmers\n", "\n", "- analysts using Python as a tool" ] }, { "cell_type": "markdown", "id": "d6d3d0e7-c1d5-4119-940e-0f94ee7542ec", "metadata": {}, "source": [ "# What is object-oriented programming?\n", "\n", "Object-oriented programming does **not** mean using an object-oriented programming language.\n", "\n", "It is rather a programming paradigm based on the concept of object, as well as on some general principles and best practices aiming at:\n", "\n", "- improving readability\n", "\n", "- improving re-usability\n", "\n", "- improving modularity\n", "\n", "- providing foundation for a more intuitive design" ] }, { "cell_type": "markdown", "id": "ad19da5c-3c21-4130-9225-d8f7cb2a7f40", "metadata": {}, "source": [ "# General principles\n", "\n", "The following four concepts / principles are presented in most object-oriented programming books:\n", "\n", "- separate and hide \"private\" details from the outside world and / or child (inheriting) functionality (*encapsulation*) - supports *separation of concerns*\n", "\n", "- separate the interface from its implementation (*abstraction*)\n", "\n", "- inherit and extend / adapt existing functionality, through \"is-a\" relationship hierarchy (*inheritance*)\n", "\n", "- execute different code / functionality based on the object's place in the hierarchy (polymorphism)" ] }, { "cell_type": "markdown", "id": "22b2bf7e-764b-44c8-a9fd-83733319fc6e", "metadata": {}, "source": [ "# Building blocks and definitions\n", "\n", "**Note:** \"Lacking universally accepted terminology to talk about classes, I will make occasional use of Smalltalk and C++ terms.\" - The Python tutorial [https://docs.python.org](https://docs.python.org)\n", "\n", "When talking about object-oriented programming, the following building blocks are involved:\n", "\n", "- class - a blueprint / template for creating objects\n", "\n", "- object - an instance of a class that may contain its own attributes as well as references to its class' attributes\n", "\n", "- attribute - variable, property, function defined in the class and present in its instances\n", "\n", "- class variable - attribute of which a single copy exists, regardless of how many instances of the class exist\n", "\n", "- object / instance variable - attribute for which each instantiated object of the class has a separate copy, or instance\n", "\n", "- method - member function - function that is an attribute" ] }, { "cell_type": "markdown", "id": "1b8cf468-202b-47d5-ab21-25e6e96e0f46", "metadata": {}, "source": [ "# Best practices and general principles\n", "\n", "## SOLID\n", "\n", "- **S**ingle responsibility principle - a class should have only a single responsibility or a single job or a single purpose. We should strictly avoid using generalized classes where the entire implementation is given in the same class. It also states that the responsibility should be entirely encapsulated by the class, module, or function.\n", "\n", "- **O**pen/closed principle - entities like classes, modules, functions, etc. should be open for extension and the classes should be closed for modification. This means that we should be able to extend a class behavior, without modifying it.\n", "\n", "- **L**iskov’s substitution principle - derived or child classes must be substitutable for their base or parent classes. This principle ensures that any class that is the child of a parent class should be usable in place of its parent without any unexpected behavior.\n", "\n", "- **I**nterface segregation principle - This is the first principle that applies to an interfaces. It is similar to the single responsibility principle. It states that we should not force any client to implement an interface that is irrelevant to them. The main goal of this concept is to focus on avoiding fat interface and give preference to many small client-specific interfaces.\n", "\n", "- **D**ependency inversion principle - high-level modules/classes should not depend on low-level modules/classes but rather, they should depend upon abstractions. We also need to ensure that the abstraction should not depend upon details but the details should depend upon abstractions.\n", "\n", "\n", "## Other\n", "\n", "- fewer arguments - write methods in such a way that the number of arguments is as minimal as possible. We can always use the values from other objects in the same class instead of asking the user the same input multiple times.\n", "\n", "- avoid global and non-deterministic behavior - \"we need to ensure that the global behavior of the variables and objects are minimized. This can be visualized with an example of creating an animal cheetah. The color of the animal doesn’t change after its creation. So, we need to ensure that the attribute is not global and is unreachable to make sure data clashes don’t occur. Therefore, the use of global variables or objects needs to be avoided. We can use the concept of encapsulation on the data members to solve this issue.\"\n", "\n", "- reducing conditional statements - The usage of conditional statements must be reduced as much as possible. Using too many conditional statements in the program increases the complexity as well as the code cannot be reused. Instead, we can make use of interfaces and abstract classes and implement the conditional logic in different methods which can be reused and also, the single responsibility of the methods and classes is maintained. Wherever we need to reuse the same conditioning, we simply call the method where it is implemented instead of writing the code again.\n", "\n", "- autonomy principle - " ] }, { "cell_type": "markdown", "id": "9e15c843-f65b-4bd3-a536-e0217b3947f8", "metadata": {}, "source": [ "# The object-oriented world of Python\n", "\n", "Some additional concepts and definitions are introduced in Python, as well as in some other programming languages:\n", "\n", "- metaclass - a class whose instances are classes\n", "\n", "- property - attribute that provides a flexible mechanism to read, write, or compute the value of a \"private\" attribute\n", "\n", "- type - old C object-oriented term, now (in Python 3) considered to be the same as class\n", "\n", "As is true for modules, classes partake of the dynamic nature of Python: they are created at runtime, and can be modified further after creation.\n", "\n", "Each object in Python has an ID - an integer which is guaranteed to be unique and constant for this object during its lifetime.\n", "Two objects with non-overlapping lifetimes may have the same *id()* value. In *CPython* this is the address of the object in memory.\n", "\n", "Python has automatic memory management using reference counting. When an object no longer has\n", "any references, the garbage collector kicks inn and removes the object from memory.\n", "\n", "\"The Zen of Python\" (import this) is still relevant and should be followed :)\n", "\n", "\"Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention.\" - PEP484" ] }, { "cell_type": "markdown", "id": "d647b1ad-948d-495d-bb30-15761b806354", "metadata": {}, "source": [ "# Example1\n", "\n", "We will create and extend classes for representing points and vectors in 2D space" ] }, { "cell_type": "code", "execution_count": 154, "id": "391e816e-c53b-454b-9e9f-3587234f1fea", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "my_first_point = \n", "my_first_point = \n", "2\n", "2\n", "8\n" ] } ], "source": [ "class Point:\n", " \"\"\"Basic class for representing points in 2D space\"\"\"\n", "\n", " def __init__(self, x: int, y: int):\n", " \"\"\"\n", " Called after the instance has been created (by __new__()), but before\n", " it is returned to the caller. The arguments are those passed to the\n", " class constructor expression.\n", "\n", " If a base class has an __init__() method, the derived class’s\n", " __init__() method, if any, must explicitly call it to ensure proper\n", " initialization of the base class part of the instance;\n", " for example: super().__init__([args...]).\n", " \"\"\"\n", " self._x = x # _ indicates \"protected\" attribute\n", " self.__y = y # not a typo: __ indicates \"private\" attribute.\n", "\n", " def __repr__(self) -> str:\n", " \"\"\"\n", " Called by the repr() built-in function to compute the “official”\n", " string representation of an object. If at all possible, this should\n", " look like a valid Python expression that could be used to recreate an\n", " object with the same value (given an appropriate environment). If this\n", " is not possible, a string of the form <...some useful description...>\n", " should be returned. The return value must be a string object.\n", "\n", " If a class defines __repr__() but not __str__(), then __repr__() is\n", " also used when an “informal” string representation of instances of\n", " that class is required. This is typically used for debugging, so it is\n", " important that the representation is information-rich and unambiguous.\n", " \"\"\"\n", " return f\"\"\n", "\n", " def __str__(self) -> str:\n", " \"\"\"\n", " Called by str(object) and the built-in functions format() and print()\n", " to compute the “informal” or nicely printable string representation of\n", " an object. The return value must be a string object.\n", "\n", " This method differs from object.__repr__() in that there is no\n", " expectation that __str__() return a valid Python expression: a more\n", " convenient or concise representation can be used.\n", "\n", " The default implementation defined by the built-in type object calls\n", " object.__repr__().\n", " \"\"\"\n", " return f\"{self._x}:{self.__y}\"\n", "\n", " @property\n", " def x(self) -> int:\n", " \"\"\"getter property x\"\"\"\n", " return self._x\n", "\n", " @property\n", " def y(self) -> int:\n", " \"\"\"getter property y\"\"\"\n", " return self.__y\n", "\n", "\n", "my_first_point = Point(2, 8)\n", "\n", "print(f\"{my_first_point = }\")\n", "print(f\"{my_first_point = !r}\")\n", "print(my_first_point.x)\n", "\n", "# we \"should not\" be accessing private and protected attributes directly\n", "print(my_first_point._x)\n", "\n", "# print(my_first_point.__y) # will not work\n", "# Out: AttributeError: 'Point' object has no attribute '__y'\n", "\n", "# will work, but should not be used by a sane programmer:\n", "print(my_first_point._Point__y)" ] }, { "cell_type": "code", "execution_count": 155, "id": "e82feefb-025d-4cbb-a8ac-ddc3cc01269c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "my_first_vector = , end=>\n", "my_first_vector = , end=>\n", "other_vector = , end=>\n", "other_vector.length = Decimal('16.97056274847714058562026469')\n", "False\n", "False\n", "True\n" ] } ], "source": [ "import decimal\n", "\n", "class Vector:\n", " \"\"\"Basic class for representing vectors in 2D space\"\"\"\n", "\n", " def __init__(self, start: Point, end: Point):\n", " self._start = start\n", " self._end = end\n", "\n", " def __repr__(self) -> str:\n", " return f\"\"\n", "\n", " def __str__(self) -> str:\n", " return f\"{self._start} -> {self._end}\"\n", "\n", " @property\n", " def end(self) -> Point:\n", " \"\"\"getter property end\"\"\"\n", " return self._end\n", "\n", " @end.setter\n", " def end(self, value: Point):\n", " \"\"\"setter property end\"\"\"\n", " self._end = value\n", "\n", " @property\n", " def length(self) -> decimal.Decimal:\n", " \"\"\"length of a Vector property\"\"\"\n", " # length: sqrt(a^2 + b^2)\n", " return (\n", " decimal.Decimal(self._end.x - self._start.x) ** 2\n", " + decimal.Decimal(self._end.y - self._start.y) ** 2\n", " ) ** decimal.Decimal(\"0.5\")\n", "\n", " @property\n", " def start(self) -> Point:\n", " \"\"\"getter property start\"\"\"\n", " return self._start\n", "\n", " @start.setter\n", " def start(self, value: Point):\n", " \"\"\"setter property start\"\"\"\n", " self._start = value\n", "\n", "\n", "my_first_vector = Vector(Point(0, 0), Point(9, 12))\n", "print(f\"{my_first_vector = !r}\")\n", "my_first_vector.end = Point(12, 12)\n", "print(f\"{my_first_vector = !r}\")\n", "other_vector = Vector(Point(0, 0), Point(12, 12))\n", "print(f\"{other_vector = !r}\")\n", "print(f\"{other_vector.length = }\")\n", "\n", "# the objects do not evaluate as \"alike\" because:\n", "# - no comparison operator has been (re)defined\n", "# - they have different IDs\n", "print(Point(1, 2) == Point(1, 2))\n", "print(my_first_vector == other_vector)\n", "print(bool(Point(0, 0)))" ] }, { "cell_type": "code", "execution_count": 156, "id": "71a90ec6-3cc0-441e-a866-c2c2b9984559", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "my_first_vector = , end=>\n", "my_first_vector = , end=>\n", "other_vector = , end=>\n", "other_vector.length = Decimal('16.97056274847714058562026469')\n", "True\n", "True\n", "False\n" ] } ], "source": [ "# Equality and truth value testing\n", "\n", "class Point(Point): # N.B. Inheritance syntax used only for presentational purposes\n", " \n", " def __init__(self, x: int, y: int):\n", " \"\"\"\n", " Redefines the previous experimental constructor\n", " \n", " self._y is now used instead of self.__y\n", " \"\"\"\n", " self._x = x\n", " self._y = y\n", "\n", " def __bool__(self):\n", " \"\"\"\n", " Called to implement truth value testing and the built-in operation\n", " bool(); should return False or True. When this method is not defined,\n", " __len__() is called, if it is defined, and the object is considered\n", " true if its result is nonzero. If a class defines neither __len__()\n", " nor __bool__(), all its instances are considered true.\n", " \"\"\"\n", " return bool(self._x or self._y)\n", "\n", " def __eq__(self, other) -> bool:\n", " \"\"\"\n", " x==y calls x.__eq__(y),\n", "\n", " A rich comparison method may return the singleton NotImplemented if it\n", " does not implement the operation for a given pair of arguments.\n", " By convention, False and True are returned for a successful comparison.\n", " However, these methods can return any value\n", " \"\"\"\n", " return self._x == other.x and self._y == other.y\n", "\n", " # \"fix\" self.__y -> self._y\n", " def __repr__(self) -> str:\n", " return f\"\"\n", "\n", " def __str__(self) -> str:\n", " return f\"{self._x}:{self._y}\"\n", "\n", " @property\n", " def y(self) -> int:\n", " \"\"\"getter property y\"\"\"\n", " return self._y\n", "\n", "\n", "class Vector(Vector):\n", " \n", " def __eq__(self, other):\n", " return self._start == other.start and self._end == other.end\n", "\n", "\n", "my_first_vector = Vector(Point(0, 0), Point(9, 12))\n", "print(f\"{my_first_vector = !r}\")\n", "my_first_vector.end = Point(12, 12)\n", "print(f\"{my_first_vector = !r}\")\n", "other_vector = Vector(Point(0, 0), Point(12, 12))\n", "print(f\"{other_vector = !r}\")\n", "print(f\"{other_vector.length = }\")\n", "\n", "print(Point(1, 2) == Point(1, 2))\n", "print(my_first_vector == other_vector)\n", "print(bool(Point(0, 0)))" ] }, { "cell_type": "markdown", "id": "696cf992-03f0-49a4-b7ab-1adfcf1f099d", "metadata": {}, "source": [ "# Iterators\n", "\n", "Objects representing a stream of data.\n", "\n", "Repeated calls to the iterator’s `__next__()` method (or passing it to the built-in function `next()`) return successive items in the stream. \n", "When no more data are available a `StopIteration` exception is raised instead. \n", "At this point, the iterator object is exhausted and any further calls to its `__next__()` method just raise `StopIteration` again.\n", "\n", "Iterators are required to have an `__iter__()` method that returns the iterator object itself so every iterator is also iterable and may be used in most places where other iterables are accepted.\n", "\n", "One notable exception is code which attempts multiple iteration passes. A container object (such as a list) produces a fresh new iterator each time you pass it to the `iter()` function or use it in a for loop.\n", "\n", "Attempting this with an iterator will just return the same exhausted iterator object used in the previous iteration pass, making it appear like an empty container.\n", "\n", "\n", "## Generators\n", "\n", "Python’s generators provide a convenient way to implement the iterator protocol. If a container object’s `__iter__()` method is implemented as a generator, it will automatically return an iterator object (technically, a generator object) supplying the `__iter__()` and `__next__()` methods.\n", "\n", "More information about generators can be found in the documentation for the `yield` expression." ] }, { "cell_type": "code", "execution_count": 157, "id": "a4d2735d-d167-4f43-ad55-cb3d13ece2dc", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "list(vector) = [, ]\n", "1:1\n", "4:6\n", "Exhausted\n" ] } ], "source": [ "# Iterators and generators\n", "\n", "class Vector(Vector):\n", " \"\"\"3rd. edition of our wonderful class\"\"\"\n", " \n", " def __iter__(self):\n", " \"\"\"\n", " This method is called when an iterator is required for a container.\n", " This method should return a new iterator object that can iterate over\n", " all the objects in the container. For mappings, it should iterate\n", " over the keys of the container.\n", " \"\"\"\n", " for element in (self._start, self._end):\n", " yield element\n", "\n", "vector = Vector(Point(1, 1), Point(4, 6))\n", "print(f\"{list(vector) = }\")\n", "\n", "# testing / playing with the iterator manually\n", "vector_iterator = iter(vector)\n", "print(next(vector_iterator, \"Exhausted\"))\n", "print(next(vector_iterator, \"Exhausted\"))\n", "print(next(vector_iterator, \"Exhausted\"))" ] }, { "cell_type": "code", "execution_count": 158, "id": "a6ead5e6-e987-4bda-bf48-30f91877278d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "len(vector) = 7\n", "start_point in vector = True\n", "Point(0, 0) in vector = True\n" ] } ], "source": [ "# Membership and length\n", "\n", "class Vector(Vector):\n", " \n", " def __contains__(self, point: Point) -> bool:\n", " \"\"\"\n", " Called to implement membership test operators. Should return True if\n", " item is in self, False otherwise. For mapping objects, this should\n", " consider the keys of the mapping rather than the values or the\n", " key-item pairs.\n", "\n", " For objects that don’t define __contains__(), the membership test\n", " first tries iteration via __iter__(), then the old sequence iteration\n", " protocol via __getitem__(), see this section in the language\n", " reference.\n", " \"\"\"\n", " # ont of the following two strategies may be employed:\n", " # return point is self._start or point is self._end\n", " # return point == self._start or point == self._end\n", " return point == self._start or point == self._end\n", "\n", " def __len__(self) -> int:\n", " \"\"\"\n", " Called to implement the built-in function len().\n", " Should return the length of the object, an integer >= 0.\n", " Also, an object that doesn’t define a __bool__() method and whose\n", " __len__() method returns zero is considered to be false in a Boolean context.\n", "\n", " CPython implementation detail: In CPython, the length is required to be at most sys.maxsize.\n", " If the length is larger than sys.maxsize some features (such as len())\n", " may raise OverflowError. To prevent raising OverflowError by truth\n", " value testing, an object must define a __bool__() method.\n", " \"\"\"\n", " return int(self.length)\n", "\n", "start_point = Point(0, 0)\n", "vector = Vector(start_point, Point(4, 6))\n", "print(f\"{len(vector) = }\") # \"real length\" 7.21...\n", "print(f\"{start_point in vector = }\")\n", "print(f\"{Point(0, 0) in vector = }\") # False if 'is' is used instead of '==' in __contains__\n" ] }, { "cell_type": "code", "execution_count": 159, "id": "11f6a3fb-64d7-40a9-a130-27548ec4b802", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "__mul__ called\n", "vector * 3 = , end=>\n", "__rmul__ called\n", "3 * vector = , end=>\n" ] } ], "source": [ "# Emulating numeric types\n", "\n", "class Vector(Vector):\n", "\n", " def __getitem__(self, key):\n", " \"\"\"\n", " Called to implement evaluation of self[key].\n", " For sequence types, the accepted keys should be integers and slice objects.\n", "\n", " Note that the special interpretation of negative indexes (if the class\n", " wishes to emulate a sequence type) is up to the __getitem__() method.\n", " If key is of an inappropriate type, TypeError may be raised; if of a\n", " value outside the set of indexes for the sequence (after any special\n", " interpretation of negative values), IndexError should be raised.\n", " For mapping types, if key is missing (not in the container),\n", " KeyError should be raised.\n", " \"\"\"\n", " if key in (\"start\", 0):\n", " return self._start\n", " if key in (\"end\", 1):\n", " return self._end\n", " raise KeyError\n", "\n", " def __mul__(self, other):\n", " \"\"\"\n", " Implements the binary arithmetic operation: *\n", "\n", " For instance, to evaluate the expression vector1 * vector2,\n", " where vector1 is an instance of a class that has an __mul__() method,\n", " vector1.__mul__(vector2) is called.\n", "\n", " If the method does not support the operation with the supplied\n", " arguments, it should return NotImplemented.\n", " \"\"\"\n", " print(\"__mul__ called\")\n", " if not isinstance(other, int):\n", " return NotImplemented\n", " return Vector(self._start, Point(self._end.x * other, self._end.y * other))\n", "\n", " def __rmul__(self, other):\n", " \"\"\"\n", " Implements the binary arithmetic operation '*' with reflected (swapped) operands.\n", " This method is only called if the left operand does not support the corresponding\n", " operation and the operands are of different types.\n", " \n", " For instance, to evaluate the expression int1 * vector1, where vector1\n", " is an instance of a class that has an __rmul__() method,\n", " vector1.__rmul__(int1) is called if int1.__int__(vector1)\n", " returns NotImplemented.\n", " \"\"\"\n", " print(\"__rmul__ called\")\n", " return Vector(self._start, Point(self._end.x * other, self._end.y * other))\n", "\n", " # __rmul__ = __mul__\n", "\n", "vector = Vector(Point(2, 2), Point(4, 6))\n", "print(f\"{vector * 3 = }\")\n", "print(f\"{3 * vector = }\")" ] }, { "cell_type": "code", "execution_count": null, "id": "c345a69f-02da-40e7-bb37-c0511b6af096", "metadata": {}, "outputs": [], "source": [ "# Class methods and static methods\n", "\n", "class Vector(Vector):\n", " \n", " @classmethod\n", " def from_str(cls, obj_str: str) -> cls:\n", " \"\"\"\n", " " ] }, { "cell_type": "markdown", "id": "540de317-ba77-4f5c-97ba-f1c0bec071fb", "metadata": {}, "source": [ "# Additional reading\n", "\n", "- [Descriptors](https://docs.python.org/3/reference/datamodel.html#implementing-descriptors)\n", "\n", "- [Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)\n", "\n", "- `__call__`\n", "\n", "- `__getattr__`\n", "\n", "- `__hash__`\n", "\n", "\n", "... preferably everything in [Data model - https://docs.python.org/3/reference/datamodel.html](https://docs.python.org/3/reference/datamodel.html)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.10" } }, "nbformat": 4, "nbformat_minor": 5 }