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

Более реалистичный пример

 

В следующей главе мы будем исследовать детали синтаксиса классов. Однако прежде чем заняться этим, имеет смысл рассмотреть пример работы с классами, более реалистичный, нежели то, что приводились до сих пор. Мы построим набор классов, делающих кое-что более конкретное — регистрацию и обработку сведений о людях. Вы увидите, что компоненты, которые в программировании на Python называются экземплярами и классами, часто способны исполнять такие же роли, как записи и программы в более традиционных терминах.
В частности мы планируем реализовать два класса:
• Person — класс, который создает и обрабатывает сведения о людях;
• Manager — настроенная версия класса Person, которая модифицирует унаследованное поведение.
Попутно мы создадим экземпляры обоих классов и протестируем их функциональность. Затем будет продемонстрирован подходящий сценарий использования для классов — мы сохраним наши экземпляры в объектно-ориентированной базе данных shelve, обеспечив их постоянство. В итоге вы сможете применять написанный код в качестве шаблона для формирования полноценной базы данных, реализованной полностью на Python.
Тем не менее, кроме реальной полезности настоящая глава также имеет и учебный характер: она предлагает руководство по ООП на Python. Люди часто схватывают основную идею синтаксиса классов, описанную в предыдущей главе, но им трудно понять, с чего начать, когда возникает необходимость в создании нового класса с нуля. С этой целью мы будем делать здесь по одному шагу за раз, чтобы помочь вам усвоить основы; классы будут строиться постепенно, так что вы сможете увидеть, как их функциональные средства объединяются в завершенные программы.
В конце концов, наши классы по-прежнему будут относительно небольшими по объему кода, но проиллюстрируют основные идеи в модели ООП на языке Python. Несмотря на синтаксические детали, система классов Python в действительности сводится всего лишь к поиску атрибута в дереве объектов и к специальному первому аргументу в функциях.
Шаг 1: создание экземпляров
Итак, закончим стадию проектирования и приступим к реализации. Наша первая задача — начать написание кода главного класса, Person. Откроем текстовый редактор и создадим новый файл для кода, который будет написан. В Python принято довольно строгое соглашение начинать имена модулей с буквы нижнего регистра, а имена классов — с буквы верхнего регистра. Как и имя аргументов self в методах, язык этого не требует, но соглашение получило настолько широкое распространение, что отклонение от него может сбить с толку тех, кто впоследствии будет читать ваш код. Для соответствия соглашению мы назовем новый файл модуля person .ру и назначим классу внутри него имя Person:
# Файл person.ру (начало)
class Person: # Начало класса
Вся работа внутри файла будет делаться далее в главе. В одном файле модуля Python можно создавать любое количество функций и классов, поэтому имя файла person .ру может утратить смысл, если позже мы добавим в данный файл несвязанные компоненты. Пока что мы предположим, что абсолютно все в этом файле будет иметь отношение к Person. Вероятно, так и должно быть в любом случае — как выяснится, модули работают лучше всего, когда они преследуют единую цель сцепления.
Написание кода конструкторов
Первое, что мы хотим делать с помощью класса Person, связано с регистрацией основных сведений о людях — заполнением полей записей, если так понятнее. Разумеется, в терминологии Python они известны как атрибуты объекта экземпляра и обычно создаются путем присваивания значений атрибутам self в функциях методов класса. Нормальный способ предоставления атрибутам экземпляра первоначальных значений предусматривает их присваивание через self в методе конструктора
_init_, который содержит код, автоматически выполняемый Python каждый раз,
когда создается экземпляр. Давайте добавим к классу метод конструктора:
# Добавление инициализации полей записи
class Person:
def__init_(self, name, job, pay) : # Конструктор принимает три аргумента
self .name = name # Заполнить поля при создании
self, job = job # self - новый объект экземпляра
self.pay = pay
Такая схема написания кода весьма распространена: мы передаем данные, подлежащие присоединению к экземпляру, в виде аргументов методу конструктора и присваиваем их атрибутам self, чтобы сохранить их на постоянной основе. В терминах ООП аргумент self является вновь созданным объектом экземпляра, а паше, job и pay становятся информацией о состоянии — описательными данными, сохраняемыми в объекте для использования в будущем. Хотя другие методики (такие как замыкания вложенных областей видимости) тоже способны сохранять детали, атрибуты экземпляра делают это очень явным и легким для понимания.
Обратите внимание, что имена аргументов здесь встречаются дважды. Поначалу код может даже показаться несколько избыточным, но это не так. Например, аргумент j ob представляет собой локальную переменную в области видимости функции _init_, но self, job — атрибут экземпляра, в котором передается подразумеваемый объект вызова метода. Они являются двумя разными переменными, по воле случая имеющие одно и то же имя. За счет присваивания локальной переменной job атрибуту self. job посредством self. job=job мы сохраняем переданное значение j ob в экземпляре для последующего применения. Как обычно в Python, предназначение имени определяется местом, где ему присваивается значение, или объектом, который ему присвоен.
Говоря об аргументах, с методом конструктора_init_в действительности не
связано ничего магического, помимо того факта, что он автоматически вызывается при создании экземпляра и имеет особым образом именованный первый аргумент. Несмотря на свое странное название, он представляет собой нормальную функцию и поддерживает все возможности функций, рассмотренные до сих пор. Скажем, мы можем указывать стандартные значения для ряда аргументов, так что их не придется предоставлять в случаях, когда они недоступны или бесполезны.
В целях демонстрации давайте сделаем аргумент j ob необязательным — он будет получать стандартное значение None, указывающее на то, что создаваемый экземпляр Person представляет человека, который (в текущий момент) не нанят на работу. Поскольку стандартным значением job будет None, тогда ради согласованности, вероятно, имеет смысл также установить стандартное значение для pay в 0 (если только какие-то ваши знакомые не умудряются получать заработную плату, не имея работы!). На самом деле мы обязаны указать стандартное значение для pay, т.к. в соответствии с правилами синтаксиса Python и главой 18 из первого тома все аргументы в заголовке функции, находящиеся после первого аргумента со стандартным значением, тоже должны иметь стандартные значения:
# Добавление стандартных значений для аргументов конструктора
class Person:
def _init_(self, name, job^None, pay=Q) : # Нормальные аргументы функции
self.name = name self.job = job self.pay = pay
Такой код означает, что при создании экземпляров Person нам необходимо передавать имя, но аргументы j ob и pay теперь необязательны; они получат стандартные значения None и 0, когда опущены. Как обычно, аргумент self заполняется Python автоматически для ссылки на объект экземпляра — присваивание значений атрибутам self присоединяет их к новому экземпляру.
Тестирование в ходе дела
Пока что класс Person делает не особо многое (по существу он всего лишь заполняет поля новой записи), но является реальным рабочим классом. К настоящему моменту мы могли бы добавить код для дополнительных возможностей, но не будем этого делать. Как вы вероятно уже начали понимать, программирование на Python в действительности представляет собой вопрос пошагового создания прототипов — вы пишете какой-то код, тестируете его, пишете дополнительный код, снова тестируете и т.д. Поскольку Python обеспечивает нас как интерактивным сеансом, так и практически немедленным учетом изменений в коде, более естественно проводить тестирование в ходе дела, нежели писать крупный объем кода и тестировать его весь сразу.
Прежде чем добавлять дополнительные возможности, давайте протестируем то, что мы получили до сих пор, создав несколько экземпляров нашего класса и отобразив их атрибуты в том виде, как их присоединил конструктор. Мы могли бы делать это интерактивно, но как вы уже наверняка догадались, интерактивному тестированию присущи свои ограничения — довольно утомительно заново импортировать модули и повторно набирать тестовые сценарии каждый раз, когда начинается новый сеанс тестирования. Чаще всего программисты на Python используют интерактивную подсказку для простых одноразовых тестов, но выполняют более существенное тестирование путем написания кода в конце файла, который содержит объекты, подлежащие тестированию:
# Добавление кода самотестирования
class Person:
def _init_(self, name, job=None, pay=0) :
self.name = name self.job = job self.pay = pay
bob = Person('Bob Smith') # Тестирование класса
sue = Person('Sue Jones', job='dev', pay=100000) # Автоматически
# выполняет_init_
print(bob.name, bob.pay) # Извлечение присоединенных атрибутов
print (sue.name, sue.pay) # Атрибуты sue и bob отличаются
Обратите внимание, что объект bob принимает стандартные значения для j ob и pay, но объект sue предоставляет значения явно. Также взгляните на то, как применяются ключевые аргументы при создании sue. Мы могли бы взамен передавать аргументы по позиции, но ключевые аргументы могут помочь вспомнить назначение данных в более позднее время и позволяют передавать аргументы в любом желаемом порядке слева направо. Опять-таки, несмотря на необычное имя,_init_является нормальной функцией, поддерживающей все то, что вы уже знаете о функциях — в том числе стандартные значения и передаваемые по имени ключевые аргументы.
В случае запуска файла per son. ру как сценария тестовый код в конце файла создает два экземпляра нашего класса и выведет значения двух атрибутов каждого (паше и pay):
C:\code> person.ру
Bob Smith 0
Sue Jones 100000
Вы можете также набрать тестовый код данного файла в интерактивной подсказке Python (предварительно импортировав класс Person), но помещение кода заготовленных тестов внутрь файла модуля, как было показано выше, значительно облегчает их повторный запуск в будущем.
Хотя приведенный тестовый код довольно прост, он уже демонстрирует кое-что важное. Обратите внимание, что атрибут паше объекта bob — не такой же, как у sue, a pay объекта sue — не такой же, как у bob. Каждый объект представляет собой независимую запись со сведениями. Формально bob и sue являются объектами пространств имен— подобно всем экземплярам классов каждый из них имеет собственную независимую копию информации о состоянии, созданную классом. Из-за того, что каждый экземпляр класса располагает своим набором атрибутов self, классы оказываются естественным инструментом для регистрации сведений для множества объектов. Как и встроенные типы вроде списков и словарей, классы служат своего рода фабриками объектов.
Другие программные структуры Python, такие как функции и модули, не поддерживают концепцию подобного рода. Функции замыканий из главы 17 первого тома близки с точки зрения сохранения состояния для каждого вызова, но не обладают множеством методов, наследованием и более крупной структурой, которые мы получает от классов.
Использование кода двумя способами
В том виде, как есть, тестовый код в конце файла работает, но есть большая загвоздка — его операторы print верхнего уровня выполняются и при запуске файла как сценария, и при импортировании как модуля. Это означает, что когда мы решим импортировать класс из файла person.py, чтобы применять его где-то в другом месте (и позже в главе мы так и поступим), то при каждом импортировании будем видеть вывод его тестового кода. Однако такая ситуация не может считаться подходящей для программного обеспечения: клиентские программы, скорее всего, не заботят наши внутренние тесты и в них нежелательно смешивать наш вывод с собственным выводом.
Хотя мы могли бы вынести тестовый код в отдельный файл, часто удобнее размещать код тестов в том же самом файле, где расположены тестируемые элементы. Было бы лучше организовать выполнение тестовых операторов в конце файла, только когда файл запускается для тестирования, а не когда он импортируется. Именно для этого предназначена проверка атрибута_паше_модуля, как вы знаете из предыдущей части книги (находящейся в первом томе). Вот как выглядит такое дополнение:
# Дать возможность импортировать этот файл, а также запускать/тестировать
class Person:
def _init_(self, name, job=None, pay=0):
self, name = name self.job = job self .pay = pay
if _name_ == '_main_’ : # Только когда запускается для тестирования
# Код самотестирования bob = Person('Bob Smith')
sue = Person('Sue Jones', job=’dev', pay=100000) print(bob.name, bob.pay) print(sue.name, sue.pay)
Теперь мы получаем в точности то поведение, к которому стремились — запуск
файла как сценария верхнего уровня тестирует его, потому что_паше_является
_main_, но импортирование его в качестве библиотеки классов не приводит к выполнению тестов:
C:\code> person.py
Bob Smith О
Sue Jones 100000
С:\code> python
3.7.3 (v3.7.3:ef4ec6edl2, Mar 25 2019, 21:26:53) [MSC v.1916 32 bit (Intel)]
»> import person
При импортировании файл определяет класс, но не использует его. Когда файл запускается напрямую, он создает два экземпляра нашего класса, как и ранее, и выводит два атрибута каждого экземпляра; и снова из-за того, что каждый экземпляр представляет собой независимый объект пространства имен, значения их атрибутов отличаются.
Переносимость версий: print
Весь код в текущей главе работает в Python 2.Х и З.Х, но я запускаю его под управлением Python З.Х, а для вывода применяю вызовы функции print с множеством аргументов из Python З.Х. Как объяснялось в главе 11 первого тома, это означает, что вывод может слегка варьироваться в случае запуска под управлением Python 2.Х. Если вы запустите код в том виде, как есть, в Python 2.Х, то он будет работать, но вы заметите круглые скобки в некоторых строках вывода, поскольку дополнительные круглые скобки в print из Python 2.Х превращают элементы в кортеж:
С: \code> с: \python27\python person .ру
(1 Bob Smith', 0)
('Sue Jones', 100000)
Если такое отличие оказывается той деталью, которая не дает вам покоя, тогда просто удалите круглые скобки, чтобы использовать оператор print из Python 2.Х, или добавьте в начало сценария оператор импортирования функции print из Python
З.Х, как делалось в главе 11 первого тома (я бы добавлял его повсюду, но слегка отвлекает):
from_future_ import print_function
Вы также можете избежать проблемы совместимости, связанной с круглыми скобками, за счет применения форматирования, чтобы выдавать одиночный объект, подлежащий выводу. Оба следующих оператора работают в Python 2.Х и З.Х, хотя форма с методом новее:
print('{0} {1}format(bob.name, bob.pay)) # Метод форматирования print(* %s %s' % (bob.name, bob.pay)) # Выражение форматирования
Как объяснялось в главе 11 первого тома, в ряде случаев подобное форматирование оказывается обязательным, потому что объекты, вложенные в кортеж, могут выводиться не так, как при выводе в виде объектов верхнего уровня. Первые выводятся с помощью_г ер г_, а вторые посредством_str_(методы перегрузки операций, обсуждаемые далее в этой главе и в главе 30).
Чтобы обойти проблему, данная версия кода отображает с помощью_г ер г_(запасной вариант во всех случаях, включая вложение и интерактивную подсказку)
вместо_str_(стандартный вариант для print), поэтому все появления объектов
выводятся одинаково в Python З.Х и 2.Х, даже те, что находятся в избыточных круглых скобках кортежей!
Шаг 2: добавление методов, реализующих поведение
Пока все выглядит хорошо — к настоящему моменту наш класс по существу является фабрикой записей; он создает и заполняет поля записей (атрибуты экземпляров, выражаясь терминами Python). Тем не менее, даже будучи настолько ограниченным классом, он позволяет выполнять некоторые операции над своими объектами. Несмотря на то что классы добавляют дополнительный уровень структуры, в конечном итоге они делают большую часть своей работы, встраивая и обрабатывая основные шипы данных наподобие списков и строк. Другими словами, если вам уже известно, как использовать простые основные типы Python, то вы уже знаете многое из истории о классах Python; классы в действительности представляют собой лишь незначительное структурное расширение.
Например, поле name в наших объектах — это простая строка, так что мы можем извлекать фамилии из объектов, разбивая по пробелам и индексируя. Все они являются операциями над основными типами данных, которые работают независимо от того, будут их объекты встроенными в экземпляры класса или нет:
>>> name = 'Bob Smith' # Простая строка, за пределами класса »> name, split () # Извлечение фамилии
['Bob', 'Smith']
>>> name.split() [-1] # Или [1], если всегда есть только две части
'Smith'
Аналогичным образом мы можем повысить человеку заработную плату за счет обновления поля pay объекта, т.е. изменяя его информацию о состоянии на месте посредством присваивания. Такая задача также включает в себя базовые операции, которые работают с основными типами Python безотносительно к тому, автономны они или встроены в структуру класса (ниже применяется форматирование, чтобы скрыть тот факт, что разные версии Python выводят отличающееся количество десятичных цифр):
>>> pay = 100000 # Простая переменная, за пределами класса
»> pay *=1.10 # Предоставить повышение на 10%
»> print('%.2f' % pay) # Или pay = pay * 1.10, если вам нравится много набирать
110000.00 # Или pay = pay + (pay * .10) ,
# если это _действительно_ так!
Чтобы применить эти операции к объектам Person, созданным в сценарии, нужно просто сделать с bob.name и sue.pay то же самое, что мы делали с name и pay. Операции остаются теми же, но объекты присоединяются в качестве атрибутов к объектам, созданным из нашего класса:
# Обработка встроенных типов: строки, изменяемость
class Person:
def _init_(self, name, job=None, pay=0):
self .name = name self. job = job self.pay = pay
if _name_ == '_main_' :
bob = Person('Bob Smith')
sue = Person('Sue Jones', job='dev', pay=100000) print(bob.name, bob.pay) print(sue.name, sue.pay)
print(bob.name.split()[-1]) # Извлечение фамилии из объекта
sue.pay *=1.10 # Предоставление этому объекту повышения
print('%.2f' % sue.pay)
Здесь мы добавили последние три строки; когда они выполняются, производится извлечение фамилии из объекта bob с использованием базовых строковых и списковых операций в отношении его поля name и повышение заработной платы объекту sue за счет модификации атрибута pay на месте с помощью базовых числовых операций. В определенном смысле sue также является изменяемым объектом — его состояние модифицируется на месте почти как список после вызова append. Ниже приведен вывод новой версии:
Bob Smith 0
Sue Jones 100000
Smith
110000.00
Предыдущий код работает, как было запланировано, но если вы покажете его опытному разработчику программного обеспечения, то он, скорее всего, сообщит вам, что такой универсальный подход — не особо хорошая идея для воплощения на практике. Жесткое кодирование операций вроде этих за пределами класса может привести к проблемам с сопровождением в будущем.
Скажем, что если вы жестко закодируете способ извлечения фамилии во многих местах программы? Когда его понадобится изменить (например, для поддержки новой структуры имени), то вам придется отыскать и обновить каждое вхождение. Подобным же образом, если изменится код повышения заработной платы (например, чтобы требовать одобрения или обновлений базы данных), то у вас может быть множество копий, подлежащих модификации. Один лишь поиск всех вхождений такого кода в крупных программах может оказаться проблематичным — они могут быть разбросаны по многим файлам, разделены на индивидуальные шаги и т.д. В прототипе подобного рода частые изменения почти гарантированы.
Написание кода методов
На самом деле мы здесь хотим задействовать концепцию проектирования программного обеспечения, известную как инкапсуляция — помещение операционной логики в оболочку интерфейсов, чтобы код каждой операции был написан только один раз в программе. Тогда если в будущем возникнет необходимость в изменении, то изменять нужно будет только одну копию. Более того, мы можем практически произвольно изменять внутренности одиночной копии, не нарушая работу кода, который ее потребляет.
Выражаясь терминами Python, мы хотим поместить код операций над объектами в методы класса, а не засорять ими всю программу. Фактически это одно из дел, с которыми классы справляются очень хорошо — вынесение кода с целью устранения избыточности и в итоге повышения удобства сопровождения. В качестве дополнительного бонуса помещение операций внутрь методов позволяет применять их к любому экземпляру класса, а не только к тем, где они были жестко закодированы для обработки.
На практике все описанное выглядит проще, чем в теории. В следующем коде инкапсуляция достигается переносом двух операций из кода за пределами класса в методы внутри класса. Далее изменим код самотестирования в конце файла, чтобы использовать в нем новые методы вместо жестко закодированных операций:
# Добавление методов для инкапсуляции операций с целью повышения удобства
сопровождения
class Person:
def _init_(self, name, job=None, pay=0):
self, name = name self. job = job self.pay = pay
def lastName (self) : # Методы реализации поведения
return self.name.split()[-1] # self - подразумеваемый объект
def giveRaise(self, percent):
self.pay = int (self.pay * (1 + percent)) # Потребуется изменять
# только здесь
if _name_ == '_main_' :
bob = Person('Bob Smith')
sue = Person('Sue Jones', job='dev', pay=100000) print(bob.name, bob.pay) print(sue.name, sue.pay)
print(bob.lastName(), sue.lastName()) # Использовать новые методы
sue.giveRaise(.10) # вместо жесткого кодирования
print(sue.pay)
Как уже известно, методы представляют собой нормальные функции, которые присоединяются к классам и предназначены для обработки экземпляров этих классов. Экземпляр является подразумеваемым объектом вызова метода и передается в аргументе self автоматически.
Трансформация в методы в данной версии прямолинейна. Например, новый метод lastName просто делает в отношении self то, что в предыдущей версии было жестко закодировано для bob, поскольку self — подразумеваемый объект при вызове метода. Метод lastName также возвращает результат, потому что теперь эта операция представляет собой вызываемую функцию; он вычисляет значение для произвольного применения в вызывающем коде, даже когда оно всего лишь выводится. Аналогично новый метод giveRaise просто делает с self то, что раньше выполнялось с sue.
Запуск файла приводит к получению такого же вывода, как ранее — мы в основном лишь провели рефакторинг кода, чтобы облегчить его модификацию в будущем, а не изменили поведение:
Bob Smith 0
Sue Jones 100000
Smith Jones
110000
Здесь стоит пояснить несколько деталей, касающихся кода. Во-первых, обратите внимание, что хранящий величину заработной платы атрибут pay в sue по-прежнему остается целым числом после поднятия — мы преобразуем математический результат в целое число, вызывая внутри метода встроенную функцию int. Изменение типа значения на int или float возможно не особо существенная проблема в рассматриваемой демонстрации: объекты целых чисел и чисел с плавающей точкой имеют те же самые интерфейсы и могут смешиваться внутри выражений. Однако в реальной системе нам может понадобиться решить проблемы с усечением и округлением — деньги для экземпляров Person наверняка важны!
Как известно из главы 5 первого тома, мы могли бы справиться с этим, используя вызов встроенной функции round (N, 2) для округления и оставления центов, применяя тип decimal с фиксированной точностью либо сохраняя денежные значения как полные числа с плавающей точкой и отображая их с использованием строки формата % . 2f или { 0: . 2 f}, которая обеспечивает вывод центов, как делалось ранее. Пока что мы просто усекаем любые центы посредством int. Другая идея была воплощена в функции money из модуля formats .ру в главе 25; можете импортировать указанный модуль, чтобы отображать величину заработной платы с запятыми, центами и символами валюты.
Во-вторых, обратите внимание на то, что на этот раз мы также выводим фамилию объекта sue — поскольку логика извлечения фамилии была инкапсулирована в методе, мы можем применять его к любому экземпляру класса. Как мы видели, Python сообщает методу, какой экземпляр обрабатывать, автоматически передавая его в первом аргументе, который обычно называется self. В частности:
• в первом вызове, bob. lastName (), bob является подразумеваемым объектом, передаваемым self;
• во втором вызове, sue. lastName (), в self передается sue.
Отследите указанные вызовы, чтобы посмотреть, каким образом экземпляр оказывается в self — это ключевая концепция. Совокупный эффект в том, что метод каждый раз извлекает фамилию из подразумеваемого объекта. То же самое происходит с методом giveRaise. Скажем, мы могли бы предоставить объекту bob повышение, вызывая аналогичным образом метод giveRaise для обоих экземпляров. Тем не менее, к сожалению, его нулевое начальное значение заработной платы воспрепятствует получению повышения при текущей реализации программы — умножение нуля на что угодно даст ноль, и вполне возможно, мы захотим решить эту проблему в будущем выпуске нашей программы.
Наконец, обратите внимание, что метод giveRaise полагается на передачу в аргументе percent числа с плавающей точкой между нулем и единицей. В реальности такое допущение может быть излишне радикальным (повышение на 1000% вероятно для большинства из нас было бы воспринято как ошибка!); в прототипе мы оставим его, как есть, но в будущем выпуске возможно понадобится организовать проверку или хотя бы документировать такую особенность. Позже в книге идея будет изложена другими словами, когда мы займемся так называемыми декораторами функций и оператором assert языка Python — альтернативы, которые могут выполнять проверки достоверности автоматически на стадии разработки. Например, в главе 39 мы реализуем инструмент, который позволит проверять достоверность с помощью удивительных магических формул вроде показанных ниже:
@rangetest (percent^ (0 . О, 1.0) ) # Использование декоратора для проверки
# достоверности
def giveRaise(self, percent):
self.pay = int(self.pay * (1 + percent))
Шаг 3: перегрузка операций
В данный момент мы располагаем довольно многофункциональным классом, который генерирует и инициализирует экземпляры, а также поддерживает две новые линии поведения для обработки экземпляров в форме методов. Пока все в порядке.
Однако в нынешнем виде тестирование все еще менее удобно, чем должно быть — для трассировки объектов нам приходится вручную извлекать и вводить индивидуальные атрибуты (скажем, bob.паше, sue.pay). Было бы хорошо, если бы отображение экземпляра всего сразу действительно давало какую-то полезную информацию. К сожалению, стандартный формат отображения для объекта экземпляра не особо подходит — он выводит имя класса объекта и его адрес в памяти (что в Python по существу бесполезно за исключением уникального идентификатора).
Чтобы увидеть это, изменим последнюю строку в сценарии на print (sue), обеспечив отображение объекта как единого целого. Ниже приведено то, что мы получим — вывод указывает на то, что sue является объектом (object) в Python З.Х и экземпляром (instance) в Python 2.Х:
Bob Smith О
Sue Jones 100000
Smith Jones
<_main_.Person object at 0x00000000029A0668>
Реализация отображения
К счастью, положение дел легко улучшить, задействовав перегрузку операций — написать код методов в классе, которые перехватывают и обрабатывают встроенные операции, когда выполняются на экземплярах класса. В частности, мы можем использовать вторые по частоте применения в Python методы перегрузки операций после
_init_: метод_герг_, который мы реализуем здесь, и его двойник_str_,
представленный в предыдущей главе.
Методы выполняются автоматически калсдый раз, когда экземпляр преобразуется в свою строку вывода. Поскольку именно это происходит при выводе объекта, в результате вывод объекта отображает то, что возвращается методом_str_или_герг_
объекта, если объект либо самостоятельно определяет такой метод, либо наследует его от суперкласса. Имена с двумя символами подчеркиваниями наследуются подобно любым другим.
Формально_str_предпочтительнее print и str, а_герг_используется в
качестве запасного варианта для данных ролей и во всех остальных контекстах. Хотя можно применить два метода для реализации отличающегося отображения в разных
контекстах, написание кода одного лишь_герг_достаточно, чтобы предоставить
единственное отображение во всех случаях — вывод с помощью print, вложенные появления и эхо-вывод в интерактивной подсказке. Клиенты по-прежнему располагают
возможностью предоставления альтернативного отображения посредством_str_,
но только для ограниченных контекстов; так как рассматриваемый пример изолирован, вопрос здесь оказывается спорным.
Метод конструктора_init_, код которого мы уже написали, строго говоря,
тоже является перегрузкой операции — он выполняется автоматически во время создания для инициализации нового экземпляра. Тем не менее, конструкторы настолько распространены, что они не выглядят похожими на особый случай. Более специализированные методы вроде_герг_позволяют подключаться к специфическим
операциям и обеспечивать специальное поведение, когда объекты используются в таких контекстах.
Давайте напишем код. Ниже наш класс расширяется, чтобы предоставляет специальное отображение со списком атрибутов при выводе экземпляров класса как единого целого, не полагаясь на менее полезное стандартное отображение:
# Добавление метода перегрузки операции_герг_ для вывода объектов
class Person:
def _init_(self, name, job=None, pay=0):
self, name = name self, job = job self.pay = pay def lastName(self):
return self.name.split()[-1] def giveRaise(self, percent):
self.pay = int(self.pay * (1 + percent))
def _repr_(self) : # Добавленный метод
return 1 [Person: %s, %s] 1 % (self, name, self .pay) # Строка для вывода
if _name_ == 1_main_' :
bob = Person('Bob Smith')
sue = Person('Sue Jones', job='dev', pay=100000) print(bob) print(sue)
print(bob.lastName(), sue.lastName()) sue.giveRaise(.10) print(sue)
Обратите внимание, что в_герг_для построения строки, подлежащей отображению, применяется операция % строкового форматирования; для выполнения своей работы классы используют объекты и операции встроенных типов, как здесь показано. И снова все, что вам уже известно о встроенных типах и функциях, применимо к коду, основанному на классах. По большому счету классы лишь добавляют дополнительный уровень структуры, которая упаковывает функции и данные вместе и поддерживает расширение.
Мы также изменили код самотестирования для отображения объектов напрямую вместо вывода индивидуальных атрибутов. Получаемый в результате запуска вывод теперь оказывается более ясным и понятным; строки [...], возвращаемые новым методом_герг_, выполняются автоматически операциями вывода:
[Person: Bob Smith, 0]
[Person: Sue Jones, 100000]
Smith Jones
[Person: Sue Jones, 110000]
Примечание по проектному решению: как будет показано в главе 30, метод _герг_, если присутствует, то часто используется, чтобы предоставить низкоуровневое отображение объекта как в коде, а метод_str_зарезервирован для более информативного отображения, дружественного к пользователям. Иногда классы предлагают и_str_для дружественного к пользователям отображения, и_герг_с
добавочными деталями для просмотра разработчиками. Поскольку операция вывода
запускает_str_, а интерактивная подсказка автоматически выводит результаты с
помощью_герг_, это можно применять для снабжения обеих целевых аудиторий
подходящим отображением.
Так как метод_герг_пригоден в большем количестве случаев отображения, в
том числе при вложенных появлениях, а нас не интересует отображение двух разных форматов, то для класса Person вполне достаточно реализации всеобъемлющего метода _герг_. Здесь это также означает, что наше специальное отображение будет
использоваться в Python 2.Х в случае указания bob и sue в вызове print из Python З.Х — формально вложенное появление, согласно врезке “Переносимость версий: print” ранее в главе.
Шаг 4: настройка поведения за счет создания подклассов
На текущем этапе наш класс реализует большую часть механизма ООП в Python: он создает экземпляры, снабжает поведением в методах и даже немного перегружает операции, чтобы перехватывать операции вывода в_герг_. Он фактически упаковывает наши данные и логику вместе в единственный автономный программный компонент, облегчая нахождение кода и его изменение в будущем. Разрешая нам инкапсулировать поведение, он также дает возможность выносить такой код во избежание избыточности и связанных затруднений при сопровождении.
Не охваченной осталась только одна значительная концепция ООП — настройка через наследование. В некотором смысле мы уже привлекали к делу наследование, потому что экземпляры наследуют методы от своих классов. Однако для демонстрации реальной мощи ООП необходимо определить отношение суперкласс/подкласс, которое позволит расширять наше программное обеспечения и замещать унаследованные линии поведения. В конце концов, это главная идея ООП; поощряя кодовую модель, основанную на настройке уже сделанной работы, ООП значительно сокращает время разработки.
Написание кода подклассов
В качестве следующего шага давайте задействуем методологию ООП для применения и настройки нашего класса Person, расширив имеющуюся программную иерархию. В целях рассматриваемого учебного пособия мы определим подкласс класса Person по имени Manager, который замещает унаследованный метод giveRaise более специализированной версией. Новый класс начинается так:
class Manager(Person): # Определение подкласса Person
Код означает, что мы определяем новый класс по имени Manager, который унаследован от суперкласса Person и может добавлять настройки. Говоря проще, класс Manager очень похож на Person, но Manager располагает специальным способом получения повышений.
В плане аргумента давайте предположим, что когда экземпляр Manager получает повышение, он принимает переданный процент обычным образом, но также становится обладателем добавочной премии, имеющей стандартное значение 10%. Например, если повышение для экземпляра Manager указано как 10%, то в действительности он получит 20%. (Разумеется, любые совпадения с реальными людьми совершенно случайны.) Новый метод начинается, как показано ниже; из-за того, что в дереве классов это переопределение giveRaise будет находиться ближе к экземплярам Manager, чем исходная версия в Person, оно фактически замещает и таким образом настраивает операцию. Вспомните, что в соответствии с правилами поиска в иерархии наследования выигрывает версия имени, расположенная ниже всех:
class Manager(Person): # Наследование атрибутов Person
def giveRaise (self, percent, bonus=.10) : # Переопределение с целью настройки
Расширение методов: плохой способ
Существуют два способа написания кода для такой настройки класса Manager: хороший и плохой. Мы начнем с плохого способа, т.к. он может быть чуть легче для понимания. Плохой способ заключается в том, чтобы вырезать код giveRaise из класса Person и вставить его в класс Manager, после чего модифицировать:
class Manager(Person):
def giveRaise(self, percent, bonus=.10):
self.pay = int(self.pay * (1 + percent + bonus)) # Плохой способ:
# вырезание и вставка
Все работает, как заявлено — когда мы позже вызываем метод giveRaise экземпляра Manager, будет выполнена созданная специальная версия, начисляющая добавочную премию. Что же тогда не так с кодом, который выполняется корректно?
Проблема здесь имеет очень общий характер: всякий раз, когда вы копируете код посредством вырезания и вставки, то по существу удваиваете объем работ по сопровождению в будущем. Подумайте об этом: поскольку мы копируем первоначальную версию, если когда-либо понадобится изменить способы выдачи повышений (что наверняка произойдет), то нам придется модифицировать код в двух местах, а не в одном.
Несмотря на то что пример является простым и искусственным, он демонстрирует универсальную проблему — всякий раз, когда возникает искушение программировать путем копирования кода, вероятно, стоит поискать более подходящий подход.
Расширение методов: хороший способ
Что мы действительно хотим здесь сделать — каким-то образом расширишь исходный метод giveRaise вместо его полного замещения. Хороший способ в Python предусматривает вызов исходной версии напрямую с дополненными аргументами:
class Manager(Person):
def giveRaise(self, percent, bonus=.10):
Person.giveRaise(self, percent + bonus) # Хороший способ: расширение
# исходной версии
В коде задействован тот факт, что метод класса всегда может быть вызван либо через экземпляр (обычный способ, когда Python автоматически передает экземпляр аргументу self), либо через класс (менее распространенная схема, при которой экземпляр должен передаваться вручную). Если более конкретно, то вспомните, что нормальный вызов метода следующего вида:
экземпляр.метод (аргументы. . .) автоматически транслируется Python в такую эквивалентную форму:
класс .метод {экземпляр, аргументы. . .)
где класс, который содержит подлежащий выполнению метод, определяется правилами поиска в иерархии наследования, применяемыми к имени метода. Вы можете использовать в своем сценарии любую из двух форм, но между ними наблюдается легкая асимметрия — вы обязаны помнить о необходимости передачи экземпляра вручную, если вызывает метод напрямую через класс. Так или иначе, но метод нуждается в объекте экземпляра, и Python предоставляет его автоматически только для вызовов, сделанных через экземпляр. В случае вызова через имя класса вы должны самостоятельно передавать экземпляр атрибуту self; для кода внутри метода вроде giveRaise аргумент self уже является объектом, на котором произведен вызов, и отсюда экземпляром, подлежащим передаче.
Прямой вызов через класс фактически отменяет поиск в иерархии наследования и запускает вызов выше в дереве классов, чтобы выполнить специфическую версию. В нашем случае мы можем применять такую методику для обращения к стандартной версии метода giveRaise из Person, несмотря на то, что он переопределен на уровне класса Manager. В ряде случаев мы просто обязаны вызывать через класс Person, т.к. вызов self. giveRaise () внутри кода giveRaise в Manager привел бы к зацикливанию. Дело в том, что self уже представляет собой экземпляр Manager, т.е. вызов self. giveRaise () был бы распознан снова как Manager. giveRaise и так далее рекурсивно до тех пор, пока не исчерпается доступная память.
“Хорошая” версия может выглядеть мало отличающейся в плане кода, но она способна значительно изменить будущее сопровождение кода — так как теперь логика giveRaise расположена лишь в одном месте (метод класса Person), у нас есть только одна версия, подлежащая модификации в будущем, если в том возникнет необходимость. И действительно, эта форма в любом случае отражает наше намерение более прямо — мы хотим выполнить стандартную операцию giveRaise, но просто добавить дополнительную премию. Ниже приведен полный код модуля с примененным последним шагом:
# Добавление настройки поведения в подкласс
class Person:
def _init_(self, name, job=None, pay=0):
self.name = name self. job = job self.pay = pay def lastName(self):
return self.name.split() [-1] def giveRaise(self, percent):
self.pay = int(self.pay * (1 + percent))
def _repr_(self):
return '[Person: %s, %s] ' % (self.name, self.pay)
class Manager(Person):
def giveRaise (self, percent, bonus=.10): # Переопределить на этом уровне Person.giveRaise(self, percent + bonus) # Вызвать версию из Person
if _name_ == '_main_' :
bob = Person('Bob Smith')
sue = Person('Sue Jones', job='dev', pay=100000) print(bob) print(sue)
print(bob.lastName(), sue.lastName()) sue.giveRaise(.10) print(sue)
tom = Manager('Tom Jones', 'mgr', 50000) # Создать экземпляр
Manager: _init_
tom.giveRaise(.10) # Выполняется специальная версия print(tom.lastName()) # Выполняется унаследованный метод print (tom) # Выполняется унаследованный_repr_
Чтобы протестировать настройку Manager, мы также добавили код самотестирования, который создает экземпляр Manager, вызывает его методы и выводит сам экземпляр. При создании экземпляра Manager мы передаем имя, а также необязательную
должность и размер заработной платы — поскольку конструктор_init_в классе
Manager отсутствует, он наследует его от Person. Вот вывод новой версии:
[Person: Bob Smith, 0]
[Person: Sue Jones, 100000]
Smith Jones
[Person: Sue Jones, 110000]
Jones
[Person: Tom Jones, 60000]
Здесь все выглядит хорошо: экземпляры bob и sue такие же, как прежде, а когда экземпляр Manager по имени tom получает повышение на 10%, то на самом деле становится обладателем 20% (его оплата возрастает с 50 ООО до 60 ООО), потому что настроенная версия giveRaise из Manager выполняется только для него. Также обратите внимание, что вывод экземпляра tom как единого целого в конце тестового кода
отображает элегантный формат, определенный в методе_герг_класса Person:
объекты Manager получают код_repr_, lastName и метода конструктора_init_
“бесплатно” от Person благодаря наследованию.
Как насчет super?
Для расширения унаследованных методов в примерах данной главы вызывались исходные методы через имя суперкласса: Person.giveRaise (...). Это традиционная и простейшая схема в Python, которая используется в большей части книги.
Возможно, программистам на Java будет особенно интересно узнать, что в Python также имеется встроенная функция super, которая позволяет вызывать методы суперкласса более обобщенно. Тем не менее, в Python 2.Х применять ее громоздко; она отличается по форме в линейках Python 2.Х и Python З.Х; полагается на необычную семантику в Python З.Х; шероховато работает с перегрузкой операций в Python; и не всегда хорошо сочетается с традиционно реализованным множественным наследованием, где обращения к единственному суперклассу будет недостаточно.
В защиту вызова super следует отметить, что с ним тоже связан допустимый сценарий использования (совместная координация одинаково именованных методов в деревьях множественного наследования). Однако он опирается на упорядочение классов MRO (Method Resolution Order — порядок распознавания методов), которое многие считают экзотическим и искусственным; нереалистично предполагает, что будет надежно применяться универсальное развертывание; не полностью поддерживает замещение методов и списки аргументов переменной длины; к тому же для многих использование сценария, редко встречающегося в реальном коде на Python, представляется невразумительным решением.
Из-за перечисленных недостатков предпочтение в книге отдается обращению к суперклассам по явным именам, а не посредством super, та же политика рекомендуется для новичков и представление встроенной функции super откладывается до главы 32. О встроенной функции super обычно лучше судить после того, как вы освоите более простые и в целом более традиционные, в стиле Python, способы достижения тех же целей, особенно если вы — новичок в ООП. Темы вроде MRO и совместной координации методов в деревьях множественного наследования одинаково именованных методов не кажутся слишком востребованными начинающими, впрочем, как и остальными.
Мой совет всем программистам на Java, входящим в читательскую аудиторию: я предлагаю не поддаваться искушению и не применять встроенной функции super из Python до тех пор, пока у вас не появится возможность изучить связанные с ней тонкие последствия. Как только вы перейдете на множественное наследование, она окажется не тем, что вы думаете, а чем-то большим, нежели ваши возможные ожидания. Вызываемый класс может вообще не быть суперклассом и даже варьироваться в зависимости от контекста. Или переформулировав фразу из фильма “Форрест Гамп”: встроенная функция super из Python как коробка шоколадных конфет — никогда не знаешь, какая начинка тебе попадется!
Полиморфизм в действии
Чтобы сделать приобретение унаследованного поведения еще более удивительным, мы можем временно добавить в конец файла следующий код:
if _name_ == '_main_' :
print('--All three—')
for obj in (bob, sue, tom) : # Обработать объекты обобщенным образом
obj.giveRaise(.10) # Выполнить метод giveRaise этого объекта
print (obj ) # Выполнить общий метод_герг_
Вот какой в результате получается вывод (новые строки выделены полужирным):
[Person: Bob Smith,
0]
[Person: Sue Jones,
100000]
Smith Jones
[Person: Sue Jones,
110000]
Jones
[Person: Tom Jones,
60000]
—All three—
[Person: Bob Smith,
0]
[Person: Sue Jones,
121000]
[Person: Tom Jones,
72000]
В добавленном коде obj является экземпляром либо Person, либо Manager, и
Python выполняет соответствующий метод giveRaise автоматически — нашу исходную версию в классе Person для bob и sue и настроенную версию в Manager для tom. Отследите вызовы методов самостоятельно, чтобы увидеть, каким образом Python выбирает правильный метод giveRaise для каждого объекта.
Это всего лишь демонстрация в работе понятия полиморфизма в Python, с которым мы встречались ранее в книге — то, что делает метод giveRaise, зависит от того, на чем он вызывается. Все становится более очевидным, когда средство полиморфизма выбирает среди написанного нами кода в классах. Практический эффект данного кода заключается в том, что экземпляр sue получает дополнительно 10%, но экземпляр tom — 20%, потому что выбор метода giveRaise координируется на основе типа объекта. Как уже известно, полиморфизм является центральной частью гибкости Python. Скажем, передача любого из трех объектов функции, которая вызывает метод giveRaise, дала бы такой же результат: в зависимости от типа переданного объекта автоматически выполнилась бы подходящая версия.
С другой стороны, вывод выполняет тот же самый метод_герг_для всех трех
объектов, т.к. он реализован только один раз в Person. Класс Manager специализирует и применяет код, который мы первоначально написали в Person. Несмотря на небольшой размер примера, он уже задействует способности ООП для настройки и многократного использования кода; благодаря классам это порой кажется почти автоматическим.
Наследование, настройка и расширение
На самом деле классы могут быть даже более гибкими, чем вытекает из рассмотренного примера. В общем случае классы способны наследовать, настраивать или расширять существующий код в суперклассах. Например, хотя внимание здесь сосредоточено на настройке, мы также добавляем в класс Manager уникальные методы, которые отсутствуют в Person, если экземпляры Manager требуют чего-то совершенно другого (тут снова на ум приходит фильм “Монти Пайтон: а теперь нечто совсем другое”). Сказанное иллюстрируется в следующем коде, где giveRaise переопределяет метод суперкласса с целью его настройки, но someThingElse определяет кое-что новое для расширения:
class Person:
def lastName(self): ... def giveRaise(self): ... def _repr_(self): ...
class Manager(Person): # Наследование
def giveRaise(self, ...):... # Настройка
def someThingElse(self, . . .) : ... # Расширение
# Буквальное наследование
# Настроенная версия
tom = Manager () tom.lastName () tom.giveRaise()
tom.someThingElse() print(tom)
# Метод расширения
# Уна следованный перегруженный метод
Дополнительные методы наподобие someThingElse в показанном коде расширяют существующее программное обеспечение и доступны только для объектов Manager, но не Person. Тем не менее, ради целей этого обучающего руководства мы ограничимся настройкой части поведения класса Person путем его переопределения, не добавляя новые линии поведения.
Объектно-ориентированное программирование: основная идея
В том виде, как есть, код может быть небольшим, но достаточно функциональным. И действительно, он демонстрирует основную идею, лежащую в основе ООП в целом: мы программируем путем настройки того, что уже было сделано, а не копирования либо изменения существующего кода. На первый взгляд это не всегда очевидный выигрыш, особенно с учетом требований по написанию добавочного кода классов. Но, в общем, стиль программирования, подразумеваемый классами, может радикально сократить время разработки по сравнению с другими подходами.
Скажем, в нашем примере теоретически мы могли бы реализовать специальную операцию giveRaise, не создавая подкласс, но никакой другой вариант не обеспечил бы получение настолько оптимального кода.
• Несмотря на то что мы могли бы просто реализовать класс Manager с нуля как новый, независимый код, нам пришлось бы повторно реализовать все линии поведения из Person, которые остались такими же в Manager.
• Хотя мы могли бы просто изменить существующий код класса Person на месте для удовлетворения требований операции giveRaise из Manager, это вероятно нарушило бы работу кода в местах, где по-прежнему необходимо исходное поведение Person.
• Несмотря на то что мы могли бы просто скопировать класс Person полностью, переименовать копию на Manager и изменить код операции giveRaise, это ввело бы избыточность кода, приводя к удваиванию работы по сопровождению — изменения, вносимые в будущем в класс Person, не будут подхватываться автоматически, и их придется вручную распространять на код Manager. Как обычно, подход с вырезанием и вставкой в текущий момент может выглядеть быстрым, но он удваивает объем работы в будущем.
Настраиваемые иерархии, которые мы можем строить с помощью классов, обеспечивают гораздо лучшее решение для программного обеспечения, развивающегося с течением времени. Другие инструменты в Python такой режим разработки не поддерживают. Располагая возможностью подгонки и расширения результатов выполненной ранее работы за счет реализации новых подклассов, мы можем задействовать то, что уже готово, а не начинать каждый раз с нуля, нарушать работу имеющегося кода или вводить множество копий кода, которые вероятно придется обновлять в будущем. При надлежащем применении ООП является мощным союзником программиста.
Шаг 5: настройка конструкторов
Код работает в том виде, как есть, но если вы более внимательно исследуете текущую версию, то столкнетесь с небольшой странностью — указание названия должное-ти mgr для объектов Manager при их создании кажется бессмысленным: это вытекает из самого класса. Было бы лучше иметь возможность каким-то образом заполнять данное значение автоматически, когда создается экземпляр Manager.
Здесь мы можем использовать тот же самый трюк, который был задействован в предыдущем разделе: мы хотим настроить логику конструктора для объектов Manager таким образом, чтобы предоставлять название должности автоматически. В переводе на код нам нужно переопределить метод_init_в классе Manager с целью предоставления строки mgr. Как и в настройке giveRaise, нам также необходимо выполнять исходный метод_init_из Person, вызывая его через имя класса, чтобы он
по-прежнему инициализировал атрибуты информации о состоянии наших объектов.
Задачу решает следующее расширение person .ру — мы написали код нового конструктора Manager и изменили вызов, который создает экземпляр tom, убрав из него передачу названия должности mgr:
# Файл person.py
# Добавление настройки конструктора в подклассе
class Person:
def _init_(self, name, job=None, pay=0):
self, name = name self. job = job self.pay = pay def lastName(self):
return self.name.split()[-1] def giveRaise(self, percent):
self.pay = int(self.pay * (1 + percent))
def _repr_(self):
return ' [Person: %s, %s] ' % (self.name, self.pay)
class Manager(Person):
# Переопределить конструктор
# Выполнить исходный с 'mgr ’
def _init_(self, name, pay) :
Person._init_(self, name, ’mgr', pay)
def giveRaise(self, percent, bonus=.10):
Person.giveRaise(self, percent + bonus)
if _name_ == '_main_' :
bob = Person('Bob Smith') sue = Person('Sue Jones', job='dev', pay=100000) print(bob) print(sue)
print(bob.lastName(), sue.lastName()) sue.giveRaise(.10) print(sue)
tom = Manager ('Tom Jones', 50000) # Название должности не требуется: tom.giveRaise(.10) # Оно подразумевается/
# устанавливается классом
print(tom.lastName()) print(tom)
При расширении конструктора _init_мы снова применяем ту же самую методику, которую ранее использовали для giveRaise — выполняем версию из суперкласса
путем вызова через имя класса напрямую и явно передаем экземпляр self. Хотя конструктор имеет странное имя, эффект идентичен. Поскольку нужно также выполнить логику создания экземпляра Person (для инициализации атрибутов экземпляра), мы действительно обязаны вызывать ее таким способом; иначе экземпляры не получат присоединенных атрибутов.
Подобного рода вызов конструкторов суперклассов из переопределенных версий оказывается весьма распространенным стилем программирования в Python. Сам по себе Python применяет наследование для поиска и вызова только одного метода
_init_на стадии конструирования — расположенного ниже всех в дереве классов.
Если необходимо, чтобы на стадии конструирования выполнялись методы_init_,
находящиеся выше в дереве классов (обычно так и есть), тогда вы должны вызывать их вручную, как правило, через имя суперкласса. Положительный аспект здесь в том, что вы можете явно указывать аргумент для передачи конструктору суперкласса или даже вообще не вызывать его: отказ от вызова конструктора суперкласса позволяет вместо расширения полностью замещать его логику.
Вывод кода самотестирования этого файла такой же, как ранее — мы не изменяли то, что он делает, а просто реструктурировали, избавившись от некоторой логической избыточности:
[Person:
Bob
Smith,
0]
[Person:
Sue
Jones,
100000]
Smith Jones
[Person:
Sue
Jones,
110000]
Jones
[Person:
Tom
Jones,
60000]
Объектно-ориентированное программирование проще, чем может казаться
Несмотря на свои относительно небольшие размеры, в такой полной форме наши классы охватывают почти все важные концепции механизма ООП в Python:
• создание экземпляров — заполнение атрибутов экземпляров;
• методы, реализующие поведение — инкапсуляция логики в методах класса;
• перегрузка операций — обеспечение линии поведения для встроенных операций вроде вывода;
• настройка поведения — переопределение методов в подклассах для их специализации;
• настройка конструкторов — добавление логики инициализации к шагам суперкласса.
Большинство перечисленных концепций основаны всего лишь на трех простых идеях: поиск атрибутов в иерархии наследования в форме деревьев объектов, особый аргумент self в методах и автоматическое сопоставление перегруженных операций с методами.
Попутно мы также сделали наш код легким для изменения в будущем, используя предрасположенность класса к вынесению кода с целью сокращения избыточности. Скажем, мы поместили логику внутрь методов и вызывали методы суперкласса из расширений, чтобы избежать наличия множества копий того же самого кода. Большинство таких шагов были естественным ростом структурной мощи классов.
В общем и целом это все, что есть в Python для ООП. Безусловно, классы могут становиться крупнее, чем было здесь продемонстрировано, и существуют более сложные концепции, связанные с классами, такие как декораторы и метаклассы, которые будут обсуждаться в последующих главах. Однако с точки зрения основ наши классы уже воплощают все упомянутые ранее концепции. На самом деле, если вы поняли работу написанных нами классов, тогда большая часть объектно-ориентированного кода на Python не должна вызывать особых вопросов.
Другие способы комбинирования классов
Сказав это, я обязан также сообщить вам о том, что хотя базовый механизм ООП в Python прост, способы объединения классов вместе в крупных программах — в определенной мере искусство. В настоящем руководстве мы концентрируемся на наследовании, потому что оно представляет собой механизм, поддерживаемый языком Python, но программисты иногда комбинируют классы и другими способами.
Например, распространенный стиль программирования предусматривает вложение объектов друг в друга для построения составных объектов. Мы детально исследуем такой стиль в главе 31, где речь пойдет больше о проектировании, чем о самом языке Python. Тем не менее, в качестве короткого примера мы могли бы применить эту идею комбинирования для реализации расширения Manager, внедряя класс Person, а не наследуя от него.
Показанная ниже альтернатива, находящаяся в файле person-composite. ру, реализует такой прием за счет использования метода для перегрузки операции_getattr_,
чтобы перехватывать извлечения неопределенных атрибутов и делегировать выполнение работы внедренному объекту посредством встроенной функции getattr. Вызов getattr был представлен в главе 25 первого тома (он аналогичен записи извлечения атрибута X. Y и потому приводит к поиску в иерархии наследования, но имя атрибута Y является строкой времени выполнения), а метод_getattr_будет полностью раскрыт в главе 30, однако здесь вполне достаточно его базового применения.
Путем комбинирования указанных инструментов метод giveRaise по-прежнему обеспечивает настройку, изменяя аргумент, который передается внедренному объекту. В действительности Manager становится уровнем контроллера, передающим вызовы вниз внедренному объекту, а не вверх методам суперкласса:
# Файл person-composite.ру
# Альтернативная версия Manager, основанная на внедрении
class Person:
. . . тот же код, что и ранее. . .
class Manager:
def _init_(self, name, pay) :
self.person = Person(name, 'mgr', pay) # Внедрить объект Person def giveRaise(self, percent, bonus=.10):
self.person.giveRaise(percent + bonus) # Перехватить и делегировать
def _getattr_(self, attr):
return getattr (self .person, attr) # Делегировать все остальные атрибуты
def _repr_(self) :
return str (self.person) # Снова должен быть перегружен
# (в Python З.Х)
if _name_ == '_main ' :
. . . тот же код, что и ранее. . .
Вывод новой версии будет таким же, как у предыдущей версии, так что нет смысла приводить его повторно. Более важно здесь то, что данная альтернативная версия Manager иллюстрирует общий стиль программирования, обычно известный как делегирование — структура на основе составного объекта, которая управляет внедренным объектом и передает ему вызовы методов.
Шаблон удалось внедрить в нашем примере, но он потребовал почти в два раза больше кода и он не настолько хорошо подходит для задействованных видов настроек, как наследование (на самом деле, ни один здравомыслящий программист на Python никогда бы не реализовывал приведенный пример подобным образом на практике, разве что при написании учебного пособия!). Здесь объект Manager не является Person, поэтому нам необходим добавочный код для ручной отправки вызовов методов внедренному объекту. Методы перегрузки операций вроде_герг_должны
быть переопределены (по крайней мере, в Python З.Х, как будет отмечено во врезке “Перехват встроенных атрибутов в Python З.Х” далее в главе). Кроме того, добавление нового поведения в Manager менее прямолинейно, поскольку информация о состоянии смещена на один уровень ниже.
Тем не менее, внедрение объектов и основанные на нем паттерны проектирования могут очень хорошо подходить, когда внедренные объекты требуют более ограниченного взаимодействия с контейнером, чем подразумевает прямая настройка. Скажем, уровень контроллера, или посредника, подобный альтернативной версии Manager, может оказаться полезным, когда мы хотим адаптировать класс к ожидаемому интерфейсу, который он не поддерживает, либо отследить или проверить достоверность обращений к методам другого объекта (и действительно мы будем использовать почти идентичный стиль программирования при изучении декораторов классов позже в книге).
Кроме того, гипотетический класс Department, подобный показанному ниже, мог бы агрегировать другие объекты с целью их трактовки как набора. Временно замените код самотестирования в конце файла person.py, чтобы опробовать прием самостоятельно; это сделано в файле person-department .ру, входящем в состав примеров для книги:
# Файл person-department .ру
# Агрегирование внедренных объектов в составном объекте
class Person:
. . . тот же код, что и ранее. . .
class Manager(Person):
. . . тот же код, что и ранее. . .
class Department:
def _init_(self, *args):
self.members = list(args) def addMember(self, person): self.members.append(person) def giveRaises(self, percent): for person in self.members: person.giveRaise(percent) def showAll(self):
for person in self.members: print(person)
if _name_ == '_main_' :
bob = Person('Bob Smith')
sue = Person('Sue Jones', job='dev’, pay=100000) tom = Manager('Tom Jones', 50000)
development = Department(bob, sue) # Внедрить объекты в составной объект development.addMember(tom)
development.giveRaises(. 10) # Выполняет giveRaise внедренных объектов development.showAll() # Выполняет_repr_ внедренных объектов
Во время выполнения метод showAll объекта Department выводит список содержащихся в нем объектов после обновления их состояния в подлинно полиморфной манере с помощью giveRaises:
[Person: Bob Smith, 0]
[Person: Sue Jones, 110000]
[Person: Tom Jones, 60000]
Интересно отметить, что в коде применяются наследование и композиция — Department является составным объектом, который внедряет и управляет другими объектами для агрегирования, но сами внедренные объекты Person и Manager для настройки используют наследование. В качестве еще одного примера графический пользовательский интерфейс может похожим образом применять наследование для настройки поведения или внешнего вида меток и кнопок, а также композицию для построения более крупных пакетов внедряемых виджетов вроде форм ввода, калькуляторов и текстовых редакторов. Используемая структура классов зависит от объектов, которые вы пытаетесь моделировать — по сути, такая возможность моделирования сущностей реального мира считается одной из сильных сторон ООП.
Вопросы проектирования наподобие композиции исследуются в главе 31, так что мы пока отложим дальнейший обзор. Но опять-таки в свете базового механизма ООП на Python наши классы Person и Manager уже отражают всю историю. Однако теперь, когда вы освоили основы ООП, разработка универсальных инструментов для его более легкого применения в своих сценариях часто является естественным следующим шагом — и темой очередного раздела.
Перехват встроенных атрибутов в Python З.Х
Примечание о реализации: в Python З.Х (и в Python 2.Х, когда включены классы “нового стиля” Python З.Х) реализованная в главе альтернативная версия класса Manager на основе делегирования (person-composite.ру) не сможет перехватывать и делегировать вызовы методов перегрузки операций наподобие_герг_, не
переопределяя их. Хотя мы знаем, что_герг_является единственным таким именем, используемым в нашем специфическом примере, это общая проблема классов, основанных на делегировании.
Вспомните, что встроенные операции вроде вывода и сложения неявно вызывают методы перегрузки операций, такие как_герг_и_add_. В классах нового
стиля Python З.Х встроенные операции подобного рода не маршрутизируют свои неявные выборки атрибутов через обобщенные диспетчеры атрибутов: не вызывается ни метод_getattr_(выполняется для неопределенных атрибутов), ни родственный ему_getattribute_(выполняется для всех атрибутов). Именно потому мы вынуждены избыточно переопределять_герг_в альтернативной версии
Manager, чтобы гарантировать направление вывода на внедренный объект Person в Python З.Х.
Закомментируйте данный метод, чтобы увидеть все вживую — экземпляр Manager производит вывод с помощью стандартного инструмента в Python З.Х, но по-прежнему применяет метод_герг_класса Person в Python 2.Х. На самом деле_герг_
из Manager в Python 2.Х вообще не требуется, т.к. он реализован для использования нормальных и стандартных (известных как “классические*) классов Python 2.Х:
c:\code> ру -3 person-composite.ру
[Person: Bob Smith, 0]
... и так далее...
<_main_.Manager object at 0x00000000029AA8D0>
c:\code> py -2 person-composite.py
[Person: Bob Smith, 0]
... и так далее. . .
[Person: Tom Jones, 60000]
Формально так происходит из-за того, что встроенные операции начинают свой неявный поиск имен методов с экземпляра в стандартных классических классах Python 2.Х, но с класса в обязательных классах нового стиля Python З.Х, полностью пропуская экземпляр. По контрасту с этим явные выборки атрибутов по имени в обеих моделях всегда направляются сначала на экземпляр. В классических классах Python 2.Х встроенные атрибуты тоже маршрутизируются подобным образом — например, вывод направляет_герг_через_getattr_. Вот почему помещение в комментарий метода_герг_класса Manager не оказывает никакого влияния в Python 2.Х:
вызов делегируется Person. Классы нового стиля также наследуют стандартную реализацию _герг_от их автоматического суперкласса obj ect, что помешает работе _getattr_, но_getattribute_нового стиля тоже не перехватывает имя.
Это изменение не является препятствием — классы нового стиля, основанные на делегировании, как правило, могут переопределять методы перегрузки операций для их делегирования вложенным объектам, либо вручную, либо через инструменты или суперклассы. Тем не менее, данная тема слишком сложна, чтобы продолжать ее исследовать в настоящем руководстве, а потому здесь не стоит вдаваться в слишком мелкие детали. В главах 31 и 32 мы еще вернемся к данной теме (в главе 32 классы нового стиля определяются более формально). При обсуждении управления атрибутами в главе 38 и декораторов класса Private в главе 39 мы снова продемонстрируем влияние изменения на примеры (и также покажем обходные пути). В почти формальном определении наследования в главе 40 вы увидите, что оно будет фактором особого случая. В языке вроде Python, который поддерживает перехват атрибутов и перегрузку операций, влияние такого изменения может оказаться настолько же обширным, как подразумевает его распространение!
Шаг 6: использование инструментов интроспекции
Давайте внесем одну финальную настройку, прежде чем отправить наши объекты в базу данных. В текущем виде наши классы завершены и демонстрируют большинство основ ООП на Python. Однако все еще остаются две проблемы, которые вероятно должны быть улажены до практического применения классов.
• Во-первых, если вы посмотрите на отображение объектов в той форме, как есть, то заметите, что при выводе экземпляр tom класса Manager помечается как Person. Формально это нельзя считать некорректным, поскольку Manager является настроенной и специализированной версией Person. Тем не менее, правильнее было бы отображать объект с самым специфическим (т.е. расположенным ниже всех) классом: тем, из которого объект создан.
• Во-вторых, и это более важно, текущий формат отображения показывает только атрибуты, которые мы включили в_герг_, что может не учитывать будущие
цели. Например, мы пока не в состоянии проверять, что название должности tom было корректно установлено в mgr конструктором класса Manager, т.к. метод _герг_, реализованный для Person, не выводит данное поле. Хуже того,
если мы когда-либо расширим или по-другому изменим набор атрибутов, назначаемых нашим объектам в_init_, нам придется не забыть об обновлении
также и метода_герг_для отображения новых имен, иначе со временем он
утратит синхронизацию.
Последний пункт означает, что мы вновь сделали потенциальную добавочную работу для себя в будущем, привнося избыточность в код. Поскольку любое рассогласование в_герг_будет отражаться в выводе программы, такая избыточность может
быть более очевидной, чем другие формы, которые мы рассмотрели ранее; однако избегание дополнительной работы в будущем обычно считается стоящей вещью.
Специальные атрибуты класса
Мы можем решить обе проблемы с помощью инструментов интроспекции Python — специальных атрибутов и функций, которые обеспечивают доступ к внутренностям реализаций объектов. Эти инструменты довольно сложны и, как правило, используются теми, кто создает инструменты для применения другими программистами, а не программистами, которые разрабатывают прикладные приложения. Тем не менее, базовое знание ряда таких инструментов полезно, потому что они позволяют писать код, обрабатывающий классы обобщенным образом. Скажем, в нашем коде есть две привязки, которые могут помочь; они обе были представлены ближе к концу предыдущей главы и использовались в ранних примерах.
• Встроенный атрибут экземпляр._class_предоставляет ссылку из экземпляра на класс, из которого экземпляр был создан. В свою очередь классы имеют
атрибут_name_, в точности как модули, и последовательность_bases_,
обеспечивающую доступ к суперклассам. Мы можем их здесь применять для вывода имени класса, из которого создавался экземпляр, вместо жестко закодированного имени.
• Встроенный атрибут объект._diet_предоставляет словарь с одной парой
“ключ-значение” для каждого атрибута, присоединенного к объекту пространства имен (включая модули, классы и экземпляры). Поскольку это словарь, мы можем извлекать список его ключей, индексировать по ключу, проходить по его ключам и т.д. для обработки всех атрибутов обобщенным образом. Мы можем использовать такой прием для вывода всех атрибутов в любом экземпляре, а не только тех, которые были жестко закодированы в методах специального отображения, во многом подобно тому, как делалось в инструментах модулей, описанных в главе 25 первого тома.
С первой из указанных категорий мы встречались в главе 25 первого тома, а ниже показан краткий пример применения последних версий классов person.py в интерактивной подсказке Python. Обратите внимание, что мы загружаем Person в интерактивной подсказке посредством оператора from — имена классов находятся в модулях и импортируются из них, в точности как имена функций и другие переменные:
>» from person import Person
>>> bob = Person ('Bob Smith1)
»> bob # Вызывается_repr_ (не_str_) объекта bob
[Person: Bob Smith, 0]
»> print(bob) # To же самое: print =>_str_ или_repr_
[Person: Bob Smith, OJ
>>> bob._class__# Показывает класс и его имя для bob
<class 'person.Person'>
»> bob._class_._name
'Person’
»> list (bob, diet_.keys()) # Атрибуты на самом деле являются ключами словаря
['pay', 'job', 'name'] # Использовать list для получения списка
# в Python З.Х
»> for key in bob._diet_:
print(key, bob._diet_[key]) # Ручная индексация
pay => 0
job => None
name => Bob Smith
»> for key in bob._diet_:
print(key, ’=>', getattr(bob, key)) # объект.атрибут, но атрибут -
# переменная
pay => О
job => None
name => Bob Smith
Как кратко отмечалось в предыдущей главе, некоторые атрибуты, доступные из экземпляра, могут не храниться в словаре_diet_, если в классе экземпляра определено средство_slots_. Необязательное и относительно неясное средство_slots_
для классов нового стиля (т.е. для всех классов в Python З.Х) сохраняет атрибуты последовательно в экземпляре, может вообще устранить словарь_diet_экземпляра
и детально исследуется в главах 31 и 32. Поскольку слоты в действительности принадлежат к классам, а не экземплярам, и в любом случае используются редко, мы можем благополучно проигнорировать их здесь и сосредоточить внимание на нормальном словаре_diet_.
Однако имейте в виду, что некоторые программы могут нуждаться в перехвате
исключений из-за отсутствующего словаря_diet_либо применять hasattr для
проверки или getattr со стандартным значением, если их пользователи развернули слоты. Как будет показано в главе 32, код из следующего раздела потерпит неудачу при использовании классом со слотами (их отсутствия достаточно, чтобы гарантировать наличие_diet_), но слоты — и другие “виртуальные” атрибуты — не будут сообщаться как данные экземпляра.
Обобщенный инструмент отображения
Мы можем задействовать эти интерфейсы в суперклассе, который отображает точные имена классов и форматирует все атрибуты экземпляра любого класса. Создайте в своем текстовом редакторе новый файл и поместите в него приведенный далее код — новый независимый модуль по имени class tools .ру, который реализует такой класс. Из-за того, что его перегруженный метод отображения __герг_применяет обобщенные инструменты интроспекции, он будет работать на любом экземпляре независимо от набора атрибутов экземпляра. И поскольку он является классом, то автоматически становится универсальным инструментом форматирования: благодаря наследованию его можно соединять с любым классом, в котором желательно использовать такой формат отображения. В качестве дополнительного бонуса, если мы когда-либо захотим изменить способ отображения экземпляров, тогда понадобится модифицировать только
этот класс, т.к. любой класс, который наследует его метод_герг_, при следующем
своем запуске подхватит новый формат:
# Файл classtools.ру (новый)
"Смешанные утилиты и инструменты для классов” class AttrDisplay:
М II II
Предоставляет наследуемый метод перегрузки отображения, который показывает экземпляры с их именами классов и пары имя=значение для каждого атрибута, сохраненного в самом экземпляре (но не атрибутов, унаследованных от его классов) . Может соединяться с любым классом и будет работать на любом экземпляре.
II II II
def gatherAttrs(self): attrs = []
for key in sorted (self._diet_) :
attrs.append('%s=%s' % (key, getattr(self, key))) return 1.join(attrs)
def _repr_(self) :
return ' [ % s: %s] ' % (self._class_._name_, self. gatherAttrs () )
if _name_ == '_main_1 :
class TopTest(AttrDisplay): count = 0
def _init_(self) :
self.attrl = TopTest.count self.attr2 = TopTest.count+1 TopTest.count += 2
class SubTest(TopTest) :
pass
X, Y = TopTest(), SubTest () print(X) print(Y)
# Создать два экземпляра
# Показать все атрибуты экземпляров
# Показать имя класса, расположенного ниже всех
Обратите внимание на строки документации — т.к. класс является инструментом универсального назначения, мы добавляем функциональную документацию для чтения потенциальными пользователями. Как объяснялось в главе 15 первого тома, строки документации могут размещаться в начале простых функций и модулей, а также классов и любых их методов; функция help и инструмент PyDoc извлекают и отображают их автоматически. Мы возвратимся к строкам документации для классов в главе 29.
Когда этот модуль запускается напрямую, его код самотестирования создает два экземпляра и выводит их; определенный здесь метод_герг_показывает имя класса
экземпляра плюс имена и значения всех его атрибутов, отсортированные по имени атрибута. Вывод одинаков в Python З.Х и 2.Х, потому что отображением каждого объекта является одиночная сконструированная строка:
C:\code> classtools.ру
[TopTest: attrl=0, attr2=l]
[SubTest: attrl=2, attr2=3]
Еще одно примечание, касающееся проектирования: поскольку класс применяет _герг_вместо_str_, его отображение используется во всех контекстах, но
клиенты также не будут иметь возможности предоставить альтернативное низкоуровневое отображение — они по-прежнему могут добавлять метод_str_, но он применяется только к print и str. В более универсальном инструменте использование взамен_str_ограничивает границы отображения, но оставляет клиенту возможность добавления_герг_для вторичного отображения в интерактивной подсказке
и во вложенных появлениях. Мы будем следовать этой альтернативной политике при реализации расширенных версий данного класса в главе 31; здесь же мы придерживаемся всеобъемлющего метода герг_.
Атрибуты экземпляра или атрибуты класса
Если вы достаточно хорошо изучите код самопроверки модуля classtools, то заметите, что его класс отображает только атрибуты экземпляра, присоединенные к объекту self в нижней части дерева наследования; это то, что содержится в словаре _diet_объекта self. Как и следовало ожидать, мы не видим атрибутов, унаследованных экземпляром от классов, расположенных выше в дереве (например, count в коде самотестирования данного файла — атрибут класса, применяемый в качестве счетчика экземпляров). Унаследованные атрибуты класса присоединяются только к классу, они не копируются в экземпляры.
Если вы когда-нибудь захотите включить также унаследованные атрибуты, тогда
можете подняться по ссылке_class_к классу экземпляра, использовать_diet_
для извлечения атрибутов класса и затем пройти через атрибут_bases_класса,
чтобы подняться к более высоко расположенным суперклассам, при необходимости повторяя процесс. Если вы поклонник простого кода, тогда выполнение встроенного вызова dir на экземпляре вместо применения_diet_и подъема имело бы во многом такой же эффект, т.к. результаты dir включают унаследованные имена в отсортированном списке результатов. В Python 2.7:
>>> from person import Person # Python 2.X: keys - списокf dir показывает меньше »> bob = Person ('Bob Smith')
>>> bob. diet_.keys() # Только атрибуты экземпляра
['РаУ't 'job', 'name’]
>>> dir (bob) # Плюс унаследованные атрибуты в классах
['_doc_', '_init_', '_module_'_repr_'giveRaise', 'job',
'lastName', 'name', 'pay']
Если вы используете Python З.Х, то вывод будет варьироваться и может оказаться больше, чем ожидалось; вот результаты выполнения последних двух операторов, полученные в Python 3.7 (порядок в списке ключей может меняться от вызова к вызову):
»> list(bob._diet_.keys ()) # Python З.Х: keys - представление, не список
['name', 'job', 'pay']
>>> dir (bob) # Python З.Х включает методы типа класса
'gatherAttrs', 'giveRaise', 'job', 'lastName', 'name', ’pay']
Код и вывод отличается для Python 2.Х и Python З.Х, поскольку diet. keys в Python
З.Х не является списком, и dir в Python З.Х возвращает добавочные атрибуты реализации типа класса. Формально dir возвращает в Python З.Х больший объем данных, потому что все классы относятся к “новому стилю” и наследуют крупный набор имен методов для перегрузки операций от типа класса. Фактически обычно вы захотите отфильтровать большинство имен_X_из результатов dir в Python З.Х, т.к. они относятся к внутренним деталям реализации, которые в нормальной ситуации отображать нежелательно:
>>> len(dir(bob))
32
»> list (name for name in dir (bob) if not name, start s with ('_'))
['gatherAttrs', 'giveRaise', 'job', 'lastName', 'name', 'pay']
В интересах пространства мы пока оставим необязательное отображение унаследованных атрибутов класса посредством либо подъема по дереву, либо dir в качестве упражнений для самостоятельной проработки. Дополнительные подсказки по этому вопросу вы можете найти в модуле classtree.py с реализацией инструмента подъема по дереву наследования (глава 29), а также в модуле lister .ру с реализациями инструментов для построения списков подъема по дереву (глава 31).
Размышления относительно имен в классах инструментов
И последняя тонкость: поскольку наш класс AttrDisplay в модуле classtools представляет собой универсальный инструмент, предназначенный для смешивания с произвольными классами, необходимо осознавать возможность непреднамеренных конфликтов имен с клиентскими классами. В текущем виде предполагается, что клиентские подклассы могут применять его методы_герг_и gatherAttrs, но последний может выйти за рамки ожидаемого подклассом — если подкласс просто определит собственное имя gatherAttrs, то вероятно нарушит работу нашего класса, т.к. вместо нашей версии будет использоваться версия из подкласса, которая находится ниже в дереве.
Чтобы увидеть это самостоятельно, добавьте gatherAttrs к классу TopTest в коде самотестирования файла. Если только новый метод не идентичен или намеренно не настраивает исходный метод, то наш класс инструмента больше не будет работать так, как планировалось — self. gatherAttrs внутри AttrDisplay находит новый метод из экземпляра TopTest:
class TopTest(AttrDisplay):
def gatherAttrs (self) : # Замещает метод в AttrDisplay! return 'Spam'
Это не обязательно плохо — иногда мы хотим, чтобы подклассам были доступны другие методы, либо для прямых вызовов, либо для настройки подобным образом. Тем
не менее, если в действительности мы намеревались предоставить только_герг_,
то такой подход далек от идеала.
Чтобы свести к минимуму вероятность конфликтов имен подобного рода, программисты на Python часто снабжают имена методов, не предназначенных для внешнего применения, префиксом в виде одиночного подчеркивания: gatherAttrs в нашем случае. Хотя такой прием не обеспечивает полной защиты от неправильного использования (что, если в другом классе тоже определено имя gatherAttrs?), но его обычно достаточно и он представляет собой распространенное соглашение по именованию для внутренних методов класса.
Лучшим, но менее часто применяемым решением было бы использование двух подчеркиваний в начале имени метода:_gatherAttrs. Интерпретатор Python автомата-чески расширяет имена с двумя подчеркиваниями с целью включения имени содержащего их класса, которое делает имена по-настоящему уникальными при поиске в иерархии наследования. Такое средство обычно называется псевдозакрытыми атрибутами класса и более подробно рассматривается в главе 31, где будет приведена расширенная версия данного класса. А пока мы сделаем оба метода доступными.
Финальная форма классов
Теперь для применения построенного обобщенного инструмента в наших классах необходимо лишь импортировать его из модуля, скомбинировать с классом верхнего
уровня, используя наследование, и избавиться от специфического метода_герг_,
который мы реализовали ранее. Новый метод перегрузки отображения будет унаследован всеми экземплярами Person, а также Manager; класс Manager получает_герг_
от класса Person, который приобретает его от класса AttrDisplay, реализованного в другом модуле. Ниже показана финальная версия файла person.py с внесенными изменениями:
# Файл classtools .ру (новый)
. . .код был представлен ранее. . .
# Файл person.py (финальная версия)
»» »» »»
Регистрирует и обрабатывает сведения о людях.
Для тестирования классов из этого файла запустите его напрямую.
from classtools import AttrDisplay # Использовать обобщенный инструмент
# отображения
class Person (AttrDisplay) : # Комбинирование с классом верхнего уровня
Создает и обрабатывает записи о людях
Vf Vf Vf
def_init_(self, name, job=None, pay=0):
self, name = name self .job = job self.pay = pay
def lastName (self) : # Предполагается, что фамилия указана последней
return self.name.split()[-1]
def giveRaise (self, percent) : # Процент должен находиться между 0 и 1 self.pay = int(self.pay * (1 + percent))
class Manager(Person):
Vf ff Vf
Настроенная версия Person со специальными требованиями f> tf ft
def _init_(self, name, pay) :
Person._init_(self, name, ’mgr', pay) # Название должности
# подразумевается
def giveRaise(self, percent, bonus=.10):
Person.giveRaise(self, percent + bonus)
if _name_ == ’_main_' :
bob = Person('Bob Smith’)
sue = Person('Sue Jones', job='dev', pay=100000) print(bob) print(sue)
print(bob.lastName(), sue.lastName())
sue.giveRaise(.10) ^
print(sue)
tom = Manager('Tom Jones', 50000) tom.giveRaise(.10) print(tom.lastName()) print(tom)
Поскольку дополнение является последним, мы добавили несколько комментариев, чтобы документировать работу — строки документации для описаний функциональности и комментарии # для небольших примечаний в соответствии с принятыми соглашениями, а также пустые строки между методами, чтобы улучшить читабельность кода. В целом такой стиль хорош, когда классы и методы становятся крупными, хотя ранее этого не делалось ради экономии места и уменьшения объема кода для таких небольших классов.
Запустив код прямо сейчас, мы увидим все атрибуты наших объектов, а не только
те, которые жестко закодированы в исходном методе_герг_. И наша финальная
проблема решена: из-за того, что AttrDisplay напрямую берет имена классов из экземпляра self, каждый объект выводится с именем своего ближайшего (находящегося ниже всех в дереве) класса. Теперь tom отображается как объект Manager, а не Person, и мы можем окончательно удостовериться в корректности заполнения его названия должности в конструкторе Manager:
C:\code> person.py
[Person: job=None, name=Bob Smith, pay=0]
[Person: job=dev, name=Sue Jones, pay=100000]
Smith Jones
[Person: job=dev, name=Sue Jones, pay=110000]
Jones
[Manager: job=mgr, name=Tom Jones, pay=60000]
Такое отображение полезнее, чем было ранее. Однако в более широком плане наш класс отображения атрибутов стал универсальным инструментом, который посредством наследования мы можем комбинировать с любым классом, чтобы задействовать определяемый им формат отображения. Кроме того, все его клиенты будут автоматически подхватывать будущие изменения, вносимые в наш инструмент. Позже в книге мы встретимся с еще более мощными концепциями инструментов для классов, такими как декораторы и метаклассы; наряду с многочисленными инструментами интроспекции Python они позволяют писать код, который дополняет и управляет классами структурированным и сопровождаемым способом.
Шаг 7 (последний): сохранение объектов в базе данных
К этому моменту наша работа практически завершена. Мы теперь располагаем двухмодульной системой, которая не только реализует первоначальные проектные цели по представлению сведений о людях, но и предлагает универсальный инструмент отображения атрибутов, подходящий для применения в других программах. За счет помещения функций и классов в файлы модулей мы гарантируем естественную поддержку ими многократного использования. И за счет реализации программного обеспечения в виде классов мы заставляем их естественным образом поддерживать расширение.
Тем не менее, хотя наши классы работают, как было запланировано, создаваемые ими объекты не являются настоящими записями базы данных. То есть если мы уничтожим сеанс Python, то наши экземпляры исчезнут — они представляют собой временные объекты в памяти и не сохраняются на более постоянном носителе вроде файла, поэтому не будут доступными при будущих запусках программы. Оказывается, сделать объекты экземпляров постоянными несложно с помощью средства Python под названием постоянство объектов, которое обеспечивает существование объектов после прекращения работы создавшей их программы. Давайте в качестве финального шага нашего учебного руководства сделаем объект постоянными.
Модули pickle, dbm и shelve
Постоянство объектов реализуется тремя стандартными библиотечными модулями, доступными во всех версиях Python.
pickle
Сериализирует произвольные объекты Python в строку байтов и десериализи-рует их обратно.
dbm (any dbm в Python 2.Х)
Реализует файловую систему с доступом по ключу для хранения строк. shelve
Применяет предшествующие два модуля для хранения объектов Python в файле по ключу.
Мы упоминали указанные модули в главе 9 при исследовании основ файлов. Они предлагают мощные варианты хранения данных. Хотя мы не можем описать их полностью в учебном руководстве или в книге, они достаточно просты, чтобы для начала работы с ними хватило и краткого введения.
Модуль pickle
Модуль pickle является своего рода суперобщим инструментом форматирования и деформатирования объектов: он способен преобразовывать практически произвольный объект Python из памяти в строку байтов, которую позже можно использовать для воссоздания первоначального объекта в памяти. Модуль pickle может обрабатывать почти любой создаваемый вами объект — списки, словари, их вложенные комбинации и экземпляры классов. Последние особенно удобны, поскольку они предоставляют данные (атрибуты) и поведение (методы); на самом деле такое сочетание можно считать грубым эквивалентом “записей” и “программ”. Из-за такой универсальности модуля pickle он может заменить дополнительный код, который пришлось бы иначе писать для создания и разбора специальных представлений объектов внутри текстовых файлов. Сохраняя полученную посредством pickle строку объекта, вы фактически делаете его постоянным: просто загрузите и с помощью pickle воссоздайте исходный объект.
Модуль shelve
Хотя модуль pickle легко применять для сохранения объектов в простых плоских файлах и загрузки из них в более позднее время, модуль shelve предлагает добавочный уровень структуры, который позволяет хранить объекты, обработанные pickle, по ключу. Модуль shelve транслирует объект в строку посредством pickle и сохраняет полученную строку под ключом в файле dbm; при последующей загрузке shelve извлекает строку по ключу и воссоздает исходный объект в памяти с помощью pickle. Все это кажется запутанным, но для вашего сценария хранилище shelve обработанных посредством pickle объектов выглядит подобно словарю — вы индексируете по ключам для извлечения, присваиваете по ключам для сохранения и используете такие словарные инструменты, как len, in и diet, keys для получения информации. Хранилища shelve автоматически отображают словарные операции на объекты, хранящиеся в файле.
В действительности для вашего сценария единственное отличие между хранилищем shelve и нормальным словарем в коде связано с тем, что вы обязаны сначала открывать хранилище и закрывать его после внесения изменений. Совокупный эффект в том, что хранилище shelve предлагает простую базу данных для сохранения и извлечения собственных объектов Python по ключам, что делает их постоянными между запусками программы. Хранилище shelve не поддерживает инструменты запросов, такие как язык SQL, и оно лишено ряда расширенных возможностей, имеющихся в базах данных производственного уровня (вроде подлинной обработки транзакций). Но собственные объекты Python, сохраненные в хранилище shelve, могут обрабатываться с привлечением всей мощи языка Python после того, как будут извлечены обратно по ключу.
Сохранение объектов в базе данных shelve
Темы модулей pickle и shelve достаточно сложны и мы не собираемся здесь погружаться во все связанные с ними детали; вы можете найти их в руководствах по стандартной библиотеке, а также в книгах, ориентированных на разработку приложений, наподобие Programming Python (http: //www. oreilly. com/catalog/9780596158101). Однако в коде все выглядит гораздо проще, так что давайте к нему и обратимся.
Мы напишем новый сценарий, который помещает объекты наших классов в хранилище shelve, создав в текстовом редакторе новый файл по имени makedb.py. Наши классы необходимо импортировать в новый файл, чтобы создать несколько экземпляров, подлежащих сохранению. Ранее для загрузки класса в интерактивной подсказке мы применяли оператор from, но на самом деле, как и с функциями и остальными переменными, существуют два способа загрузки класса из файла (имена классов являются переменными, похожими на любые другие, и в этом контексте вообще нет ничего магического):
import person # Загрузить класс с помощью import
bob = person. Person (. . .) # Указывать имя модуля
from person import Person # Загрузить класс с помощью from
bob = Person (. . .) # Использовать имя напрямую
Для загрузки класса в сценарии мы будем использовать оператор from, т.к. он требует чуть меньшего объема набора. Ради простоты можно скопировать или набрать заново строки самотестирования из файла person.py, которые создают экземпляры наших классов, чтобы у нас было что сохранять (в рассматриваемом демонстрационном примере не имеет смысла беспокоиться об избыточности тестового кода). Когда у нас есть несколько экземпляров, сохранить их в хранилище shelve практически тривиально. Мы всего лишь импортируем модуль shelve, открываем новое хранилище с внешним именем, присваиваем объекты по ключам в хранилище и по завершении закрываем хранилище, потому что в него были внесены изменения:
from person import Person, Manager bob = Person('Bob Smith') sue = Person('Sue Jones', job='dev', tom = Manager('Tom Jones', 50000)
# Загрузить наши классы
# Создать объекты для сохранения рау=100000)
import shelve
db = shelve.open('persondb') for obj in (bob, sue, tom) :
db[obj.name] = obj db.close ()
# Имя файла, в котором хранятся объекты
# Использовать атрибут name объекта
# в качестве ключа
# Сохранить объект в shelve по ключу
# Закрыть после внесения изменений
Обратите внимание на то, как мы присваиваем объекты хранилищу shelve с применением собственных имен объектов в качестве ключей. Мы поступаем так ради удобства; в хранилище shelve ключом может быть любая строка, в том числе та, которую мы могли бы создать для обеспечения уникальности, используя такие средства, как идентификаторы процессов и отметки времени (доступные в стандартных библиотечных модулях os и time). Единственная норма — ключи обязаны быть строками и должны быть уникальными, т.к. мы можем сохранять только один объект на ключ, хотя этот объект может быть списком, словарем или другим объектом, содержащим внутри себя множество объектов.
Фактически значения, сохраняемые под ключами, могут быть объектами Python почти любого вида — встроенными типами вроде строк, списков и словарей, а также экземплярами определяемых пользователем классов, вложенными комбинациями всех подобных типов и т.д. Скажем, атрибуты паше и j ob наших объектов могли бы быть вложенными словарями и списками, как в предшествующих изданиях настоящей книги (правда, тогда текущий код пришлось бы слегка перепроектировать).
Вот и все, что нужно было сделать — если после запуска сценария никакой вывод не был получен, тогда он, вероятно, выполнил свою работу; мы ничего не выводили, а лишь создавали и сохраняли объекты в базе данных, основанной на файле:
C:\code> makedb.py
Исследование хранилища shelve в интерактивной подсказке
На этой стадии в текущем каталоге присутствует один или большее количество реальных файлов, имена которых начинаются с persondb. Создаваемые в действительности файлы могут варьироваться от платформы к платформе, и точно как во встроенной функции open, имя файла в shelve.open () считается относительным текущего рабочего каталога, если только оно не включает путь к каталогу. Где бы они ни хранились, эти файлы реализуют файл с доступом по ключу, который содержит представление pickle наших трех объектов Python. Не удаляйте упомянутые файлы — они являются вашей базой данных, т.е. именно тем, что вы будете копировать или перемещать при выполнении резервного копирования или переноса хранилища.
Вы можете при желании просмотреть файлы shelve в проводнике Windows или в командной оболочке Python, но они представляют собой двоичные файлы хеш-данных, большая часть содержимого которых имеет мало смысла за рамками контекста модуля shelve. В случае Python З.Х без установленного дополнительного программного обеспечения наша база данных хранится в трех файлах (в Python 2.Х файл всего лишь один, persondb, потому что модуль расширения bsddb заранее установлен для
shelve; в Python З.Х модуль bsddb является необязательным сторонним дополнением с открытым кодом).
Например, стандартный библиотечный модуль glob в Python позволяет получать в коде перечни файлов в каталогах, чтобы проверять наличие в них файлов, и мы можем открывать файлы в текстовом или двоичном режиме для исследования строк и байтов:
>» import glob
>>> glob.glob('person*')
['person-composite.py', 'person-department.py', 'person.py', 'person.pyc', 'persondb.bak', 'persondb.dat', 'persondb.dir']
>>> print(open(’persondb.dir').read())
'Sue Jones', (512, 92)
'Tom Jones', (1024, 91)
'Bob Smith', (0, 80)
>» print (open (' persondb. dat' , ’ rb ’) . read ())
b'\x80\x03cperson\nPerson\nq\x00)\x81q\x01}q\x02(X\x03\x00\x00\x00jobq\ x03NX\x03\x00
. . . остальные данные не показаны. . .
Нельзя сказать, что содержимое не поддается расшифровке, но оно может меняться на разных платформах и не совсем подходит для дружественного к пользователям интерфейса базы данных! Чтобы лучше проконтролировать нашу работу, мы можем написать еще один сценарий или заняться исследованием хранилища shelve в интерактивной подсказке. Поскольку хранилища shelve представляют собой объекты Python, содержащие другие объекты Python, мы можем обрабатывать их с помощью нормального синтаксиса Python и режимов разработки. Здесь интерактивная подсказка фактически становится клиентом базы данных.
>>> import shelve
>>> db = shelve.open('persondb') # Повторно открыть хранилище shelve
»> len(db) # Сохранены три 'записи'
3
>>> list (db. keys ()) # Ключи являются индексом
['Sue Jones', 'Tom Jones', 'Bob Smith'] # list() для создания списка в Python З.Х
»> bob = db['Bob Smith1] # Извлечь объект bob по ключу
>>> bob # Выполняет_герг_ из AttrDisplay
[Person: job=None, name=Bob Smith, pay=0]
»> bob.lastName() # Выполняет lastName из Person 'Smith'
>>> for key in db: # Проход, извлечение, вывод print(key, '=>', db[key])
Sue Jones => [Person: job=dev, name=Sue Jones, pay=100000]
Tom Jones => [Manager: job=mgr, name=Tom Jones, pay=50000]
Bob Smith => [Person: job=None, name=Bob Smith, pay=0]
>>> for key in sorted (db) :
print (key, '=>' , db[key]) # Проход по сортированным ключам
Bob Smith => [Person: job=None, name=Bob Smith, pay=0]
Sue Jones => [Person: job=dev, name=Sue Jones, pay=100000]
Tom Jones => [Manager: job=mgr, name=Tom Jones, pay=50000]
Обратите внимание, что для загрузки либо использования сохраненных объектов импортировать классы Person или Manager необязательно. Скажем, мы можем свободно вызвать метод lastName объекта bob и автоматически получить специальный формат отображения, несмотря на отсутствие в области видимости класса Person, к которому bob относится. Подобное возможно потому, что при сохранении посредством pickle экземпляра класса Python записывает его атрибуты экземпляра self вместе с именем класса, из которого он был создан, и модуля, где класс находится. Когда объект bob позже извлекается из хранилища shelve и воссоздается с помощью pickle, то Python автоматически повторно импортирует класс и связывает с ним bob.
Результатом такой схемы оказывается то, что экземпляры класса автоматически обзаводятся поведением их класса при загрузке в будущем. Мы должны импортировать классы, только чтобы создать новые экземпляры, но не обрабатывать существующие. Хотя так сделано умышленно, схема влечет за собой смешанные последствия.
• Недостаток в том, что классы и файлы их модулей обязаны быть импортируемыми, когда экземпляр позже загружается. Более формально классы, допускающие обработку посредством pickle, должны записываться на верхнем уровне файла модуля, который доступен в каталоге из пути поиска модулей sys.path (и не
находиться в модуле_main_самого верхнего файла сценария, если только
при использовании они не всегда присутствуют в данном модуле). Из-за такого требования к внешнему файлу модуля в некоторых приложениях принимается решение обрабатывать с помощью pickle более простые объекты, подобные словарям или спискам, особенно если они передаются по сети.
• Преимущество в том, что изменения в файле исходного кода класса автоматически подхватываются, когда экземпляры этого класса загружаются снова; часто нет необходимости обновлять сами сохраненные объекты, т.к. обновление кода их класса изменяет поведение.
Хранилища shelve также обладают хорошо известными ограничениями (некоторые из них упоминаются в соображениях относительно баз данных в конце главы). Тем не менее, для простого хранилища объектов модули shelve и pickle являются удивительно легкими для применения инструментами.
Обновление объектов в хранилище shelve
Теперь о последнем сценарии: давайте напишем программу, которая при каждом запуске обновляет экземпляр (запись), чтобы доказать постоянство наших объектов — что их текущие значения доступны в любой момент, когда программа на Python выполняется. Файл updatedb.py, код которого приведен ниже, выводит содержимое базы данных и каждый раз предоставляет повышение одному из сохраненных объектов. Если вы отследите происходящее, то заметите, что мы получаем много возможностей “бесплатно” — вывод наших объектов автоматически задействует универсальный метод перегрузки_герг_, и мы предоставляем повышения, вызывая реализованный
ранее метод giveRaise. Все они “просто работают” для объектов, основанных на модели наследования ООП, даже когда объекты располагаются в файле:
• Файл updatedb.py: обновление объекта Person в базе данных
import shelve
db = shelve.open('persondb') # Заново открыть хранилище shelve
# с тем же самым именем файла
for key in sorted (db) : # Проход для отображения объектов из базы данных
print(key, '\t=>', db[key]) # Выводит в специальном формате
sue = db['Sue Jones'] sue.giveRaise(.10) db['Sue Jones'] = sue
db.close()
# Индексация по ключу с целью извлечения
# Обновление в памяти, используя метод класса
# Присваивание по ключу для обновления
# в хранилище shelve
# Закрытие после внесения обновлений
Поскольку сценарий выводит содержимое базы данных при начальном запуске, чтобы увидеть, изменился ли объект, мы должны запустить его, по меньшей мере, дважды. Далее сценарий показан в действии, отображая все записи и увеличивая заработную плату объекта sue каждый раз, когда он запускается (довольно хороший сценарий для sue... возможно, его стоило бы запланировать к запуску на регулярной основе как задание cron?):
C:\code> updatedb.py
Bob Smith => [Person: job^None, name=Bob Smith, pay=0]
Sue Jones => [Person: job-dev, name=Sue Jones, pay=100000]
Tom Jones => [Manager: job-mgr, name=Tom Jones, pay=50000]
C:\code> updatedb.py
Bob Smith => [Person: job=None, name=Bob Smith, pay=0]
Sue Jones => [Person: job=dev, name=Sue Jones, pay=110000]
Tom Jones => [Manager: job=mgr, name=Tom Jones, pay=50000]
C:\code> updatedb.py
Bob Smith => [Person: job=None, name=Bob Smith, pay=0]
Sue Jones => [Person: job=dev, name=Sue Jones, pay=121000]
Tom Jones => [Manager: job=mgr, name=Tom Jones, pay=50000]
C:\code> updatedb.py
Bob Smith => [Person: job=None, name=Bob Smith, pay=0]
Sue Jones => [Person: job=dev, name=Sue Jones, pay=133100]
Tom Jones => [Manager: job-mgr, name=Tom Jones, pay=50000]
Опять-таки то, что мы видим, является результатом функционирования инструментов shelve и pickle, которые мы получаем от Python, и поведения, реализованного в наших классах. Мы снова можем проверить работу нашего сценария в интерактивной подсказке — эквиваленте клиента базы данных shelve:
C:\code> python »> import shelve
»> db = she!ve.open('persondb') # Повторно открыть базу данных »> rec « db['Sue Jones'] # Извлечь объект по ключу
>>> гее
[Person: job=dev, name-Sue Jones, pay=146410]
>>> rec.lastName()
'Jones'
>» rec.pay
146410
Еще один пример постоянства объектов описан во врезке “Что потребует внимания: классы и постоянство” в главе 31. Он сохраняет несколько больший составной объект в плоском файле с помощью pickle вместо shelve, но эффект аналогичен. Дополнительные сведения, касающиеся работы с модулями pickle и shelve, ищите в главах 9 (основы файлов) и 37 (изменения строковых инструментов Python З.Х), в других книгах, а также в руководствах по Python.
Указания на будущее
На этом наше учебное руководство завершается. К настоящему моменту вы видели в действии все основы механизма ООП в Python и изучили способы избегания избыточности и связанных с нею проблем сопровождения кода. Были построены полнофункциональные классы, выполняющие реальную работу. В качестве дополнительного бонуса они были сделаны настоящими записями базы данных за счет помещения в хранилище shelve, так что информация в них сохранялась на постоянной основе.
Разумеется, мы могли бы здесь исследовать и многое другое, например, расширить классы, сделав их более реалистичными, добавить к ним новые линии поведения и т.д. Скажем, на практике операция предоставления повышения должна проверять, находится ли процент повышения между 0 и 1 — расширение, которое мы добавим при обсуждении декораторов позже в книге. Рассмотренный пример можно было бы превратить в базу данных личных контактов за счет изменения сведений, хранящихся в объектах, а также методов классов, используемых для их обработки. Мы оставляем это предполагаемое упражнение полностью открытым для вашего воображения.
Мы могли бы также расширить масштаб охвата и применять инструменты, которые либо поступают вместе с Python, либо свободно доступны в мире открытого кода.
Графические пользовательские интерфейсы
В том виде, как есть, мы можем обрабатывать базу данных только с помощью основанного на командах интерфейса интерактивной подсказки и сценариев. Мы могли бы потрудиться над расширением удобства эксплуатации нашей базы данных объектов, добавив настольный графический пользовательский интерфейс для просмотра и обновления ее записей. Графические пользовательские интерфейсы можно строить переносимым образом посредством либо стандарт^ ной библиотечной поддержки tkinter (Tkinter в Python 2.Х), либо сторонних инструментальных комплектов, таких как wxPython и PyQt. Комплект tkinter поставляется вместе с Python, позволяет быстро строить простые графические пользовательские интерфейсы, и он идеален для изучения методик программирования интерфейсов подобного рода; wxPython и PyQt сложнее в использовании, но часто в итоге дают интерфейсы более высокого качества.
Веб-сайты
Хотя графические пользовательские интерфейсы являются удобными и быстрыми, веб-сеть трудно превзойти с точки зрения доступности. Для просмотра и обновления записей мы могли бы также реализовать веб-сайт взамен или в дополнение к графическому пользовательскому интерфейсу и интерактивной подсказке. Веб-сайты можно создавать с помощью либо базовых инструментов для написания сценариев CGI, имеющихся в составе Python, либо полномасштабных сторонних веб-фреймворков вроде Django, TurboGears, Pylons, web2Py, Zope или Google App Engine. В веб-сети ваши данные по-прежнему могут находиться в хранилище shelve, в файле pickle или в другой среде, основанной на Python. Сценарии обработки данных запускаются автоматически на сервере в ответ на запросы из веб-браузеров и других клиентов, и они производят HTML-разметку для взаимодействия с пользователем, либо напрямую, либо через API-интерфейсы фреймворка. Системы обогащенных Интернет-приложений (Rich Internet application — RIA), такие как Silverlight и pyjamas, также пытаются объединить взаимодействие, подобное обеспечиваемому графическими пользовательскими интерфейсами, с развертыванием в веб-сети.
Веб-службы
Хотя веб-клиенты часто способны проводить разбор информации в ответах от веб-сайтов (методика, красочно называемая “анализом экранных данных”), мы могли бы двинуться дальше и обеспечить более прямой способ извлечения записей через веб-сеть посредством интерфейса веб-служб, такого как SOAP или вызовы XML-RPC — API-интерфейсов, поддерживаемых либо самим Python, либо сторонними инструментами с открытым кодом, которые в целом отображают данные в формат XML и обратно с целью передачи. Для сценариев на Python подобного рода API-интерфейсы возвращают данные более непосредственно, чем текст, внедренный в HTML-разметку страницы ответа.
Базы данных
Если наша база данных становится объемной или важной, тогда со временем мы могли бы перейти от модуля shelve к более полнофункциональному механизму хранения. Им может быть система управления объектно-ориентированными базами данных с открытым кодом ZODB или более традиционная система управления реляционными базами данных на основе SQL, такая как MySQL, Oracle или PostgreSQL. Сам Python поставляется со встроенным модулем внутрипро-цессной системы управления базами данных SQLite, но в веб-сети свободно доступны другие варианты с открытым кодом. Например, система ZODB похожа на модуль shelve из Python, но лишена множества его ограничений, лучше поддерживает крупные базы данных, параллельные обновления, обработку транзакций и автоматическую сквозную запись при изменениях в памяти (хранилища shelve могут кешировать объекты и сбрасывать их на диск во время закрытия посредством параметра writeback, но с этим связаны ограничения). Системы на основе SQL, подобные MySQL, предлагают инструменты производственного уровня для хранилища в виде базы данных и могут напрямую применяться в сценарии на Python. Как известно из главы 9 первого тома, MongoDB обеспечивает альтернативный подход, предусматривающий хранение документов JSON, которые близко напоминают словари и списки Python, но в отличие от данных pickle нейтральны к языку.
Средства объектно-реляционного отображения
В случае перевода хранилища на систему правления реляционными базами данных мы не должны приносить в жертву инструменты ООП в Python. Средства объектно-реляционного отображения (object-relational mapper — ORM) вроде SQLObject и SQLAlchemy способны автоматически отображать реляционные таблицы и строки на классы и экземпляры Python и обратно, так что мы можем обрабатывать сохраненные данные с использованием нормального синтаксиса классов Python. Такой подход предлагает альтернативу системам управления объектно-ориентированными базами данных наподобие shelve и ZODB и задействует мощь реляционных баз данных и модели классов Python.
Хотя я надеюсь, что предложенное введение разожгло у вас интерес к дальнейшим исследованиям, все эти темы, конечно же, выходят далеко за рамки данного руководства и книги в целом. Если вы хотите продолжить изучение самостоятельно, тогда ищите информацию в веб-сети, в руководствах по стандартной библиотеке Python и в ориентированных на разработку приложений книгах, таких как Programming Python (http: //oreilly.com/catalog/9780596009250/). В последней начатый в главе пример продолжается демонстрацией того, как добавить графический пользовательский интерфейс и веб-сайт поверх базы данных, чтобы сделать возможным удобный просмотр и обновление записей экземпляров. А теперь давайте возвратимся к основам классов и закончим остаток истории с языком Python.
Резюме
В главе мы исследовали все основы классов Python и ООП в действии, построив простой, но реалистичный пример шаг за шагом. Мы добавили конструкторы, методы, перегрузку операций, настройку с помощью подклассов и инструменты интроспекции, а также попутно ознакомились с другими концепциями, такими как композиция, делегирование и полиморфизм.
В итоге мы взяли объекты, созданные нашими классами, и сделали их постоянными за счет сохранения в базе данных объектов shelve — легкой в применении системы для сохранения и извлечения собственных объектов Python по ключу. Во время изучения основ классов мы также ознакомились с несколькими способами вынесения кода, призванными сократить избыточность и минимизировать будущие затраты на сопровождение. Наконец, был приведен краткий обзор подходов к расширению кода с помощью инструментов для реализации приложений, в числе которых графические пользовательские интерфейсы и базы данных, раскрываемые в отдельных книгах.
В последующих главах этой части книги мы возвратимся к изучению деталей, лежащих в основе модели классов Python, и исследованию ее применимости к ряду проектных концепций, используемых для объединения классов в более крупных программах. Однако прежде чем двигаться дальше, ответьте на контрольные вопросы главы. Поскольку в главе уже было проделано много практической работы, мы завершаем главу набором вопросов в основном по теории, которые заставят вас пройтись по коду и обдумать стоящие за ним идеи.
Проверьте свои знания: контрольные вопросы
1. Когда мы извлекаем объект Manager из хранилища shelve и выводим его, то откуда поступает логика форматирования отображения?
2. Когда мы извлекаем объект Person из хранилища shelve, не импортируя его модуль, то каким образом объект узнает, что он имеет метод giveRaise, который мы можем вызвать?
3. Почему настолько важно перемещать обработку внутрь методов вместо ее жесткого кодирования за пределами класса?
4. Почему лучше настраивать за счет создания подклассов, а не копировать исходный код и модифицировать копию?
5. Почему для выполнения стандартных действий лучше вызывать метод суперкласса, а не копировать и модифицировать их код в подклассе?
6. Почему лучше применять инструменты вроде_diet_, которые позволяют обрабатывать объекты обобщенно, чем писать дополнительный специализированный код для каждой разновидности класса?
7. Опишите в общих чертах, когда вы могли бы выбрать использование внедрения и композиции объектов вместо наследования?
8. Что бы вы изменили, если бы объекты, реализованные в настоящей главе, применяли словарь для имен и список для названий должностей, как в похожих примерах, приводимых ранее в книге?
9. Как бы вы модифицировали классы в этой главе для реализации базы данных личных контактов в Python?
Проверьте свои знания: ответы
1. В финальной версии наших классов Manager в конечном итоге наследует свой метод вывода_герг_от класса AttrDisplay из отдельного модуля classtools,
находящегося двумя уровнями выше в дереве классов. Сам класс Manager не имеет такого метода, поэтому поиск в иерархии наследования поднимается до
его суперкласса Person; из-за того, что там тоже нет метода_герг_, поиск
поднимается выше и обнаруживает его в классе AttrDisplay. Имена классов, указанные внутри круглых скобок в строке заголовка оператора class, предоставляют ссылки на более высоко расположенные суперклассы.
2. Хранилища shelve (в действительности используемый ими модуль pickle) автоматически повторно связывают экземпляр с классом, из которого он был создан, когда экземпляр позже загружается обратно в память. Python внутренне заново импортирует класс из его модуля, создает экземпляр с сохраненными атрибутами и устанавливает ссылку_class_экземпляра, чтобы она указывала на
первоначальный класс. Таким образом, загруженные экземпляры автоматически получают все свои первоначальные методы (наподобие lastName, giveRaise и
_repr_), даже притом, что мы не импортировали класс экземпляра в текущую
область видимости.
3. Важно переносить обработку внутрь методов, чтобы в будущем приходилось изменять только одну копию и методы могли выполняться на любом экземпляре. Это понятие инкапсуляции в Python — помещение логики в оболочку интерфейсов для лучшей поддержки будущего сопровождения кода. Если вы не поступите так, то создадите избыточность кода, которая может увеличить трудозатраты по мере развития кода в будущем.
4. Настройка с помощью создания подклассов сокращает объем усилий, требующихся для разработки. При ООП мы пишем код, настраивая то, что уже было сделано, вместо копирования либо изменения существующего кода. На самом деле это “основной смысл” ООП — из-за возможности легкого расширения предыдущей работы путем реализации новых подклассов мы можем задействовать то, что было сделано ранее. Поступать так гораздо лучше, чем либо начинать каждый раз с нуля, либо вводить многочисленные избыточные копии кода, которые все могут потребовать обновления в будущем.
5. Копирование и модификация кода удваивает потенциальные трудозатраты в будущем независимо от контекста. Если подкласс нуждается в выполнении стандартных действий, реализованных в методе суперкласса, то намного лучше обратиться к исходному методу через имя суперкласса, чем копировать его код. Это также остается справедливым для конструкторов суперкласса. Опять-таки копирование кода создает избыточность, которая с развитием кода превращается в крупную проблему.
6. Обобщенные инструменты в состоянии избежать жестко закодированных решений, которые должны быть синхронизированы с остальной частью класса по мере того, как класс развивается с течением времени. Например, обобщенный
метод вывода_герг_не придется обновлять каждый раз, когда к экземплярам
добавляется новый атрибут в конструкторе _init_. Вдобавок обобщенный
метод print, наследуемый всеми классами, обнаруживается и должен быть модифицирован только в одном месте — изменения в обобщенной версии подхватываются всеми классами, которые унаследованы от обобщенного класса. И снова устранение избыточности кода сокращает будущие усилия по разработке; это одно из основных полезных качеств, привносимых классами.
7. Наследование лучше всего подходит при реализации расширений, основанных на прямой настройке (подобных нашей специализации Manager из класса Person). Композиция хорошо подходит в сценариях, где множество объектов агрегируются в единое целое и управляются классом уровня контроллера. Наследование передает вызовы вверх для многократного использования, а композиция передает их вниз для делегирования. Наследование и композиция не являются взаимно исключающими; зачастую объекты, внедренные в контроллер, сами представляют собой настройки, основанные на наследовании.
8. Не особенно много, т.к. это был действительно первый прототип, но метод lastName потребовалось бы обновить для нового формата имени; в конструкторе Person пришлось бы изменить стандартное значение названия должности на пустой список; и в конструкторе класса Manager вероятно понадобилось бы передавать список названий должностей вместо одиночной строки (разумеется, нужно также изменить код самотестирования). Хорошая новость в том, что упомянутые изменения необходимо вносить всего лишь в одном месте — в наших классах, где инкапсулированы такие детали. Сценарий базы данных должен работать в том виде, как есть, поскольку хранилища shelve поддерживают произвольно глубоко вложенные данные.
9. Классы в настоящей главе можно было бы применять в качестве стереотипного “шаблонного” кода для реализации различных типов баз данных. По существу вы можете изменить их назначение, модифицируя конструкторы для регистрации других атрибутов и предоставляя любые методы, которые подходят целевому приложению. Скажем, для базы данных контактов вы могли бы использовать такие атрибуты, как name, address, birthday, phone, email и т.д., и подходящие для этой цели методы. Например, метод по имени sendmail мог бы применять стандартный библиотечный модуль smtplib в Python для отправки сообщения электронной почты одному из контактов автоматически при вызове (дополнительные сведения об инструментах подобного рода ищите в руководствах по Python или в книгах, посвященных разработке приложений). Написанный здесь инструмент AttrDisplay можно было бы дословно использовать для вывода объектов, поскольку он намеренно сделан обобщенным. Большую часть кода базы данных shelve можно было бы применять для хранения ваших объектов, внеся лишь незначительные изменения.
ГЛАВА 29
Назад: Основы написания классов
Дальше: Детали реализации классов