Книга: Марк Лутц - Изучаем Python, 5-е изд., Т. 2
Назад: Изучаем Python
Дальше: Основы написания классов

Предисловие

 

По причине большого объема книга разделена на два тома.
Часть I (том 1)
Мы начнем с общего обзора Python, который ответит на часто задаваемые вопросы — почему люди используют язык, для чего он полезен и т.д. В первой главе представлены главные идеи, лежащие в основе технологии, чтобы ввести вас в курс дела. В остальных главах этой части исследуются способы, которыми Python и программисты запускают программы. Главная цель — дать вам достаточный объем информации, чтобы вы были в состоянии работать с последующими примерами и упражнениями.
Часть II (том 1)
Далее мы начинаем тур по языку Python с исследования основных встроенных объектных типов Python и выяснения, что посредством них можно предпринимать: чисел, списков, словарей и т.д. С помощью только этих инструментов уже можно многое сделать, и они лежат в основе каждого сценария Python. Данная часть книги является самой важной, поскольку она образует фундамент для материала, рассматриваемого в оставшихся главах. Здесь мы также исследуем динамическую типизацию и ссылки — ключевые аспекты для правильного применения Python.
Часть III (том 1)
В этой части будут представлены операторы Python — код, набираемый для создания и обработки объектов в Python. Здесь также будет описана общая синтаксическая модель Python. Хотя часть сконцентрирована на синтаксисе, в ней затрагиваются связанные инструменты (такие как система PyDoc), концепции итерации и альтернативные способы написания кода.
Часть IV (том 1)
В этой части начинается рассмотрение высокоуровневых инструментов структурирования программ на Python. Функции предлагают простой способ упаковки кода для многократного использования и избегания избыточности кода. Здесь мы исследуем правила поиска в областях видимости, приемы передачи аргументов, пресловутые лямбда-функции и многое другое. Мы также пересмотрим итераторы с точки зрения функционального программирования, представим определяемые пользователем генераторы и выясним, как измерять время выполнения кода Python для оценки производительности.
Часть V (том I)
Модули Python позволяют организовывать операторы и функции в более крупные компоненты; в этой части объясняется, каким образом создавать, применять и перезагружать модули. Мы также обсудим такие темы, как пакеты модулей, перезагрузка модулей, импортирование пакетов, появившиеся в Python 3.3 пакеты пространств имен и атрибут_name_.
Часть VI (том 2)
Здесь мы исследуем инструмент объектно-ориентированного программирования Python — класс, который является необязательным, но мощным способом структурирования кода для настройки и многократного использования, что почти естественно минимизирует избыточность. Как вы увидите, классы задействуют идеи, раскрытые к этому месту в книге, и объектно-ориентированное программирование в Python сводится главным образом к поиску имен в связанных объектах с помощью специального первого аргумента в функциях. Вы также увидите, что объектно-ориентированное программирование в Python необязательно, но большинство находит объектно-ориентированное программирование на Python более простым, чем на других языках, и оно способно значительно сократить время разработки, особенно при выполнении долгосрочных стратегических проектов.
Часть VII (том 2)
Мы завершим рассмотрение основ языка в книге исследованием модели и операторов обработки исключений Python, а также кратким обзором инструментов разработки, которые станут более полезными, когда вы начнете писать крупные программы (например, инструменты для отладки и тестирования). Хотя исключения являются довольно легковесным инструментом, эта часть помещена после обсуждения классов, поскольку теперь все определяемые пользователем исключения должны быть классами. Мы также здесь раскроем более сложные темы, такие как диспетчеры контекста.
Часть VIII (том 2)
В этой части мы рассмотрим ряд дополнительных тем: Unicode и байтовые строки, инструменты управляемых атрибутов вроде свойств и дескрипторов, декораторы функций и классов и метаклассы. Главы данной части предназначены для дополнительного чтения, т.к. не всем программистам обязательно понимать раскрываемые в них темы. С другой стороны, читатели, которые должны обрабатывать интернационализированный текст либо двоичные данные или отвечать за разработку API-интерфейсов для использования другими программистами, наверняка найдут в этой части что-то интересное для себя. Приводимые здесь примеры крупнее большинства других примеров в книге и могут служить материалом для самостоятельного изучения.
Часть IX (том 2)
Книга завершается четырьмя приложениями, в которых приведены советы по установке и применению Python на разнообразных платформах; представлен запускающий модуль Windows, появившийся в Python 3.3; подытожены изменения, внесенные в различные версии Python; и предложены решения упражнений для каждой части. Ответы на контрольные вопросы по главам приводятся в конце самих глав.
ЧАСТЬ VI
Классы и объектноориентированное программирование
ГЛАВА 26
Объектноориентированное программирование: общая картина
До сих пор в книге мы использовали термин “объект” в общем смысле. На самом деле код, написанный вплоть до этого момента, был основанным на объектах — мы передавали объекты повсюду в сценариях, применяли их в выражениях, вызывали методы объектов и т.д. Однако чтобы код получил право называться подлинно объектно-ориентированнымнаши объекты, как правило, должны также принимать участие в том, что называется иерархией наследования.
В настоящей главе начинается исследование класса Python — кодовой структуры и механизма, используемого для реализации в Python новых видов объектов, которые поддерживают наследование. Классы являются главным инструментом объектно-ориентированного программирования (ООП) на языке Python, так что в этой части книги мы также рассмотрим его основы. ООП предлагает отличающийся и часто более эффективный способ программирования, который предусматривает разложение кода на составляющие с целью минимизации избыточности и написания новых программ путем настройки существующего кода, а не его изменения на месте.
Классы в Python создаются посредством нового оператора class. Как вы увидите, определяемые с помощью классов объекты могут выглядеть очень похожими на встроенные типы, которые мы изучали ранее в книге. В действительности классы всего лишь применяют и расширяют уже раскрытые нами идеи; грубо говоря, они представляют собой пакеты функций, которые используют и обрабатывают объекты встроенных типов. Тем не менее, классы предназначены для создания и управления новыми объектами и поддерживают наследование — механизм настройки и многократного применения кода, выходящий за рамки всего того, что мы видели до сих пор.
Одно предварительное замечание: ООП в Python является совершенно необязательным и вам не нужно использовать классы, когда вы только начинаете программировать. Большой объем работы вы можете делать с применением простых конструкций вроде функций и даже кода верхнего уровня сценариев. Поскольку эффективное использование классов требует заблаговременного планирования, они более интересны тем, кто работает в стратегическом режиме (занимается долгосрочной разработкой продуктов), нежели тем, кто работает в тактическом режиме (испытывая острый дефицит времени).
И все же, как вы увидите в этой части книги, классы оказываются одним из самых полезных инструментов, предоставляемых Python. При надлежащем применении классы способны радикально сократить время разработки. Они также задействованы в популярных инструментах Python наподобие API-интерфейса tkinter, предназначенного для построения графических пользовательских интерфейсов, поэтому большинство программистов на Python обычно сочтут полезным, по крайней мере, практическое знание основ классов.
Для чего используются классы?
Помните ли вы высказывание о том, что программы “делают дела с помощью оснащения”, приведенное в главах 4 и 10? Выражаясь простыми терминами, классы являются лишь способом определения новых видов оснащения, отражающих реальные объекты в предметной области программы. Например, пусть мы решили реализовать гипотетический робот по приготовлению пиццы, который применялся в качестве примера в главе 16. Если мы реализуем его с использованием классов, то сможем моделировать больше элементов его реальной структуры и взаимосвязей. Здесь полезны следующие два аспекта ООП.
Наследование
Роботы по приготовлению пиццы представляют собой разновидность роботов, поэтому они обладают обычными свойствами, присущими роботам. В терминах ООП мы говорим, что они “наследуют” свойства у общей категории всех роботов. Такие обычные свойства должны быть реализованы только один раз для общего случая и могут многократно применяться частично или полностью всеми типами роботов, которые возможно придется строить в будущем.
Композиция
Роботы по приготовлению пиццы являются совокупностями компонентов, которые работают вместе как одна команда. Скажем, чтобы наш робот был успешным, ему могут понадобиться манипуляторы для раскатывания теста, двигатели для перемещения к духовому шкафу и т.п. Выражаясь в манере ООП, наш робот представляет собой пример композиции; он содержит другие объекты и активизирует их для выполнения соответствующих распоряжений. Каждый компонент может быть реализован в виде класса, который определяет собственное поведение и взаимосвязи.
Общие идеи ООП вроде наследования и композиции применимы к любому приложению, которое может быть разложено на набор объектов. Например, в типовых системах с графическими пользовательскими интерфейсами сами интерфейсы реализованы как совокупности виджетов (кнопок, меток и т.д.), которые рисуются тогда, когда рисуется их контейнер (композиция). Кроме того, мы можем располагать возможностью написания собственных виджетов (кнопок с уникальными шрифтами, меток с новыми цветовыми схемами и т.п.), которые являются специализированными версиями общих механизмов интерфейса (наследование).
С более конкретной точки зрения программирования классы представляют собой программные единицы Python в точности как функции и модули: они являются еще одним средством для пакетирования логики и данных. На самом деле во многом подобно модулям классы также определяют новые пространства имен. Но по сравнению с другими программными единицами, встречавшимися ранее, классы обладают тремя важными отличиями, которые делают их более удобными, когда наступает время построения новых объектов.
Множество экземпляров
Классы по существу представляют собой фабрики для генерирования одного и более объектов. При каждом обращении к классу мы генерируем новый объект с отдельным пространством имен. Каждый объект, сгенерированный из класса, имеет доступ к атрибутам класса и получает собственное пространство имен для данных, которые варьируются от объекта к объекту. Методика похожа на сохранение состояния для каждого вызова функциями замыканий из главы 17, но в классах она явная и естественная, к тому же отражает лишь одно из дел, обеспечиваемых классами. Классы предлагают завершенное программное решение.
Настройка через наследование
Классы также поддерживают понятие наследования, принятое в ООП; мы можем расширить класс за счет переопределения его атрибутов вне самого класса в новых программных компонентах, реализованных как подклассы. В более общем смысле классы могут образовывать иерархии пространств имен, которые определяют имена для использования объектами, созданными из классов в иерархии. Таким образом, классы поддерживают множество настраиваемых линий поведения более прямо, нежели другие инструменты.
Перегрузка операций
За счет предоставления специальных методов протокола классы могут определять объекты, реагирующие на всевозможные операции, которые мы видели в работе со встроенными типами. Скажем, к созданным с помощью классов объектам можно применять нарезание, конкатенацию, индексирование и т.д. Python предлагает привязки, которые классы могут использовать для перехвата и реализации любой операции для встроенных типов.
По своей сути механизм ООП в Python — это всего лишь две порции магии: особый первый аргумент в функциях (для получения объекта, на котором произведен вызов) и поиск в иерархии наследования (для поддержки программирования через настройку). Помимо указанных особенностей модель почти полностью сводится к функциям, которые в конечном итоге обрабатывают встроенные типы. Не являясь радикально новым, ООП добавляет дополнительный уровень структуры, которая поддерживает более эффективное программирование, чем обычные процедурные модели. Наряду7 с рассмотренными ранее функциональными инструментами ООП олицетворяет собой значительный шаг в сторону абстрагирования от компьютерного оборудования, который помогает нам строить более сложно устроенные программы.
Объектно-ориентированное программирование с высоты птичьего полета
Прежде чем мы увидим, что все это означает в переводе на код, я хотел бы кратко коснуться общих идей, лежащих в основе ООП. Если до сих пор вы не делали ничего объектно-ориентированного, тогда некоторые термины в настоящей главе на первый взгляд могут показаться слегка озадачивающими. Более того, мотивировка данных терминов может ускользать, пока вы не получите возможность изучить способы, которыми программисты применяют их в более крупных системах. ООП — такая же практика, как и технология.
Поиск в иерархии наследования
Хорошая новость в том, что ООП в Python гораздо проще для понимания и использования, чем в других языках, таких как C++ или Java. Будучи динамически типизированным языком написания сценариев, Python устраняет большую часть синтаксического беспорядка и сложности, которые затуманивают ООП в других инструментах. На самом деле большинство истории ООП в Python сводится до следующего выражения:
объект.атрибут
Мы применяли такое выражение повсюду в книге для доступа к атрибутам модуля, вызова методов объектов и т.п. Однако когда мы используем его в отношении объекта, который получен из оператора class, выражение инициирует поиск в Python — он ищет в дереве связанных объектов первое появление атрибута. Вот что фактически предусматривает предыдущее выражение Python при участии классов:
Найти первое вхождение атрибута, просматривая объект, а затем все классы выше него, снизу вверх и слева направо.
Другими словами, извлечение атрибутов — это просто поиск в дереве. Термин наследование применяется из-за того, что объекты ниже в дереве наследуют атрибуты, присоединенные к объектам выше в дереве. По мере того, как поиск продолжается снизу вверх, связанные в дерево объекты в некотором смысле представляют собой объединение всех атрибутов, определенных во всех родителях в дереве на всем пути вверх.
В Python все происходит буквально: мы действительно строим дерево связанных объектов с помощью кода, a Python во время выполнения на самом деле поднимается по такому дереву в поисках атрибутов каждый раз, когда встречается выражение объект. атрибут. Пример одного из таких деревьев показан на рис. 26.1.
На рис. 26.1 изображено дерево из пяти объектов, помеченных переменными, которые имеют присоединенные атрибуты, готовые для поиска. Более конкретно дерево связывает вместе три объекта классов (овалы Cl, С2 и СЗ) и два объекта экземпляров (прямоугольники II и 12), образуя дерево поиска в иерархии наследования. Обратите внимание, что в объектной модели Python классы и генерируемые из них экземпляры являются двумя отдельными типами объектов.
Классы
Служат фабриками экземпляров. Атрибуты классов обеспечивают поведение (данные и функции), которое наследуется всеми экземплярами, сгенерированными из них (например, функция для расчета заработной платы сотрудника на основе оклада и отработанных часов).
Экземпляры
Представляют конкретные элементы в предметной области программы. Атрибуты экземпляров хранят данные, которые варьируются для каждого отдельного объекта (скажем, номер карточки социального страхования сотрудника).
С точки зрения деревьев поиска экземпляр наследует атрибуты от своего класса, а класс наследует атрибуты от всех классов выше в дереве.
На рис. 26.1 мы можем продолжить категоризацию овалов по их относительным позициям в дереве. Классы, расположенные более высоко в дереве (наподобие С2 и СЗ), мы обычно называем суперклассами, классы, находящиеся ниже в дереве (вроде С1) известны как подклассы. Указанные термины касаются относительных позиций в дереве и исполняемых ролей. Суперклассы обеспечивают поведение, разделяемое всеми их подклассами, но поскольку поиск направлен снизу вверх, подклассы могут переопределять поведение, определенное их суперклассами, за счет переопределения имен суперклассов ниже в дереве.
Так как последние несколько слов на самом деле отражают суть работы по настройке программного обеспечения в ООП, давайте расширим данную концепцию. Предположим, что мы построили дерево, приведенное на рис. 26.1, и затем написали:
12.w
Код сразу же обращается к наследованию. Поскольку он представляет собой выражение объект, атрибут, инициируется поиск в дереве, изображенном на рис. 26.1 — Python будет искать атрибут w в 12 и выше. В частности, он будет проводить поиск внутри связанных объектов в следующем порядке:
12, Cl, С2, СЗ
и остановится при нахождении первого присоединенного атрибута w (или сообщит об ошибке, если w не удалось отыскать). В данном случае атрибут w не будет найден до тех пор, пока не пройдет поиск в СЗ, потому что он присутствует только в этом объекте. Другими словами, благодаря автоматическому поиску 12 .w распознается как СЗ. w. В терминах ООП экземпляр 12 “наследует” атрибут w от СЗ.
В конечном итоге два экземпляра наследуют от своих классов четыре атрибута: w, х, у и z. Ссылки на другие атрибуты будут вызывать проход по другим путям в дереве.
Вот примеры.
• Для II.х и 12 .х атрибут х обнаруживается в С1 и поиск останавливается, т.к. С1 располагается в дереве ниже, чем С2.
• Для II. у и 12 . у атрибут у обнаруживается в С1, поскольку это единственное место, где присутствует у.
• Для II. z и 12 . z атрибут z обнаруживается в С2, потому что С2 располагается в дереве левее, чем СЗ.
• Для 12 . паше атрибут паше обнаруживается в 12 вообще без подъема по дереву.
Отследите описанные варианты в дереве на рис. 26.1, чтобы получить представление о том, как работает поиск в иерархии наследования в Python.
В предыдущем списке первый элемент является, вероятно, наиболее важным — поскольку класс С1 переопределяет атрибут х ниже в дереве, он фактически замещает его версию, находящуюся выше в С2. Как вы вскоре увидите, такие переопределения являются центральной частью настройки программного обеспечения в ООП — за счет переопределения и замещения атрибута класс С1 по существу настраивает то, что он наследует от своих суперклассов.
Классы и экземпляры
Хотя формально классы и экземпляры, помещаемые в деревья наследования, являются разными типами объектов в модели Python, они практически идентичны; главная цель каждого типа заключается в том, чтобы служить еще одним видом пространства имен — пакета переменных и места, куда мы можем присоединять атрибуты. Следовательно, если классы и экземпляры выглядят подобными модулям, то так и должно быть; тем не менее, объекты в деревьях классов также имеют автоматически просматриваемые ссылки на другие объекты пространств имен и классы соответствуют операторам, а не целым файлам.
Основное отличие между классами и экземплярами состоит в том, что классы представляют собой своего рода фабрики для генерирования экземпляров. Скажем, в реалистичном приложении мы могли бы иметь класс Employee, который определяет все то, что характерно для сотрудника; из этого класса мы генерируем действительные экземпляры Employee. Есть еще одно отличие между классами и модулями — мы можем иметь только один экземпляр отдельного модуля в памяти (именно потому модуль приходится перезагружать, чтобы получить его новый код), но для классов допускается создавать столько экземпляров, сколько нужно.
Что касается эксплуатации, то классы обычно будут располагать присоединенными к ним функциями (например, computeSalary), а экземпляры будут иметь больше базовых элементов данных, используемых функциями класса (скажем, hoursWorked). На самом деле объектно-ориентированная модель ничем не отличается от классической модели обработки данных с программами и записями — в ООП экземпляры похожи на записи с “данными”, а классы являются “программами” для обработки этих записей. Однако в ООП также присутствует понятие иерархии наследования, которая лучше поддерживает настройку программного обеспечения, чем более ранние модели.
Вызовы методов
В предыдущем разделе мы видели, что ссылка на атрибут I2.w в примере дерева классов была оттранслирована в СЗ. w посредством процедуры поиска внутри иерархии наследования в Python. Тем не менее, столь же важно понимать, что происходит, когда мы пытаемся вызывать методы — функции, присоединенные к классам в качестве атрибутов.
Если ссылка 12 . w представляет собой вызов функции, тогда в действительности она означает “вызвать функцию СЗ. w для обработки 12”. То есть Python будет автоматически отображать вызов 12 . w () на вызов СЗ . w (12), передавая унаследованной функции экземпляр в первом аргументе.
Фактически всякий раз, когда вызывается функция, подобным образом присоединенная к классу, всегда подразумевается экземпляр класса. Подразумеваемый объект или контекст отчасти является причиной того, что мы называем это объектно-ориентированной моделью — при выполнении операции всегда имеется подчиненный объект. В более реалистичном примере мы могли бы вызывать метод повышения по имени giveRaise, присоединенный в виде атрибута к классу сотрудника Employee; такой вызов не имеет смысла, если только он не уточнен объектом сотрудника, в отношении которого должно быть применено повышение.
Как мы увидим позже, Python передает подразумеваемый экземпляр методу в особом первом аргументе, который по соглашению называется self. Методы принимают этот аргумент для обработки объекта, на котором произведен вызов. Как мы также узнаем, методы можно вызывать либо через экземпляр (bob. giveRaise ()), либо через класс (Employee.giveRaise (bob)), и обе формы служат своим целям в наших сценариях. Вызовы также иллюстрируют обе ключевые идеи в ООП: ниже описано, что делает Python, чтобы выполнить вызов метода bob. giveRaise ().
1. Ищет giveRaise в bob с помощью поиска в иерархии наследования.
2. Передает bob найденной функции giveRaise в особом аргументе self.
Когда вы вызываете Employee. giveRaise (bob), то всего лишь самостоятельно выполняете оба шага. Формально приведенное описание отражает стандартный случай (в Python имеются дополнительные типы методов, с которыми мы встретимся позже), но оно применимо к подавляющему большинству написанного кода с использованием ООП. Однако чтобы посмотреть, как методы принимают свои подчиненные объекты, нам необходимо перейти к написанию какого-нибудь кода.
Создание деревьев классов
Несмотря на то что мы говорим обо всех идеях абстрактно, конечно же, за ними стоит овеществленный код. Мы создадим с помощью операторов class и обращений к классам деревья и их объекты, которые затем исследуем более детально. Ниже приведена краткая сводка.
• Каждый оператор class генерирует новый объект класса.
• При каждом обращении к классу он генерирует новый объект экземпляра.
• Экземпляры автоматически связываются с классами, из которых они были созданы.
• Классы автоматически связываются со своими суперклассами в соответствии со способом их перечисления внутри круглых скобок в строке заголовка class; порядок слева направо здесь дает порядок в дереве.
Например, чтобы построить дерево, показанное на рис. 26.1, мы могли бы запустить представленный далее код Python, Подобно определениям функций код классов обычно помещается в файлы модулей и выполняется во время импортирования (для краткости внутренности операторов class опущены):
class C2: ... # Создание объектов классов (овалов)
class СЗ: ...
class С1(С2, СЗ) : . . . # Связывание с его суперклассами (в указанном порядке)
11 = С1 () # Создание объектов экземпляров (прямоугольников)
12 = С1() # Связывание с его классом
Здесь мы создаем три объекта классов посредством трех операторов class и два объекта экземпляров за счет двукратного обращения к классу С1, как если бы он был функцией. Экземпляры запоминают класс, из которого были созданы, а класс С1 запоминает свои перечисленные суперклассы.
Формально в примере применяется то, что называется множественным наследованием, которое просто означает наличие у класса более одного суперкласса выше в дереве классов — удобный прием, когда желательно объединить несколько инструментов. В Python при перечислении более одного суперкласса внутри круглых скобок в операторе class (как выше в С1) их порядок слева направо задает порядок, в котором будет производиться поиск атрибутов в этих суперклассах. По умолчанию используется крайняя слева версия имени, хотя вы всегда можете выбрать имя, запросив его у класса, где имя находится (скажем, СЗ. z).
Из-за особенностей выполнения поиска в иерархии наследования объект, к которому вы присоединяете атрибут, оказывается критически важным — он определяет область видимости имени. Атрибуты, присоединенные к экземплярам, сохраняются только для этих одиночных экземпляров, но атрибуты, присоединенные к классам, разделяются всеми их подклассами и экземплярами. Позже мы более глубоко исследуем код, который присоединяет атрибуты к таким объектам. Мы увидим, что:
• атрибуты обычно присоединяются к классам с помощью присваиваний, выполняемых на верхнем уровне блоков операторов class, а не во вложенных операторах def определения функций;
• атрибуты обычно присоединяются к экземплярам посредством присваиваний особому аргументу, передаваемому функциям внутри классов, по имени self.
Например, классы обеспечивают поведение для своих экземпляров с помощью функций методов, которые мы создаем за счет написания кода операторов def внутри операторов class. Поскольку такие вложенные операторы def присваивают имена внутри класса, они в итоге присоединяют к объекту класса атрибуты, которые будут наследоваться всеми экземплярами и подклассами:
class С2: ... # Создание объектов суперклассов class СЗ: ...
class С1(С2, СЗ) : # Создание и связывание класса С1
def setname(self, who): # Присваивание name: Cl.setname
self .name = who # self является либо II, либо 12
известен как метод и автоматически получает особый первый аргумент, по соглашению называемый self, который предоставляет возможность обращаться к обрабатываемому экземпляру. Любые значения, которые вы передаете методу самостоятельно, отправляются аргументам, следующим после self (здесь who).
Из-за того, что классы представляют собой фабрики для множества экземпляров, их методы обычно задействуют этот автоматически передаваемый аргумент self всякий раз, когда необходимо извлекать или устанавливать атрибуты отдельного экземпляра, обрабатываемого вызовом метода. В предыдущем коде self применяется для сохранения name в одном из двух экземпляров.
Как и простые переменные, атрибуты классов и экземпляров не объявляются заблаговременно, но появляются во время присваивания значений в первый раз. Когда метод выполняет присваивание атрибуту self, он создает или изменяет атрибут в экземпляре в нижней части дерева классов (т.е. один из прямоугольников на рис. 26.1), потому что self автоматически ссылается на обрабатываемый экземпляр — объект, на котором произведен вызов.
На самом деле, поскольку все объекты в деревьях классов являются всего лишь объектами пространств имен, мы можем извлекать или устанавливать любой из их атрибутов, указывая подходящее имя. Выражение Cl. setname в той же мере допустимо, как и II. setname, при условии, что имена С1 и II находятся в области видимости вашего кода.
Перегрузка операций
В текущем виде наш класс С1 не присоединяет атрибут name к экземпляру до тех пор, пока не будет вызван метод setname. В действительности ссылка II .name перед вызовом II. setname привела бы к возникновению ошибки неопределенного имени. Если в классе желательно гарантировать, что атрибут вроде name всегда устанавливает^ ся в экземплярах, тогда более типично будет заполнять его во время конструирования:
class С2: . . . # Создание объектов суперклассов
class СЗ: ...
class Cl(С2, СЗ):
def _init_(self, who) : # Установка name при конструировании
self .name = who # self является либо II, либо 12
11 = Cl ('bob') # Установка II. name в 'bob'
12 = Cl ('sue') # Установка 12. name в 'sue'
print (II. name) # Выводит 'bob'
Каждый раз, когда экземпляр генерируется из класса, Python автоматически вызывает метод по имени_init_, будь он реализован или унаследован. Как обычно,
новый экземпляр передается в аргументе self метода_init_, а значения, перечисленные в круглых скобках при обращении к классу, передаются второму и последующим аргументам. Результатом оказывается инициализация экземпляров, когда они создаются, без необходимости в дополнительных вызовах методов.
Метод_init_известен как конструктор из-за момента своего запуска. Он является самым часто используемым представителем крупной группы методов, называемых методами перегрузки операций, которые мы обсудим более подробно в последующих главах. Такие методы обычным образом наследуются в деревьях классов и содержат в начале и конце своих имен по два символа подчеркивания, чтобы акцентировать внимание на их особенности. Python запускает их автоматически, когда экземпляры, поддерживающие методы, встречаются в соответствующих операциях, и они главным образом выступают в качестве альтернативы применению простых вызовов методов. Кроме того, они необязательны: если операции опущены, то они не поддерживаются.
При отсутствии_init_обращения к классам возвращают пустые экземпляры без
их инициализации.
Скажем, для реализации пересечения множеств класс может либо предоставить метод по имени intersect, либо перегрузить операцию выражения & и обеспечить
требующуюся логику за счет реализации метода по имени_and_. Поскольку схема с
операциями делает экземпляры больше похожими на встроенные типы, она позволяет ряду классов предоставлять согласованный и естественный интерфейс, а также быть совместимыми с кодом, который ожидает встроенного типа. Однако за исключением
конструктора_init_, который присутствует в большинстве реалистичных классов,
во многих программах лучше использовать более просто именованные методы, если только их объекты не подобны объектам встроенных типов. Метод giveRaise может иметь смысл для класса Employee, но операция & — нет.
Объектно-ориентированное программирование — это многократное использование кода
Наряду с несколькими синтаксическими деталями все описанное ранее является значительной частью истории об ООП в Python. Разумеется, здесь есть нечто большее, нежели просто наследование. Например, перегрузка операций намного более универсальна, чем обсуждалось до сих пор — классы могут также предоставлять собственные реализации операций, таких как индексирование, извлечение атрибутов, вывод и т.д. Тем не менее, в общем ООП сводится к поиску атрибутов в деревьях и особому первому аргументу в функциях.
Почему нас может интересовать создание и поиск в деревьях объектов? При надлежащем применении классы поддерживают многократное использование кода способами, которые не могут обеспечить другие программные компоненты Python, хотя для этого необходим определенный опыт. Фактически в том и заключается их наивысшая цель. С помощью классов мы настраиваем существующее программное обеспечение вместо того, чтобы либо изменять имеющийся код на месте, либо начинать с нуля каждый новый проект. В итоге они оказываются мощной парадигмой в реальном программировании.
На фундаментальном уровне классы являются всего лишь пакетами функций и других имен, что очень похоже на модули. Однако получаемый благодаря классам автоматический поиск атрибутов в иерархии наследования поддерживает настройку программного обеспечения, которая выходит за рамки того, что можно делать посредством модулей и функций. Кроме того, классы обеспечивают естественную структуру для кода, которая упаковывает и локализует логику и имена, что оказывает помощь в отладке.
Например, поскольку методы представляют собой функции с особым первым аргументом, мы можем частично имитировать их поведение, вручную передавая подлежащие обработке объекты простым функциям. Тем не менее, участие методов в наследовании классов позволяет нам естественным образом настраивать существующее программное обеспечение путем создания подклассов с новыми определениями методов, а не изменять имеющийся код на месте. В случае модулей и функций такой возможности нет.
Полиморфизм и классы
В качестве примера предположим, что вам поручили реализовать приложение с базой данных сотрудников. Как программист на Python, применяющий ООП, вы можете начать с создания универсального суперкласса, в котором определены стандартные линии поведения, общие для всех типов сотрудников в организации:
class Employee: # Универсальный суперкласс
def computeSalary (self) : . . . # Общие или стандартные линии поведения def giveRaise(self): ... def promote(self): ... def retire(self): ...
После написания кода общего поведения вы можете специализировать его для каждого индивидуального типа сотрудника, отражая его отличия от нормы. То есть вы можете создавать подклассы, настраивающие только те фрагменты поведения, которые отличаются в зависимости от типа сотрудника; остальное поведение будет унаследовано от более универсального класса. Скажем, если с инженерами связано уникальное правило подсчета заработной платы (возможно, они не на почасовой оплате), тогда вы можете заменить в подклассе только один метод:
class Engineer (Employee) : # Специализированный подкласс
def computeSalary(self) : ... # Что-то специальное
Из-за того, что версия computeSalary находится ниже в дереве классов, она заместит (переопределит) универсальную версию в Employee. Затем вы создаете экземпляры разновидностей классов сотрудников, к которым принадлежат реальные сотрудники, чтобы получить корректное поведение:
bob = Employee () # Стандартное поведение
sue = Employee () # Стандартное поведение
tom = Engineer () # Специальный расчет заработной платы
Обратите внимание, что вы можете создавать экземпляры любого класса в дереве, а не только классов в нижней части — класс, из которого вы создаете экземпляр, определяет уровень, откуда будет начинаться поиск атрибутов, и соответственно то, какие версии методов он будет задействовать.
В конце концов, эти три объекта экземпляров могут оказаться встроенными в более крупный контейнерный объект (например, список или экземпляр другого класса), который представляет отдел или компанию, воплощая упомянутую в начале главы идею композиции. Когда вы позже запросите заработные платы сотрудников, они будут рассчитываться в соответствии с классами, из которых создавались объекты, благодаря принципам поиска в иерархии наследования:
company = [bob, sue, tom] # Составной объект
for emp in company:
print (emp. computeSalary () ) # Выполнить версию для данного объекта:
# стандартную или специальную
Мы имеем еще одну иллюстрацию идеи полиморфизма, которая была представлена в главе 4 и расширена в главе 16. Как вы наверняка помните, полиморфизм означает, что смысл операции зависит от объекта, с которым она работает. Таким образом, код не должен заботиться о том, чем объект является, а лишь о том, что он делает. Перед вызовом метод computeSalary ищется в иерархии наследования для каждого объекта. Совокупный эффект заключается в том, что мы автоматически запускаем корректную версию для обрабатываемого объекта. Для лучшего понимания отследите код.
В других приложениях полиморфизм также может использоваться для сокрытия (т.е. инкапсуляции) отличий в интерфейсах. Скажем, программа обработки потоков данных может быть реализована так, чтобы ожидать объекты с методами ввода и вывода, не заботясь о том, что в действительности делают эти методы:
def processor(reader, converter, writer): while True:
data = reader.read() if not data: break data = converter(data) writer.write(data)
Передавая экземпляры подклассов, которые специализируют обязательные интерфейсные методы read и write для различных источников данных, мы можем многократно применять функцию processor для любого необходимого источника данных, как сейчас, так и в будущем:
class Reader:
def read (self) : . . . # Стандартное поведение и инструменты
def other(self): ...
class FileReader(Reader):
def read (self) : . . . # Читать из локального файла
class SocketReader(Reader):
def read(self): ... # Читать из сетевого сокета
processor(FileReader(...), Converter, FileWriter (. . .) )
processor(SocketReader(...), Converter, TapeWriter (. . .))
processor(FtpReader(...), Converter, XmlWriter (. . .))
Более того, поскольку внутренние реализации методов read и write вынесены в отдельные места, их можно изменять, не оказывая влияние на код, в котором они используются. Функцию processor можно было бы даже превратить в класс, чтобы позволить логике преобразования converter наполняться через наследование и дать возможность подклассам чтения и записи встраиваться посредством композиции (позже в данной части книги будет показано, как это работает).
Программирование путем настройки
Как только вы привыкнете к программированию в таком стиле (путем настройки программного обеспечения), вы обнаружите, что когда наступает время написания новой программы, большая часть работы может оказаться сделанной — в значительной степени ваша задача сводится к смешиванию существующих суперклассов, которые уже реализуют поведение, требующееся для программы. Например, кто-то другой мог написать классы Employee, Reader и Writer для применения в совершенно разных программах. В таком случае вы получаете весь их код “бесплатно”.
Фактически во многих прикладных областях вы можете выбрать или приобрести наборы суперклассов, известные как фреймворку,, которые реализуют распространенные задачи программирования в виде классов, готовых к смешиванию в ваших приложениях. Подобные фреймворки могут предоставлять интерфейсы к базам данных, протоколы тестирования, комплекты инструментов для построения графических пользовательских интерфейсов и т.д. Располагая фреймворками, вы часто просто пишете код подкласса, в котором реализуете один или два ожидаемых метода; большую часть работы выполняют классы фреймворков, находящиеся выше в дереве. Программирование в таком мире ООП представляет собой всего лишь комбинирование и специализацию уже отлаженного кода за счет написания собственных подклассов.
Конечно, обучение тому, как задействовать классы для достижения идеального мира ООП, требует времени. На практике ООП также влечет за собой значительную работу по проектированию, чтобы получить все преимущества от многократного использования кода классов. С этой целью программисты начали каталогизировать распространенные структуры ООП, известные как паттерны проектирования, которые призваны помочь в решении проблем, возникающих при проектировании. Однако действительный код, который вы пишете с применением ООП в Python, настолько прост, что сам по себе он не будет дополнительным препятствием для вашего путешествия в ООП. Чтобы удостовериться в этом, читайте главу 27.
Резюме
В главе мы кратко рассмотрели классы и ООП, предоставив общую картину, прежде чем углубляться в детали синтаксиса. Как вы видели, ООП — это в основном аргумент по имени self и поиск атрибутов в деревьях связанных объектов, называемых наследованием. Объекты в нижней части дерева наследуют атрибуты от объектов выше в дереве — характеристика, которая делает возможным программирование путем настройки кода, а не его изменения или написания с нуля. При надлежащем использовании такая модель программирования может радикально сократить время разработки.
В следующей главе начнется наполнение общей картины недостающими деталями написания кода. Тем не менее, по мере углубления в классы имейте в виду, что модель ООП в Python очень проста; как будет показано, в действительности она сводится всего лишь к поиску атрибутов в деревьях объектов и особому аргументу функций. Но до того как двигаться дальше, закрепите пройденный материал главы, ответив на контрольные вопросы.
Проверьте свои знания: контрольные вопросы
1. В чем сущность ООП в Python?
2. Где процедура поиска в иерархии наследования ищет атрибуты?
3. В чем отличие между объектом класса и объектом экземпляра?
4. Почему первый аргумент в функции метода класса является особым?
5. Для чего применяется метод_init_?
6. Как бы вы создали экземпляр класса?
7. Как бы вы создали класс?
8. Как бы вы указали суперклассы класса?
Проверьте свои знания: ответы
1. ООП предназначено для многократного использования кода — вы производите разложение кода с целью минимизации избыточности и программируете путем настройки того, что уже существует, а не изменяете код на месте или пишете его с нуля.
2. Процедура поиска в иерархии наследования ищет атрибут сначала в объекте экземпляра, затем в классе, из которого был создан экземпляр, далее во всех расположенных выше суперклассах, двигаясь от нижней части дерева объектов к его верхней части и слева направо (по умолчанию). Поиск останавливается в первом месте, где найден атрибут. Поскольку выигрывает самая нижняя версия имени, найденная в ходе поиска, иерархии классов естественным образом поддерживают настройку путем расширения в новых подклассах.
3. Объекты классов и объекты экземпляров представляют собой пространства имен (пакеты переменных, которые выступают в качестве атрибутов). Основное отличие между ними в том, что классы являются своего рода фабриками для создания множества экземпляров. Классы также поддерживают методы перегрузки операций, которые экземпляры наследуют, а любые функции, вложенные внутрь классов, трактуются как методы для обработки экземпляров.
4. Первый аргумент в функции метода класса является особым, потому что он всегда получает объект экземпляра, представляющий собой подразумеваемый объект, на котором вызван метод. По соглашению он называется self. Поскольку по умолчанию функции методов всегда имеют такой подразумеваемый объект и объектный контекст, мы говорим, что они являются “объектноориентированными” (т.е. предназначенными для обработки либо изменения объектов).
5. Метод _init_ реализуется или наследуется в классе, и Python вызывает
его автоматически каждый раз, когда создается экземпляр этого класса. Он известен как метод конструктора; ему неявно передается новый экземпляр, а также любые аргументы, указанные явно с именем класса. Кроме того, он является наиболее часто применяемым методом перегрузки операций. В случае отсутствия
метода_init_экземпляры просто начинают свое существование как пустые
пространства имен.
6. Вы создаете экземпляр класса с помощью обращения к имени класса так, как если бы оно было функцией. Любые аргументы, указанные с именем класса, становятся вторым и последующими аргументами в методе конструктора
_init_. Новый экземпляр запоминает класс, из которого он был создан, для
целей, связанных с наследованием.
7. Вы создаете класс посредством выполнения оператора class; подобно определениям функций такие операторы обычно выполняются при импортировании включающего модуля (более подробно об этом речь пойдет в следующей главе).
8. Вы указываете суперклассы класса, перечисляя их внутри круглых скобок в операторе class после имени нового класса. Порядок слева направо, в котором классы перечисляются в круглых скобках, дает порядок слева направо при поиске в иерархии наследования, представленной деревом классов.
ГЛАВА 27
Назад: Изучаем Python
Дальше: Основы написания классов