{ "cells": [ { "cell_type": "markdown", "id": "d6cf097e", "metadata": {}, "source": [ "# Магические методы #" ] }, { "cell_type": "markdown", "id": "6db9e5ec", "metadata": {}, "source": [ "Привет. Добро пожаловать на четвёртый блок нашего с вами курса. В прошлом блоке вы разбирали объектно-ориентированное программирование на Python'е. А в этом блоке мы поподробнее познакомимся о том, как на самом деле всё работает внутри - как создаются объекты, как создаются классы, что происходит при выполнении определённых методов, и так далее.\n", "\n", "Давайте начнём с магических методов, с частью из которых вы уже знакомы. Итак, магический метод — это метод, определённый внутри класса, который начинается и заканчивается с двух подчёркиваний. Например, магическим методом является метод `__init__`, который отвечает за инициализацию созданного объекта. Давайте определим класс `User`, который будет переопределять магический метод `__init__`. Мы просто будем записывать полученые имя и email в атрибуты класса. Ну и можно определить, например, метод, который возвращает словарь." ] }, { "cell_type": "code", "execution_count": 15, "id": "f8cc84e5", "metadata": {}, "outputs": [], "source": [ "class User:\n", " def __init__(self, name, email):\n", " self.name = name\n", " self.email = email\n", " \n", " def get_email_data(self):\n", " return {\n", " \"name\": self.name,\n", " \"email\": self.email\n", " }" ] }, { "cell_type": "markdown", "id": "bb1f0133", "metadata": {}, "source": [ "Теперь при создании класса мы передадим атрибуты, которые запишутся в соответствующие значения нашего объекта. Ну и мы можем вызвать какой-то метод. С этим вы уже должны быть знакомы, потому что работали с классами уже довольно долго." ] }, { "cell_type": "code", "execution_count": 16, "id": "156d080e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'name': 'Jane Doe', 'email': 'janedoe@example.com'}\n" ] } ], "source": [ "jane = User(\"Jane Doe\", \"janedoe@example.com\")\n", "print(jane.get_email_data())" ] }, { "cell_type": "markdown", "id": "05b3d341", "metadata": {}, "source": [ "Ещё одним магическим методом является метод `__new__`, который отвечает за то, что происходит в момент создания объекта класса. Метод `__new__` возвращает только что созданный объект класса и может как-то переопределять поведение при создании. Например, можно создать класс `Singleton`, который гарантирует то, что объект может быть создан только один от этого класса." ] }, { "cell_type": "code", "execution_count": 14, "id": "81dbbd40", "metadata": {}, "outputs": [], "source": [ "class Singleton:\n", " instance = None\n", " \n", " def __new__(cls):\n", " if cls.instance is None:\n", " cls.instance = super().__new__(cls)\n", " \n", " return cls.instance" ] }, { "cell_type": "markdown", "id": "700b1e8e", "metadata": {}, "source": [ "Например, мы можем попытаться создать два объекта `a` и `b`, и окажется, что это один и тот же объект, потому что мы проверяем, был ли уже создан какой-то объект, и, собственно, если он уже создан, мы не будем создавать новый объект." ] }, { "cell_type": "code", "execution_count": 17, "id": "15a2b9d0", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "a = Singleton()\n", "b = Singleton()\n", "\n", "a is b" ] }, { "cell_type": "markdown", "id": "0714f906", "metadata": {}, "source": [ "Существует также метод `__del__`, который определяет поведение при удалении объекта. Однако, он работает не всегда очевидно. Он вызывается не когда мы удаляем объект оператором `del`, а когда количество ссылок на наш объект стало равно нулю и вызывается `garbage collector`. Это не всегда происходит тогда, когда мы думаем это должно произойти, поэтому переопределять его нежелательно. Будьте внимательны.\n", "\n", "Собственно, на этой лекции мы просмотрим какой-то набор магических методов, который чаще всего используется и переопределяется. Посмотрим, как они себя ведут и как писать классы, которые их используют.\n", "\n", "Одним из магических методов является метод `__str__`, который определяет поведение, например, при вызове функции `print`. Метод `__str__` должен определить какое-то человекочитаемое описание нашего класса, которое может вывести потом пользователь где-то, например, в интерфейсе. Классическим вариантом метода `__str__` может быть выведение имени и email." ] }, { "cell_type": "code", "execution_count": 12, "id": "224f2c77", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Jane Doe \n" ] } ], "source": [ "class User:\n", " def __init__(self, name, email):\n", " self.name = name\n", " self.email = email\n", " \n", " def __str__(self):\n", " return f\"{self.name} <{self.email}>\"\n", " \n", "jane = User(\"Jane Doe\", \"janedoe@example.com\")\n", "print(jane)" ] }, { "cell_type": "markdown", "id": "506ab28e", "metadata": {}, "source": [ "Обратите внимание, мы используем тот же самый класс, что и раньше, но теперь, если мы будем принтить наш объект, у нас будет не какой-то `object` типа `user`, а понятное и читаемое название нашего объекта.\n", "\n", "Ещё двумя полезными методами магическими являются методы `__hash__` и `equal`,`__eq__`, которые определяют то, как сравниваются наши объекты и что происходит при вызове функции `hash`. Магический метод `__hash__` может, например, переопределить функцию хеширования, которая используется, например, когда мы получаем ключи в словаре." ] }, { "cell_type": "code", "execution_count": 11, "id": "aff3ed05", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "True\n" ] } ], "source": [ "class User:\n", " def __init__(self, name, email):\n", " self.name = name\n", " self.email = email\n", " \n", " def __hash__(self):\n", " return hash(self.email)\n", " \n", " def __eq__(self, obj):\n", " return self.email == obj.email\n", " \n", "jane = User(\"Jane Doe\", \"jdoe@example.com\")\n", "joe = User(\"Joe Doe\", \"jdoe@example.com\")\n", "\n", "print(jane == joe)" ] }, { "cell_type": "markdown", "id": "bed51c07", "metadata": {}, "source": [ "В данном случае нашего класса `user` мы можем сказать, что при вызове функции `__hash__` мы хотим хешировать email, то есть у нас в качестве хеша берётся всегда имейл пользователя. Ну а при сравнении мы эти email просто сравниваем, при сравнении двух объектов. Таким образом, если мы создадим двух юзеров с разными именами, но одинаковыми имейлами, при вызове функции сравнения, то есть когда мы используем `==`, Python будет говорить нам, что это один и тот же объект, потому что вызывается метод `__eq__`, который сравнивает только email. Точно так же функция `__hash__` возвращает теперь одно и то же значение, потому что используется значение email, которое в данном случае одинаковое.\n", "\n", "Но если мы попробуем создать словарь, где в качестве ключа будет использоваться наш объект юзера, то у нас создастся словарь только с одним ключом, а не с двумя объектами, несмотря на то, что мы итерируемся здесь по двум объектам, потому что в качестве хеша используется одно и то же значение, и в качестве `__eq__` у нас сравниваются email." ] }, { "cell_type": "code", "execution_count": 9, "id": "8c53d626", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2122083289\n", "2122083289\n" ] } ], "source": [ "print(hash(jane))\n", "print(hash(joe))" ] }, { "cell_type": "code", "execution_count": 10, "id": "6ebd5adf", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{<__main__.User object at 0x038176E8>: 'Joe Doe'}\n" ] } ], "source": [ "user_email_map = {user: user.name for user in [jane, joe]}\n", "\n", "print(user_email_map)" ] }, { "cell_type": "markdown", "id": "5301ba65", "metadata": {}, "source": [ "Очень важными магическими методами являются методы, определяющие доступ к атрибутам. Это методы `__getattr__` и `__getattribute__`. Очень важно понимать между ними отличия, потому что очень часто происходит путаница. Итак, метод `__getattr__` определяет поведение, когда наш атрибут, который мы пытаемся получить, не найден. Таким образом, если мы обратимся к атрибуту какого-то объекта и у нас он будет не найден, у нас всегда вызовется метод `__getattr__` и мы можем определить какое-то поведение дефолтное при той ситуации, когда атрибута нет.\n", "\n", "Метод `__getattribute__` вызывается в любом случае. Когда мы обращаемся к какому-то атрибуту, у нас всегда вызывается метод `__getattribute__`, и мы можем, например, логировать какие-то обращения к атрибутам или переопределять поведение при поиске соответствующих атрибутов объекта. Например, мы можем возвращать всегда какую-то строчку и ничего не делать, как в данном случае." ] }, { "cell_type": "code", "execution_count": 8, "id": "fdfe0e1e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "nope\n", "nope\n", "nope\n" ] } ], "source": [ "class Researcher:\n", " def __getattr__(self, name):\n", " return \"Nothing found :(\"\n", " \n", " def __getattribute__(self, name):\n", " return \"nope\"\n", " \n", "obj = Researcher()\n", "\n", "print(obj.attr)\n", "print(obj.method)\n", "print(obj.DFG2H3J00KLL)" ] }, { "cell_type": "markdown", "id": "d70a8034", "metadata": {}, "source": [ "Мы определили класс и переопределили метод `__getattribute__`, который всегда возвращает строку и ничего дальше не делает. Таким образом, что бы мы не делали, как бы мы не пытались обращаться к атрибутам, даже которых ещё нет, как в данном случае, у нас всегда выведется эта строка. `__getattr__` работает немного по-другому. `__getattr__`, как я уже говорил, вызывается в том случае, если атрибут не найден." ] }, { "cell_type": "code", "execution_count": 7, "id": "2b992fcb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Looking for attr\n", "Nothing found :(\n", "Looking for method\n", "Nothing found :(\n", "Looking for DFG2H3J00KLL\n", "Nothing found :(\n" ] } ], "source": [ "class Researcher:\n", " def __getattr__(self, name):\n", " return \"Nothing found :(\"\n", " \n", " def __getattribute__(self, name):\n", " print(f\"Looking for {name}\")\n", " return object.__getattribute__(self, name)\n", " \n", "obj = Researcher()\n", "\n", "print(obj.attr)\n", "print(obj.method)\n", "print(obj.DFG2H3J00KLL)" ] }, { "cell_type": "markdown", "id": "981d22e6", "metadata": {}, "source": [ "В данном случае внутри `__getattribute__`, который вызывается всегда, мы просто логируем, что мы пытаемся найти соответствующий атрибут и продолжаем выполнение, используя класс `object`. Таким образом, если у нас пытается найтись атрибут, которого не существует, у нас вызовется `__getattr__`, что здесь и происходит. У нас ничего не найдено. Отлично." ] }, { "cell_type": "markdown", "id": "aa031adb", "metadata": {}, "source": [ "`__setattr__`, как вы могли догадаться, определяет поведение при присваивании значения к атрибуту. Например, вместо того, чтобы присвоить значение, мы можем опять же вернуть какую-то строчку и ничего не делать. В данном случае, если мы попытаемся присвоить значение атрибуту, у нас ничего не произойдёт и атрибут не создастся." ] }, { "cell_type": "code", "execution_count": 19, "id": "7b83ad70", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Not gonna set math!\n" ] } ], "source": [ "class Ignorant:\n", " def __setattr__(self, name, value):\n", " print(f\"Not gonna set {name}!\")\n", "\n", "obj = Ignorant()\n", "obj.math = True" ] }, { "cell_type": "code", "execution_count": 20, "id": "fb3f8c31", "metadata": {}, "outputs": [ { "ename": "AttributeError", "evalue": "'Ignorant' object has no attribute 'math'", "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[1;32m----> 1\u001b[1;33m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mobj\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mmath\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: 'Ignorant' object has no attribute 'math'" ] } ], "source": [ "print(obj.math)" ] }, { "cell_type": "markdown", "id": "852a889f", "metadata": {}, "source": [ "Ну а `__delattr__` управляет поведением, когда мы пытаемся удалить какой-то атрибут объекта. Мы можем не удалять, а, например, переопределить как-то поведение или добавить какую-то функциональность. Например, если мы хотим каскадно удалить какие-то объекты, связанные с нашим классом." ] }, { "cell_type": "code", "execution_count": 21, "id": "684b1176", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Goodbye attr, you were 10!\n" ] } ], "source": [ "class Polite:\n", " def __delattr__(self, name):\n", " value = getattr(self, name)\n", " print(f\"Goodbye {name}, you were {value}!\")\n", " object.__delattr__(self, name)\n", "\n", "obj = Polite()\n", "obj.attr = 10\n", "del obj.attr" ] }, { "cell_type": "markdown", "id": "dffcc1d4", "metadata": {}, "source": [ "В данном случае мы просто продолжаем удаление с помощью класса `object`, ну и как-то логируем то, что у нас происходит удаление.\n", "\n", "Ещё одним методом является метод `__call__`, который в соответствии со своим названием определяет поведение, когда наш класс вызывается. Например, с помощью метода `__call__` мы можем определить logger, который будем потом использовать в качестве декоратора. Да, декоратором может быть не только функция, но и класс." ] }, { "cell_type": "code", "execution_count": 22, "id": "f16c0418", "metadata": {}, "outputs": [], "source": [ "class Logger:\n", " def __init__(self, filename):\n", " self.filename = filename\n", " def __call__(self, func):\n", " with open(self.filename, \"w\") as f:\n", " f.write(\"Oh Danny boy...\")\n", " \n", " return func\n", "\n", "logger = Logger(\"log.txt\")\n", "\n", "@logger\n", "def completely_useless_function():\n", " pass" ] }, { "cell_type": "code", "execution_count": 24, "id": "2df4685b", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Oh Danny boy...\n" ] } ], "source": [ "completely_useless_function()\n", "\n", "with open('log.txt') as f:\n", " print(f.read())" ] }, { "cell_type": "code", "execution_count": 25, "id": "0decbc80", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Oh Danny boy...\n" ] } ], "source": [ "! type log.txt" ] }, { "cell_type": "markdown", "id": "c51d3828", "metadata": {}, "source": [ "В данном случае при инициализации класса мы запоминаем `filename`, который ему передан, и каждый раз, когда мы будем вызывать наш класс, мы будем возвращать какую-то новую функцию в соответствии с протоколом декораторов и записывать значения, записывать какую-то строчку о том, что у нас наша функция вызвана. В данном случае, мы просто определяем пустую функцию, декоратор которой записывает все её вызовы. Ну и когда мы вызовем нашу функцию, у нас в нашем файле появится строка." ] }, { "cell_type": "markdown", "id": "7c12bb61", "metadata": {}, "source": [ "Классическим примером на перегрузку операторов в других языках является перегрузка оператора сложения. В данном случае за операцию сложения в Python'е отвечает оператор `__add__`. Существуют также другие операторы вроде `__sub__`, который определяет поведение при вычитании, что логично. И мы можем определить наш класс, который будет перегружать операцию сложения." ] }, { "cell_type": "code", "execution_count": 26, "id": "6e7dab94", "metadata": {}, "outputs": [], "source": [ "import random\n", "\n", "class NoisyInt:\n", " def __init__(self, value):\n", " self.value = value\n", " def __add__(self, obj):\n", " noise = random.uniform(-1, 1)\n", " return self.value + obj.value + noise\n", "\n", "a = NoisyInt(10)\n", "b = NoisyInt(20)" ] }, { "cell_type": "markdown", "id": "7b189df8", "metadata": {}, "source": [ "В данном случае мы можем написать какой-то `NoisyInt`, который будет работать почти как `integer`, но добавлять какой-то шум, например, при сложении. Мы определим два наших Int'а со значением 10 и 20, и в операции сложение, то есть в методе `__add__` мы будем добавлять какое-то случайное значение от минус единицы до единицы. Таким образом, когда мы попытаемся сложить два наших числа, у нас выведется число в окрестности искомого, то есть у нас будет 29.5, например, вместо 30." ] }, { "cell_type": "code", "execution_count": 27, "id": "5eb86601", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "29.838970125536626\n", "29.283700512158727\n", "30.074755836157177\n" ] } ], "source": [ "for _ in range(3):\n", " print(a + b)" ] }, { "cell_type": "markdown", "id": "84513ba8", "metadata": {}, "source": [ "Это просто пример, вы можете переопределять операцию сложения так, как вам удобно, как вам подходит для вашей задачи. \n", "\n", "Предлагаю вам попробовать попрактиковаться и самостоятельно написать контейнер с помощью методов `__getitem__` и `__setitem__`." ] }, { "cell_type": "markdown", "id": "9bed86fa", "metadata": {}, "source": [ "Отлично, надеюсь, у вас получилось. Ну а в качестве примера я реализовал свой собственный класс `PascalList`, который имитирует поведение списков в Паскале. Как вы знаете, в Python'e списки нумеруются с нуля, ну а в Паскале — с единицы. Мы можем переопределить методы `__getitem__` и `__setitem__` так, чтобы они работали как в Паскале. Методы `__getitem__` и `__setitem__` определяют поведение, когда мы работаем с нашим классом с помощью квадратных скобочек, обращаясь по какому-то индексу, то есть как в случае с list'ами, со списками, или в случае со словарями." ] }, { "cell_type": "code", "execution_count": 28, "id": "805d5032", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1\n" ] } ], "source": [ "class PascalList:\n", " def __init__(self, original_list=None):\n", " self.container = original_list or []\n", " \n", " def __getitem__(self, index):\n", " return self.container[index - 1]\n", " \n", " def __setitem__(self, index, value):\n", " self.container[index - 1] = value\n", " \n", " def __str__(self):\n", " return self.container.__str__()\n", " \n", "numbers = PascalList([1, 2, 3, 4, 5])\n", "print(numbers[1])" ] }, { "cell_type": "code", "execution_count": 29, "id": "ff758ccd", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[1, 2, 3, 4, 25]\n" ] } ], "source": [ "numbers[5] = 25\n", "\n", "print(numbers)" ] }, { "cell_type": "markdown", "id": "1475fc66", "metadata": {}, "source": [ "И в данном примере мы определяем класс `PascalList`, который принимает какой-то список, запоминает его и каждый раз, когда мы пытаемся достучаться до какого-то индекса, он обращается к этому индексу минус единица. Таким образом, если мы попытаемся взять первый элемент, у нас, на самом деле, возьмётся нулевой элемент, как в Python'e. Ну и, например, мы можем создать `PascalList` от одного до пяти, и, обратившись по первому индексу, мы получим элемент один, как и сделали бы в Паскале. Ну и, например, к пятому мы можем переопределить значение в конце.\n", "\n", "Собственно, на этом всё, мы с вами обсудили какой-то набор магических методов. Более полно можно посмотреть про них в документации. Их огромное количество, и они управляют поведением класса в Python'e и тем, как работают объекты, созданные от этих классов." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.8" } }, "nbformat": 4, "nbformat_minor": 5 }