{ "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": "be3062ad-9437-4ba2-9975-c2837b0af9dc", "metadata": {}, "source": [ "# The big picture - the course so far\n", "\n", "- Basics: About the language, the Python eco-system, types, modules, functions, scopes, decorators, string formatting\n", "\n", "- **Object-oriented programming in Python: How Python \"really works\"**\n", "\n", "- Control flow: if / for / while / try, iterators, \"tactical programming\" tips\n", "\n", "- A brief tour through Python's standard library\n", "\n", "- Code and application design and best practices: How to design your code\n" ] }, { "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", "Access modifiers in Python are not enforced by the classic 'public', 'protected' and 'private' keywords, but rather by convention.\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\n", "\n" ] }, { "cell_type": "markdown", "id": "4a57a560-bd01-48dc-99b8-2af816f9c83b", "metadata": {}, "source": [ "# Useful builtin functions\n", "\n", "\n", "- `dir(obj)` - returns a list of valid attributes for that object\n", "\n", "- `help(object)` - invokes the built-in help system. This function is intended for interactive use.\n", "\n", "- `id(object)` - returns the \"identity\" (unique number) of the *object*\n", "\n", "- `isinstance(object, classinfo)` - returns `True` if the object argument is an instance of the classinfo argument, or a subclass thereof (**N.B** don't use `type(x) == type(y)`)\n", "\n", "- `issubclass(class, classinfo)` - returns `True` if class is a subclass of classinfo. A class is considered a subclass of itself.\n", "\n", "- `super([type[, object-or-type]])` - returns a proxy object that delegates method calls to a parent or sibling class of type. This is useful for accessing inherited methods that have been overridden in a class. **More on that later**.\n", "\n", "- `type(object)` - returns the type of an object. Useful for debugging / analysis." ] }, { "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": 70, "id": "391e816e-c53b-454b-9e9f-3587234f1fea", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2:8\n", "\n", "my_first_point = 2:8\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}\") # !s is implicit\n", "print(f\"{my_first_point!r}\")\n", "print(f\"{my_first_point = !s}\")\n", "print(f\"{my_first_point = !r}\") # !r is implicit when using =\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": 71, "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", " print('str-called')\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 = }\")\n", "my_first_vector.end = Point(12, 12)\n", "print(f\"{my_first_vector = }\")\n", "other_vector = Vector(Point(0, 0), Point(12, 12))\n", "print(f\"{other_vector = }\")\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": 72, "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 = }\")\n", "my_first_vector.end = Point(12, 12)\n", "print(f\"{my_first_vector = }\")\n", "other_vector = Vector(Point(0, 0), Point(12, 12))\n", "print(f\"{other_vector = }\")\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": 73, "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": 74, "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": 75, "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": 76, "id": "c345a69f-02da-40e7-bb37-c0511b6af096", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "9\n", "Vector.from_str('1:1:5:6') = , end=>\n" ] } ], "source": [ "# Class methods and static methods\n", "\n", "class Point(Point):\n", "\n", " @staticmethod\n", " def get_manhattan_distance(point1: Point, point2: Point) -> int:\n", " \"\"\"Returns the Manhattan distance of two points\"\"\"\n", " return abs(point1.x - point2.x) + abs(point1.y - point2.y)\n", "\n", "\n", "class Vector(Vector):\n", "\n", " @classmethod\n", " def from_str(cls, obj_str: str):\n", " \"\"\"\n", " Creates an object from a string with the following format:\n", " :::\n", " \"\"\"\n", " coordinates = [int(coordinate_str) for coordinate_str in obj_str.split(\":\")]\n", " return cls(\n", " Point(coordinates[0], coordinates[1]),\n", " Point(coordinates[2], coordinates[3]),\n", " )\n", "\n", "print(Point.get_manhattan_distance(Point(1, 1), Point(5, 6)))\n", "print(f\"{Vector.from_str('1:1:5:6') = }\")" ] }, { "cell_type": "markdown", "id": "61c1e910-9398-455d-b0c2-cda6c6870224", "metadata": {}, "source": [ "# Inheritance in Python\n", "\n", "In object-oriented programming, inheritance is the mechanism of basing an object or class upon another object (prototype-based inheritance) or class (class-based inheritance), retaining similar implementation. Also defined as deriving new classes (sub classes) from existing ones such as super class or base class and then forming them into a hierarchy of classes.\n", "\n", "In most class-based object-oriented languages, an object created through inheritance, a \"child object\", acquires all the properties and behaviors of the \"parent object\" , with the exception of: constructors, destructor, overloaded operators and friend functions of the base class. \n", "Inheritance allows programmers to create classes that are built upon existing classes, to specify a new implementation while maintaining the same behaviors (realizing an interface), to reuse code and to independently extend original software via public classes and interfaces.\n", "\n", "One can simply view inheritance as a tool for code reuse.\n", "\n", "Inheritance is not the only mechanism for extending classes and functionality in general." ] }, { "cell_type": "markdown", "id": "eac035eb-7757-485a-be6c-c450bf9ee748", "metadata": {}, "source": [ "# The `super([type[, object-or-type])` function\n", "\n", "A common misconception is that `super()` returns an object of the parent class.\n", "\n", "There are two typical use cases for `super()`:\n", "\n", "In a class hierarchy with **single inheritance**, `super()` can be used to refer to parent classes without naming them explicitly, thus making the code more maintainable.\n", "This use closely parallels the use of super in other programming languages.\n", "\n", "The second use case is to support cooperative multiple inheritance in a dynamic execution environment. \n", "This use case is unique to Python and is not found in statically compiled languages or languages that only support single inheritance. \n", "This makes it possible to implement \"diamond diagrams\" where multiple base classes implement the same method. \n", "Good design dictates that such implementations have the same calling signature in every case (because the order of calls is determined at runtime, because that order adapts to changes in the class hierarchy, and because that order can include sibling classes that are unknown prior to runtime)." ] }, { "cell_type": "markdown", "id": "d1d17f26-de6e-4510-b0f0-3ae02761ae24", "metadata": {}, "source": [ "# Example 2\n", "\n", "We will create a small and incomplete Animal class hierarchy" ] }, { "cell_type": "code", "execution_count": 77, "id": "44d47e1c-1a06-4577-8db6-6ec78ce5cef8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Moo\n", "Help on class TigerShark in module __main__:\n", "\n", "class TigerShark(Fish)\n", " | TigerShark(weight: int, alive: bool = True, **kwargs)\n", " | \n", " | Base class for all tiger sharks\n", " | \n", " | Method resolution order:\n", " | TigerShark\n", " | Fish\n", " | Animal\n", " | builtins.object\n", " | \n", " | Methods defined here:\n", " | \n", " | __init__(self, weight: int, alive: bool = True, **kwargs)\n", " | Initialize self. See help(type(self)) for accurate signature.\n", " | \n", " | ----------------------------------------------------------------------\n", " | Data descriptors inherited from Animal:\n", " | \n", " | __dict__\n", " | dictionary for instance variables (if defined)\n", " | \n", " | __weakref__\n", " | list of weak references to the object (if defined)\n", " | \n", " | alive\n", " | getter property alive\n", " | \n", " | weight\n", " | getter property weight\n", "\n", "(, , , )\n" ] } ], "source": [ "# This example demonstrates a simple and incomplete animal class hierarchy\n", "\n", "# N.B. The use of super() in this example is not optimal\n", "\n", "\n", "class Animal:\n", " \"\"\"Base class for all animals, inheriting only from 'builtins.object'\"\"\"\n", "\n", " def __init__(self, weight: int, alive: bool = True, **kwargs):\n", " self._weight = weight\n", " self._alive = alive\n", "\n", " @property\n", " def weight(self) -> int:\n", " \"\"\"getter property weight\"\"\"\n", " return self._weight\n", "\n", " @weight.setter\n", " def weight(self, value: int):\n", " \"\"\"setter property weight\"\"\"\n", " self._weight = value\n", "\n", " @property\n", " def alive(self) -> bool:\n", " \"\"\"getter property alive\"\"\"\n", " return self._alive\n", "\n", " @alive.setter\n", " def alive(self, value: bool):\n", " \"\"\"setter property alive\"\"\"\n", " self._alive = value\n", "\n", "\n", "class Mammal(Animal):\n", " \"\"\"Base class for all mammals\"\"\"\n", "\n", " def __init__(self, weight: int, tooth_replacement: bool, alive: bool = True, **kwargs):\n", " super().__init__(weight, alive, **kwargs)\n", " self._tooth_replacement = tooth_replacement\n", "\n", "\n", "class Fish(Animal):\n", " \"\"\"Base class for all fish\"\"\"\n", "\n", " def __init__(self, weight: int, gill_openings: int, alive: bool = True, **kwargs):\n", " super().__init__(weight, alive, **kwargs)\n", " self._gill_openings = gill_openings\n", "\n", "\n", "class Tiger(Mammal):\n", " \"\"\"Base class for all tigers\"\"\"\n", "\n", " def __init__(self, weight: int, conservation_status: str, alive: bool = True, **kwargs):\n", " super().__init__(\n", " weight=weight, tooth_replacement=True, alive=alive, **kwargs\n", " )\n", " self_conservation_status = conservation_status\n", "\n", "\n", "class Cow(Mammal):\n", " \"\"\"Base class for all cows\"\"\"\n", "\n", " def __init__(self, weight: int, alive: bool = True, **kwargs):\n", " super().__init__(\n", " weight=weight, tooth_replacement=True, alive=alive, **kwargs\n", " )\n", "\n", " def make_a_sound(self):\n", " if self._alive:\n", " print(\"Moo\" if self._weight > 60 else \"Mee\")\n", "\n", "\n", "class TigerShark(Fish):\n", " \"\"\"Base class for all tiger sharks\"\"\"\n", "\n", " def __init__(self, weight: int, alive: bool = True, **kwargs):\n", " super().__init__(weight=weight, gill_openings=3, alive=alive, **kwargs)\n", "\n", "tiger1 = Tiger(200, \"Endangered\")\n", "tiger2 = Tiger(183, \"Critically endangered\")\n", "cow1 = Cow(350)\n", "cow1.make_a_sound()\n", "tiger_shark1 = TigerShark(80)\n", "help(TigerShark)\n", "print(TigerShark.__mro__) # more elegant will be to use the inspect module: inspect.getmro(TigerShark)" ] }, { "cell_type": "markdown", "id": "cc950a82-40a0-4bdb-89a0-ea6e2ebe5861", "metadata": {}, "source": [ "# Challenges\n", "\n", "At least two obvious problems arise:\n", "\n", "- how do we handle the growing amount of params we have to send to `super()`?\n", "\n", "- how to insert functionality that does not necessarily follow the structure of our hierarchy. F.i predator functionality?\n", "\n", "\n", "## Possible solutions\n", "\n", "- use `**kwargs` to collect and transmit even 'unwanted' params\n", "\n", "- use multiple inheritance and mixins\n", "\n", "\n", "## Mixins\n", "\n", "A mixin is a class that provides method implementations for reuse by multiple related child classes. However, the inheritance is not implying an is-a relationship.\n", "\n", "A mixin doesn't define a new type. Therefore, it is not intended for direction instantiation.\n", "\n", "A mixin bundles a set of methods for reuse. Each mixin should have a single specific behavior, implementing closely related methods.\n", "\n", "Typically, a child class uses multiple inheritance to combine the mixin classes with a parent class.\n", "\n", "Since Python doesn’t define a formal way to define mixin classes, it’s a good practice to name mixin classes with the suffix *Mixin*.\n", "\n", "Usually, a mixin class should not have any attributes that overlap with the established hierarchy.\n", "\n", "A mixin class is like an interface in Java, hence the phrase 'implements X functionality' should fit in the mixin description.\n" ] }, { "cell_type": "code", "execution_count": 78, "id": "e6076925-75f0-456c-aed5-7db0a24a67a1", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'_weight': 80, '_alive': True, '_gill_openings': 3}\n", "{\"_weight\": 80, \"_alive\": true, \"_gill_openings\": 3}\n", "{\"_weight\": 80, \"_alive\": true, \"_gill_openings\": 3}\n" ] } ], "source": [ "# Multiple inheritance and mixins\n", "import json\n", "\n", "class DictMixin:\n", " \"\"\"Implements a dictionary conversion functionality\"\"\"\n", "\n", " def to_dict(self):\n", " return self._traverse_dict(self.__dict__)\n", "\n", " def _traverse_dict(self, attributes):\n", " result = {}\n", " for key, value in attributes.items():\n", " result[key] = self._traverse(key, value)\n", " return result\n", "\n", " def _traverse(self, key, value):\n", " if isinstance(value, DictMixin):\n", " return value.to_dict()\n", " elif isinstance(value, dict):\n", " return self._traverse_dict(value)\n", " elif isinstance(value, list):\n", " return [self._traverse(key, v) for v in value]\n", " elif hasattr(value, '__dict__'):\n", " return self._traverse_dict(value.__dict__)\n", " else:\n", " return value\n", "\n", "\n", "class JSONMixin:\n", " \"\"\"Implements a JSON conversion functionality\"\"\"\n", "\n", " def to_json(self):\n", " return json.dumps(self._traverse_dict(self.__dict__))\n", "\n", "\n", "class AnotherJSONMixin(DictMixin, JSONMixin):\n", " \"\"\"Another JSON mixin that implements JSON conversion\"\"\"\n", "\n", "\n", "class DictableTigerShark(TigerShark, DictMixin):\n", " \"\"\"\n", " A tiger shark class that provides a dictionary conversion functionality\n", " \"\"\"\n", "\n", "\n", "class JSONConvertableTigerShark(TigerShark, DictMixin, JSONMixin):\n", " \"\"\"A tiger shark class that provides JSON conversion functionality\"\"\"\n", "\n", "\n", "class AnotherJSONConvertableTigerShark(TigerShark, AnotherJSONMixin):\n", " \"\"\"\n", " Another tiger shark class that provides JSON conversion functionality\n", " \"\"\"\n", "\n", "\n", "dict_tiger_shark = DictableTigerShark(80)\n", "json_tiger_shark = JSONConvertableTigerShark(80)\n", "another_json_tiger_shark = AnotherJSONConvertableTigerShark(80)\n", "print(f\"{dict_tiger_shark.to_dict()}\")\n", "print(f\"{json_tiger_shark.to_json()}\")\n", "print(f\"{another_json_tiger_shark.to_json()}\")" ] }, { "cell_type": "markdown", "id": "7624fb8e-f1dd-4ad6-a1c2-f01cca6b1f2a", "metadata": {}, "source": [ "# Multiple inheritance and `super`\n", "\n", "A common misconception is that `super()` returns an object of the parent class. (repeated)\n", "\n", "Whenever you use `super()` you **can not know in advance** where / what it is going to call.\n", "\n", "In other words, you don't get to choose who your `super()` is going to call, your children get to choose. \n", "Single inheritance is just a special case and simplification of the general rule.\n", "\n", "Python>=2.3 uses the [C3 Method Resolution Order (MRO)](https://www.python.org/download/releases/2.3/mro/), replacing the old *depth first and then left to right* in Python<=2.2.\n", "\n", "The *MRO* is the set of rules that construct the linearization. In the Python literature, the idiom \"the MRO of C\" is also used as a synonymous for the linearization of the class C.\n", "\n", "MRO is monotonic when the following is true: if C1 precedes C2 in the linearization of C, then C1 precedes C2 in the linearization of any subclass of C.\n", "\n", "Not all classes admit a linearization. There are cases, in complicated hierarchies, where it is not possible to derive a class such that its linearization respects all the desired properties. \n", "`TypeError: Cannot create a consistent method resolution order (MRO) for bases X, Y` is raised then.\n", "\n" ] }, { "cell_type": "code", "execution_count": 79, "id": "cf79617a-95d2-48f2-be66-fa1fa34e4e04", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Simeon's MRO: (, , , , , , , , , )\n" ] } ], "source": [ "class Adam:\n", " pass\n", "\n", "\n", "class Eve:\n", " pass\n", "\n", "\n", "class Grandmother1(Adam, Eve):\n", " pass\n", "\n", "\n", "class Grandmother2(Adam, Eve):\n", " pass\n", "\n", "\n", "class Grandfather1(Adam, Eve):\n", " pass\n", "\n", "\n", "class Grandfather2(Adam, Eve):\n", " pass\n", "\n", "\n", "class Mother(Grandmother1, Grandfather1):\n", " pass\n", "\n", "\n", "class Father(Grandmother2, Grandfather2):\n", " pass\n", "\n", "\n", "class Simeon(Mother, Father):\n", " pass\n", "\n", "\n", "print(f\"Simeon's MRO: {Simeon.__mro__}\")" ] }, { "cell_type": "code", "execution_count": 80, "id": "95a1d0cc-0006-4ddc-b08d-d0b8b02acfba", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Getting data using a proprietary driver, from a proprietary high tech & mega secured cluster while being charged 100USD per MB\n", "Extracting production plans from data\n", "some magic is happening here...\n", "{}\n" ] } ], "source": [ "# Real multiple inheritance\n", "\n", "# Inspired by Raymond Hettinger - \"Super considered super!\" - PyCon 2015\n", "import json\n", "\n", "\n", "class ConnectionManager:\n", " \"\"\"Handles connection and extraction of raw data\"\"\"\n", "\n", " def get_data(self, *args, **kwargs):\n", " \"\"\"difficult and expensive to test functionality here\"\"\"\n", " print(\n", " \"Getting data using a proprietary driver, from a proprietary high \"\n", " \"tech & mega secured cluster while being charged 100USD per MB\"\n", " )\n", " \n", " def extract_production_plans(self, *args, **kwargs):\n", " print(\"Extracting production plans from data\")\n", "\n", "\n", "class CoolTSPreprocessor(ConnectionManager):\n", " \"\"\"Produces JSON content for production plans\"\"\"\n", " \n", " def to_json(self, **kwargs):\n", " # super().get_data(**kwargs)\n", " self.get_data(**kwargs)\n", " prod_plans = self.extract_production_plans(**kwargs)\n", " return json.dumps(self.__do_some_magic(prod_plans), indent=kwargs.get(\"indent\", 2))\n", " \n", " def __do_some_magic(self, prod_plans):\n", " processed_plans = {}\n", " print(\"some magic is happening here...\")\n", " return processed_plans\n", "\n", "\n", "class MockConnectionManager(ConnectionManager):\n", " \"\"\"\n", " Mock connection manager using a static data without really connecting to a real cluster\n", " \"\"\"\n", "\n", " def get_data(self, *args, **kwargs):\n", " print(\"Using mock data\")\n", "\n", "\n", "cool_ts_proprocessor = CoolTSPreprocessor()\n", "print(cool_ts_proprocessor.to_json(indent=4))" ] }, { "cell_type": "code", "execution_count": 81, "id": "38066107-0190-4383-ba14-a4e9cd454a9c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Using mock data\n", "Extracting production plans from data\n", "some magic is happening here...\n", "(, , , , )\n" ] } ], "source": [ "class MockedButStillCoolTSPreprocessor(CoolTSPreprocessor, MockConnectionManager):\n", " pass\n", "\n", "mocked_but_still_cool_ts_preprocessor = MockedButStillCoolTSPreprocessor()\n", "mocked_but_still_cool_ts_preprocessor.to_json(indent=4)\n", "print(MockedButStillCoolTSPreprocessor.__mro__)" ] }, { "cell_type": "markdown", "id": "540de317-ba77-4f5c-97ba-f1c0bec071fb", "metadata": {}, "source": [ "# Additional reading\n", "\n", "- Preferably everything in [Data model - https://docs.python.org/3/reference/datamodel.html](https://docs.python.org/3/reference/datamodel.html)\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", "\n", "- [Raymond Hettinger - Super considered super! - PyCon 2015](https://www.youtube.com/watch?v=EiOglTERPEo)\n", "\n", "- [C3 Method Resolution Order (MRO)](https://www.python.org/download/releases/2.3/mro/) - for the real enthusiasts" ] } ], "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 }