You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

674 lines
30 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{
"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 <janedoe@example.com>\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<ipython-input-20-f41a5f04e4f0>\u001b[0m in \u001b[0;36m<module>\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
}