Привет. Добро пожаловать на четвёртую неделю нашего с вами курса. На прошлой неделе вы разбирали объектно-ориентированное программирование на Python'е. А на этой неделе мы поподробнее познакомимся о том, как на самом деле всё работает внутри - как создаются объекты, как создаются классы, что происходит при выполнении определённых методов, и так далее. Давайте начнём с магических методов, с частью из которых вы уже знакомы. Итак, магический метод — это метод, определённый внутри класса, который начинается и заканчивается с двух подчёркиваний. Например, магическим методом является метод _init_, который отвечает за инициализацию созданного объекта. Давайте определим класс User, который будет переопределять магический метод _init_. Мы просто будем записывать полученые имя и имейл в атрибуты класса. Ну и можно определить, например, метод, который возвращает словарь. Теперь при создании класса мы передадим атрибуты, которые запишутся в соответствующие значения нашего объекта. Ну и мы можем вызвать какой-то метод. С этим вы уже должны быть знакомы, потому что работали с классами уже довольно долго. Ещё одним магическим методом является метод _new_, который отвечает за то, что происходит в момент создания объекта класса. Метод _new_ возвращает только что созданный объект класса и может как-то переопределять поведение при создании. Например, можно создать класс Singleton, который гарантирует то, что объект может быть создан только один от этого класса. Например, мы можем попытаться создать два объекта a и b, и окажется, что это один и тот же объект, потому что мы проверяем, был ли уже создан какой-то объект, и, собственно, если он уже создан, мы не будем создавать новый объект. Существует также метод _del_, который определяет поведение при удалении объекта. Однако, он работает не всегда очевидно. Он вызывается не когда мы удаляем объект оператором del, а когда количество ссылок на наш объект стало равно нулю и вызывается garbage collector. Это не всегда происходит тогда, когда мы думаем это должно произойти, поэтому переопределять его нежелательно. Будьте внимательны. Собственно, в этом видео мы просмотрим какой-то набор магических методов, который чаще всего используется и переопределяется. Посмотрим, как они себя ведут и как писать классы, которые их используют. Одним из магических методов является метод _str_, который определяет поведение, например, при вызове функции print. Метод _str_ должен определить какое-то человекочитаемое описание нашего класса, которое может вывести потом пользователь где-то, например, в интерфейсе. Классическим вариантом метода _str_ может быть выведение имени и имейла. Обратите внимание, мы используем тот же самый класс, что и раньше, но теперь, если мы будем принтить наш объект, у нас будет не какой-то object типа user, а понятное и читаемое название нашего объекта. Ещё двумя полезными методами магическими являются методы _hash_ и equal, _eq_, которые определяют то, как сравниваются наши объекты и что происходит при вызове функции hash. Магический метод _hash_ может, например, переопределить функцию хеширования, которая используется, например, когда мы получаем ключи в словаре. В данном случае нашего класса user мы можем сказать, что при вызове функции _hash_ мы хотим хешировать имейл, то есть у нас в качестве хеша берётся всегда имейл пользователя. Ну а при сравнении мы эти имейлы просто сравниваем, при сравнении двух объектов. Таким образом, если мы создадим двух юзеров с разными именами, но одинаковыми имейлами, при вызове функции сравнения, то есть когда мы используем ==, Python будет говорить нам, что это один и тот же объект, потому что вызывается метод _eq_, который сравнивает только имейлы. Точно так же функция _hash_ возвращает теперь одно и то же значение, потому что используется значение имейла, которое в данном случае одинаковое. Но если мы попробуем создать словарь, где в качестве ключа будет использоваться наш объект юзера, то у нас создастся словарь только с одним ключом, а не с двумя объектами, несмотря на то, что мы итерируемся здесь по двум объектам, потому что в качестве хеша используется одно и то же значение, и в качестве _eq_ у нас сравниваются имейлы. Очень важными магическими методами являются методы, определяющие доступ к атрибутам. Это методы _getattr_ и _getattribute_. Очень важно понимать между ними отличия, потому что очень часто происходит путаница. Итак, метод _getattr_ определяет поведение, когда наш атрибут, который мы пытаемся получить, не найден. Таким образом, если мы обратимся к атрибуту какого-то объекта и у нас он будет не найден, у нас всегда вызовется метод _getattr_ и мы можем определить какое-то поведение дефолтное при той ситуации, когда атрибута нет. Метод _getattribute_ вызывается в любом случае. Когда мы обращаемся к какому-то атрибуту, у нас всегда вызывается метод _getattribute_, и мы можем, например, логировать какие-то обращения к атрибутам или переопределять поведение при поиске соответствующих атрибутов объекта. Например, мы можем возвращать всегда какую-то строчку и ничего не делать, как в данном случае. Мы определили класс и переопределили метод _getattribute_, который всегда возвращает строку и ничего дальше не делает. Таким образом, что бы мы не делали, как бы мы не пытались обращаться к атрибутам, даже которых ещё нет, как в данном случае, у нас всегда выведется эта строка. _getattr_ работает немного по-другому. _getattr_, как я уже говорил, вызывается в том случае, если атрибут не найден. В данном случае внутри _getattribute_, который вызывается всегда, мы просто логируем, что мы пытаемся найти соответствующий атрибут и продолжаем выполнение, используя класс object. Таким образом, если у нас пытается найтись атрибут, которого не существует, у нас вызовется _getattr_, что здесь и происходит. У нас ничего не найдено. Отлично. _setattr_, как вы могли догадаться, определяет поведение при присваивании значения к атрибуту. Например, вместо того, чтобы присвоить значение, мы можем опять же вернуть какую-то строчку и ничего не делать. В данном случае, если мы попытаемся присвоить значение атрибуту, у нас ничего не произойдёт и атрибут не создастся. Ну а _delattr_ управляет поведением, когда мы пытаемся удалить какой-то атрибут объекта. Мы можем не удалять, а, например, переопределить как-то поведение или добавить какую-то функциональность. Например, если мы хотим каскадно удалить какие-то объекты, связанные с нашим классом. В данном случае мы просто продолжаем удаление с помощью класса object, ну и как-то логируем то, что у нас происходит удаление. Ещё одним методом является метод _call_, который в соответствии со своим названием определяет поведение, когда наш класс вызывается. Например, с помощью метода _call_ мы можем определить logger, который будем потом использовать в качестве декоратора. Да, декоратором может быть не только функция, но и класс. В данном случае при инициализации класса мы запоминаем filename, который ему передан, и каждый раз, когда мы будем вызывать наш класс, мы будем возвращать какую-то новую функцию в соответствии с протоколом декораторов и записывать значения, записывать какую-то строчку о том, что у нас наша функция вызвана. В данном случае, мы просто определяем пустую функцию, декоратор которой записывает все её вызовы. Ну и когда мы вызовем нашу функцию, у нас в нашем файле появится строка. Классическим примером на перегрузку операторов в других языках является перегрузка оператора сложения. В данном случае за операцию сложения в Python'е отвечает оператор _add_. Существуют также другие операторы вроде _sub_, который определяет поведение при вычитании, что логично. И мы можем определить наш класс, который будет перегружать операцию сложения. В данном случае мы можем написать какой-то NoisyInt, который будет работать почти как integer, но добавлять какой-то шум, например, при сложении. Мы определим два наших Int'а со значением 10 и 20, и в операции сложение, то есть в методе _add_ мы будем добавлять какое-то случайное значение от минус единицы до единицы. Таким образом, когда мы попытаемся сложить два наших числа, у нас выведется число в окрестности искомого, то есть у нас будет 29.5, например, вместо 30. Это просто пример, вы можете переопределять операцию сложения так, как вам удобно, как вам подходит для вашей задачи. Предлагаю вам попробовать попрактиковаться и самостоятельно написать контейнер с помощью методов _getitem_ и _setitem_. Остановите видео и продолжите, когда закончите. Отлично, надеюсь, у вас получилось. Ну а в качестве примера я реализовал свой собственный класс PascalList, который имитирует поведение списков в Паскале. Как вы знаете, в Python'e списки нумеруются с нуля, ну а в Паскале — с единицы. Мы можем переопределить методы _getitem_ и _setitem_ так, чтобы они работали как в Паскале. Методы _getitem_ и _setitiem_ определяют поведение, когда мы работаем с нашим классом с помощью квадратных скобочек, обращаясь по какому-то индексу, то есть как в случае с list'ами, со списками, или в случае со словарями. И в данном примере мы определяем класс PascalList, который принимает какой-то список, запоминает его и каждый раз, когда мы пытаемся достучаться до какого-то индекса, он обращается к этому индексу минус единица. Таким образом, если мы попытаемся взять первый элемент, у нас, на самом деле, возьмётся нулевой элемент, как в Python'e. Ну и, например, мы можем создать PascalList от одного до пяти, и, обратившись по первому индексу, мы получим элемент один, как и сделали бы в Паскале. Ну и, например, к пятому мы можем переопределить значение в конце. Собственно, на этом всё, мы с вами обсудили какой-то набор магических методов. Более полно можно посмотреть про них в документации. Их огромное количество, и они управляют поведением класса в Python'e и тем, как работают объекты, созданные от этих классов. Увидимся в следующем видео.