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.

509 lines
19 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": "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": [
"<class '__main__.NewClass'>\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': <class '__main__.A'>, 'b': <class '__main__.B'>}\n",
"[<class '__main__.A'>, <class '__main__.B'>]\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<ipython-input-15-a03e0b46bd6a>\u001b[0m in \u001b[0;36m<module>\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
}