мета-данные страницы
Базовая информация о объектно-ориентированном программировании
Процедурные языки
Чтобы понять что такое объектно-ориентированное программирование (ООП) нужно сначала понять что оно заменяет. Ранние языки были процедурными: программисту требовалось описать конкретный набор процедур которые выполнял компьютер.
В те времена процедуры записывались на перфокартах. На каждом шаге данные читались, обрабатывались и сохранялись. Этот подход хорошо работал тогда, да и сейчас в общем-то свою функцию выполняет. Однако, когда нам требуется написать программу, которая сложнее чем набор простых шагов - то в этом случае решение с помощью процедурного подход становится сложным для работы. Одним из вариантов преодоления результирующей сложности программы является ООП.
Объектно-ориентированное программирование
В первом ООП языке (Simula) была введена идея объекта. Объекты это наборы информации, воспринимаемые как целое. Туманно, но разберем позже на примерах, пока же поговорим о классах. В контексте ООП это можно сказать «предобъекты». Они содержат список свойств, которые после определения становятся объектом.
Рассмотрим пример с шахматами. Предположим что нам требуется написать шахматную игру. Вероятно нам захочется использовать класс Фигура. Каждая Фигура имеет следующий набор свойств:
- Цвет
- Высота
- Форма
- Как ходит
Объект определяет конкретного представителя, принадлежащего классу. Положим что мы хотим объект с названием БелаяКоролева. У этого объекта будут следующие определения для свойств (белый, высокий, цилиндрический с зубцами, в любом направлении на любое расстояние). Так же объект может содержать функции, которые в ООП принято называть методами.
Чем ОО подход лучше процедурного? Если кратко, то ООП языки позволяют аккуратнее организовать данные и код так, что в больших проектах работа с ними происходит более гибко. Познакомимся со основными столпами ООП.
Абстракция
К нашему счастью, чтобы пользоваться чем-то не обязательно знать как оно работает. Автомобиль это сложная инженерная система, но чтобы ехать на нем не обязательно знать в подробностях как функционирует каждая его часть. 1)
Тоже самое можно сказать и о объектах в ООП. Вернемся к шахматам, вероятно у фигуры есть метод move(), для эта функция может использовать множество данных или другие методы: начальная и конечная позиция, методы для определения того, не стоит ли какая-нибудь фигура на пути следования, метод взятия фигуры и т.д. Но нам не требуется знать всех этих тонкостей, мы пишем move() и фигура двигается. Это и есть абстракция.
Инкапсуляция или сокрытие
В ООП это метод с помощью которого в ООП реализуется абстракция. Каждый объект это набор данных, рассматриваемый как единая сущность. К данным относятся как переменные, так и методы.
Обычно переменные считаются скрытыми, приватными. Это означает, что другие объекты и их методы к ним доступа не имеют. Значения переменных меняются только собственными методами объекта.
Шахматная фигура Слон может, например, переменную с названием position - текущая позиция. Значение этой переменной необходимо для метода move(). Естественно Слону приписан еще и цвет.
Когда программист делает переменную приватной а метод move() общественным (публичным) он защищает ее значение от изменения каким-нибудь внешним действием. Аналогично, если цвет это приватная переменная, никто не сможет его изменить, если в объекте не существует метода которы бы это сделал.
Все эти переменные и методы хранятся в пределах объекта Слон. Так как они сокрыты, программист может менять структуру и содержимое объекта не беспокоясь о его интерфейсе: наборе публичных методов.
Наследование
В дополнение к классам, в ООП языках еще есть подклассы. Они содержат все атрибуты родительского класса, но могут иметь и дополнительные свои. В наших шахматах пешка должна иметь метод transformPiece(), который превратит ее в другую фигуру, когда она дойдет до конца доски. Однако, этот метод нужен только пешке, и нет никакого смысла иметь его в других фигурах. Поэтому создадим для класса Фигура подкласс Пешка. Так как он подкласс Фигуры, он будет содержать все ее параметры, иными словами объект класса Пешка включает цвет, высоту, форму и набор перемещений. А еще он имеет метод transformPiece() которого нет у других. И нам не нужно будет беспокоиться, если случайно попытаемся вызвать этот метод у Ладьи, это сразу приведет к ошибке.
Создание подклассов позволяет экономить время. Вместо создания новых классов для всего программист создает базовый класс и затем расширяет его новыми подклассами по мере необходимости.
Полиморфизм
Полиморфизм это следствие наследования. Чтобы полностью его понять потребуется некоторый опыт в программировании, поэтому рассмотрим только самые основы. Если кратко, полиморфизм это возможность использовать метод с одинаковым именем для разных объектов.
Например, класс Фигура может содержать метод move(), который двигает фигуру на одну клетку в каждом направлении. Это хорошо подходит Королю, но малоприменимо для всех остальных фигур. Чтобы исправить эту проблему, мы можем описать новый метод move() для подкласса Ладья. Этот метод опишет способ перемещения на произвольное количество клеток по горизонтали и вертикали.
В этом случае, когда программист выполнит метод move() и передаст ему фигуру в качестве аргумента, программа будет точно знать как эта фигура должна двигаться. Это сэкономит кучу времени, которое ушло бы на попытки определить какой из кучи методов (например, move_horizontally(), move_vertically(), move_horizontally_one_step() и т.д.) нам потребуется использовать.
Резюме
Прочитанное наверняка вызывает больше вопросов об ООП чем дает ответов. Перечислим кратко основное:
- ООП собирает информацию в единую сущность, именуемую объектом.
- Каждый объект это конкретный экземпляр класса.
- Абстракция скрывает внутренние механизмы объекта, когда нам не нужно о них знать.
- Инкапсуляция хранит и защищает соответствующие переменные и методы внутри объекта.
- Наследование позволяет подклассам использовать атрибуты родительских классов.
- Полиморфизм позволяет объектам и методам работать с разными ситуациями с помощью одного интерфейса.
Если запомните эти пункты, то этого будет достаточно чтобы иметь представление об ООП. Сущности довольно туманные, но понимание придет с практикой. Мы обсудили только очень общие свойства ООП, каждый ООП язык имеет свои особенности, подходы и способы как заставить все работать. Рассмотрим ООП на примере Питона.
ООП в Питоне
Классы
Класс это базовый строительный блок в ООП. Можно считать класс шаблоном или чертежом объекта, который описывает характеристики объекта. Предположим что у нас есть класс Автомобиль. Он может содержать такие параметры: четыре колеса, двигатель и как минимум одно кресло.
class Vehicle: """ Этот класс описывает средство передвижения """ pass
Комментарий в начале класса это специальный вид комментария, который называется докстринг в переводе, строка документации. Он должен содержать краткое описание того что делает код. Использование тройных кавычек и говорит Питону о том, что это докстринг.
Ключевое слово pass
говорит интерпретатору Питона ничего не делать. Требуется так как синтаксис Питона не допускает пустоты после :.
Если мы этот код запустим, то ничего не произойдет. Сначала требуется создать экземпляр класса. Что буквально означает: построить объект используя план, описанный в классе. Можно сделать сколько угодно копий, каждую со своими свойствами.
red_car = Vehicle() green_car = Vehicle() blue_car = Vehicle()
И все равно, если мы запустим код ничего не произойдет. Он работает корректно, но мы нигде не сказали программе сделать что-то заметное. Класс Vehicle описывает шаблон средства передвижения, а дальше создаются объекты с именами red_car, greed_car, blue_car.
Дополним наш код. Добавим в класс Vehicle метод init
:
class Vehicle: """ Этот класс описывает средство передвижения """ def __init__(self, color='plain'): """ задает параметры средства передвижения """ print('Сделали машину!') self.color = color red_car = Vehicle() green_car = Vehicle() blue_car = Vehicle()
Обратите особое внимание на метод init
. Это специальный метод в Питоне и должен начинаться и заканчиваться двумя символами подчеркивания. Он автоматически вызывается всякий раз, когда создается новый объект. Теперь, если мы запустим этот код, то на экране будет три раза написана строка Сделали машину!
Так же метод init
имеет аргумент с именем color
. Запись color='plain
' в описании аргументов функции задает значение аргумента по-умолчанию (т.е. значение, которое получит переменная color если мы не укажем никакого аргумента. Добавим цвета:
red_car = Vehicle(color='red') green_car = Vehicle(color='green') blue_car = Vehicle(color='blue')
Если напечатать на экран значение переменной, можно заметить, что у каждого экземпляра цвет свой, хотя созданы они были по одному описанию (классу).
print(red_car.color) print(green_car.color) print(blue_car.color)
Так вышло потому, что мы присвоили значение переменной self.color
. self
это еще одно ключевое слово в Питоне, оно ссылается на конкретный экземпляр класса (объект). Всякий раз когда мы используем self
, мы можем изменить или прочитать данные, уникальные для объекта: красная машина - красная.
Расширим метод init
и добавим в него
self.noise = 'Дрыннь!'
Можно, конечно, просто напечатать на экран значение этой переменной. Сделаем немного иначе, пусть мы хотим чтобы машина ехала, и в будущем используем это в коде. Или изменим метод передвижения. Заведем функцию (метод) чтобы иметь контроль над тем как все работает. Добавим его сразу после метода init
:
def drive(self): print(self.noise)
Вызвать этот метод можно написав:
red_car.drive()
Всякий раз когда мы будем вызывать метод Питон будет печатать на экран звук движения машины. Вся наша программа сейчас должна выглядеть вот так:
class Vehicle: """ Этот класс описывает средство передвижения """ def __init__(self, color='plain'): """ задает параметры средства передвижения """ print('Сделали машину!') self.color = color self.noise = 'Дрыннь!' def drive(self): print(self.noise) red_car = Vehicle(color='red') green_car = Vehicle(color='green') blue_car = Vehicle(color='blue') print(red_car.color) print(green_car.color) print(blue_car.color) red_car.drive()
Наследование
Наследование это способ уменьшить дублирование и переиспользовать код. В терминах предков-потомков, наследование позволяет потомку делить общий код с предком. Создадим новый класс ElectricCar
class ElectricCar(Vehicle): """ Электромобиль. """ def charge(self): print('⚡🔋') electric_car = ElectricCar() electric_car.charge() electric_car.noise = 'Вжух!' electric_car.drive()
Во время описания класса электромобиля мы указали имя класса Vehicle в скобках. Это инструкция Питону считать класс ElectricCar потомком или подклассом класса Vehicle. Это дает доступ до данных и методов, существующим в Vehicle.
Электромобиль имеет так же свои методы. Он может заряжаться (этого другие машины не умеют). И издает другой звук при движении. Обратите внимание, мы не описывали метод drive() в новом классе, однако он работает и выдает верный результат.