{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Классы и исключения" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В этой лекции рассказывается о том, что такое классы и как с помощью них определять собственные сложные типы данных. Отдельно рассматриваются специальные классы, называемые исключениями, которые используются для обработки ошибок, возникающих в процессе выполнения программы. Также мы представим концепцию и основные принципы объектно-ориентированного программирования, которое пришло на смену процедурному, рассмотренному в предыдущей лекции." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Содержание лекции" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* [Классы и объекты](#Классы-и-объекты)\n", " * [Конструктор](#Конструктор)\n", " * [Статические атрибуты](#Статические-атрибуты)\n", " * [Пространства имен](#Пространства-имен)\n", "* [Объектно-ориентированное программирование](#Объектно-ориентированное-программирование)\n", " * [Инкапсуляция](#Инкапсуляция)\n", " * [Наследование и полиморфизм](#Наследование-и-полиморфизм)\n", " * [Композиция](#Композиция)\n", "* [Исключения](#Исключения)\n", "* [Продвинутые приемы программирования](#Продвинутые-приемы-программирования)\n", " * [Декораторы](#Декораторы)\n", " * [Перегрузка операций](#Перегрузка-операций)\n", " * [Множественное наследование](#Множественное-наследование)\n", " * [Абстрактные базовые классы](#Абстрактные-базовые-классы)\n", "* [Вопросы для самоконтроля](#Вопросы-для-самоконтроля)\n", "* [Задание](#Задание)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Классы и объекты" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Класс** - это сложный составной тип данных, состоящий из произвольного количества атрибутов и методов. **Атрибутами** (иногда называемыми также полями) класса называются его внутренние переменные, а **методами** - функции, которые выполняют различные операции над ними. Атрибуты и методы определяют структуру, общую для всех конкретных представителей класса, которых называют **объектами** или **экземплярами**. Вообще, понятия класса и его объекта связаны друг с другом так же, как, например, понятия \"автомобиль\" (класс, имеющие такие атрибуты, как тип кузова и мощность) и \"toyota corolla\" (конкретный представитель, со значениями атрибутов \"седан\" и \"124\")." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Классы создаются (или по другому - определяются) в Python с помощью инструкции `class`, имеющей следующий синтаксис:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n",
    "def class_name(base_classes):\n",
    "    instruction1\n",
    "    ...\n",
    "    instructionN\n",
    "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Имя класса *class_name* должно быть [допустимым идентификатором](04_Data_Types.ipynb#Допустимые-идентификаторы) в языке Python. Если имя состоит из нескольких слов, то они записываются слитно без пробелов, при этом каждое слово пишется с заглавной буквы (такой стиль называется [CamelCase](https://ru.wikipedia.org/wiki/CamelCase)), например, `Circle`, `MyClass`, `SomeLongNameForClass`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Предложение *base_classes* содержит список базовых классов, разделенных запятой (подробнее об этом рассказывает в разделе, посвященном наследованию)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Инструкции *instructionK* могут быть любыми корректными инструкциями языка Python, однако в большинстве случаев используются иснтрукции присваивания `=` и определения функции `def`." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "class MyClass:\n", " def print_hello(self):\n", " print('hello, object:', id(self))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В примере выше мы создали наш первый очень простой класс, в котором описан только один метод `print_hello`. Методы вызываются для конкретного объекта и имеют как минимум один обязательный параметр, который по общепринятому соглашению называется `self`. Этот параметр инициализируется самим интерпретатором, и в качестве значения получает ссылку на тот объект, для которого вызван метод." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итак, для того, чтобы вызывать метод `print_hello`, нам для начала нужно создать объект класса `MyClass`. Делается это очень просто:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "my_object = MyClass() # обратите внимание на скобки после имени класса - их нужно обязательно указывать!\n", "print(type(my_object)) # убедимся, что my_object имеет тип данных MyClass" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь мы можем вызвать метод `print_hello` для объекта `my_object`. Делается это тем же способом, как мы вызывали функции из модуля в предыдущей лекции, а именно - с помощью операции `.`:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "hello, object: 113878632\n" ] } ], "source": [ "my_object.print_hello()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Обратите внимание, что несмотря на то, что при определении метода `print_hello` мы указали один параметр, при вызове мы ничего в него не передаем. Как говорилось чуть выше, первый параметр класса инициализируется самим интерпретатором, и принимает в качестве значения ссылку на объект, для которого вызвался метод (в нашем случае - `my_object`). Если мы создадим второй объект, то увидим, что для него метод `print_hello` выведет другое число:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "hello, object: 113878856\n" ] } ], "source": [ "my_object2 = MyClass()\n", "my_object2.print_hello()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Конструктор" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Добавить атрибут к объекту можно с помощью обычной операции присваивания:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "hello, Alice\n" ] } ], "source": [ "class MyClass:\n", " def print_hello(self):\n", " print('hello,', self.name) # обращаемся к атрибуту name объекта\n", "\n", "my_object = MyClass()\n", "my_object.name = 'Alice' # добавляем атрибут name объекту\n", "\n", "my_object.print_hello()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Тем не менее, этот способ не очень удобен и чреват ошибками. Дело в том, что нам придется в явном виде добавлять нужные атрибуты каждый раз при создании нового объекта класса. Для этого нам, во-первых, нужно помнить, какие атрибуты должен иметь объект, а во-вторых, если где-то в исходном коде мы забудем проинициализировать хотя бы один из них, то в дальнешем при работе с этим объектом будут происходить ошибки. Например, если бы в пример выше мы не добавили атрибут `name` перед вызовом метода `print_hello`, то последний сгенерировал бы исключение `AttributeError`, так как не смог бы найти атрибут с таким именем у объекта `my_object` (убедитесь в этом сами)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Чтобы решить обе проблемы, описанные выше, нам нужен некоторый метод, который бы гарантированно вызывался при создании любого объекта класса и содержал код для правильной его инициализации. Такой специальный метод называется **конструктором**, и в языке Python он имеет предопределенное имя `__init__`. Каждый раз, когда интерпретатор создает новый объект, он пытается найти в его классе этот метод, и если ему это удается - вызывает его." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> В языке Python идентификаторы, начинающиеся и заканчивающиеся двумя символами подчеркивания, зарезервированы для специальных методов и переменных, к которым обращается сам интерпретатор во время выполнения кода. Для обычных идентификаторов не рекомендуется использовать такую схему именования." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "in constructor\n", "next instruction\n", "hello, Bob\n" ] } ], "source": [ "class MyClass:\n", " def __init__(self, name): # конструктор с параметром name\n", " print('in constructor')\n", " self.name = name\n", " def print_hello(self):\n", " print('hello,', self.name)\n", "\n", "my_object = MyClass('Bob') # создаем объект, в качестве name передаем строку 'Bob'\n", "print('next instruction')\n", "my_object.print_hello()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Мы добавили в конструктор вызов функции `print`, чтобы вы могли увидеть, что он завершает свою работу **до** следующей после создания объекта инструкции в коде. Таким образом, интерпретатор гарантирует, что программист может обратиться к объекту только после того, как для него был вызван конструктор." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Статические атрибуты" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Все атрибуты, с которыми мы сталкивались ранее в этой лекции, принадлежат своему объекту, и чтобы получить доступ к ним, нужно иметь ссылку на этот объект. Такие атрибуты еще называются переменными экземпляра (*instance variables*)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Атрибуты, которые являются общими для всех объектов класса, называются **статическими**, или переменными класса (*class variables*). Чтобы создать такой атрибут, достаточно просто поместить в определение класса инструкцию присваивания для него:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "class MyClass:\n", " greeting = 'hello'\n", " \n", " def __init__(self, name):\n", " self.name = name\n", " \n", " def print_hello(self):\n", " print(self.greeting, self.name)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Статический атрибут является частью класса, а не объекта, поэтому обращаться к нему можно не только через объект, но и через сам класс:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "hello\n", "hello\n" ] } ], "source": [ "my_object = MyClass('John')\n", "\n", "print(my_object.greeting) # правильно, статические атрибуты доступны через любой объект класса\n", "print(MyClass.greeting) # правильно, статические атрибуты доступны через сам класс\n", "\n", "# print(MyClass.name) # неправильно, name обычный атрибут, доступен только через объект, если\n", " # выполнить этот код, то будет сгенерировано исключение!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если изменить статический атрибут, обратившись к нему через класс, то изменение будет видно в каждом объекте, а если изменить через объект - то только для данного объекта:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "hello Alice\n", "hello Bob\n", "bonjour Alice\n", "bonjour Bob\n", "aloha Alice\n", "bonjour Bob\n" ] } ], "source": [ "my_object1 = MyClass(\"Alice\")\n", "my_object2 = MyClass(\"Bob\")\n", "\n", "my_object1.print_hello();\n", "my_object2.print_hello();\n", "\n", "MyClass.greeting = \"bonjour\" # это изменение влияет на оба объекта\n", "\n", "my_object1.print_hello();\n", "my_object2.print_hello();\n", "\n", "my_object1.greeting = \"aloha\" # это изменение влияет только на my_object1\n", "\n", "my_object1.print_hello();\n", "my_object2.print_hello();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Пространства имен" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В предыдущей лекции мы говорили о [трех разновидностях](07_Functions_And_Modules.ipynb#namespaces_types) пространств имен, имеющихся в Python. Классы и их экземпляры добавляют к ним еще два:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Пространство имен класса, содержащее все методы и статические атрибуты, входящие в класс.\n", "* Пространство имен объекта, содержащее имена из пространства имен класса, плюс имена атрибутов объекта." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'greeting', 'print_hello']\n", "\n", "['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'greeting', 'name', 'print_hello']\n" ] } ], "source": [ "my_object = MyClass('Kate')\n", "print(dir(MyClass)) # выводим пространство имен класса\n", "print('') # выводим пустую строку для наглядности\n", "print(dir(my_object)) # выводим пространство имен объекта" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В обоих пространствах присутствует много имен, являющихся зарезервированными (те, которые начинаются и заканчиваются двумя символами подчеркивания). Кроме этих идентификаторов, мы видим, что в пространство имен класса попали имена `greeting` и `print_hello`, а в пространство имен объекта еще и `name`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Из-за того, что атрибуты и методы класса содержатся в собственном пространстве имен, при обращении к ним мы используем операцию `.`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Отметим еще один важный момент: интерпретатор не пытается искать идентификаторы в пространстве имен класса или объекта, если это не запрашивается явно с помощью операции `.`. В [прошлой лекции](07_Functions_And_Modules.ipynb#Пространства-имен) мы рассказывали о том, что интерпретатор пытается найти имя вначале в локальном пространстве имен, затем в глобальном и, наконец, во встроенном. Так вот, пространства имен класса и объекта *не используются* интерпретатором в этой цепочке - **любое** обращение к атрибутам и методам класса должно выполняться с помощью операции `.`, даже если оно осуществляется внутри методов этого же класса." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Это - неочевидное требование, и порой случайное игнорирование его приводит к труднообнаруживаемым ошибкам. Посмотрите на следующий пример и его результат и попробуйте сами объяснить, что в нем произошло:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "10\n" ] } ], "source": [ "var = 10 # эта переменная в глобальном пространстве имен\n", "\n", "class Test:\n", " var = 20 # эта переменная в пространстве имен класса\n", " \n", " def print_var(self):\n", " print(var) # программист хотел вывести на экран значение статического атрибута класса (var)\n", "\n", "t = Test()\n", "t.print_var()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Такой результат получился потому, что при выполнении метода `print_var` интерпретатор вообще не искал имя `var` в пространстве имен объекта или класса. Вначале он попытался найти его в локальном пространстве имен, но потерпел неудачу. Затем, в соответствие с упомянутой чуть выше схемой, начал искать имя `var` в глобальном пространстве имен, где оно и было обнаружено. Чтобы пример работал так, как хотел разработчик, его нужно исправить:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "20\n" ] } ], "source": [ "var = 10\n", "\n", "class Test:\n", " var = 20\n", " \n", " def print_var(self):\n", " print(self.var) # также можно было написать print(Test.var)\n", "\n", "t = Test()\n", "t.print_var()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Объектно-ориентированное программирование" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Классы и объекты являются ключевыми понятиями **объекто-ориентированного программирования** (ООП) - подхода к разработке программ, при котором они представляются как множество взаимодействующих друг с другом объектов. Это ключевое отличие от более старого [процедурного программирования](07_Functions_And_Modules.ipynb#Процедурное-программирование), в котором базовым элементом программ является функция." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "При использовании объектно-ориентированного подхода, разработчик пытается выделить из предметной области, в рамках которой создается программа, сущности, которые обладают определенным *состоянием* и *поведением*. Такие сущности в дальнейшем становятся объектами некоторого класса, причем их состояние описывается атрибутами объекта, а поведение - методами. Например, в исходном коде программы, позволяющей отправлять электронные письма и реализованной в соответствии с принципами ООП, можно обнаружить объекты класса `Email`, атрибутами которых будут `subject`, `from`, `to` и `text`, а среди методов вероятно найдется `attach_file`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В продолжение этого раздела мы рассмотрим ключевые особенности объектно-ориентированных программ, которые используются для достижения следующих целей:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Расширяемости программы, т.е. возможности относительно простого внесения в нее изменений после того, как она уже написана. Чтобы считаться расширяемой, программа должна быть понятна и обладать четкой структурой, потому что изменения в нее скорее всего будут вноситься не тем человеком, который ее писал.\n", "* Повторной используемости исходного кода, т.е. возможности использовать одни и те же компоненты программы в разных ее частях или в других программах." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Обе этих цели тесно связаны друг с другом и должны достигаться совместно. Например, улучшая повторную используемость исходного кода, мы уменьшаем его размер (потому что нужно какой-то блок написать всего один раз, а потом просто к нему обращаться), а следовательно упрощаем программу и повышаем степень ее расширяемости." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Инкапсуляция" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Инкапсуляцией** называется механизм, объединяющий данные и манипулирующий ими код, а также обеспечивающий сокрытие деталей реализации и защищающий объекты от неправильного использования." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Рассмотрим для примера следующий класс, позволяющий получить информацию о произвольном тексте, а именно - количество символов, слов и предложений. Мы не будем обрабатывать сложные ситуации, например, когда текст содержит прямую речь. Будем предполагать, что он состоит из предложений, оканчивающихся на точку, вопросительный или восклицательный знак, а предложения состоят из слов, разделенных пробелами, запятыми, двоеточиями и тире." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Symbols: 278\n", "Words: 51\n", "Sentences: 4\n" ] } ], "source": [ "class TextInfo:\n", " def __init__(self, text):\n", " self.text = text\n", " \n", " def get_symbols_count(self):\n", " return len(self.text)\n", " \n", " def get_words_count(self):\n", " words_count = 0\n", " in_word = False # булевая переменная, с помощью которой мы будем определять,\n", " # находится ли следующий цикл внутри слова или нет\n", " \n", " for symbol in self.text:\n", " # первая строка в if проверяет, что мы не встретили конец слова\n", " # вторая строка в if проверяет, что мы не встретили конец предложения\n", " \n", " # специальный символ \"\\\" в конце первой строки нужен, чтобы интерпретатор\n", " # понял, что выражение продолжается на следующей строке (иначе он бы выдал\n", " # ошибку SyntaxError)\n", " if symbol == ' ' or symbol == ',' or symbol == ':' or symbol == '-' or \\\n", " symbol == '.' or symbol == '!' or symbol == '?':\n", " if in_word:\n", " words_count += 1\n", " in_word = False\n", " else:\n", " in_word = True\n", " \n", " return words_count\n", " \n", " def get_sentences_count(self):\n", " sentences_count = 0\n", " in_sentence = False\n", " \n", " for symbol in self.text:\n", " if symbol == '.' or symbol == '!' or symbol == '?':\n", " if in_sentence:\n", " sentences_count += 1\n", " in_sentence = False\n", " else:\n", " in_sentence = True\n", " \n", " return sentences_count\n", " \n", " def print_info(self):\n", " print('Symbols:', self.get_symbols_count())\n", " print('Words:', self.get_words_count())\n", " print('Sentences:', self.get_sentences_count())\n", "\n", "\n", "# пример использования\n", "# обратите внимание на удобный способ записи очень длинных строковых литералов\n", "\n", "text = ('Tom appeared on the sidewalk with a bucket of whitewash and a long-handled brush.'\n", " 'He surveyed the fence, and all gladness left him and a deep melancholy settled down upon his spirit.'\n", " 'Thirty yards of board fence nine feet high.'\n", " 'Life to him seemed hollow, and existence but a burden.')\n", "\n", "text_info = TextInfo(text)\n", "text_info.print_info()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Здесь мы применили первый аспект инкапсуляции - объединили данные и код, который их обрабатывает, в один класс. Реализация, представленная выше, далека от идеальной, потому что в Python существуют функции, позволяющие сделать все то же самое быстрее и лаконичнее. Мы однако пока о них не знаем, так что решили задачу \"в лоб\" - проходом по всему тексту и подсчетом нужных значений. Единственное исключение - метод `get_symbols_count`, в котором мы воспользовались уже знакомой нам встроенной функцией `len`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Еще одним существенным недостатоком нашей реализации является то, что при каждом вызове методов `get_words_count` и `get_sentences_count` выполняется проход в цикле по всему тексту. Если текст будет очень большим, то эти операции будут занимать много времени. Немного подумав, мы можем догадаться до такого решения: подсчитывать все значения один раз при первом вызове любого метода, а затем просто возвращать уже готовый результат. Оптимизация, при которой какие-то трудновычисляемые результаты сохраняются в переменных, чтобы потом просто возвращать их значения, называется **кэшированием**. Заодно в следующей версии `TextInfo` можно объединить подсчет количества слов и предложений в одном цикле." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "class TextInfo:\n", " def __init__(self, text):\n", " self.text = text\n", " self.is_calculated = False\n", " self.words_count = 0\n", " self.sentences_count = 0\n", " \n", " def get_symbols_count(self):\n", " return len(self.text)\n", " \n", " def get_words_count(self):\n", " self.calculate()\n", " return self.words_count\n", " \n", " def get_sentences_count(self):\n", " self.calculate()\n", " return self.sentences_count\n", " \n", " def print_info(self):\n", " print('Symbols:', self.get_symbols_count())\n", " print('Words:', self.get_words_count())\n", " print('Sentences:', self.get_sentences_count())\n", " \n", " def calculate(self):\n", " if self.is_calculated:\n", " # ничего делать не нужно, вся информация уже подсчитана (выводим строку для\n", " # того, чтобы убедиться, что метод действительно не делает лишней работы)\n", " print('already calculated')\n", " return\n", " \n", " self.words_count = 0\n", " self.sentences_count = 0\n", " in_word = False\n", " in_sentence = False\n", " \n", " for symbol in self.text:\n", " if symbol == ' ' or symbol == ',' or symbol == ':' or symbol == '-':\n", " if in_word:\n", " self.words_count += 1\n", " in_word = False\n", " elif symbol == '.' or symbol == '!' or symbol == '?':\n", " if in_word:\n", " self.words_count += 1\n", " if in_sentence:\n", " self.sentences_count += 1\n", " in_word = in_sentence = False\n", " else:\n", " in_word = in_sentence = True\n", " \n", " # все подсчитано, не забываем зафиксировать это!\n", " self.is_calculated = True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Убедимся, что наша новая версия работает так, как ожидается (обратите внимание, что при вызове `get_sentences_count` был сразу возвращен результат, проход в цикле по тексту не понадобился):" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Symbols: 278\n", "Words: 51\n", "already calculated\n", "Sentences: 4\n" ] } ], "source": [ "text_info = TextInfo(text)\n", "text_info.print_info()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь мы добились быстрой работы нашего класса `TextInfo`, однако появилась новая проблема: если программист изменит атрибут `text`, но забудет установить атрибут `is_calculated` в `False`, то при вызове методов `get_words_count` и `get_sentences_count` он получит неверные результаты, так как соответствующие атрибуты не будут пересчитаны. Как мы помним, инкапсуляция должна защищать объекты от неправильного использования, значит нам нужно сделать так, чтобы при любом изменении атрибута `text`, атрибут `is_calculated` становился равен `False`. Этого можно добиться, если добавить такой метод для установки значения атрибута `text` (для краткости мы не будем приводить код класса `TextInfo` целиком):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class TextInfo:\n", "...\n", "def set_text(self, text):\n", " self.text = text\n", " self.is_calculated = False\n", "..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь мы спокойно можем писать следующий код:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Symbols: 278\n", "Words: 51\n", "already calculated\n", "Sentences: 4\n", "\n", "Symbols: 26\n", "Words: 5\n", "already calculated\n", "Sentences: 1\n" ] } ], "source": [ "text_info = TextInfo(text)\n", "text_info.print_info()\n", "\n", "print('')\n", "\n", "text_info.set_text('This is my favourite book!')\n", "text_info.print_info()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Наша реализация уже почти идеальна! Тем не менее, ее недостаток заключается в том, что по-прежнему есть возможность повредить наш объект, если напрямую изменить его атрибуты:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Symbols: 278\n", "Words: 51\n", "already calculated\n", "Sentences: 4\n", "\n", "Symbols: 12\n", "already calculated\n", "Words: 51\n", "already calculated\n", "Sentences: 4\n" ] } ], "source": [ "text_info = TextInfo(text)\n", "text_info.print_info()\n", "\n", "print('')\n", "\n", "text_info.text = 'This is bad!' # ничто не мешает нам присвоить значение атрибуту\n", " # text напрямую, не используя метод set_text\n", "text_info.print_info()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как видите, из-за того, что атрибут `is_calculated` не был установлен в `False` после замены текста в обход метода `set_text`, только количество символов отображено правильно, а остальная информация получена из закэшированных для старого текста данных. Чтобы обезопасить свои объекты от такого использования, нам нужно ограничить доступ к его внутренним данным и методам, которые могут быть изменены или вызваны не так, как мы предполагали." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В языке Python нет инструкции, указывающей интерпретатору атрибуты и методы, доступ к которым должен быть запрещен, если он происходит извне. Вместо этого создатели Python предлагают следовать соглашению, по которому программисты не должны в принципе обращаться в своем коде к атрибутам и методам, начинающимся с одного символа подчеркивания. Учитывая это, правильнее будет так переписать наш пример:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "class TextInfo:\n", " def __init__(self, text):\n", " self._text = text\n", " self._is_calculated = False\n", " self._words_count = 0\n", " self._sentences_count = 0\n", " \n", " def set_text(self, text):\n", " self._text = text\n", " self._is_calculated = False\n", " \n", " # так как мы указываем, что не хотели бы, чтобы к атрибуту _text обращались\n", " # напрямую, нам нужно предоставить метод для его получения\n", " def get_text(self):\n", " return self._text;\n", " \n", " def get_symbols_count(self):\n", " return len(self._text)\n", " \n", " def get_words_count(self):\n", " self._calculate()\n", " return self._words_count\n", " \n", " def get_sentences_count(self):\n", " self._calculate()\n", " return self._sentences_count\n", " \n", " def print_info(self):\n", " print('Symbols:', self.get_symbols_count())\n", " print('Words:', self.get_words_count())\n", " print('Sentences:', self.get_sentences_count())\n", " \n", " # эту функцию мы вызываем сами когда нужно, поэтому не хотим, чтобы к ней\n", " # обращались напрямую\n", " def _calculate(self): \n", " if self._is_calculated:\n", " # ничего делать не нужно, вся информация уже подсчитана (выводим строку для\n", " # того, чтобы убедиться, что метод действительно не делает лишней работы)\n", " print('already calculated')\n", " return\n", " \n", " self._words_count = 0\n", " self._sentences_count = 0\n", " in_word = False\n", " in_sentence = False\n", " \n", " for symbol in self.text:\n", " if symbol == ' ' or symbol == ',' or symbol == ':' or symbol == '-':\n", " if in_word:\n", " self._words_count += 1\n", " in_word = False\n", " elif symbol == '.' or symbol == '!' or symbol == '?':\n", " if in_word:\n", " self._words_count += 1\n", " if in_sentence:\n", " self._sentences_count += 1\n", " in_word = in_sentence = False\n", " else:\n", " in_word = in_sentence = True\n", " \n", " # все подсчитано, не забываем зафиксировать это!\n", " self._is_calculated = True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Представленное решение не избавит нас полностью от проблемы, озвученной ранее (потому что напрямую изменить атрибут по-прежнему возможно), однако по крайней мере даст понять другому программисту, что мы не планировали такого использования нашего класса." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Наследование и полиморфизм" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Классы в рамках объектно-ориентированного программирования могут образовывать иерархическое отношение, при котором один класс, называемый **дочерним**, получает все атрибуты и методы **родительского** класса. Это отношение называется отношением **наследования**, при этом дочерний класс еще называют наследником или подклассом, а родительский - предком, суперклассом или базовым классом. Иногда говорят, что с помощью механизма наследования мы *специализируем* класс, потому что в подклассе можно изменить работу произвольного количества методов базового класса, а также добавить новые." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Заметим, что в Python вообще все типы данных (в том числе встроенные, вроде `int` и `bool`) являются классами, причем наследующими общий базовый класс, который называется `object`. При создании своего класса нам не нужно явно указывать, что он наследует от `object`, потому что интерпретатор делает это автоматически." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Воспользовавшись функцией `dir` мы можем посмотреть, какие методы и атрибуты предоставляет класс `object`:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']\n" ] } ], "source": [ "print(dir(object))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Класс `object` обеспечивает базовый набор операций для любого объекта в Python. Все методы в нем используются интерпретатором в определенные моменты выполнения кода. Например, `__new__` вызывается, когда создается объект какого-либо класса для того, чтобы выделить память для него." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В Python есть несколько встроенных функций, специально предназначенных для работы с иерархией классов, например:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* `isinstance` - определяет, является ли объект экземпляром указанного класса\n", "* `issubclass` - определяет, является ли класс подклассом другого класса" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "A is object subclass: True\n", "a is A: True\n", "a is object: True\n" ] } ], "source": [ "# создаем пустые классы\n", "# инструкция pass никак не обрабатывается интерпретатором и нужна только для\n", "# того, чтобы при создании пустого класса не возникло синтаксической ошибки\n", "class A: pass\n", "\n", "a = A()\n", "\n", "print('A is object subclass:', issubclass(A, object))\n", "print('a is A:', isinstance(a, A))\n", "print('a is object:', isinstance(a, object))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Этот пример демонстрирует нам важнейший принцип наследования - объект дочернего класса считается также и объектом базового. Это выглядит логично, потому что дочерний класс может лишь добавлять новые методы (и атрибуты) к родительскому классу и изменять работу существующих, но не может удалять их. По этой причине код, который работает с объектами базового класса, может без малейших изменений работать с объектами подклассов, при этом для переопределенных методов будет вызываться реализация из подкласса. Это явление называется **полиморфизмом**, и оно позволяет писать общий код для всех классов с одинаковым предком." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Построение грамотной иерархии классов - творческий процесс, требующий большого опыта от программиста, однако есть простое правило, с помощью которого можно определить, стоит ли сделать один класс (B) наследником другого (A): нужно вслух произнести \"B *является* A\" и оценить, насколько логично это звучит. Традиционный пример, использующийся в книгах по ООП - отношение наследования между классами \"Кот\" и \"Животное\" (\"кот является животным\"). Конечная иерархия классов может быть весьма сложной, например:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![Иерархия наследования](./images/08/inheritance.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Особый интерес в этой иерархии представляют классы `Pegasus` и `Dolphin`. Они отличаются от всех остальных тем, что имеют сразу два родительских класса (потому что обладают свойствами, присущими сразу нескольким видам животных). Это пример так называемого **множественного наследования**, которое мы вкратце обсудим в разделе, посвященному продвинутым приемам программирования." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Наследование в Python реализуется очень просто: в инструкции `class` нужно после имени класса в скобках указать имя базового класса (или нескольких, но этот случай мы будем рассматривать позже). Следующий пример является весьма надуманным, однако он хорошо показывает, как использовать наследование в Python." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'parent_method']\n", "\n", "['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'descendant_method', 'parent_method']\n" ] } ], "source": [ "class A:\n", " def parent_method(self):\n", " print('parent_method')\n", "\n", "class B(A): # B наследник A\n", " def descendant_method(self):\n", " print('descendant_method')\n", "\n", "print(dir(A))\n", "print('')\n", "print(dir(B))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как видите, в пространстве имен класса `B` присутствуют и все имена из класса `A` (а также, разумеется, и из класса `object`, потому что `A` в свою очередь неявно наследуется от него). Это означает, что с помощью объектов класса `B` мы можем обращаться к методам класса `A` (но не наоборот!):" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "parent_method\n", "parent_method\n" ] }, { "ename": "AttributeError", "evalue": "'A' object has no attribute 'descendant_method'", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[0ma\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mparent_method\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;31m# правильно, parent_method опеределен в собственном классе\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[0mb\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mparent_method\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;31m# правильно, parent_method определен в родительском классе\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 6\u001b[1;33m \u001b[0ma\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdescendant_method\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;31m# ошибка, descendant_method определен только в наследнике!\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;31mAttributeError\u001b[0m: 'A' object has no attribute 'descendant_method'" ] } ], "source": [ "a = A()\n", "b = B()\n", "\n", "a.parent_method() # правильно, parent_method опеределен в собственном классе\n", "b.parent_method() # правильно, parent_method определен в родительском классе\n", "a.descendant_method() # ошибка, descendant_method определен только в наследнике!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Давайте теперь продемонстрируем, как используется полиморфизм в объектно-ориентированном программировании." ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "class A:\n", " def get_class_name(self):\n", " return 'class A'\n", "\n", "class B(A):\n", " def get_class_name(self):\n", " return 'class B'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Метод `get_class_name` называется **полиморфным** - для него существует реализация и в родительском, и в дочернем классе. Какой именно вариант использовать, интерепретатор будет решать в зависимости от того, для объекта какого типа он вызывается:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "class A\n", "class B\n" ] } ], "source": [ "def print_class_name(obj):\n", " print(obj.get_class_name())\n", "\n", "a = A()\n", "b = B()\n", "\n", "print_class_name(a)\n", "print_class_name(b)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Часто в ООП используется прием, когда в обычном (не полиморфном) методе базового класса реализуется общая логика работы, но в некоторых местах вызывается другой метод, который может быть переопределен наследниками. Рассмотрим пример класса, предоставляющего метод для подсчета разного вида символов в тексте." ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "class SymbolCounter:\n", " def get_counted_symbols(self):\n", " return 'All'\n", " \n", " def get_count(self, text):\n", " count = 0\n", " for symbol in text:\n", " # делегируем решение о том, учитывать символ или нет, подклассам\n", " if self._should_count(symbol):\n", " count += 1\n", " return count\n", " \n", " # этот метод будет переопределяться в наследниках\n", " def _should_count(self, symbol):\n", " # по умолчанию подсчитываем все символы\n", " return True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Обратите внимание, что имя метода `_should_count` начинается с символа подчеркивания - как мы уже говорили, это означает, что мы не хотим, чтобы этот метод вызывался сам по себе. Вместо этого мы предполагаем, что он будет переопределен в наследниках класса `SymbolsCounter` для того, чтобы обеспечить подсчет только конкретных символов." ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], "source": [ "class VowelCounter(SymbolCounter):\n", " def get_counted_symbols(self):\n", " return 'Vowels'\n", " def _should_count(self, symbol):\n", " # подсчитываем только гласные\n", " if symbol in 'aeiouyAEIOUY':\n", " return True\n", " return False\n", "\n", "class ConsonantCounter(SymbolCounter):\n", " def get_counted_symbols(self):\n", " return 'Consonants'\n", " def _should_count(self, symbol):\n", " # подсчитываем только согласные\n", " if symbol in 'bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ':\n", " return True\n", " return False" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь мы можем написать небольшую программу, которая подсчитывает количество разных видов символов в тексте и выводит его на экран." ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "All: 12\n", "Vowels: 3\n", "Consonants: 7\n" ] } ], "source": [ "def print_symbols_count(counter, text):\n", " print('{}: {}'.format(counter.get_counted_symbols(), counter.get_count(text)))\n", "\n", "text = 'Hello World!'\n", "\n", "all_counter = SymbolCounter()\n", "vowel_counter = VowelCounter()\n", "consonant_counter = ConsonantCounter()\n", "\n", "print_symbols_count(all_counter, text)\n", "print_symbols_count(vowel_counter, text)\n", "print_symbols_count(consonant_counter, text)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Наследование, когда применяется с умом, позволяет значительно упростить написание программ и увеличить степень повторной используемости классов. Рассмотрим пример, в котором мы реализуем примитивный класс `Line`, представляющий прямую на плоскости (напомним, что уравнение прямой имеет вид $y=k*x+b$). Также нам понадобится класс `Point`, описывающий произвольную точку на плоскости. В своей реализации для хранения координат мы будем использовать тип `Decimal`, потому что только он обеспечивает абсолютную точность при вычислениях с вещественными числами." ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "point (2, 6.45) belongs to line\n" ] } ], "source": [ "from decimal import Decimal\n", "\n", "class Point:\n", " def __init__(self, x, y):\n", " self.x = Decimal(x)\n", " self.y = Decimal(y)\n", "\n", "class Line:\n", " def __init__(self, k, b):\n", " self.k = Decimal(k)\n", " self.b = Decimal(b)\n", " \n", " def has(self, point):\n", " return point.y - (self.k * point.x + self.b) == 0\n", "\n", "\n", "# пример использования\n", "\n", "line = Line('1.5', '3.45')\n", "p = Point('2', '6.45')\n", "\n", "if line.has(p):\n", " print('point ({}, {}) belongs to line'.format(p.x, p.y))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Предположим, что теперь нам понадобилось ввести в программу класс для работы с отрезками. Про отрезок можно сказать, что он *является* прямой, органиченной с двух сторон, поэтому неплохой идей будет использовать `Line` в качестве базового класса." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для отрезка имеет смысл понятие длины, поэтому нам понадобится новый метод для ее вычисления. Кроме того, нам потребуется модифицировать метод `has` базового класса `Line`, чтобы учесть тот факт, что отрезки ограничены." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "При работе с полиморфными методами часто нужно вызвать их версию из базового класса. Это можно сделать с помощью встроенной в Python функции `super`, возвращающей специальный объект, через который доступны атрибуты и методы базового класса." ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "point (4, 3) belongs to segment\n", "Segment length: 5.000000000000000000000000000\n" ] } ], "source": [ "class LineSegment(Line):\n", " def __init__(self, k, b, x_min, x_max):\n", " # нам обязательно нужно вызывать конструктор базового класса Line, чтобы его\n", " # атрибуты были проинициализированы; используем для этого функцию super\n", " super().__init__(k, b)\n", " \n", " # инициализируем данные, имеющие отношение к отрезку\n", " self.x_min = Decimal(x_min)\n", " self.x_max = Decimal(x_max)\n", " \n", " def has(self, point):\n", " # проверяем, что точка лежит на прямой, на которой лежит сам отрезок\n", " if not super().has(point):\n", " return False\n", " \n", " # проверяем, что точка лежит на отрезке\n", " if self.x_min <= point.x <= self.x_max:\n", " return True\n", " return False\n", " \n", " def get_length(self):\n", " # применим теорему Пифагора для вычисления длины отрезка\n", " \n", " catheter1 = self.x_max - self.x_min\n", " catheter2 = (self.k * self.x_max + self.b) - (self.k * self.x_min + self.b)\n", " return (catheter1**2 + catheter2**2) ** Decimal('0.5')\n", "\n", "\n", "# пример использования\n", "\n", "segment = LineSegment('0.75', '0', '1', '5')\n", "p1 = Point('4', '3')\n", "p2 = Point('0', '0')\n", "\n", "if segment.has(p1):\n", " print('point ({}, {}) belongs to segment'.format(p1.x, p1.y))\n", "\n", "if segment.has(p2):\n", " print('point ({}, {}) belongs to segment'.format(p2.x, p2.y))\n", "\n", "print('Segment length:', segment.get_length())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "С помощью представленных классов мы можем таким образом реализовать функцию для определения точки пересечения двух прямых, отрезков или прямой и отрезка:" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "line1 intersects line2 at (3.5, 5.0)\n", "line1 intersects segment1 at (-2, 5)\n", "line2 does not intersect segment1\n" ] } ], "source": [ "def get_intersection_point(line1, line2):\n", " if line1.k == line2.k:\n", " # прямые (или отрезки) параллельны\n", " return None\n", " \n", " intersection_point = Point('0', '0')\n", " intersection_point.x = (line2.b - line1.b) / (line1.k - line2.k)\n", " intersection_point.y = line1.k * intersection_point.x + line1.b\n", " \n", " # отдельно нужно проверить, что точка пересечения прямых принадлежит\n", " # и line1, и line2 (это может быть не так, если одна из прямых на\n", " # самом деле является отрезком)\n", " if not line1.has(intersection_point) or not line2.has(intersection_point):\n", " return None\n", " return intersection_point\n", "\n", "\n", "# пример использования\n", "\n", "line1 = Line('0', '5')\n", "line2 = Line('2', '-2')\n", "segment1 = LineSegment('-0.5', '4', '-3', '-2')\n", "\n", "p1 = get_intersection_point(line1, line2)\n", "p2 = get_intersection_point(line1, segment1)\n", "p3 = get_intersection_point(segment1, line2)\n", "\n", "if p1 is not None:\n", " print('line1 intersects line2 at ({}, {})'.format(p1.x, p1.y))\n", "else:\n", " print('line1 does not intersect line2')\n", "\n", "if p2 is not None:\n", " print('line1 intersects segment1 at ({}, {})'.format(p2.x, p2.y))\n", "else:\n", " print('line1 does not intersect segment1')\n", "\n", "if p3 is not None:\n", " print('line2 intersects segment1 at ({}, {})'.format(p3.x, p3.y))\n", "else:\n", " print('line2 does not intersect segment1')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Композиция" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Композицией** или **агрегированием** в ООП называется методика создания класса из уже существующих путем их использования внутри нового класса для реализации его методов." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В качестве примера снова рассмотрим задачу из геометрии:" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "True\n", "False\n" ] } ], "source": [ "from decimal import Decimal\n", "\n", "class Point:\n", " def __init__(self, x, y):\n", " self.x = Decimal(x)\n", " self.y = Decimal(y)\n", " \n", " def get_distance(self, point):\n", " # возвращает расстояние до точки, вычисленное по теореме Пифагора\n", " catheter1 = self.x - point.x\n", " catheter2 = self.y - point.y\n", " return (catheter1**2 + catheter2**2) ** Decimal('0.5')\n", "\n", "class Circle:\n", " def __init__(self, center, radius):\n", " self.center = center\n", " self.radius = Decimal(radius)\n", " \n", " def has_intersection(self, circle):\n", " # обращаемся к методу класса Point для вычисления\n", " # расстояния между центрами окружности\n", " distance = self.center.get_distance(circle.center)\n", " \n", " if distance > self.radius + circle.radius:\n", " return False\n", " \n", " # теперь нужно определить, что одна окружность не вложена в другую\n", " \n", " min_radius = self.radius\n", " max_radius = circle.radius\n", " \n", " if self.radius > circle.radius:\n", " min_radius = circle.radius\n", " max_radius = self.radius\n", " \n", " if distance + min_radius < max_radius:\n", " return False\n", " \n", " return True\n", "\n", "circle1 = Circle(Point('2', '2'), '3')\n", "circle2 = Circle(Point('1', '5'), '5')\n", "circle3 = Circle(Point('2.2', '2.2'), '1.5')\n", "\n", "print(circle1.has_intersection(circle2))\n", "print(circle1.has_intersection(circle3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для реализации метода `has_intersection` класса `Circle` мы воспользовались композицией, делегировав задачу вычисления расстояния между центрами окружности объекту класса `Point`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Исключения" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Значительная часть исходного кода любой серьезной программы посвящена обработке ошибок, возникающих в процессе ее выполнения. При этом зачастую ошибочная ситуация обнаруживается в одном месте, а обрабатывается в другом, поэтому появляется необходимость каким-то образом передавать информацию о ней между частями исходного кода." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В процедурном программировании был принят подход, когда функция, обнаружившая ошибку при своем выполнении, в качестве результата возвращала некоторый числовой код, с помощью которого вызывающая функция могла определить тип ошибки. Например, так может выглядеть программа на языке программирования C, которая отправляет файл по сети на удаленный компьютер:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```c\n", "...\n", "#define FILE_NOT_FOUND 1\n", "#define CONNECTION_FAILED 2\n", "#define SEND_FAILED 3\n", "\n", "result = open_file('file.txt')\n", "if (result == FILE_NOT_FOUND)\n", "{ /*обрабатывается ошибка, когда файл не найден*/ }\n", "\n", "result = connect_to_host('drive.google.com')\n", "if (result == CONNECTION_FAILED)\n", "{ /*обрабатывается ошибка, когда не удается законнектиться к удаленному компьютеру*/ }\n", "\n", "result = send_file(connection, file)\n", "if (result == SEND_FAILED)\n", "{ /*обрабатывается ошибка, когда не удалось послать файл*/ }\n", "...\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Существенный недостаток такого подхода заключается в том, что код, в котором выполняется полезная работа, перемешивается с кодом, в котором обрабатываются ошибки, из-за чего программу становится сложнее читать и понимать." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В объектно-ориентированных языках программирования для обработки ошибок используется механизм исключений, базовый принцип работы которого заключается в следующем:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. При обнаружении ошибки, функция (или метод) возбуждает (еще говорят генерирует) **исключение**, представляющее собой объект некоторого класса, содержащий информацию об ошибке (например, текст ошибки, место, где она произошла и другое).\n", "2. Интерпретатор пытается найти код, который готов обработать данное исключение (**обработчик**), в функции (методе), где произошло исключение, затем в функции (методе), которая вызвала ее и т.д. Если подходящий обработчик был найден, то интерпретатор выполняет его, а затем продолжает выполнение основной программы (в этом случае говорят, что исключение было **перехвачено**) .\n", "3. Если исключение не было перехвачено, то интерпретатор сам обрабатывает его (как правило - выводит текст сообщения об ошибке) и завершает работу программы." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В Python исключения используются очень широко. Только во встроенном пространстве имен можно обнаружить более 30 классов исключений (они имеют слово \"error\" в своем названии), в чем можно убедиться с помощью функции `dir` или обратившись к [примеру](07_Functions_And_Modules.ipynb#builtins) из лекции 7. Более полную информацию об имеющихся исключениях можно получить из [справочного руководства](https://docs.python.org/3/library/exceptions.html) Python. Там же в [разделе 5.4](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) можно увидеть, какую иерархию образуют все типы исключений." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Базовым классом для всех исключений является `BaseException`. Он реализует методы, нужные для того, чтобы выводить текстовую информацию об исключении на экран. К этой информации в том числе относятся все аргументы, переданные в конструктор класса `BaseException`, которые сохраняются в атрибуте `args`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Использовать исключения для ошибочных ситуаций очень просто. Для этого в Python существует инструкция:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n",
    "raise exception_type(argument_list)\n",
    "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Здесь *exception_type* представляет класс исключения, а *argument_list* список (возможно пустой) аргументов, которые передаются в его конструктор." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Давайте напишем функцию `divide`, которая будет генерировать исключение, если в качестве знаменателя указан 0. Если обратиться к справочному руководству Python, то можно обнаружить в нем исключение `ZeroDivisionError`, которое является как раз тем, что нам нужно." ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "ename": "ZeroDivisionError", "evalue": "Can't divide 5 by 0!", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mZeroDivisionError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0ma\u001b[0m \u001b[1;33m/\u001b[0m \u001b[0mb\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 6\u001b[1;33m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdivide\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m5\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 7\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'program is finished'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32m\u001b[0m in \u001b[0;36mdivide\u001b[1;34m(a, b)\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mdivide\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0ma\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mb\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 2\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mb\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 3\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mZeroDivisionError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'Can\\'t divide {} by {}!'\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0ma\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mb\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 4\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0ma\u001b[0m \u001b[1;33m/\u001b[0m \u001b[0mb\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;31mZeroDivisionError\u001b[0m: Can't divide 5 by 0!" ] } ], "source": [ "def divide(a, b):\n", " if b == 0:\n", " raise ZeroDivisionError('Can\\'t divide {} by {}!'.format(a, b))\n", " return a / b\n", "\n", "print(divide(5, 0))\n", "print('program is finished')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Сгенерированное нами исключение осталось неперехваченным, и интерпретатор сам обработал его, завершив выполнение программы, вследствие чего последняя функция `print` так и не была вызвана." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ничто не мешает создавать нам собственные типы исключений. Единственное условие - они должны быть наследниками встроенного класса `Exception`. Часто собственные исключения представляют собой пустой класс, потому что вся необходимая функциональность уже есть в базовых классах." ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3.0\n" ] }, { "ename": "SqrtError", "evalue": "(\"Can't extract square root!\", -9)", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mSqrtError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 8\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 9\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0msqrt\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m9\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 10\u001b[1;33m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0msqrt\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m-\u001b[0m\u001b[1;36m9\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;32m\u001b[0m in \u001b[0;36msqrt\u001b[1;34m(a)\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0msqrt\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0ma\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0ma\u001b[0m \u001b[1;33m<\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 6\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mSqrtError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'Can\\'t extract square root!'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0ma\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 7\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0ma\u001b[0m \u001b[1;33m**\u001b[0m \u001b[1;36m0.5\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 8\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;31mSqrtError\u001b[0m: (\"Can't extract square root!\", -9)" ] } ], "source": [ "class SqrtError(Exception): pass\n", "\n", "# вычисляет квадратный корень из числа\n", "def sqrt(a):\n", " if a < 0:\n", " raise SqrtError('Can\\'t extract square root!', a)\n", " return a ** 0.5\n", "\n", "print(sqrt(9))\n", "print(sqrt(-9))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Обратите внимание, что при возбуждении исключения `SqrtError` мы указали текст ошибки и значение, которое ее вызвало, а интерпретатор потом вывел эту информацию на экран. Это работает потому, что в базовых классах исключения `SqrtError` уже реализовано все необходимое для этого." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для перехвата исключений используются блоки `try...except...finally`, имеющие следующий синтаксис:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n",
    "try:\n",
    "    try_code_block\n",
    "except exception_list_1 as variable_1:\n",
    "    except_code_block_1\n",
    "...\n",
    "except exception_list_N as variableN:\n",
    "    except_code_block_N\n",
    "else:\n",
    "    else_code_block\n",
    "finally:\n",
    "    finally_code_block\n",
    "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Давайте разберем подробно компоненты этой конструкции:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* *try_code_block* содержит инструкции, для которых мы хотим выполнять перехват исключений\n", "* *except_code_block_K* содержит блок кода, являющийся обработчиком исключений, указанных в соответствующем *exception_list_K*\n", "* *else_code_block* содержит инструкции, которые должны быть выполнены, если никаких исключений при выполнении *try_code_block* не возникло\n", "* *finally_code_block* содержит инструкции, которые должны быть выполнены в любом случае, причем в самом конце (либо после *try_code_block*, если не было исключений, либо после *except_code_block_K*, если произошло одно из *exception_list_K* исключений, либо после того, как было сгенерировано исключение, но не был найден подходящий обработчик, и интерпретатор продолжил его поиск в вызывающей функции или методе)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Корректный вариант представленной конструкции обязательно должен содержать предложение `try` и хотя бы одно предложение `except` или `finally`, все остальное можно не указывать, если в этом нет необходимости." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Список перехватываемых исключений *exception_list_K* может отсутствовать в предложении `except`. В этом случае, перехватываться будет исключение любого типа." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Часть *as* предложения `except` также является необязательной. Она используется для того, чтобы при перехвате исключения создать новую переменную с именем *variableK*, и присвоить ей ссылку на объект исключения. Тогда в обработчике можно будет обращаться к данным, хранящимся в исключении. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Начнем рассматривать примеры обработки исключений, начиная с простых и двигаясь к все более сложным. Для начала реализуем функцию деления в старом процедурном стиле:" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "def divide_old(a, b):\n", " if b == 0:\n", " return None\n", " return a / b" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "С помощью такой функции мы можем написать такую программу:" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3.0\n", "program is finished\n" ] } ], "source": [ "a = 10\n", "b = 5\n", "\n", "result = divide_old(a, b)\n", "result += 1\n", "print(result)\n", "print('program is finished')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "С этим кодом все в порядке ровно до тех пор, пока переменная `b` не окажется равной 0:" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "unsupported operand type(s) for +=: 'NoneType' and 'int'", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[0mresult\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mdivide_old\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0ma\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mb\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 5\u001b[1;33m \u001b[0mresult\u001b[0m \u001b[1;33m+=\u001b[0m \u001b[1;36m1\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 6\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mresult\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 7\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'program is finished'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;31mTypeError\u001b[0m: unsupported operand type(s) for +=: 'NoneType' and 'int'" ] } ], "source": [ "a = 10\n", "b = 0\n", "\n", "result = divide_old(a, b)\n", "result += 1\n", "print(result)\n", "print('program is finished')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "По этой причине нам еще нужно добавить проверку на то, что функция `divide_old` отработала без ошибок:" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "error!\n", "program is finished\n" ] } ], "source": [ "a = 10\n", "b = 0\n", "\n", "result = divide_old(a, b)\n", "\n", "if result is not None:\n", " result += 1\n", " print(result)\n", "else:\n", " print('error!')\n", "\n", "print('program is finished')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим, как эту же задачу можно решить с помощью функции `divide`, использующей исключение при попытке деления на ноль:" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "error!\n", "program is finished\n" ] } ], "source": [ "a = 10\n", "b = 0\n", "\n", "try:\n", " result = divide(a, b)\n", " # следующие инструкции не выполнятся, если в divide произойдет исключение\n", " \n", " result += 1\n", " print(result) \n", "\n", "except: # перехватываем любое исключение\n", " print('error!')\n", "\n", "print('program is finished')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "По выводу нашей программы мы видим, что при возникновении исключения интерпретатор немедленно прекращает выполнение *try_code_block* и переходит к коду в подходящем *except_code_block*. После того, как этот блок кода выполнен, интерпретатор считает, что исключение обработано, и начинает выполнение первой инструкции после `try...except...finally`. Важно понять, что интерпретатор **не продолжает** выполнение *try_code_block* после обработки исключения, поэтому внутри него мы можем писать код, не задумываясь об ошибках, которые могли произойти ранее в этом блоке. Мы добились того, что у нас четко разделена основная часть программы (то, что в блоке *try_code_block*) и обработка ошибок (то, что в *except_code_block*), в отличие от примера с функцией `divide_old`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если мы хотим перехватить конкретный тип исключений, то нужно указать это в `except`:" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(\"Can't extract square root!\", -1)\n" ] } ], "source": [ "try:\n", " print(sqrt(-1))\n", "except SqrtError as err:\n", " print(err)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В одном предложении `except` можно перечислить сразу несколько типов исключений, например так:" ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(\"Can't extract square root!\", -4.0)\n" ] } ], "source": [ "try:\n", " result = divide(-8, 2)\n", " print(sqrt(result))\n", "except (ZeroDivisionError, SqrtError) as err:\n", " print(err)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если разные типы исключений нужно обрабатывать по-разному, то для этого потребуется использовать несколько предложений `except`:" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "some argument was invalid!\n" ] } ], "source": [ "try:\n", " # при попытке применить операцию деления к строковому литералу внутри\n", " # функции divide, интерпретатор сгенерируется исключение TypeError\n", " result = divide('10', 2) \n", " print(sqrt(a))\n", "except (ZeroDivisionError, SqrtError) as err:\n", " print(err)\n", "except TypeError:\n", " print('some argument was invalid!')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Когда интерпретатор выбирает, какой *except_code_block* использовать для обработки исключения, он просматривает предложения `except` сверху вниз и сравнивает типы, указанные в них, с типом исключения. При этом *except_code_block* считается найденным, если:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* тип, указанный в предложении `except`, в точности совпадает с типом исключения\n", "* тип, указанный в предложении `except`, является родительским для типа исключения" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Рассмотрим пример, поясняющий это (заметим, что встроенное исключение `ZeroDivisionError` является потомком более общего типа `ArithmeticError`, см. [здесь](https://docs.python.org/3/library/exceptions.html#exception-hierarchy)):" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Can't divide 1 by 0!\n" ] } ], "source": [ "try:\n", " print(divide(1, 0))\n", "except ArithmeticError as err:\n", " print(err)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Про эту особенность выбора обработчика исключения стоит помнить, иначе можно допустить труднообнаруживаемую ошибку, например:" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "not interesting error\n" ] } ], "source": [ "try:\n", " print(divide(1, 0))\n", "except ArithmeticError:\n", " print('not interesting error')\n", "except ZeroDivisionError as err:\n", " # никогда не выполнится, потому что при возникновении этого исключения,\n", " # первым для него будет найден предыдущий обработчик\n", " print(err)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Чтобы этот пример работал правильно, нужно переписать его таким образом:" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Can't divide 1 by 0!\n" ] } ], "source": [ "try:\n", " print(divide(1, 0))\n", "except ZeroDivisionError as err:\n", " # обрабатываем ошибку, связанную с делением на ноль\n", " print(err)\n", "except ArithmeticError:\n", " # обрабатываем все остальные арифметические ошибки\n", " print('not interesting error')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как мы уже говорили, если тип возникшего исключения не был найден среди типов, указанных в предложениях `except`, интерпретатор начинает поиск обработчика в функции, вызвавшей ту, где произошло исключение. Этот процесс продолжается до тех пор, пока не найден соответствующий обработчик, либо пока интерпретатор не дойдет до функции самого верхнего уровня." ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "incorrect call of function try_divide!\n" ] } ], "source": [ "def try_divide(a, b):\n", " try:\n", " print(divide(a, b)) \n", " except ZeroDivisionError as err:\n", " print(err)\n", "\n", "\n", "# основная часть программы является функцией самого верхнего уровня\n", "\n", "try:\n", " try_divide(10, '10')\n", "except TypeError:\n", " print('incorrect call of function try_divide!')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В примере выше исключение с типом `TypeError` было сгенерировано внутри функции `divide`, а обработчик для него нашелся только в функции верхнего уровня." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В заключение расскажем о том, для чего нужны предложения `else` и `finally`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как было сказано ранее, предложение `else` позволяет определять блок кода, который будет выполнен только если *try_code_block* завершился нормально (не было сгенерировано исключение). Это может быть полезно в такой ситуации:" ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "division successful\n", "error occurred in do_something function\n" ] } ], "source": [ "# предположим, что это некоторая сложная функция, которая генерирует\n", "# различные исключения (в нашем примере, конечно, это не так)\n", "def do_something(value):\n", " print(sqrt(value))\n", "\n", "def start(a, b):\n", " try:\n", " res = divide(a, b)\n", " except:\n", " print('error occurred while dividing numbers!')\n", " else:\n", " print('division successful')\n", " do_something(res)\n", "\n", "\n", "# основная программа\n", "\n", "try:\n", " start(-4, 2)\n", "except:\n", " print('error occurred in do_something function')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В примере выше если бы мы вызвали `do_something` внутри *try_code_block* функции `start`, то любые исключения, сгенерированные в процессе ее выполнения, были бы перехвачены в самой функции `start`. Однако там у нас может быть недостаточно информации о том, как их правильно обработать, поэтому нам хотелось бы, чтобы функция `start` перехватывала только исключения, связанные с делением, а все остальные были переданы в функции верхнего уровня." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Предложение `finally` нужно для того, чтобы иметь возможность выполнить некоторый код при любом результате выполнения *try_code_block*. В подавляющем большинстве случаев, в *finally_code_block* пишут код, который освобождает ресурсы, которые были использованы при выполнении *try_code_block*. В одной из следующих лекций мы поговорим о том, как работать с файлами с помощью языка Python, пока лишь скажем, что вначале файл нужно открыть, а после работы с ним - закрыть. Если этого не сделать, то изменения могут не сохраниться. Один из способов гарантировать, что открытый файл всегда будет закрыт, такой:" ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "open\n", "saved 3.0\n", "close\n" ] } ], "source": [ "# определим функции, которые как бы \"работают\" с файлом\n", "\n", "def open_file():\n", " print('open')\n", "def save_to_file(number):\n", " print('saved', number)\n", "def close_file():\n", " print('close')\n", "\n", "try:\n", " open_file()\n", " \n", " result = divide(9, 1)\n", " result = sqrt(result)\n", " \n", " save_to_file(result)\n", "except SqrtError as err:\n", " print(err)\n", "finally:\n", " close_file()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В том, что функция `close_file` будет вызвана, даже если произойдет любое исключение, вам предлагается убедиться самостоятельно." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Продвинутые приемы программирования" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В этом разделе мы вкратце познакомим вас с еще несколькими конструкциями языка Python, которые могут быть очень полезными при объектно-ориентированном программировании. Дополнительную информацию по ним можно найти в справочном руководстве Python, которое откроется, если нажать *Python Reference* в меню *Help*." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Декораторы" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Декоратором** в Python называется специальная функция, которая в качестве аргумента принимает другую функцию или метод и возвращает ее \"декорированную\" версию, то есть версию, измененную некоторым образом." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Одним из наиболее часто используемых декораторов называется `property`. Он используется для создания **свойств** - методов, которые выглядят и используются как обычные атрибуты. Вспомним класс `TextInfo`, который мы реализовали чуть раньше. В нем при изменении текста нам важно было установить атрибут `_is_calculated` в `False`, чтобы в дальнейшем были пересчитаны атрибуты, хранящие количество слов и предложений. Чтобы обезопасить наш класс от неправильного использования, мы скрыли атрибут, хранящий текст, и предоставили два метода (`get_text` и `set_text`) для доступа к нему." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Более изящным решением в данном случае будет использование декоратора `property`. Он позволяет создать два метода - один для чтения атрибута (*getter*-метод), другой для записи (*setter*-метод) - и присвоить им одинаковое имя. Делается это следующим образом:" ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [], "source": [ "class TextInfo:\n", " def __init__(self, value):\n", " self._text = value\n", " self._is_calculated = False\n", " self._words_count = 0\n", " self._sentences_count = 0\n", " \n", " # создаем свойство text и определяем getter-метод для получения текста\n", " @property\n", " def text(self):\n", " print('getter called')\n", " return self._text;\n", " \n", " # для свойства text устанавливаем setter-метод для установки текста\n", " @text.setter\n", " def text(self, value):\n", " print('setter called')\n", " self._text = value\n", " self._is_calculated = False\n", " \n", " def get_symbols_count(self):\n", " return len(self._text)\n", " \n", " def get_words_count(self):\n", " self._calculate()\n", " return self._words_count\n", " \n", " def get_sentences_count(self):\n", " self._calculate()\n", " return self._sentences_count\n", " \n", " def _calculate(self): \n", " if self._is_calculated:\n", " return\n", " \n", " self._words_count = 0\n", " self._sentences_count = 0\n", " in_word = False\n", " in_sentence = False\n", " \n", " for symbol in self._text:\n", " if symbol == ' ' or symbol == ',' or symbol == ':' or symbol == '-':\n", " if in_word:\n", " self._words_count += 1\n", " in_word = False\n", " elif symbol == '.' or symbol == '!' or symbol == '?':\n", " if in_word:\n", " self._words_count += 1\n", " if in_sentence:\n", " self._sentences_count += 1\n", " in_word = in_sentence = False\n", " else:\n", " in_word = in_sentence = True\n", " \n", " # все подсчитано, не забываем зафиксировать это!\n", " self._is_calculated = True" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь мы можем писать такой код:" ] }, { "cell_type": "code", "execution_count": 49, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "getter called\n", "text: Hello World!\n", "words count: 2\n", "\n", "setter called\n", "getter called\n", "text: Testing setter method of a property.\n", "words count: 6\n" ] } ], "source": [ "info = TextInfo('Hello World!')\n", "\n", "print('text:', info.text)\n", "print('words count:', info.get_words_count())\n", "print('')\n", "\n", "info.text = 'Testing setter method of a property.'\n", "print('text:', info.text)\n", "print('words count:', info.get_words_count())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Еще два встроенных в Python декоратора называются `staticmethod` и `classmethod`. Оба они позволяют создать методы, которые можно вызывать без объекта (этим они напоминают [статические атрибуты](#Статические-атрибуты))." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Метод, который вы хотите определить с помощью декоратора `classmethod` обязательно должен содержать как минимум один параметр с общепринятым именем `cls`. Если обычные методы в параметре `self` получают ссылку на объект, для которого они вызваны, то методы, декорированные с помощью `classmethod`, в параметре `cls` получают класс, для которого они вызваны." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "На метод, определяемый с помощью декоратора `staticmethod`, никаких специальных ограничений не накладывается." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Рассмотрим в качестве примера наш класс `LineSegment`. Для создания его объектов мы должны передать параметры прямой, на которой лежит отрезок, а также два значения, которые ограничивают эту прямую слева и справа - полезно было бы также дать возможность создавать отрезок, просто указывая две точки. Второй момент - мы предполагаем, что операция получения длины будет часто использоваться сама по себе, поэтому нам бы не хотелось каждый раз создавать объект для того, чтобы выполнить лишь этот метод. Ну и наконец, добавим проверки на случай, если отрезок создается неправильно." ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "segment equation: 2*x + 1 in [0, 3]\n", "6.708203932499369089227521006\n", "6.708203932499369089227521006\n" ] } ], "source": [ "class LineError(Exception): pass\n", "\n", "class LineSegment(Line):\n", " def __init__(self, k, b, x_min, x_max):\n", " if x_min >= x_max:\n", " raise LineError('incorrect segment bounds')\n", " \n", " super().__init__(k, b)\n", " self.x_min = Decimal(x_min)\n", " self.x_max = Decimal(x_max)\n", " \n", " @classmethod\n", " def create_from_points(cls, p1, p2):\n", " if p1.x == p2.x:\n", " raise LineError('can\\'t create line equation')\n", " \n", " # находим параметры прямой, которая проходит через две точки\n", " k = (p1.y - p2.y) / (p1.x - p2.x)\n", " b = p1.y - k * p1.x\n", " \n", " # создаем и возвращаемый новый объект класса LineSegment\n", " if p1.x < p2.x:\n", " return cls(k, b, p1.x, p2.x)\n", " else:\n", " return cls(k, b, p2.x, p1.x)\n", " \n", " @staticmethod\n", " def calc_length(p1, p2):\n", " catheter1 = p1.x - p2.x\n", " catheter2 = p1.y - p2.y\n", " return (catheter1**2 + catheter2**2) ** Decimal('0.5')\n", " \n", " def has(self, point):\n", " if not super().has(point):\n", " return False\n", " if self.x_min <= point.x <= self.x_max:\n", " return True\n", " return False\n", " \n", " def get_length(self):\n", " p1 = Point(self.x_min, self.k * self.x_min + self.b)\n", " p2 = Point(self.x_max, self.k * self.x_max + self.b)\n", " \n", " # используем статический метод calc_length для вычисления длины\n", " return self.calc_length(p1, p2)\n", "\n", "\n", "# пример использования\n", "\n", "p1 = Point('0', '1')\n", "p2 = Point('3', '7')\n", "segment1 = LineSegment.create_from_points(p1, p2)\n", "\n", "# символ \\ в конце строки используется для того, чтобы переносить длинные строки;\n", "# когда интерпретатор встречает его, он понимает, что выражение будет продолжено\n", "# на следующей строке\n", "print('segment equation: {}*x + {} in [{}, {}]'.format(\\\n", " segment1.k, segment1.b, segment1.x_min, segment1.x_max))\n", "\n", "print(LineSegment.calc_length(p1, p2))\n", "print(segment1.get_length())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В заключение стоит сказать, что в языке Python существует возможность создавать свои собственные декораторы, которые в дальнейшем могут использоваться ровно так же, как и встроенные. Например, можно создать декоратор, который проверяет определенным образом аргументы, с которыми был вызван метод (чтобы они были определенного типа или значения), и генерирует исключение в случае обнаружения ошибки. Информацию о том, как это делается, можно получить из справочного руководства, а в нашем курсе мы не будем углубляться в эту тему." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Перегрузка операций" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как мы уже говорили, все типы данных в языке Python представляют собой объекты некоторых классов. Про выполнении **любой** [операции](05_Operations.ipynb) (`+`, `-`, `not` и т.д.) с объектом, интерпретатор ищет в его пространстве имен строго определенный специальный метод, который считается реализацией данной операции, и вызывает его." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Мощным инструментом, позволяющим использовать общепринятую в математике форму записи арифметических и иных выражений для собственных типов данных, является **перегрузка операций**. Смысл ее заключается в том, что определяя в классе специальные методы, мы затем можем использовать обычные операции Python для его объектов." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Рассмотрим в качестве примера уже встречавшийся нам класс `Circle`, но теперь определим для него метод, возвращающий площадь круга:" ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [], "source": [ "from math import pi\n", "\n", "class Circle:\n", " def __init__(self, radius):\n", " self.radius = radius\n", " \n", " def get_area(self):\n", " return pi * (self.radius ** 2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Мы бы хотели иметь возможность сравнивать два круга по их площади. Если мы сейчас попытаемся воспользоваться для этой цели операцией `==`, то всегда будем получать результат `False`, поскольку интерпретатор не знает, как нужно обрабатывать ее для объектов наших типов:" ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "False\n" ] } ], "source": [ "c1 = Circle(3)\n", "c2 = Circle(3)\n", "print(c1 == c2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Конечно, мы могли бы определить для этой цели метод и использовать его, но код при этом станет менее удобочитаемым. Лучшим вариантом будет перегрузка операции `==` для типа `Circle`. Для этого нам нужно определить в нем специальный метод `__eq__`, который ищется интерпретатором, когда он выполняет операцию `==`. В примерах ниже мы дополнительно выводим некоторую информацию об объектах, чтобы удобнее было анализировать результаты программы." ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "id=114806120, radius=3\n", "id=114804048, radius=3\n", "id=114806120: Circle.eq\n", "True\n" ] } ], "source": [ "class Circle:\n", " def __init__(self, radius):\n", " print('id={}, radius={}'.format(id(self), radius))\n", " self.radius = radius\n", " \n", " def get_area(self):\n", " return pi * (self.radius ** 2)\n", " \n", " def __eq__(self, other):\n", " print('id={}: Circle.eq'.format(id(self)))\n", " return self.get_area() == other.get_area()\n", "\n", "\n", "c1 = Circle(3)\n", "c2 = Circle(3)\n", "print(c1 == c2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Все специальные методы для [бинарных](05_Operations.ipynb#Определение-операции) операций (в том числе и [операций сравнения](05_Operations.ipynb#Операции-сравнения)) в качестве аргументов принимают два объекта, участвующие в ней. Например, при выполнении операции `==` в примере сверху, интерпретатор по сути выполняет инструкцию `c1.__eq__(c2)`. Для унарной операции соответствующий специальный метод имеет один параметр `self`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Рассмотрим другие специальные методы, с помощью которых можно перегрузить операции сравнения:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "| Операция | Какой метод вызывается |\n", "|----------------------------------------------|----------------------------------------|\n", "|
x < y
|x.__lt__(y)|\n", "|
x <= y
|x.__le__(y)|\n", "|
x == y
|x.__eq__(y)|\n", "|
x != y
|x.__ne__(y)|\n", "|
x >= y
|x.__ge__(y)|\n", "|
x > y
|x.__gt__(y)|" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если для типа определена операция `==`, то операцию `!=` переопределять не обязательно - в случае ее отсутствия интерпретатор просто воспользуется операцией `==`, а затем возьмет противоположный результат:" ] }, { "cell_type": "code", "execution_count": 54, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "id=114715784, radius=3\n", "id=114715504, radius=1\n", "id=114715784: Circle.eq\n", "True\n" ] } ], "source": [ "c1 = Circle(3)\n", "c2 = Circle(1)\n", "print(c1 != c2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для того, чтобы добавить к своему типу поддержку вообще всех операций сравнения, достаточно определить в своем типе лишь три метода:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. `__eq__` - наличие его в классе позволяет интерпретатору не только выполнять операцию `== `с объектами этого класса, но и операцию `!=`, как было показано выше\n", "2. `__lt__` или `__gt__`, потому что интерпретатор умеет выводить один из другого (выражение \"x меньше y\" является тем же самым, что выражение \"y больше x\")\n", "3. `__le__` или `__ge__` по той же причине, что указана в предыдущем пункте" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Добавим поддержку всех операций сравнения в наш класс `Circle`:" ] }, { "cell_type": "code", "execution_count": 55, "metadata": {}, "outputs": [], "source": [ "class Circle:\n", " def __init__(self, radius):\n", " print('id={}, radius={}'.format(id(self), radius))\n", " self.radius = radius\n", " \n", " def get_area(self):\n", " return pi * (self.radius ** 2)\n", " \n", " def __eq__(self, other):\n", " print('id={}: Circle.eq'.format(id(self)))\n", " return self.get_area() == other.get_area()\n", " \n", " def __lt__(self, other):\n", " print('id={}: Circle.lt'.format(id(self)))\n", " return self.get_area() < other.get_area()\n", "\n", " def __le__(self, other):\n", " print('id={}: Circle.le'.format(id(self)))\n", " return self.get_area() <= other.get_area()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проверим, как это работает (обратите пристальное внимание на то, для каких именно объектов вызывается тот или иной метод):" ] }, { "cell_type": "code", "execution_count": 56, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "id=114910264, radius=1\n", "id=113973512, radius=2\n", "\n", "id=114910264: Circle.eq\n", "True\n", "id=114910264: Circle.lt\n", "True\n", "id=113973512: Circle.lt\n", "False\n", "id=113973512: Circle.le\n", "False\n" ] } ], "source": [ "c1 = Circle(1)\n", "c2 = Circle(2)\n", "\n", "print('')\n", "print(c1 != c2)\n", "print(c1 < c2)\n", "print(c1 > c2)\n", "print(c1 >= c2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В следующей таблице представлены специальные методы, которые вызываются, когда интерпретатор выполняет соответствующие [арифметические](05_Operations.ipynb#Арифметические-операции) или [битовые](05_Operations.ipynb#Битовые-операции) операции:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "| Арифметическая операция | Метод ||||| Битовая операция | Метод |\n", "|---------------------------------------------|----------------------------------|||||---------------------------------------------|----------------------------------|\n", "|
x + y
|x.__add__(y) |||||
x & y
|x.__and__(y) |\n", "|
x - y
|x.__sub__(y) |||||
x | y
|x.__or__(y) |\n", "|
x * y
|x.__mul__(y) |||||
x ^ y
|x.__xor__(y) |\n", "|
x / y
|x.__truediv__(y) |||||
x << y
|x.__lshift__(y) |\n", "|
x // y
|x.__floordiv__(y)|||||
x >> y
|x.__rshift__(y) |\n", "|
x % y
|x.__mod__(y) |||||
~x
|x.__invert__() |\n", "|
x ** y
|x.__pow__(y) ||||\n", "|
-x
|x.__neg__() ||||" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для краткости мы не стали включать в представленные таблицы имена специальных методов для комбинированных инструкций присваивания, имеющих вид *x op= y*. Эти методы имеют такие же имена, как и те, что соответствуют простой бинарной операции *op*, но с префиксом \"i\". Например, для операции `+=` интерпретатор будет искать метод `__iadd__`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Также для каждой бинарной операции существует версия соответствующего специального метода с префиксом \"r\", например `__radd__`. О том, как они используется интерпретатором, мы поговорим чуть позже." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для демонстрации всей выразительной мощи переопределения операций создадим свою реализацию встроенного в Python типа `complex`, который используется для представления [комплексных чисел](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BC%D0%BF%D0%BB%D0%B5%D0%BA%D1%81%D0%BD%D0%BE%D0%B5_%D1%87%D0%B8%D1%81%D0%BB%D0%BE)." ] }, { "cell_type": "code", "execution_count": 57, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1.1 + 2.5i\n", "1.1 + 3.0i\n" ] } ], "source": [ "# представляет числа в формате a + b*i, где i - мнимая единица\n", "class MyComplex:\n", " def __init__(self, real, imag):\n", " self.a = real # действительная часть комплексного числа\n", " self.b = imag # мнимая часть комплексного числа\n", " \n", " # c1 + c2\n", " def __add__(self, other):\n", " result = MyComplex(self.a + other.a, self.b + other.b)\n", " return result\n", " \n", " # c1 += c2\n", " def __iadd__(self, other):\n", " self.a += other.a\n", " self.b += other.b\n", " return self\n", "\n", "c1 = MyComplex(1.1, -3)\n", "c2 = MyComplex(0, 5.5)\n", "\n", "c3 = c1 + c2\n", "print('{} + {}i'.format(c3.a, c3.b))\n", "\n", "c3 += MyComplex(0, 0.5)\n", "print('{} + {}i'.format(c3.a, c3.b))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для комплексных чисел имеет смысл операция сложения с действительными и целыми. Пока наш класс это не поддерживает, потому что в реализации метода `__add__` мы обращаемся к атрибутам `a` и `b` параметра `other`, и если в качестве него будет передано обычное число (тип `float` или `int`), то интерпретатор сгенерирует такое исключение:" ] }, { "cell_type": "code", "execution_count": 58, "metadata": {}, "outputs": [ { "ename": "AttributeError", "evalue": "'float' object has no attribute 'a'", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mc3\u001b[0m \u001b[1;33m+\u001b[0m \u001b[1;36m1.1\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;32m\u001b[0m in \u001b[0;36m__add__\u001b[1;34m(self, other)\u001b[0m\n\u001b[0;32m 7\u001b[0m \u001b[1;31m# c1 + c2\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 8\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0m__add__\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mother\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 9\u001b[1;33m \u001b[0mresult\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mMyComplex\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0ma\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0mother\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0ma\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mb\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0mother\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mb\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 10\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mresult\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 11\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;31mAttributeError\u001b[0m: 'float' object has no attribute 'a'" ] } ], "source": [ "c3 + 1.1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Исправить это нам поможет рассмотренная ранее функция `isinstance`, которая позволяет узнать, объектом какого класса является переменная. Используя ее, доработаем наш класс:" ] }, { "cell_type": "code", "execution_count": 59, "metadata": {}, "outputs": [], "source": [ "class MyComplex:\n", " def __init__(self, real, imag):\n", " self.a = real # действительная часть комплексного числа\n", " self.b = imag # мнимая часть комплексного числа\n", " \n", " def __add__(self, other):\n", " if isinstance(other, MyComplex):\n", " return MyComplex(self.a + other.a, self.b + other.b)\n", " else:\n", " return MyComplex(self.a + other, self.b)\n", " \n", " def __iadd__(self, other):\n", " if isinstance(other, MyComplex):\n", " self.a += other.a\n", " self.b += other.b\n", " else:\n", " self.a += other\n", " return self" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь мы свободно можем писать следующие выражения:" ] }, { "cell_type": "code", "execution_count": 60, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2.0 + 1i\n" ] } ], "source": [ "c1 = MyComplex(1, 1)\n", "c2 = c1 + 0.5\n", "c2 += 0.5\n", "print('{} + {}i'.format(c2.a, c2.b))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "К сожалению, поскольку встроенные типы Python ничего не знают о нашем классе `MyComplex`, при попытке поменять местами слагаемые в выражении `c1 + 0.5` мы снова получим ошибку. Это произойдет потому, что интерпретатор вызовет метод `__add__` у класса `float` и передаст ему в качестве аргумента `c1`, с которым, очевидно, тип `float` не будет знать, что делать:" ] }, { "cell_type": "code", "execution_count": 61, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "unsupported operand type(s) for +: 'float' and 'MyComplex'", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[1;36m0.5\u001b[0m \u001b[1;33m+\u001b[0m \u001b[0mc1\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;31mTypeError\u001b[0m: unsupported operand type(s) for +: 'float' and 'MyComplex'" ] } ], "source": [ "0.5 + c1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "На помощь к нам в этой ситуации приходят специальные методы с префиксом \"r\". Дело в том, что интерпретатор ищет нужный специальный метод по следующему алгоритму (на примере операции `x + y`):" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. Вначале он пытается найти метод `__add__` у объекта `x`. Если метод найден, то интерпретатор вызывает его с аргументом `y`.\n", "2. Если метод не найден, или он вернул специальное значение `NotImplemented`, то интерпретатор пытается найти метод `__radd__` у объекта `y`. Если метод найден, то интерпретатор вызывает его с аргументом `x`, а иначе генерирует исключение." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Продемонстрируем вышесказанное на простом примере:" ] }, { "cell_type": "code", "execution_count": 62, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "A.add\n", "(a + 1) is done\n", "B.radd\n", "(a + b) is done\n" ] } ], "source": [ "class A:\n", " def __add__(self, other):\n", " if isinstance(other, int):\n", " print('A.add')\n", " return\n", " return NotImplemented\n", "\n", "class B:\n", " def __radd__(self, other):\n", " print('B.radd')\n", "\n", "a = A()\n", "b = B()\n", "\n", "a + 1\n", "print('(a + 1) is done')\n", "a + b\n", "print('(a + b) is done')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как видите, во втором сложении метод `A.__add__` вернул `NotImplemented`, поэтому интерпретатор продолжил поиск подходящего метода в классе `B` и вызвал его." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Все встроенные типы Python возвращают `NotImplemented` в случаях, когда встречают в своих специальных методах неизвестные типы. Такому подходу рекомендуют следовать всем программистам на Python, потому что это позволяет в дальнейшем интегрировать новые типы в существующую систему." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "С учетом того, что мы теперь знаем, давайте представим окончательный вариант реализации класса `MyComplex`:" ] }, { "cell_type": "code", "execution_count": 63, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "11 + 3*i\n" ] } ], "source": [ "class MyComplex:\n", " def __init__(self, real, imag):\n", " self.a = real # действительная часть комплексного числа\n", " self.b = imag # мнимая часть комплексного числа\n", " \n", " def __add__(self, other):\n", " if isinstance(other, MyComplex):\n", " return MyComplex(self.a + other.a, self.b + other.b)\n", " elif isinstance(other, int) or isinstance(other, float):\n", " return MyComplex(self.a + other, self.b)\n", " else:\n", " return NotImplemented\n", " \n", " def __radd__(self, other):\n", " if isinstance(other, int) or isinstance(other, float):\n", " return MyComplex(self.a + other, self.b)\n", " raise TypeError('Unsupported Type')\n", " \n", " def __iadd__(self, other):\n", " if isinstance(other, MyComplex):\n", " self.a += other.a\n", " self.b += other.b\n", " else:\n", " self.a += other\n", " return self\n", " \n", " def __str__(self):\n", " return '{} + {}*i'.format(self.a, self.b)\n", "\n", "\n", "# пример использования\n", "\n", "c1 = MyComplex(1, 3)\n", "c1 += 10\n", "\n", "# интерпретатор использует метод `__str__` для того, чтобы получить\n", "# строковое представление объекта тогда, когда это ему нужно (например,\n", "# при передаче объекта в функцию print)\n", "print(c1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Мы добавили еще специальный метод `__str__`, который используется интерпретатором для получения строкового представления объекта (например, когда он передается в функцию `print`). Похожим методом является `__repr__`, возвращающий строку, с помощью которой можно воссоздать объект, т.е. если написать ее в исходном коде и выполнить, интерпретатором будет создан идентичный объект. Метод `__repr__` используется функцией `print`, в случае, если метод `__str__` не определен." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Еще одним интересным специальным методом, который часто перегружается, является метод `__call__`. Если класс содержит реализацию этого метода, то его объекты могут использоваться как функции - их можно \"вызывать\". Такие объекты в Python называются **вызываемыми** объектами или **функторами**. Их прелесть в том, что как объекты, они могут иметь атрибуты и использовать их для хранения некоторого состояния между вызовами. Рассмотрим пример функции, которая генерирует [арифметическую прогрессию](https://ru.wikipedia.org/wiki/%D0%90%D1%80%D0%B8%D1%84%D0%BC%D0%B5%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B5%D1%81%D1%81%D0%B8%D1%8F):" ] }, { "cell_type": "code", "execution_count": 64, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0\n", "5\n", "10\n" ] } ], "source": [ "class ArithmeticProgression:\n", " def __init__(self, step, start = 0):\n", " self._start = start\n", " self._next_element = start\n", " self._step = step\n", " \n", " def __call__(self):\n", " result = self._next_element\n", " self._next_element += self._step\n", " return result\n", "\n", "\n", "# пример использования\n", "\n", "get_next = ArithmeticProgression(5) \n", "\n", "# выводим три первых члена арифметической прогрессии\n", "print(get_next())\n", "print(get_next())\n", "print(get_next())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В примере выше `get_next` является вызываемым объектом. Мы намеренно дали ему имя, похожее на имя функции, чтобы было понятнее, как используется этот объект. В своем внутреннем атрибуте `_next_element` он хранит следующее значение прогрессии, которое должно быть возвращено." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "На этом мы заканчиваем рассмотрение специальных методов в языке Python. Мы познакомили вас не со всеми такими методами - их достаточно много, и большинство имеют узкоспециализированное применение. Более полную информация на эту тему можно получить из раздела [Special Method Names](https://docs.python.org/3/reference/datamodel.html#special-method-names) справочного руководства." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Множественное наследование" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Множественное наследование позволяет создавать классы, которые имеют более одного предка. Эта возможность используется редко, и во многих объектно-ориентированных языках программирования отсутствует в принципе. Среди программистов наиболее распространена точка зрения, что нужно избегать множественного наследования в своем коде, поскольку это создает весьма запутанную иерархию классов, и порождает несколько трудно разрешимых проблем. Одним из примеров является ситуация, когда в двух родительских классах существует метод с одним и тем же именем, и он не переопределен в дочернем классе. При обращении к такому методу нужно как-то выбрать, какую реализацию использовать для вызова, и в разных языках это решается по-разному. В Python, например, используется подход, заключающийся в том, что вначале метод ищется в базовом классе, указанном первым в списке, затем во втором и так далее:" ] }, { "cell_type": "code", "execution_count": 65, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Base1.test\n" ] } ], "source": [ "class Base1:\n", " def test(self):\n", " print('Base1.test')\n", "\n", "class Base2:\n", " def test(self):\n", " print('Base2.test')\n", "\n", "class Derived(Base1, Base2): pass\n", "\n", "d = Derived()\n", "d.test()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "И это только цветочки. Если предположить, что сам класс `Base1` наследует от нескольких предков, причем одним из них является `Base2`, то мы получим еще более запутанную ситуацию. Преимущества, которые может дать множественное наследование зачастую не могут перевесить все проблемы, создаваемые им." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В заключение мы все-таки приведем один пример использования множественного наследования, когда оно не создает особых проблем и может быть полезно. Этот пример заключается в создании специальных небольших классов, иногда называемых **примесями** (*mixins*). Объекты таких классов обычно не создаются, потому что основной способ использования примеси заключается в наследовании от нее своих классов и тем самым получении доступа к реализованной в ней функциональности." ] }, { "cell_type": "code", "execution_count": 66, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "13\n", "18\n", "13\n" ] } ], "source": [ "# класс, выполняющий операцию op (это должен быть вызываемый объект,\n", "# лямбда или функция) над некоторым значением value\n", "class Operation:\n", " def __init__(self, val, op):\n", " self._value = val\n", " self._op = op\n", " \n", " def do(self, param):\n", " self._value = self._op(self._value, param)\n", " \n", " # не определяем setter-метод, потому что хотим, чтобы значение,\n", " # над которым выполняется операция, опеределялось только в конструкторе\n", " @property\n", " def value(self):\n", " return self._value;\n", "\n", "\n", "# примесь для для хранения параметра операции;\n", "# в нашем простом примере параметр хранится только в памяти, в\n", "# реальности мы возможно хотели бы сохранять его и на диске\n", "class ParamStorageMixin:\n", " def __init__(self):\n", " self._param = None\n", " def push(self, param):\n", " self._param = param\n", " def pop(self):\n", " result = self._param\n", " self._param = None\n", " return result\n", "\n", "\n", "# класс, который позволяет откатить последнюю операцию и использующий\n", "# примесь для того, чтобы хранить параметр, с которым операция выполнялась\n", "# последний раз\n", "class RevertableOperation(Operation, ParamStorageMixin):\n", " def __init__(self, target, op, undo_op):\n", " super().__init__(target, op)\n", " self._undo_op = undo_op\n", " \n", " def do(self, param):\n", " self.push(param)\n", " super().do(param)\n", " \n", " def undo(self):\n", " param = self.pop()\n", " \n", " if param is None: # для операции уже выполнен откат\n", " return\n", " \n", " self._value = self._undo_op(self._value, param)\n", "\n", "\n", "# пример использования\n", "\n", "add_op = lambda x, y: x + y\n", "add_undo_op = lambda x, y: x - y\n", "\n", "add = RevertableOperation(10, add_op, add_undo_op)\n", "\n", "add.do(3)\n", "print(add.value)\n", "\n", "add.do(5)\n", "print(add.value)\n", "\n", "add.undo()\n", "print(add.value)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В нашей реализации мы можем отменить только последнюю сделанную операцию, потому что только для нее у нас в классе-примеси сохраняется параметр, с которым она была выполнена. Однако после того, как мы познакомимся в следующей лекции с коллекциями, мы сможем модернизировать класс `ParamStorageMixin` так, чтобы он мог хранить неограниченное количество параметров и, соответственно, получим возможность запоминать и затем откатывать произвольное количество операций." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Абстрактные базовые классы" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Важным понятием в программировании является **интерфейс**. Оно является очень широким, но основной смысл заключается в том, что интерфейс представляет собой некоторую общую границу между двумя программами (или двумя частями одной программы), на которой определяется *контракт*, в соответствии с которым должно осуществляться взаимодействие программ (или частей одной программы)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В качестве аналогии из реальной жизни, рассмотрим взаимодействие человека с автомобилем. В простейшем случае, интерфейсом, с помощью которого человек использует автомобиль, является рулевое колесо и педали газа и тормоза. При этом четко определено, что происходит при том или ином воздействии на интерфейс, например, при повороте руля или нажатии на газ." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Интерфейсы в программировании служат той же цели, что и в реальной жизни: они упрощают использование исходного кода, написанного другим разработчиком, скрывая ненужные детали сложной реализации. Если вернуться к примеру с автомобилем, то аналогия здесь в том, что для того, чтобы им управлять, вам не нужно знать устройство двигателя или КПП." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В процедурном программировании в качестве интерфейсов используется набор функций для решения каких-то задач. Эти функции могут быть достаточно сложными (в том числе вызывать много других функций, не являющихся частью интерфейса), но использовать их должно быть достаточно просто и интуитивно понятно." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В объектно-ориентированном программировании интерфейсы реализуются методами и свойствами класса, причем инкапсуляция требует, чтобы интерфейсы содержали только необходимое, а детали, имеющие отношение исключительно к их реализации, были скрыты. Например, в классе [`TextInfo`](#text_info) мы отметили метод `_calculate` как внутренний, потому что он не понадобится тем, кто будет использовать наш класс." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В контексте наследования, интерфейс базового класса позволяет определить, какие методы будут реализованы всеми наследниками. После этого можно писать код, который работает с объектами любого дочернего типа так, словно это объекты родительского класса." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Рассмотрим пример программы, которая будет выполнять различные операции с геометрическими фигурами на плоскости (круг, прямоугольник и т.д.). Нам потребуется в ней базовый класс, который будет общим предком для всех классов, представляющих конкретные фигуры. Заметим, что любая фигура имеет некоторую площадь, поэтому метод, вычисляющий этот параметр разумно поместить в базовый класс. Однако, поскольку в нем самом мы не знаем, как именно это нужно делать, этот метод будет просто генерировать встроенное исключение `NotImplementedError` (не путайте со специальным значением `NotImplemented`, рассмотренном в разделе про перегрузку операторов!), как раз для этого и предназначенное." ] }, { "cell_type": "code", "execution_count": 67, "metadata": {}, "outputs": [], "source": [ "class Shape:\n", " def __init__(self, name):\n", " self.name = name\n", " def get_name(self):\n", " return self.name\n", " def get_area(self):\n", " raise NotImplementedError()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Используя этот базовый класс мы можем создать дочерние классы `Circle` и `Rectangle`, в которых будет реализован метод `get_area`:" ] }, { "cell_type": "code", "execution_count": 68, "metadata": {}, "outputs": [], "source": [ "from math import pi\n", "\n", "class Circle(Shape):\n", " def __init__(self, radius):\n", " super().__init__('circle')\n", " self.radius = radius\n", " def get_area(self):\n", " return pi * (self.radius ** 2)\n", "\n", "class Rectangle(Shape):\n", " def __init__(self, a, b):\n", " super().__init__('rectangle')\n", " self.a = a\n", " self.b = b\n", " def get_area(self):\n", " return self.a * self.b" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Эти классы теперь могут быть использованы таким образом:" ] }, { "cell_type": "code", "execution_count": 69, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "circle: area=28.274333882308138\n", "rectangle: area=30\n" ] } ], "source": [ "def print_shape_info(shape):\n", " print('{}: area={}'.format(shape.get_name(), shape.get_area()))\n", "\n", "shape1 = Circle(3)\n", "shape2 = Rectangle(5, 6)\n", "\n", "print_shape_info(shape1)\n", "print_shape_info(shape2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Обратите внимание, что для написания функции `get_shape_info` на не нужно знать, какие фигуры вообще бывают, а также какая именно фигура была передана в качестве аргумента, потому что нам известно, что все они реализуют интерфейс класса `Shape`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Классы наподобие `Shape` называются в ООП **абстрактными** (сокращенно ABC - *abstract base class*), потому что они лишь описывают интерфейс, но сами не реализуют его, или реализуют лишь частично (в `Shape` реализованы только конструктор и `get_name`). Поэтому нам бы не хотелось давать возможность создавать объекты таких классов - это может приводить к случайным ошибкам, если нерелизованные методы будут вызваны. В нашем же примере это допустимо:" ] }, { "cell_type": "code", "execution_count": 70, "metadata": {}, "outputs": [ { "ename": "NotImplementedError", "evalue": "", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mNotImplementedError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[0mshape\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mShape\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'absract'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mshape\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget_area\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;32m\u001b[0m in \u001b[0;36mget_area\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mname\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 6\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mget_area\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 7\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mNotImplementedError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;31mNotImplementedError\u001b[0m: " ] } ], "source": [ "shape = Shape('absract')\n", "shape.get_area()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Чтобы исправить эту неточность, нам потребуется сделать следующее:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. Подключить модуль `abc`, содержащие необходимые типы для создания абстрактных классов.\n", "2. Для класса `Shape` указать в качестве метакласса тип `ABCMeta`. Мы не будем подробно рассматривать работу с метаклассами, скажем лишь, что они нужны для управления процессом создания классов, то есть играют для них ту же роль, что сами классы играют по отношению к своим объектам.\n", "3. С помощью декоратора `abstractmethod` указать методы, для которых отсутствует реализация в абстрактном классе." ] }, { "cell_type": "code", "execution_count": 71, "metadata": {}, "outputs": [], "source": [ "import abc\n", "\n", "class Shape(metaclass=abc.ABCMeta):\n", " def __init__(self, name):\n", " self.name = name\n", " def get_name(self):\n", " return self.name\n", " @abc.abstractmethod\n", " def get_area(self):\n", " raise NotImplementedError()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если мы теперь попробуем создать объект класса `Shape`, то получим следующую ошибку:" ] }, { "cell_type": "code", "execution_count": 72, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "Can't instantiate abstract class Shape with abstract methods get_area", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mshape\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mShape\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'abstract'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;31mTypeError\u001b[0m: Can't instantiate abstract class Shape with abstract methods get_area" ] } ], "source": [ "shape = Shape('abstract')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> Декоратор `abstractmethod` можно использовать вместе с декоратором `property` для того, чтобы указать интерпретатору, что свойство является абстрактным, и оно должно быть реализовано в наследниках. В случае, когда это необходимо, декоратор `abstractmethod` нужно указать *после* декоратора `property`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Поскольку `Circle` и `Rectangle` реализуют все абстрактные методы, их нельзя назвать абстрактными классами, поэтому их объекты можно спокойно создавать:" ] }, { "cell_type": "code", "execution_count": 73, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "circle: area=314.1592653589793\n", "rectangle: area=1\n" ] } ], "source": [ "circle = Circle(10)\n", "rectangle = Rectangle(1, 1)\n", "\n", "print_shape_info(circle)\n", "print_shape_info(rectangle)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Вопросы для самоконтроля" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. Что такое класс? Из чего он состоит?\n", "2. Что такой объект? Объясните разницу между объектом и классом. Что называют состоянием объекта? Поведением?\n", "3. Что такое конструктор? Для чего он нужен?\n", "4. Что такое переменная экземпляра и переменная класса?\n", "5. Перечислите и опишите все известные вам разновидности пространств имен.\n", "6. Что такое инкапсуляция? Какие основные ее принципы?\n", "7. Что такое наследование? Какие классы наследуются от класса `object`? Что такое множественное наследование?\n", "8. Как связаны наследование и полиморфизм? Что такое переопределение метода?\n", "9. Что такое композиция? Как еще она называется?\n", "10. Опишите работу механизма обработки исключений.\n", "11. Что такое декоратор?\n", "12. Для чего нужна перегрузка операций?\n", "13. Что такое вызываемый объект? Как его создать?\n", "14. Что такое класс-примесь?\n", "15. Что такое интерфейс? Зачем он нужен? Что такое абстрактный базовый класс?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Задание" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. Для типа [`TextInfo`](#text_info) вместо методов `get_symbols_count`, `get_words_count` и `get_sentences_count` создайте свойства, доступные только для чтения (для этого просто не нужно определять setter-метод).\n", "2. Для типа [`Circle`](#circle) реализуйте в явном виде все операции сравнения (`<`, `<=`, `==`, `!=`, `>=`, `>`), но с одним условием - вычислять площадь можно только в методе `__lt__`, а все остальные не должны внутри себя содержать этот код, но могут использовать метод `__lt__` для решения своей задачи. Тривиальный пример - метод `__ge__` для реализации которого можно вызвать метод `__lt__` и инвертировать результат.\n", "3. Доделайте тип [`MyComplex`](#my_complex). Нужно добавить в него реализацию специальных методов для вычитания, умножения и деления комлексных чисел, а также метод для создания объектов `MyComplex` из показательной формы записи $r*e^{i\\varphi}$ (используйте для него декоратор `classmethod`).\n", "4. Модифицируйте пример с абстрактным базовым классом [`Shape`](#shape):\n", " 1. вместо абстрактного метода `get_area` используйте абстрактное свойство `area`\n", " 2. добавьте абстрактное свойство `perimeter` и определите его в наследниках\n", " 3. создайте исключение, которое генерируется в случае, если конкретные фигуры создаются с некорректными параметрами\n", " 3. добавьте к базовому типу `Shape` поддержку операций сравнения (сравнивать фигуры нужно по их площади)\n", " 4. реализуйте еще один дочерний класс `Triangle`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- - -\n", "[Предыдущая: Функции и модули](07_Functions_And_Modules.ipynb) |\n", "[Содержание](00_Overview.ipynb#Содержание) |\n", "[Следующая: Коллекции](09_Collections.ipynb)" ] } ], "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.4" } }, "nbformat": 4, "nbformat_minor": 2 }