ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 04.12.2023
Просмотров: 137
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
90
Рис. 123. Пример использования функций модуля functools
В модуле functools также содержатся функции для обработки последовательностей. functools.reduce(func, iterable) позволяет применить функцию ко всем элементам последовательности, используя в качестве первого аргумента накопленный результат. Например, если в последовательности были элементы myList = [A, B, C], то результатом применения reduce(f, myList) будет f(f(A, B), C). С помощью reduce, например, можно найти НОД всех чисел в iterable (рис. 124).
Рис. 124. Пример использования функций модуля functools itertools.accumulate(iterable, func) возвращает iterable со всеми промежуточными значениями, т. е. для списка [A, B, C] accumulate вернет значения A, f(A, B), f(f(A, B), C). Например, можно узнать максимальный элемент для каждого префикса (некоторого количества первых элементов) заданной последовательности (рис. 125).
91
Рис. 125. Пример использования функций модуля functools
Итераторы и генераторы
В Питоне итераторы – это объекты, которые имеют внутреннее состояние и метод __next__ для перехода к следующему состоянию.
Например, можно сконструировать итератор от списка и перебрать все значения (рис. 126).
Рис. 126. Пример использования итератора
В этой программе два цикла эквивалентны.
В Питоне можно создавать и свои итераторы. Их разумно использовать в том случае, если нужно перебрать большое количество значений.
Существует правило, по которому можно получить следующее значение, однако хранение всех этих значений не имеет смысла, т. к. они пригодятся только один раз.
Для создания итераторов в Питоне используется специальный вид функций, называемых генераторами. В обычной функции return прекращает работу функции. В генераторе вместо return используется оператор yield, который также возвращает значение, но не прекращает выполнение функции, а приостанавливает его до тех пор, пока не потребуется следующее значение итератора. При этом работа функции продолжится с того места и в том состоянии, в котором она находилась на момент вызова yield. Посмотрим, как может быть реализован генератор, аналогичный стандартному range с одним параметром (рис. 127).
92
Рис. 127. Пример использования генератора
Генераторы могут иметь и сложную рекурсивную структуру.
Например, мы можем написать генератор, который будет выдавать все числа заданной длины, цифры в которых не убывают, и старшая цифра не превосходит заданного параметра (рис. 128).
Рис. 128. Пример использования генератора
Вывод этой программы будет выглядеть так: 0 10 11 20 21 22 30 31 32 33.
В этой программе рекурсивный генератор перебирал все допустимые цифры в качестве той, которая должна стоять на заданной позиции, и генерировал все возможные последующие цифры.
В этой программе мы использовали также одну особенность функции print: если перед именем iterable (а результатом, возвращаемым генератором, является iterable) поставить *, то будут напечатаны все значения через пробел.
93
Результат работы генератора можно сохранить, например, в список с помощью функции list, как мы уже делали это с результатом работы range.
2.4. О
бъектно-ориентированное программирование при анализе
данных
Объектно-ориентированное программирование
Объектно-ориентированное программирование (ООП) является одной из парадигм программирования, созданной на базе императивного программирования для более удобного повторного использования кода и облегчения читаемости программ.
ООП не является «серебряной пулей», которая решает все задачи наиболее удобным образом. Императивное программирование удобно для решения простых задач, использующих стандартные объекты. Повторное использование кода в императивной парадигме обеспечивается с помощью циклов и функций, написанных в императивном стиле.
Функциональное программирование удобно для задач, где существует ярко выраженный поток данных (data flow), который «перетекает» из одной функции в другую, изменяясь по пути.
ООП же позволяет обеспечить повторное использование кода за счет того, что обрабатываемые программой объекты имеют много общего и лишь незначительные отличия в своем поведении.
ООП основано на трёх концепциях: инкапсуляции, наследовании, полиморфизме.
Инкапсуляция – это помещение в «капсулу»: логическое объединение данных и функций для работы с ними, а также сокрытие внутреннего устройства объекта с предоставлением интерфейса взаимодействия с ним
(публичные методы).
94
Наследование – это получение нового типа объектов на основе уже существующего с частично или полностью заимствованной у родительского типа функциональностью.
Полиморфизм
– это предоставление одинаковых средств взаимодействия с объектами разной природы. Например, операция + может работать как с числами, так и со строками, несмотря на разную природу этих объектов.
Классом в Питоне называется описание структуры объекта (полей структуры) и методов (функций) для работы с данными в этой структуре.
Объектом называется экземпляр класса, где поля заполнены конкретными значениями. Объекты находятся в памяти программы и могут изменять своё состояние или выполнять какие-то действия с помощью вызова методов класса для этого объекта.
Инкапсуляция и конструкторы
Мы уже использовали ключевое слово class для создания структур – набора именованных полей, совокупность которых описывает объект.
Однако мы пользовались для их обработки отдельно лежащими функциями или кусками кода.
Было бы намного удобнее, если бы описание структуры объекта и методов работы с ним лежало рядом для удобства изучения, модификации и использования.
Мы будем рассматривать элементы ООП на примере компле́ксных
(ударение на «е») чисел. Это забавный математический объект, который состоит из действительной (real) и мнимой (imaginary) части. Запись этого числа выглядит как re + im * i, где i – это квадратный корень из –1. Глубокое математическое понимание комплексных чисел нам не понадобится.
Достаточно понимать, что это структура с двумя полями re и im, где оба эти поля – вещественные числа.
95
Для создания новых объектов класса используется специальный метод, который называется «конструктор». Методы класса записываются внутри описания класса как функции, конструктор должен называться __init__. В качестве первого параметра они должны принимать переменную self – конкретный объект класса, с которым они работают.
Рассмотрим класс для комплексного числа, вызов конструктора и печать полей объекта (рис. 129).
Рис. 129. Задание конструктора класса
Здесь конструктор содержит три параметра: self – пустой объект класса
Complex, re и im, по умолчанию равные нулю. Вызов конструктора осуществляется с помощью написания названия класса, в скобках указываются параметры конструктора (все, кроме self). Названия классов принято записывать с большой буквы, а объекты – с маленькой.
Вывод этой программы будет:
1 2 3 0 0 0
Обратите внимание, что мы меняем переменные конкретного объекта класса, который передан в качестве параметра self.
Если бы мы создали какие-то переменные в описании класса, то их значения были бы доступны во всех объектах этого класса и их можно было бы даже изменить,
96 перечислив их имена в начале метода после выражения nonlocal. Такие переменные называются статическими, обычно они предназначены для хранения каких-то констант (что очень удобно, например, при описании какого-то класса для физических вычислений). Их изменение может понадобиться, например, в экзотических ситуациях при подсчете количества объектов класса.
Определение методов и стандартные функции
Некоторые стандартные функции языка Питон являются всего лишь обертками над вызовом метода для передаваемого параметра. Например, функция str вызывает метод __str__ для своего параметра. Если мы опишем такой метод для нашего класса, то можно будет применять к нему функцию str явно и неявно (например, она автоматически вызовется при вызове print для объекта нашего класса).
Мы бы хотели, чтобы __str__ возвращал текстовое представление нашего комплексного числа. Например, число с действительной частью 1 и мнимой 2 должно быть представлено в виде строки ''1+2i'', а число с действительной частью 3 и мнимой –4.5 – как ''3-4.5i''. Полное описание класса с добавленным методом будет выглядеть так, как представлено на рис.
130.
97
Рис. 130. Определение методов и стандартные функции
Переопределение операторов
В языке Питон можно переопределить и поведение операторов.
Например, если у нас есть два числа x и y, то запись x + y реально преобразуется в вызов метода x.__add__(y). Значок операции + является всего лишь удобным для человека переопределением вызова метода add.
Для комплексных чисел логично определена операция сложения: это сложение отдельно действительных и отдельно мнимых частей. В результате вызова метода для сложения двух чисел должен конструироваться новый объект класса Complex, а переданные в качестве параметров объекты не должны изменяться. Действительно, когда мы выполняем операцию z = x + y для обычных чисел, то ожидаем, что сконструируется новый объект, к которому привяжется ссылка z, а x и y останутся без изменения.
Будем придерживаться этой же логики при реализации метода для сложения двух комплексных чисел. Наш метод __add__ должен принимать два параметра, каждый из которых является комплексным числом (рис. 131).
Рис. 131. Переопределение операторов
98
Переопределять метод add имеет смысл только в тех ситуациях, когда программисту, использующему ваш класс, будет очевиден смысл операции +.
Например, если бы вы создали класс для описания некоторых характеристик человека, то операция + для двух объектов-людей воспринималась бы разными пользователями вашего класса совершенно по-разному, в зависимости от развитости фантазии читателя. Такого неоднозначного понимания лучше избегать и вовсе не переопределять операцию +, если результат её работы не очевиден.
Проверка класса объекта
Переопределим для комплексных чисел ещё одну операцию – умножение. При этом мы хотим уметь умножать комплексные числа как на целые или действительные, так и на другие комплексные числа.
При умножении комплексного числа вида a + b
* i$ на целое или вещественное число x результатом будет комплексное число a * x + b * x * b.
Перемножение двух комплексных чисел производится аналогично умножению двух многочленов первой степени:
(a + b * i) * (c + d * i) = a * c + (a * d + b * c) * i + b * d * i**2.
Мы знаем, что i**2 = –1. Значит окончательно результат умножения будет выглядеть так: a * c – b * d + (a * d + b * c) * i.
Нам осталось понять, как определить, передано ли в наш метод комплексное или не комплексное число.
В языке Питон существует функция isinstance, которая в качестве первого параметра принимает объект, а в качестве второго – название класса.
Она возвращает истину, если объект относится к данному классу, и ложь в противном случае. Эта функция позволит нам добиться нужной функциональности от метода __mul__, умножающего числа (рис. 132).
99
Рис. 132. Проверка класса объектов
Кроме добавленного метода __mul__, внимания также заслуживает строка __rmul__ = __mul__. Это присваивание одного метода (функции) другому, т. е. при вызове метода __rmul__ будет вызываться тот же самый метод __mul__.
В языке Питон операция a * b заменяется на вызов метода a.__mul__(b).
Если a было комплексным числом, а b – вещественным, то вызовется метод
__mul__ для объекта a нашего класса Complex.
Однако, если a было вещественным числом, а b – комлексным, то произойдет попытка вызвать метод __mul__ для объекта класса float.
Естественно, разработчики стандартной библиотеки языка Питон не могли
100 предположить, что мы когда-нибудь напишем класс Complex и будем пытаться умножить на него вещественное число. Поэтому метод __mul__, где в качестве параметра передается нечто неизвестное, будет заканчивать свою работу с ошибкой. Чтобы избежать таких ситуаций, в языке Питон после неудачной попытки совершить a.__mul__(b) происходит попытка совершить действие b.__rmul__(a), и в нашем случае она заканчивается успехом.
Обработка ошибок
Время от времени в программах возникают ошибочные ситуации, которые не могут быть обработаны в том месте, где возникла ошибка, а должны быть обработаны тем или иным образом в иной части программы.
Например, если на этапе выполнения промежуточной логики вы пытаетесь записать строку в численную переменную
, вы не сможете этого сделать. В таком случае нужно выходить из стека вызовов функций или методов промежуточной логики до тех пор, пока не дойдем до фронтенда, который сообщит пользователю о том, что он ввел недопустимое значение, и попросит, например, ввести его заново.
Иногда в ситуации заполнения огромной формы попытка отправить ее приводит к тому, что появляется окошко со словом «ошибка» без каких-либо уточнений
. Это неправильный подход, сообщение об ошибке должно быть информативным, чтобы позволить быстро её найти и исправить. Таким образом, при создании ошибки нужно передавать исчерпывающую информацию о ней.
В примере с вещественными числами можно рассмотреть такую ошибку: умножение комплексного числа на что-то, отличное от целого, вещественного или комплексного числа. На этапе умножения уже ничего нельзя предпринять для исправления этой ошибки, кроме как просигнализировать о ней в то место, откуда была вызвана операция умножения.