{ "cells": [ { "cell_type": "markdown", "id": "90fa98ae", "metadata": {}, "source": [ "# Метаклассы #" ] }, { "cell_type": "markdown", "id": "a49613fc", "metadata": {}, "source": [ "В этой лекции мы поговорим с вами о мета-классах. Как вы уже знаете, всё в Python'е является объектом, и классы не исключение, а значит эти классы кто-то создаёт. Давайте определим класс с названием Class и его объект. Тип нашего объекта является Class, потому что Class создал наш объект." ] }, { "cell_type": "code", "execution_count": 1, "id": "341462e8", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "__main__.Class" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class Class:\n", " ...\n", " \n", "obj = Class()\n", "type(obj)" ] }, { "cell_type": "markdown", "id": "5c17b22a", "metadata": {}, "source": [ "Однако, у класса тоже есть тип. Этот тип `type`, потому что `type` создал наш класс." ] }, { "cell_type": "code", "execution_count": 2, "id": "03dc5e19", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "type" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(Class)" ] }, { "cell_type": "markdown", "id": "9e88bd36", "metadata": {}, "source": [ "В данном случае `type` является мета-классом. Он создаёт другие классы. Типом самого `type`, кстати, является он же сам." ] }, { "cell_type": "code", "execution_count": 3, "id": "7c5cab2a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "type" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(type)" ] }, { "cell_type": "markdown", "id": "27b06d18", "metadata": {}, "source": [ "Это рекурсивное замыкание, которое реализовано с помощью С внутри. Очень важно понимать разницу между созданием и наследованием." ] }, { "cell_type": "code", "execution_count": 4, "id": "fd58b7cb", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "issubclass(Class, type)" ] }, { "cell_type": "markdown", "id": "75d5b828", "metadata": {}, "source": [ "В данном случае класс не является `subclass`'ом `type`. `Type` его создаёт, но класс не наследуется от него. Класс наследуется от класса объекта." ] }, { "cell_type": "code", "execution_count": 5, "id": "410a1c95", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "issubclass(Class, object)" ] }, { "cell_type": "markdown", "id": "114f8dd7", "metadata": {}, "source": [ "Чтобы понять, как вообще классы задаются, можно написать простую функцию, которая класс возвращает." ] }, { "cell_type": "code", "execution_count": 8, "id": "21dc42eb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "False\n" ] } ], "source": [ "def dummy_factory():\n", " class Class:\n", " pass\n", " \n", " return Class\n", "\n", "Dummy = dummy_factory()\n", "print(Dummy() is Dummy())" ] }, { "cell_type": "markdown", "id": "d2107543", "metadata": {}, "source": [ "В данном случае мы определяем функцию, которая возвращает какой-то новый класс — `dummy_factory`. Классы можно создавать на лету. В данном случае мы создаём два разных объекта и просто их возвращаем.\n", "\n", "Однако, на самом деле, Python работает, конечно, не так. Для создания классов используется мета-класс `type`, и вы можете на лету создать `type`, просто вызвав его и передав название класса." ] }, { "cell_type": "code", "execution_count": 9, "id": "8186063e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "<__main__.NewClass object at 0x7f993b956d68>\n" ] } ], "source": [ "NewClass = type(\"NewClass\", (), {})\n", "\n", "print(NewClass)\n", "print(NewClass())" ] }, { "cell_type": "markdown", "id": "05936a27", "metadata": {}, "source": [ "В данном случае мы создаём класс `NewClass` без родителей и без каких-то атрибутов. `NewClass` действительно является классическим классом. Вы можете его вывести, можете создать какой-то объект этого класса. Это настоящий класс, мы создали его на лету без литерала `class`.\n", "\n", "Однако, чаще всего классы создаются всё-таки по-другому. Они создаются с помощью мета-классов, и в данном случае давайте определим свой собственный мета-класс, который будет управлять поведением при создании класса." ] }, { "cell_type": "code", "execution_count": 10, "id": "c6a34b73", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Creating A\n" ] } ], "source": [ "class Meta(type):\n", " def __new__(cls, name, parents, attrs):\n", " print(f\"Creating {name}\")\n", " \n", " if \"class_id\" not in attrs:\n", " attrs[\"class_id\"] = name.lower()\n", " \n", " return super().__new__(cls, name, parents, attrs)\n", "\n", "class A(metaclass=Meta):\n", " pass" ] }, { "cell_type": "markdown", "id": "596e5a1e", "metadata": {}, "source": [ "Мы определим класс `Meta`. Для того чтобы он бы мета-классом, он должен наследоваться от другого мета-класса. В данном случае это мета-класс `type`, базовый мета-класс. И, как вы уже знаете, метод `__new__` управляет поведением при создании объекта. В данном случае объектом является другой класс, поэтому мы можем изменять поведение при создании другого класса. Метод `__new__` принимает название класса, его родителей и какие-то атрибуты. Мы можем определить какой-то новый класс `A` и указать, что его мета-классом является наш мета-класс. Именно этот мета-класс и будет управлять поведением при создании нового класса. В данном случае мы выводим строчку о том, что у нас класс создаётся. При определении `class` у нас вызывается мета-класс и функция `__new__`, метод `__new__`. Мы выводим, что у нас наш класс создаётся. Записываем в какой-то атрибут нашего класса, в данном случае в атрибут `class_id`, значение." ] }, { "cell_type": "code", "execution_count": 11, "id": "f83fe579", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "A.class_id: 'a'\n" ] } ], "source": [ "print(f\"A.class_id: '{A.class_id}'\")" ] }, { "cell_type": "markdown", "id": "c17782d2", "metadata": {}, "source": [ "Таким образом, мы можем переопределить поведение при создании класса. Например, добавить ему какой-то атрибут или сделать что-нибудь другое." ] }, { "cell_type": "code", "execution_count": 12, "id": "1fa3f00a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Initializing — Base\n", "Initializing — A\n", "Initializing — B\n" ] } ], "source": [ "class Meta(type):\n", " def __init__(cls, name, bases, attrs):\n", " print(f\"Initializing — {name}\")\n", " \n", " if not hasattr(cls, \"registry\"):\n", " cls.registry = {}\n", " else:\n", " cls.registry[name.lower()] = cls\n", " \n", " super().__init__(name, bases, attrs)\n", "\n", "class Base(metaclass=Meta): pass\n", "\n", "class A(Base): pass\n", "\n", "class B(Base): pass" ] }, { "cell_type": "markdown", "id": "b4ba7f87", "metadata": {}, "source": [ "Например, мы можем определить мета-класс, который переопределяет функцию `__init__`, и в данном случае наш мета-класс будет логировать, запоминать все созданные подклассы. Давайте определим функцию `__init__`, которая будет вызываться при инициализации нашего объекта. В данном случае нашим объектом является класс. При инициализации класса у нас она будет вызываться. Наш `__init__` принимает те же самые аргументы, однако, делает немного другое. Он записывает свой собственный атрибут значения созданных классов. В данном случае, у нас вначале создаётся класс `Base`, мета-классом которого является `Meta`, и у него создаётся `registry`, в который мы потом будем записывать все его подклассы. Каждый раз, когда у нас создаётся какой-то класс, который наследуется от `Base`, мы записываем в наш `registry` соответствующее значение, то есть название созданного класса и ссылку на него, то есть объект `class`. И мы можем вывести теперь все подклассы нашего `Base`, просто обратившись к `registry`, ну или написав обращение к методу `subclasses`." ] }, { "cell_type": "code", "execution_count": 13, "id": "d4c2e1a5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'a': , 'b': }\n", "[, ]\n" ] } ], "source": [ "print(Base.registry)\n", "print(Base.__subclasses__())" ] }, { "cell_type": "markdown", "id": "b9aae82f", "metadata": {}, "source": [ "## Абстрактные методы ##" ] }, { "cell_type": "markdown", "id": "2245e2a3", "metadata": {}, "source": [ "Очень часто при работе с объектно-ориентированной парадигмой в Python'е возникают вопросы про абстрактные методы, потому что абстрактные методы являются центральным понятием, например, в языке программирования C++. В Python'е есть абстрактные методы, вы можете их использовать с помощью стандартной библиотеки `abc`.\n", "\n", "В данном случае здесь также работают мета-классы и мы можем определить абстрактный какой-то класс с методом `abstractmethod`." ] }, { "cell_type": "code", "execution_count": 14, "id": "a7a09dc8", "metadata": {}, "outputs": [], "source": [ "from abc import ABCMeta, abstractmethod\n", "\n", "class Sender(metaclass=ABCMeta):\n", " @abstractmethod\n", " def send(self):\n", " \"\"\"Do something\"\"\"" ] }, { "cell_type": "markdown", "id": "49c0d57b", "metadata": {}, "source": [ "О чём говорит наш декоратор `abstractmethod`? Что у нас не получится создать какой-то класс, не определив этот метод. То есть у нас метод абстрактный и мы обязаны его переопределить в классе, который наследуется от нашего класса. В данном случае у нас `Child` не переопределяет метод `send`, и поэтому вызывается ошибка." ] }, { "cell_type": "code", "execution_count": 15, "id": "05e82b55", "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "Can't instantiate abstract class Child with abstract methods send", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;32mclass\u001b[0m \u001b[0mChild\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mSender\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32mpass\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mChild\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: Can't instantiate abstract class Child with abstract methods send" ] } ], "source": [ "class Child(Sender): pass\n", "\n", "Child()" ] }, { "cell_type": "markdown", "id": "3bdd199c", "metadata": {}, "source": [ "Если мы переопределим метод `send`, то всё будет, как мы хотели, у нас абстрактный метод переопределён, всё работает." ] }, { "cell_type": "code", "execution_count": 16, "id": "c0599a42", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "<__main__.Child at 0x7f993b90fef0>" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class Child(Sender):\n", " def send(self):\n", " print(\"Sending\")\n", "\n", "Child()" ] }, { "cell_type": "markdown", "id": "6ff82354", "metadata": {}, "source": [ "Ну и, на самом деле, абстрактные методы используются в Python'е довольно редко, чаще всего вызывается просто исключение `NotImplementedError`, которое говорит о том, что этот метод нужно реализовать. Программист, когда видит в определении класса, что вызывается в методе `raise NotImplementedError`, что этот класс нужно в потомке переопределить." ] }, { "cell_type": "code", "execution_count": 17, "id": "a7ef9b3f", "metadata": {}, "outputs": [], "source": [ "class PythonWay:\n", " def send(self):\n", " raise NotImplementedError" ] }, { "cell_type": "markdown", "id": "66529f93", "metadata": {}, "source": [ "Что ж, мы с вами разобрали, как работают мета-классы. На самом деле мета-класс — это просто класс, который создаёт другие классы, потому что классы тоже являются объектом. Мы посмотрели, как можно написать свой собственный мета-класс и переопределить поведение при создании другого класса. Также вы можете определять собственные абстрактные методы, если вам необходимо сказать о том, что в классе, который наследуется от вашего другого класса, обязательно должен реализован быть какой-то метод, какой-то интерфейс. На этом всё." ] } ], "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.6.8" } }, "nbformat": 4, "nbformat_minor": 5 }