Введение в ООП на примере Python
Язык программирования Python появился в 1991 году. К этому времени была разработана теоретическая база объектно-ориентированного программирования, появились исследовательские языки программирования, проверившие эти идеи на практике, и даже возникло первое поколение объектно-ориентированных языков для широкого круга программистов.
Поэтому, ориентируясь на чужие успехи и неудачи, Гвидо ван Россум и его коллеги смогли спроектировать достаточно простую и мощную реализацию ООП. Python поддерживает ООП на сто процентов: все данные в нем являются объектами. Числа всех типов, строки, списки, словари, даже функции, модули и, наконец, сами типы данных — все это объекты!
Давайте для начала рассмотрим несколько предметов из реального мира. Вообще говоря, прямой аналогии между объектами материального мира и объектами из мира программирования нет. Ведь в программировании есть объекты, которые обозначают какой-то процесс (например, функции), или состояние процесса, или вообще произвольные абстрактные понятия.
Например, массив или число с плавающей точкой — сами по себе достаточно абстрактные понятия, которые имеют отдаленные аналоги в реальном мире. Но все же давайте пока порассуждаем о реальных объектах.
Представим себе комнату. В ней есть мебель: несколько столов, стулья, шкафы. Стулья могут отличаться цветом, формой, количеством ножек, но все равно мы всегда сможем отличить стул от шкафа.
Если задуматься, у каждого объекта есть набор свойств и действия, в которых он может участвовать. Основываясь на этих свойствах (наличие сиденья) и действиях (на стуле можно сидеть), мы классифицируем объекты, то есть относим их к тому или иному классу.
Основные понятия
Класс
Описывает модель объекта, его свойства и поведение. Говоря языком программиста, класс — такой тип данных, который создается для описания сложных объектов.
Экземпляр
Для краткости вместо «Объект, порожденный классом Стул» говорят «экземпляр класса Стул».
Объект
Хранит конкретные значения свойств и информацию о принадлежности к классу. Может выполнять методы.
Атрибут
Свойство, присущее объекту. Класс объекта определяет, какие атрибуты есть у объекта. Конкретные значения атрибутов — характеристика уже не класса, а конкретного экземпляра этого класса, то есть объекта.
Метод
Действие, которое объект может выполнять над самим собой или другими объектами.
Чтобы стало чуть понятнее, давайте разберем на примере: 1, 2, 3, "abc", [10, 20, 30]
— объекты. А int, str, list
— классы.
Да, все типы данных, которые мы изучали ранее, на самом деле — классы:
1, 2, 3
- экземпляры классаint
"abc"
- экземпляры классаstr
"[10, 20, 30]"
- экземпляры классаlist
в который вложены экземплярыint
Чтобы узнать, к какому классу относится тот или иной объект, можно воспользоваться функцией type
. Например:
Давайте создадим простейший класс, который будет моделировать обычный фрукт. На языке Python это будет выглядеть так:
Имена классов по стандарту именования PEP 8 должны начинаться с большой буквы. Встроенные классы (int, float, str, list и др.) этому правилу не следуют, однако в вашем коде его лучше придерживаться — так делает большинство программистов на Python.
Определение этого класса состоит из зарезервированного слова class, имени класса и пустой инструкции после отступа.
Внутри класса с дополнительным уровнем отступов должны определяться его методы, но сейчас их нет. Однако хотя бы одна инструкция должна быть, поэтому приходится использовать пустую инструкцию-заглушку pass
. Она предназначена как раз для таких случаев.
Описав класс, мы создали модель фрукта.
Теперь создадим два конкретных фрукта — экземпляра класса Fruit:
Переменные f1 и f2 содержат ссылки на два разных объекта — экземпляра класса Fruit, которые можно наделить разными атрибутами:
Атрибуты можно не только устанавливать, но и читать. При чтении еще не созданного атрибута будет появляться ошибка AttributeError. Вы ее часто увидите, допуская неточности в именах атрибутов и методов.
Методы классов
На данный момент мы пользуемся объектами только для хранения соответствий между именами атрибутов и их значениями. Но на самом деле возможности объектов значительно шире. Давайте рассмотрим, как можно запрограммировать объекты на выполнение определенных действий:
После беглого осмотра этого кода видно, что внутри класса Greeter
находится определение чего-то, похожего на функцию, печатающую фразу «Привет, Мир!» На самом деле мы написали метод, с синтаксисом вызова которого вы хорошо знакомы по методу строк split
или методу списков append
. Теперь мы можем создавать такие методы самостоятельно.
При создании собственных методов обратите внимание на два момента:
Метод должен быть определен внутри класса (добавляется уровень отступов)
У методов всегда есть хотя бы один аргумент, и первый по счету аргумент должен называться
self
Аргументу self
следует уделить особое внимание. В него передается тот объект, который вызвал этот метод. Поэтому self
еще часто называют контекстным объектом. Рассмотрим чуть подробнее. Когда программа вызывает метод объекта, Python передает ему в первом аргументе экземпляр вызывающего объекта, который всегда связан с параметром self
.
Иными словами, greet.hello_world()
преобразуется в вызов Greeter.hello_world(greet)
. Этот факт объясняет особую важность параметра self
и то, почему он всегда должен быть первым в любом методе объекта, который вы пишете. Вызывая метод, вы не должны передавать значение для self
явно — интерпретатор сделает это за вас.
Вообще говоря, self
— обычная переменная, которая может называться по-другому. Но так категорически не рекомендуется делать: соглашение об имени контекстного объекта — самое строгое из всех соглашений в мире Python. Его выполняют 99,9 % программистов. Если нарушить это соглашение, другие программисты просто не будут понимать ваш код. Кроме того, некоторые текстовые редакторы подсвечивают слово self
цветом, и это удобно.
В примере выше мы не передавали нашему методу никаких аргументов. Это довольно скучно, ведь мы уже умеем передавать аргументы функциям. Давайте расширим пример и добавим в наш класс два новых метода:
Значение self
автоматически получается из объекта, на котором сделан вызов метода, но мы его пока никак не используем.
Давайте попробуем запоминать информацию из предыдущих вызовов методов. Напишем класс «Машина», которую, как известно, надо сначала завести, а потом уже ехать:
Итак, первая версия класса «Машина» специально сделана нерабочей, чтобы показать, что переменные внутри методов ведут себя точно так же, как и переменные функций. То есть если мы инициализируем переменную внутри метода, то после его завершения все созданные таким образом переменные уничтожаются и оказываются недоступны как следующему вызову этого же метода, так и другим методам.
Под «уничтожением» мы понимаем исчезновение самих переменных, а не объектов, на которые они ссылаются. Если ссылка на объект сохранилась где-нибудь (например, мы вернули объект с помощью return), он все еще доступен. Если ссылок не осталось, объект будет скоро переработан сборщиком мусора.
Напомним, что такие переменные называются локальными.
Инициализация экземпляров класса
Но вернемся к методам. Пора нашей машине наконец поехать — и в этом нам поможет контекстный объект self
. Он общий для всех методов класса, и именно в нем мы с помощью атрибутов сохраним информацию о состоянии двигателя:
Теперь наша машина отлично заведется и поедет, однако одна проблема осталась. При попытке выехать на незаведенной машине:
вместо красивого сообщения о том, что незаведенная машина не поедет, получим «падение» программы с ошибкой AttributeError
(отсутствие атрибута или метода). Еще бы! Ведь атрибут создавался в методе start_engine
, а мы не вызвали его для объекта car
.
Кроме того, стоит добавить метод stop_engine
, чтобы не только заводить машину, но и глушить двигатель. Этот метод помог бы нам избежать вышеуказанной ошибки, но странно глушить еще не заведенную машину, чтобы избежать ошибки: ведь интуитивно мы понимаем, что машина должна создаваться с выключенным двигателем.
Нет ли способа задать значение атрибута engine_on
по умолчанию? Да. Есть метод __init__
, который относится к группе так называемых специальных методов, которые имеют особое значение для интерпретатора Python.
Особое значение метода __init__
заключается в том, что, если такой метод в классе определен, интерпретатор автоматически вызывает его при создании каждого экземпляра этого класса для инициализации экземпляра. Особое значение метода __init__
заключается в том, что, если такой метод в классе определен, интерпретатор автоматически вызывает его при создании каждого экземпляра этого класса для инициализации экземпляра.
Давайте воспользуемся этим, чтобы при создании объекта создать атрибут engine_on
и записать в него False
.
Метод __init__
после self
может получать параметры, передаваемые ему при создании экземпляра:
Еще раз обратите внимание на комментарии в тексте. Они показывают, что при записи атрибутов в self
метод изменяет только свой контекстный объект. Мы знаем, что объекты класса совместно используют код методов класса (то есть поведение), но хранят свои собственные копии всех атрибутов данных (то есть состояние). Это достигается за счет связывания значений атрибутов с объектом, то есть с self
.
Обсудим еще один вопрос: зачем нам понадобился метод start_engine
, ведь его можно было бы заменить строчкой car.engine_on = True
? Казалось бы, это лишнее усложнение. На самом деле нет. При дальнейшей разработке нашей программы может оказаться, что завести двигатель можно только в машине, в которой есть бензин. Если бы мы в нескольких десятках мест программы написали car.engine_on = True
, нам пришлось бы найти все эти места и вставить в них проверку на наличие бензина в баке. А с методом start_engine
мы можем изменить только этот метод.
Такая технология сокрытия информации о внутреннем устройстве объекта за внешним интерфейсом из методов называется инкапсуляцией. Надо стараться делать интерфейс методов достаточно полным. Тогда вы, как и другие программисты, будете пользоваться этими методами, а изменения в атрибутах не будут расползаться по коду, использующему ваш класс. Кроме того, инкапсуляция позволяет шире использовать такое понятие, как полиморфизм.
В некоторых языках программирования автор класса может закрыть доступ к атрибутам извне класса и заставить всех использующих его программистов работать только с методами. К сожалению, в Python так делать нельзя, однако стоит по возможности пользоваться только методами.
Соглашения об именовании, вызов методов атрибутов
Давайте разберемся еще с одним примером. Напишем класс робота-почтальона, который должен разносить письма в определенные дома и квартиры. (Для простоты считаем, что робот обслуживает одну улицу, и не будем ее указывать.) Класс назовем длинным именем RoboticMailDelivery
, чтобы показать, как в Python принято называть классы с длинным составным именем.
Имя класса должно начинаться с большой буквы, между словами не должно быть прочерка, каждое слово внутри имени должно начинаться с большой буквы.
Например, RoboticMailDelivery
.
Метод add_mail
добавляет кортеж (номер_дома, номер_квартиры) в список-атрибут с помощью метода append. Как видно, вызовы методов для объектов-атрибутов производят обычным образом, вызов метода дописывается справа от объекта: self.house_flat_pairs.append(...)
.
Для имен атрибутов и методов применяются те же правила, что и для имен переменных и функций. Имя должно быть записано в нижнем регистре, слова внутри имени разделяются подчеркиванием: flat_numbers_for_house
, house_flat_pairs
.
Документация с описанием методов записывается в '''многострочных строках''' перед первой инструкцией как в функциях, так и в методах.