{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Пользовательские классы\n", "\n", "## Объявление класса\n", "\n", "\n", "Сравним базовый синтаксис объявления классов в `C/C++` и в `python`. Пока оставим наследование на потом, но заметим, что пользовательские классы наследуют от самого общего класса `object`, если не указанно иначе. Так или иначе, любой тип является производным от `object`.\n", "\n", "````{panels}\n", "C++\n", "^^^\n", "\n", "Объявление класса в `C++` выглядит примерно следующим образом.\n", "\n", "```{code} c++\n", "class MyClass\n", "{\n", "\t// все \n", "\t// детали\n", "\t// класса\n", "\t// т.е. тело класса\n", "};\n", "```\n", "- объявление класса начинается с ключевого слова `class`;\n", "- за ним следует имя класса (`MyClass` в нашем случае);\n", "- далее следует тело класса которое помещается в фигурные скобки `{}`;\n", "- завершается объявление класса точкой с запятой \"`;`\".\n", "\n", "---\n", "python\n", "^^^\n", "\n", "В `python` класс объявляется очень похоже, если вспомнить разницу в объявлении функций в `C++` и `python`.\n", "```{code} python\n", "class MyClass: # заголовок класса\n", "\t# все \n", "\t# детали \n", "\t# класса\n", "\t# т.е. тело класса\n", "```\n", "\n", "- объявление класса начинается с ключевого слова `class`;\n", "- за ним следует имя класса (`MyClass` в нашем случае) и символ двоеточия;\n", "- далее следует тело класса (состоящее хотя бы из одной инструкции), которое обозначается постоянным отступом в право;\n", "- завершается объявление класса тогда, когда отступ возвращается к прежнему уровню;\n", "- опционально, тело класса может начинаться с документирующей строки.\n", "````\n", "\n", "```{note}\n", "В отличие от имен переменных, которые принято задавать в стиле [snake_case](https://ru.wikipedia.org/wiki/Snake_case), имена классов принято давать в стиле [CamelCase](https://ru.wikipedia.org/wiki/CamelCase).\n", "```\n", "\n", "Инструкции внутри тела класса выполняются сразу после того, как поток программы доберется до объявления класса и интерпретатор прочитает заголовок класса. В результате создаётся объект объявления класса (**не** экземпляр класса), который имеет тип `type`, т.е. этот объект задает новый тип данных. Этот объект будет использоваться в качестве фабрики для создания экземпляров (смотри [](create_instance)).\n", "\n", "Этого знания уже хватает, чтобы объявить примитивный класс с пустым телом. Объявим класс `EmptyClass` с только документирующей строкой и убедимся, что создался объект объявления этого класса и связался с именем `EmptyClass`." ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "Пустой класс без атрибутов и методов.\n", "\n" ] } ], "source": [ "class EmptyClass:\n", " \"\"\"Пустой класс без атрибутов и методов.\"\"\"\n", "\n", "print(EmptyClass)\n", "print(type(EmptyClass))\n", "print(EmptyClass.__doc__)\n", "print(type(int))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Важно понимать, что все инструкции внутри тела класса выполняются в тот момент, когда интерпретатор читает объявление класса, а не когда создаются экземпляры этого класса. В примере ниже объявляется класс, но не создаётся ни один его экземпляр. Инструкция `print` все равно выполняется." ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "До создания класса MyClass\n", "Создаётся класс MyClass\n", "Класс MyClass создан\n" ] } ], "source": [ "print(f\"До создания класса MyClass\")\n", "\n", "class MyClass:\n", " print(f\"Создаётся класс MyClass\")\n", "\n", "print(f\"Класс {MyClass.__name__} создан\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```{note}\n", "Объект объявления класса создаётся после того, как завершится исполнение всех инструкций в теле класса. Т.е. если попытаться в примере выше изменить инструкцию в теле класса на `print(f\"Создаётся класс {MyClass.__name__}\")`, то возникнет ошибка [NameError](https://docs.python.org/3/library/exceptions.html#NameError), т.к. объект объявления класса ещё не создан и имя `MyClass` ни с чем не связанно. \n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(create_instance)=\n", "## Создание экземпляра \n", "\n", "Несмотря на то, что `EmptyClass` не содержит описания того, какими должны быть его экземпляры (кроме документирующей строки), уже можно создавать экземпляры. Чтобы создать экземпляр класса, необходимо вызвать имя класса, как если бы оно было функцией. В данном случае при вызове имени класса передавать аргументов не надо, но в более содержательных случаях это может потребоваться (смотри [](initialization))." ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 47, "metadata": {}, "output_type": "execute_result" } ], "source": [ "e = EmptyClass()\n", "isinstance(e, EmptyClass)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итак, инструкция `EmptyClass()` создаётся экземпляр класса `EmptyClass`, который связывается с именем `e`. \n", "\n", "Команда `isinstance(e, EmptyClass)` демонстрирует, что созданный объект --- экземпляр искомого класса. Это возможно сделать в runtime, т.к. несмотря на то, что тело класса `EmptyClass` по сути дела пустое, у созданного объекта всегда создаётся специальный атрибут [\\_\\_class\\_\\_](https://docs.python.org/3/library/stdtypes.html#instance.__class__), который ссылается на класс, к которому относится экземпляр." ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "__main__.EmptyClass" ] }, "execution_count": 48, "metadata": {}, "output_type": "execute_result" } ], "source": [ "e.__class__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`__class__` --- не единственный такой атрибут. Изучим как создавать атрибуты самостоятельно.\n", "\n", "```{note}\n", "Атрибуты и методы, имена которых начинаются и завершается двумя нижними подчеркиваниями, считаются [специальными](https://docs.python.org/3/reference/datamodel.html#special-method-names). Часто их называются [магическими](https://docs.python.org/3/glossary.html#term-magic-method). Они автоматически генерируются `python` для внутренних целей. За исключением ряда ситуаций, не принято давать имена в таком стиле своим атрибутам. Хотя их переопределение в производном классе --- очень распространенная практика. \n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(class_attributes)=\n", "## Атрибуты класса и атрибуты объекта\n", "\n", "\n", "Если при объявлении аттрибутов в `python` действовать по аналогии с `С++`, то сразу возникнут трудности. Рассмотрим объявление класса `C++` с одним атрибутом.\n", "\n", "```C++\n", "class A\n", "{\n", "public:\n", " int x;\n", "};\n", "```\n", "\n", "Здесь объявляется атрибут `x` целочисленного типа, который инициализируется при создании объекта. Но в `python` нельзя объявить переменную и не инициализировать её: каждое имя должно ссылаться на какой-то объект. Попробуем построить аналогичное объявление, инициализировав атрибут числом `0`." ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [], "source": [ "class A:\n", " x = 0" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь вспомним, что инструкции в теле класса выполняются не в момент создания экземпляра, а в момент чтения объявления класса интерпретатором. Т.е. атрибут `x` должен уже существовать, несмотря на то, что не было создано ни одного экземпляра. Чтобы убедиться в этом, проверим наличие атрибута `x` у объекта объявления класса `A`, используя точечную нотацию." ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0\n" ] } ], "source": [ "print(A.x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итак, такое объявление создаёт атрибут самого класса (атрибут объекта объявления класса). В терминах `C++` это статический атрибут. Иными словами, этот атрибут разделяется всеми экземплярами этого класса. \n", "\n", "Чтобы продемонстрировать это, создадим два экземпляра этого класса. " ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "До изменения атрибута\n", "0 0 0\n", "После изменения атрибута\n", "1 1 1\n" ] } ], "source": [ "a1 = A()\n", "a2 = A()\n", "\n", "print(\"До изменения атрибута\")\n", "print(A.x, a1.x, a2.x)\n", "\n", "A.x = 1\n", "\n", "print(\"После изменения атрибута\")\n", "print(A.x, a1.x, a2.x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Изменение значения атрибута класса привело к его изменению и у всех экземпляров. Таким образом, следующие два блока кода схожи между собой в `C++` и `python`.\n", "\n", "````{panels}\n", "C++\n", "^^^\n", "```{code} c++\n", "class A\n", "{\n", "public:\n", " static int x = 0;\n", "};\n", "```\n", "Класс `A` с целочисленным **статическим** атрибутом `x`.\n", "\n", "---\n", "python\n", "^^^\n", "```{code} python\n", "class A:\n", " x = 0\n", "\n", "```\n", "\n", "Класс `A` с **атрибутом класса** `x`.\n", "\n", "````\n", "\n", "На самом деле `x` принадлежит самому классу `A`, но к нему удается получить доступ через экземпляры этого класса `a1` и `a2` из-за механизма поиска атрибута в python: если не удается найти атрибут у самого экземпляра, то интерпретатор ищет его у класса этого объекта. \n", "\n", "\n", "```{note}\n", "Доступ к атрибуту осуществляется через точку (`obj.attr`), как и в `C++`. Встроенная функция [getattr](https://docs.python.org/3/library/functions.html#getattr) предоставляет альтернативу: `getattr(obj, \"attr\")` то же самое, что и `obj.attr`. В обоих случаях, если атрибут найти не удается, то бросается исключение [AttributeError](https://docs.python.org/3/library/exceptions.html#AttributeError). Проверить, есть ли такой атрибут у объекта можно функцией [hasattr](https://docs.python.org/3/library/functions.html#getattr).\n", "\n", "Приведенные альтернативы могут быть полезны, если у вас уже есть имя атрибута, значение которого вы хотите получить, в виде строки. \n", "```\n", "\n", "\n", "Чтобы создать атрибут у самого объекта, достаточно просто присвоить к желаемому атрибуту какое-нибудь значение." ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [], "source": [ "a1.y = 42" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В большинстве случаев, объекты пользовательских классов хранят свои атрибуты в словаре, доступ к которым можно получить по полю [\\_\\_dict\\_\\_](https://docs.python.org/3/library/stdtypes.html#object.__dict__). Встроенная функция [vars](https://docs.python.org/3/library/functions.html#vars) возвращает этот словарь. Воспользуемся этой функцией, чтобы проверить, какие атрибуты есть у экземпляров `a1` и `a2`." ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'y': 42}\n", "{}\n" ] } ], "source": [ "print(vars(a1))\n", "print(vars(a2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видно, что у объекта `a1` появился атрибут `y`, а у объекта `s2` нет, т.е. атрибут `y` принадлежит экземпляру, а не классу целиком. \n", "\n", "```{note}\n", "`__class__`, `__dict__` и ряд других [специальных атрибутов](https://docs.python.org/3/library/stdtypes.html#special-attributes) хранятся особым образом и командой `vars` не выводятся.\n", "\n", "Возможно сделать так, чтобы все или часть атрибутов хранилась не в словаре (например, [объявив атрибут \\_\\_slots\\_\\_](https://docs.python.org/3/reference/datamodel.html#slots)). В таких случаях специального атрибута `__dict__` у объекта может не быть вообще и вызов функции `vars` бросит ошибку [TypeError](https://docs.python.org/3/library/exceptions.html#TypeError). \n", "\n", "Встроенная функция [dir](https://docs.python.org/3/library/functions.html#dir) не полагается на наличия атрибута `__dict__` у объекта и пытается вывести все значимые атрибуты объекта. \n", "```\n", "\n", "Создание атрибутов объекта обычно происходит когда объект уже создан. Однако, стараются избегать ситуации, когда атрибуты (и методы) экземпляра создаются вне тела класса, т.к. это может привести к ситуации, когда у объектов одного типа есть разные атрибуты и методы (интерфейс). Если так случилось, то стоит задаться вопросом, а действительно ли эти сущности должны быть одного типа (класса), так уж ли много у них общего, чтобы считать их объектами одного типа? \n", "\n", "Чтобы проконтролировать, что у объектов одного типа одинаковый набор атрибутов, определяют специальный метод-\"конструктор\" класса. Но прежде разберем, как объявляются и вызываются методы класса вообще. \n", "\n", "## Методы класса \n", "\n", "Методы класса объявляются в теле класса как обычные функции в модуле. " ] }, { "cell_type": "code", "execution_count": 54, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Метод f класса A\n" ] } ], "source": [ "class A:\n", " def f():\n", " print(\"Метод f класса A\")\n", "\n", "A.f()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видим, что вызвать метод класса можно используя точечную нотацию через объект объявления этого класса. \n", "\n", "Попробуем создать экземпляр этого класса и вызвать эту функцию через него." ] }, { "cell_type": "code", "execution_count": 55, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "f() takes 0 positional arguments but 1 was given\n" ] } ], "source": [ "a = A()\n", "\n", "try:\n", " a.f()\n", "except TypeError as msg:\n", " print(msg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "При вызове функции через экземпляр класса возникла ошибка [TypeError](https://docs.python.org/3/library/exceptions.html#TypeError), которая сообщает, что при попытке вызвать функцию `f()` был передан 1 аргумент, а ожидалось 0 аргументов. Это объясняется тем, что инструкция\n", "```python\n", "a.f()\n", "``` \n", "на самом деле неявно преобразуется к инструкции\n", "```python\n", "A.f(a)\n", "```\n", "Мы объявили функцию `f` в теле класса `A` без параметров, а `python` передал в неё ссылку на экземпляр класса, что и привело к ошибке.\n", "\n", "```{note}\n", "Такое поведение присуще всем методам, объявленным в теле класса обычным образом. Смотри [](staticmethod), чтобы узнать, как объявить метод, не принимающий экземпляр первым параметром. \n", "```\n", "\n", "\n", "\n", "Т.е. при вызове метода через экземпляр класса первым аргументом неявно передаётся ссылка на этот самый экземпляр. Это позволяет таким методам получать доступ к вызвавшему экземпляру, его атрибутам, методам и т.п. Однако программистам приходится явно указывать дополнительный параметр в объявлении метода класса на первой позиции. Имя этого параметра может быть произвольным, но общепринято называть его `self`. Этот параметр очень похож на `this` в `C++`, но в `python` приходится явно указывать этот параметр.\n", "\n", "\n", "\n", "Приведем пример, того как обычно объявляются методы класса." ] }, { "cell_type": "code", "execution_count": 56, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Метод f класса A\n", "Метод g класса A вызван с параметром 42\n" ] } ], "source": [ "class A:\n", " def f(self):\n", " print(\"Метод f класса A\")\n", " \n", " def g(self, x):\n", " print(f\"Метод g класса A вызван с параметром {x}\")\n", "\n", "a = A()\n", "\n", "a.f()\n", "a.g(42)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Т.е. методы класса объявляются как обычные функции, но с одним дополнительным параметром `self` на первом месте. При вызове этого метода через экземпляр класса этот параметр указывать уже не надо.\n", "\n", "
\n", "Под капотом.\n", "\n", "\n", "Методы класса --- можно считать обычными атрибутами класса. Они объявляются в теле класса, а значит их объекты создаются во время создания объекта объявления класса, а не во время создания экземпляра. Однако, есть интересная деталь, которая объясняет почему первым параметром передаётся ссылка на вызвавший экземпляр: все функции являются [дескрипторами](https://docs.python.org/3/howto/descriptor.html), т.е. функции --- объекты, у которых специализирован метод [\\_\\_get\\_\\_](https://docs.python.org/3/reference/datamodel.html#object.__get__). Если атрибут класса является дескриптором, то при получении доступа к нему через экземпляр класса возвращается не сам дескриптор, а результат вызова его метода `__get__`. \n", "\n", "У всех функций по умолчанию, метод `__get__` принимает на вход экземпляр и тип этого экземпляра, а возвращает обертку над этой самой функцией, которая подставляет экземпляр класса в качестве первого аргумента при вызове функции. Такая обертка над функцией называется связанным методом (`bound method`): метод привязан к экземпляру.\n", "\n", "Ссылка на исходную функцию хранится в атрибуте `__func__` связанного метода. \n", "\n", "Продемонстрируем факт того, что `A.f` и `a.f` в предыдущем примере --- разные сущности.\n", "\n", "```python\n", "print(A.f) # \n", "print(a.f) # >\n", "print(A.f is a.f) # False\n", "print(A.f is a.f.__func__) # True\n", "```\n", "
\n", "\n", "(staticmethod)=\n", "## Статические методы\n", "\n", "Итак, вызов метода через объект объявления класса и через экземпляр класса приводит к разным эффектам. В первом случае метод вызывается в точности, как он объявлен, а во втором случае в качестве первого параметра передаётся ссылка на вызвавший экземпляр. Но что, если мы хотим объявить метод внутри класса, который никак не взаимодействует с экземпляром класса, но хотим иметь возможность вызывать его одинакового через экземпляр и через класс? \n", "\n", "Для таких целей есть встроенная декорирующая функция [staticmethod](https://docs.python.org/3/library/functions.html#staticmethod). " ] }, { "cell_type": "code", "execution_count": 57, "metadata": {}, "outputs": [], "source": [ "class A:\n", " def my_instance_method():\n", " print(\"Обычный метод.\") \n", "\n", " @staticmethod\n", " def my_static_method():\n", " print(\"Статический метод.\")\n", "\n", "\n", "a = A()" ] }, { "cell_type": "code", "execution_count": 58, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ ">\n", "Обычный метод.\n", "my_instance_method() takes 0 positional arguments but 1 was given\n" ] } ], "source": [ "print(a.my_instance_method)\n", "A.my_instance_method()\n", "try:\n", " a.my_instance_method()\n", "except TypeError as msg:\n", " print(msg)" ] }, { "cell_type": "code", "execution_count": 59, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Статический метод.\n", "Статический метод.\n" ] } ], "source": [ "print(a.my_static_method)\n", "A.my_static_method()\n", "a.my_static_method()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Из примера выше видно, что \n", "1. Статический метод не отображается в качестве связанного `bound` с экземпляром;\n", "2. Статический метод с одинаковым успехом вызывается и через класс и через экземпляр без дополнительных параметров.\n", "\n", "\n", "```{note}\n", "Часто возникают сомнения, должен ли метод быть объявлен статическим внутри класса или снаружи в виде обычной функции, если они никак не взаимодействуют с экземпляром. Общего правила на это счет нет. Иногда удобно объявить метод статическим, чтобы поместить его в пространство имен класса и тем самым не только освободить глобальное пространство имен модуля, но и явно указать, что этот метод как-то связан с этим типом данных. Ещё часто статические методы применяют для реализации альтернативных конструкторов: действительно, конструктор класса, по определению не должен принимать на вход экземпляр класса, а должен его возвращать.\n", "```\n", "\n", "(initialization)=\n", "## Инициализация объекта\n", "\n", "Теперь, когда мы знаем, как объявляются методы, разберем, как модифицировать создание экземпляров, чтобы гарантировать наличие у них уникальных атрибутов.\n", "\n", "При создании экземпляра класса (вызов объекта объявления класса) сначала вызывается специальный метод [\\_\\_new\\_\\_](https://docs.python.org/3/reference/datamodel.html?highlight=__new__#object.__new__) соответствующего класса, который именно создаёт объект и возвращает его. Затем у этого уже созданного объекта вызывается специальный метод [\\_\\_init\\_\\_](https://docs.python.org/3/reference/datamodel.html?highlight=__init__#object.__init__), который его инициализирует. По умолчанию эти методы наследуются от базового класса (`object` во всех предыдущих примерах). Чтобы модифицировать создание объекта, необходимо переопределить один (или оба) из методов `__new__` и `__init__`. \n", "\n", "Метод `__new__` переопределяется относительно редко и раскрываться здесь не будет. При инициализации объекта методом `__init__` обычно создаются атрибуты объекта. Ниже приводятся примеры объявления классов с одним **не** статическим атрибутом в `C++` и `python`. \n", "\n", "````{panels}\n", "C++\n", "^^^\n", "```{code} c++\n", "class A\n", "{\n", "public:\n", " int x = 0;\n", " A(int x): x(x) {};\n", "};\n", "```\n", "В конструкторе `A::A(int)` инициализируется целочисленная переменная `x` значением, переданным в конструктор.\n", "\n", "---\n", "python\n", "^^^\n", "```{code} python\n", "class A:\n", " def __init__(self, x):\n", " self.x = x\n", "\n", "\n", "\n", "```\n", "\n", "В инициализирующем методе `A.__init__` у объекта `self` создаётся атрибут `x` и связывается со значением, переданным в качестве второго параметра.\n", "````\n", "\n", "Метод `__init__` является обычным методом класса, которому неявно передаётся ссылка `self` на созданный методом `__new__` экземпляр. Раз уж этот объект уже существует, то у него можно создавать атрибуты (смотри [](class_attributes)), что и происходит в инструкции `self.x = x`. \n", "\n", "```{note}\n", "Метод `__init__` обязательно должен ничего не возвращать (иными словами возвращать `None`). Если это нарушится, то при создании экземпляра возникнет ошибка [TypeError](https://docs.python.org/3/library/exceptions.html#TypeError).\n", "```\n", "\n", "\n", "Объявим такой класс `A` с дополнительной инструкцией `print` в методе `__init__`, чтобы явно видеть, когда и с какими параметрами он вызывается. " ] }, { "cell_type": "code", "execution_count": 60, "metadata": {}, "outputs": [], "source": [ "class A:\n", " def __init__(self, x):\n", " print(f\"Инициализируется экземпляр класса A с атрибутом x = {x}\")\n", " self.x = x" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь создадим два экземпляра этого класса с разными значениями и убедимся, что атрибут `x` уникален для каждого экземпляра. \n", "\n", "Т.к. метод `__init__` теперь принимает дополнительный параметр `x`, то необходимо передать какое-нибудь значение при вызове объекта объявления класса для создания экземпляра. " ] }, { "cell_type": "code", "execution_count": 61, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Инициализируется экземпляр класса A с атрибутом x = 3\n", "Инициализируется экземпляр класса A с атрибутом x = 42\n", "a1.x = 3\n", "a2.x = 42\n" ] } ], "source": [ "a1 = A(3)\n", "a2 = A(42)\n", "print(f\"a1.x = {a1.x}\\na2.x = {a2.x}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Из примера видно, что переопределенный метод `__init__` вызывается дважды: первый раз с параметром 3, а второй раз с параметром 42. В результате создаётся два объекта, у которых атрибут `x` ссылается на уникальные значения.\n", "\n", "Ещё раз явно подметим, что метод `__init__` вызывается, когда объект уже создан метод `__new__`. Вызовем метод `__new__` у типа `object`, указав какого типа объект необходимо создать. " ] }, { "cell_type": "code", "execution_count": 62, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "True\n", "{}\n" ] } ], "source": [ "a3 = A.__new__(A) # то же самое, что и object.__new__(A) \n", "print(isinstance(a3, A))\n", "print(vars(a3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видим, что объект `a3` ссылается на экземпляр класса `A`, но инициализации объекта методом `__init__` не произошло. \n", "\n", "Инициализируем `a3` явно вызвав метод `__init__` у экземпляра." ] }, { "cell_type": "code", "execution_count": 63, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Инициализируется экземпляр класса A с атрибутом x = 13\n", "13\n" ] } ], "source": [ "a3.__init__(13)\n", "print(a3.x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Инициализация объекта `a3` произошла успешно. Но ничто не мешает вызвать метод `__init__` повторно, но в данном случае атрибут `x` не создаётся, а перезаписывается, т.к. он уже существует после первой инициализации. " ] }, { "cell_type": "code", "execution_count": 64, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Инициализируется экземпляр класса A с атрибутом x = new value\n", "new value\n" ] } ], "source": [ "a3.__init__(\"new value\")\n", "print(a3.x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Публичные и не публичные поля. Справка по классу\n", "\n", "В python нет поистине приватных атрибутов: какие бы меры вы не приняли, будет возможно получить доступ ко всем атрибутам класса. От части в связи с этим, в `python` не принято принимать больших усилий для скрытия атрибутов. Вместо этого существует общепринятое правило, которое отличает публичные имена в классе от не публичных.\n", "\n", "1. Все атрибуты и методы, имена которых начинаются с одинарного нижнего подчеркивания, не считаются публичными. Разработчик класса не рассчитывает, что к ним будет осуществляться доступ напрямую вне самого класса. \n", "2. Все атрибуты и методы, имена которых не начинаются с нижнего подчеркивания, считаются публичными. \n", "\n", "Так, метод `public_method` в примере ниже считается публичным, а `_private_method` нет." ] }, { "cell_type": "code", "execution_count": 102, "metadata": {}, "outputs": [], "source": [ "class A:\n", " \"docstring of class\"\n", " def public_method(self):\n", " \"docstring of method\"\n", "\n", " def _private_method(self):\n", " pass" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Набор публичных атрибутов методов часто называют интерфейсом класса. Разработчик класса (модуля, библиотеки) берет на себя обязательство поддерживать интерфейс в ближайших обновлениях. Публичные методы документируются. Пользователь может смело получать доступ к публичным атрибутам и вызывать публичные методы.\n", "\n", "Остальные атрибуты и методы не входят интерфейс. Разработчик вправе не составлять исчерпывающую документацию для непубличных методов или не документировать их вовсе. Разработчик может в любой момент убрать или изменить роль любого таких атрибутов и методов по своему усмотрению (например, при рефакторинге). Пользователь обращается к ним на свой страх и риск.\n", "\n", "В частности, встроенная функция справки `help` не перечисляет непубличные методы." ] }, { "cell_type": "code", "execution_count": 103, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Help on class A in module __main__:\n", "\n", "class A(builtins.object)\n", " | docstring of class\n", " | \n", " | Methods defined here:\n", " | \n", " | public_method(self)\n", " | docstring of method\n", " | \n", " | ----------------------------------------------------------------------\n", " | Data descriptors defined here:\n", " | \n", " | __dict__\n", " | dictionary for instance variables (if defined)\n", " | \n", " | __weakref__\n", " | list of weak references to the object (if defined)\n", "\n" ] } ], "source": [ "help(A)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```{note}\n", "Имена, начинающиеся с двух нижних подчеркиваний (но не заканчивающиеся двумя подчеркиваниями), тоже имеют особую роль, которая проявляется при наследовании. \n", "```\n", "\n", "\n", "## Пример c треугольниками\n", "\n", "Предположим, вы пишите программу, которая должна работать с треугольниками, вычислять их площадь и периметр. В результате у вас может получиться что-то следующее." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from math import asin, sqrt, cos, sin, pi\n", "\n", "\n", "def degree_to_radian(angle):\n", " return angle * pi / 180\n", "\n", "class Triangle:\n", " \"Класс для работы с треугольниками. \"\n", " def __init__(self, AB, BC, CA):\n", " \"Конструктор. В качестве параметров принимает длины 3 сторон.\"\n", " if not Triangle.is_valid(AB, BC, CA):\n", " raise ValueError(\"Нарушено неравенство треугольника.\")\n", "\n", " self._AB = AB\n", " self._BC = BC\n", " self._CA = CA\n", "\n", " @staticmethod\n", " def is_valid(AB, BC, CA):\n", " \"Проверяет выполнение неравенства треугольника.\"\n", " if AB + BC < CA:\n", " return False\n", " if BC + CA < AB:\n", " return False\n", " if CA + AB < BC:\n", " return False\n", " return True\n", "\n", " @staticmethod\n", " def _is_valid_angle(angle):\n", " if angle <= 0:\n", " return False\n", " if angle >= pi:\n", " return False\n", " return True \n", "\n", " @staticmethod\n", " def from_2_edges_and_1_angle(AB, CA, CAB, degree=False):\n", " \"Конструктор. В качестве параметров принимает 2 стороны и угол между ними.\"\n", " if degree:\n", " CAB = degree_to_radian(CAB)\n", "\n", " if not Triangle._is_valid_angle(CAB):\n", " raise ValueError(\"Угол должен быть в интервале от 0 до Pi.\")\n", " \n", " BC = sqrt(CA*CA + AB*AB - 2*AB*CA*cos(CAB))\n", " return Triangle(AB, BC, CA)\n", "\n", " @staticmethod\n", " def from_1_edge_and_2_angles(AB, ABC, CAB, degree=False):\n", " \"Конструктор. В качестве параметров принимает сторону и прилежащие углы.\"\n", " if degree:\n", " ABC = degree_to_radian(ABC)\n", " CAB = degree_to_radian(CAB)\n", "\n", " if not Triangle._is_valid_angle(ABC):\n", " raise ValueError(\"Угол должен быть в интервале от 0 до Pi.\")\n", " if not Triangle._is_valid_angle(CAB):\n", " raise ValueError(\"Угол должен быть в интервале от 0 до Pi.\")\n", " \n", " BCA = pi - ABC - CAB\n", " if not Triangle._is_valid_angle(BCA):\n", " raise ValueError(\"Сумма углов должна быть меньше Pi.\")\n", "\n", " CA = AB / sin(BCA) * sin(ABC)\n", " BC = AB / sin(BCA) * sin(CAB)\n", " return Triangle(AB, BC, CA)\n", "\n", " def perimeter(self):\n", " \"Возвращает периметр треугольника.\"\n", " return self._AB + self._BC + self._CA\n", " \n", " def area(self):\n", " \"Возвращает площадь треугольника.\"\n", " p = self.perimeter() / 2.\n", " return sqrt(p*(p - self._AB)*(p - self._BC)*(p - self._CA))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Обсудим реализованную структуру объекта.\n", "\n", "- Треугольник однозначно задаётся длинами его сторон, а значит разумно задать эти длины в качестве атрибутов объекта треугольника. \n", "- Далее встаёт вопрос, считать ли эти атрибуты публичными или нет? Не из любых отрезков можно сложить треугольник. Чтобы треугольник с заданным набором длин сторон существовал, необходимо, чтобы выполнялось [неравенство треугольника](https://ru.wikipedia.org/wiki/%D0%9D%D0%B5%D1%80%D0%B0%D0%B2%D0%B5%D0%BD%D1%81%D1%82%D0%B2%D0%BE_%D1%82%D1%80%D0%B5%D1%83%D0%B3%D0%BE%D0%BB%D1%8C%D0%BD%D0%B8%D0%BA%D0%B0). Таким образом, если сделать атрибуты публичными, то пользователь сможет по неосторожности сделать из возможного треугольника невозможный. Сделаем эти атрибуты непубличными.\n", "- Треугольник с заданной комбинацией сторон может не существовать, а значит логично будет выделить отдельную процедуру для проверки неравенства треугольника. Проверка на существование заданного треугольника по логике должна осуществляться до создания объекта. Иначе, в какой-то момент будет существовать невозможный треугольник. По этой причине логично считать, что эта процедура должна в качестве параметров принимать длинны сторон, а не экземпляр класса. Выше принято решение сделать такой метод статическим (`Triangle.is_valid`). В качестве альтернативы можно было рассмотреть реализацию этого метода в виде функции снаружи класса. При этом, так как такой метод может быть полезен и в других ситуациях, то можно сделать его публичным.\n", "- Стандартный конструктор принимает в качестве параметров длины отрезков. Если отрезки не удовлетворяют неравенству треугольника, то возбуждается исключение [ValueError](https://docs.python.org/3/library/exceptions.html#ValueError). В обратной ситуации создаётся объект с непубличными атрибуты.\n", "- Два альтернативных конструктора, позволяющие задавать треугольники в виде комбинации сторон и углов, реализованны в качестве статически методов.\n", "- Разумно ограничить все углы в отрезке $(0, \\pi)$. Для этого реализован непубличный статический метод `_is_valid_angle`.\n", "- Публичные методы `perimeter` и `area` вычисляют периметр и площадь треугольника соответственно. " ] }, { "cell_type": "code", "execution_count": 109, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "6.0 6.0 6.0\n" ] } ], "source": [ "t1 = Triangle(3, 4, 5)\n", "t2 = Triangle.from_2_edges_and_1_angle(3, 4, 90, degree=True)\n", "\n", "alpha = asin(3 / 5)\n", "beta = asin(4 / 5)\n", "t3 = Triangle.from_1_edge_and_2_angles(5, alpha, beta)\n", "\n", "print(t1.area(), t2.area(), t3.area())" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Help on class Triangle in module __main__:\n", "\n", "class Triangle(builtins.object)\n", " | Triangle(AB, BC, CA)\n", " | \n", " | Класс для работы с треугольниками.\n", " | \n", " | Methods defined here:\n", " | \n", " | __init__(self, AB, BC, CA)\n", " | Конструктор. В качестве параметров принимает 3 длины сторон.\n", " | \n", " | area(self)\n", " | Возвращает площадь треугольника.\n", " | \n", " | perimeter(self)\n", " | Возвращает периметр треугольника.\n", " | \n", " | ----------------------------------------------------------------------\n", " | Static methods defined here:\n", " | \n", " | from_1_edge_and_2_angles(AB, ABC, CAB, degree=False)\n", " | Конструктор. В качестве параметров принимает сторону и прилежащие углы.\n", " | \n", " | from_2_edges_and_1_angle(AB, CA, CAB, degree=False)\n", " | Конструктор. В качестве параметров принимает 2 стороны и угол между ними.\n", " | \n", " | is_valid(AB, BC, CA)\n", " | Проверяет выполнение неравенства треугольника.\n", " | \n", " | ----------------------------------------------------------------------\n", " | Data descriptors defined here:\n", " | \n", " | __dict__\n", " | dictionary for instance variables (if defined)\n", " | \n", " | __weakref__\n", " | list of weak references to the object (if defined)\n", "\n" ] } ], "source": [ "help(Triangle)" ] } ], "metadata": { "interpreter": { "hash": "196ade6f477ee0922199b767e69f530090abd1652d0b07faa3fef0670dc28e2c" }, "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.10" } }, "nbformat": 4, "nbformat_minor": 2 }