Программирование на Python. Том 1 (fb2)


Использовать online-читалку "Книгочей 0.2" (Не работает в Internet Explorer)


Настройки текста:


Programming

Python

Forth Edition

Mark Lutz

O REILLY

Программирование

на Python

том I

Четвертое издание


Марк Лутц

Марк Лутц

Программирование на Python, том I, 4-е издание

Перевод А. Киселева

А. Галунов Н. Макарова П. Щеголев Ю. Бочина С. Минин К. Чубаров

Главный редактор

Зав. редакцией

Выпускающий редактор

Редактор

Корректор

Верстка

Лутц М.

Программирование на Python, том I, 4-е издание. - Пер. с англ. - СПб.: Символ-Плюс, 2011. - 992 с., ил.

ISBN 978-5-93286-210-0

Вы овладели основами Python. Что дальше? Эта книга представляет собой подробное руководство по применению этого языка программирования в основных прикладных областях - системном администрировании, создании графических интерфейсов и веб-приложений. Исследуются приемы работы с базами данных, программирования сетевых взаимодействий, создания интерфейсов для сценариев, обработки текста и многие другие.

Издание описывает синтаксис языка и методики разработки, содержит большое количество примеров, иллюстрирующих типичные идиомы программирования и корректное их применение. Кроме того, исследуется эффективность Python в качестве инструмента разработки программного обеспечения, в отличие от просто инструмента «создания сценариев».

В четвертое издание включено описание новых особенностей языка, библиотек и практических приемов программирования для Python 3.X. Примеры, представленные в книге, опробованы под третьей альфа-версией Python 3.2.

Можно смело утверждать, что это обстоятельная и всесторонняя книга предназначена быть первой ступенью на пути овладения мастерством разработки приложений на языке Python.

ISBN 978-5-93286-210-0 ISBN 978-0-596-15810-1 (англ)

© Издательство Символ-Плюс, 2011

Authorized translation of the English edition © 2011 O’Reilly Media Inc. This translation is published and sold by permission of O’Reilly Media Inc., the owner of all rights to publish and sell the same.

Все права на данное издание защищены Законодательством РФ, включая право на полное или частичное воспроизведение в любой форме. Все товарные знаки или зарегистрированные товарные знаки, упоминаемые в настоящем издании, являются собственностью соответствующих фирм.

Издательство «Символ-Плюс». 199034, Санкт-Петербург, 16 линия, 7, тел. (812) 380-5007, www.symbol.ru. Лицензия ЛП N 000054 от 25.12.98. Подписано в печать 31.07.2011. Формат 70x100 1/16.

Печать офсетная. Объем 62 печ. л.

Оглавление

Предисловие...........................................................................15

«А теперь нечто совершенно иное...».......................................15

Об этой книге.......................................................................16

О четвертом издании.............................................................18

Влияние Python 3.X на эту книгу............................................26

Использование примеров из книги..........................................31

Как связаться с издательством O’Reilly....................................33

Типографские соглашения.....................................................34

Благодарности........................................................................35

Об авторе...............................................................................38

Часть I. Начало..........................................................................39

Глава 1. Предварительный обзор.............................................41

«Программирование на Python»: краткий очерк.......................41

Постановка задачи................................................................42

Шаг 1: представление записей................................................43

Списки............................................................................43

Словари ..........................................................................48

Шаг 2: сохранение записей на длительное время.......................54

Текстовые файлы.............................................................55

Модуль pickle...................................................................61

Работа модуля pickle с отдельными записями.......................64

Модуль shelve..................................................................66

Шаг 3: переход к ООП............................................................69

Использование классов .....................................................71

Добавляем поведение........................................................73

Добавляем наследование ...................................................74

Реструктуризация программного кода.................................75

Добавляем возможность сохранения...................................79

Другие разновидности баз данных ......................................81

Шаг 4: добавляем интерфейс командной строки........................83

Шаг 5: добавляем графический интерфейс ...............................86

Основы графических интерфейсов......................................87

ООП при разработке графических интерфейсов.....................89

Получение ввода от пользователя.......................................92

Графический интерфейс к хранилищу.................................94

Шаг 6: добавляем веб-интерфейс...........................................102

Основы CGI....................................................................103

Запуск веб-сервера..........................................................106

Использование строки запроса и модуля urllib....................109

Форматирование текста ответа.........................................110

Веб-интерфейс к хранилищу с данными.............................111

Конец демонстрационного примера.......................................123

Часть II. Системное программирование................................127

Глава 2. Системные инструменты..........................................129

«os.path - дорога к знанию» .................................................129

Зачем здесь нужен Python?..............................................129

В следующих пяти главах................................................130

Знакомство с разработкой системных сценариев.....................132

Системные модули Python...............................................133

Источники документации по модулям...............................134

Постраничный вывод строк документации.........................135

Сценарий постраничного вывода.......................................137

Основы использования строковых методов.........................138

Другие особенности строк в Python 3.X:

Юникод и тип bytes........................................................141

Основы операций с файлами ............................................142

Два способа использования программ ...............................143

Руководства по библиотекам Python..................................144

Коммерческие справочники.............................................145

Модуль sys ........................................................................ 146

Платформы и версии ......................................................146

Путь поиска модулей......................................................146

Таблица загруженных модулей........................................148

Сведения об исключениях ...............................................149

Другие элементы, экспортируемые модулем sys .................150

Модуль os..........................................................................150

Инструменты в модуле os.................................................151

Средства администрирования...........................................152

Константы переносимости ............................................... 153

Основные инструменты os.path ........................................153

Выполнение команд оболочки из сценариев ....................... 156

Другие элементы, экспортируемые модулем os .................. 163

Глава 3. Контекст выполнения сценариев...............................167

«Ваши аргументы, пожалуйста!»..........................................167

Текущий рабочий каталог ...................................................168

Текущий рабочий каталог, файлы и путь поиска модулей____168

Текущий рабочий каталог и командные строки ..................170

Аргументы командной строки .............................................. 171

Анализ аргументов командной строки...............................172

Переменные окружения оболочки.........................................175

Получение значений переменных оболочки........................176

Изменение переменных оболочки.....................................177

Особенности переменных оболочки:

родители, putenv и getenv................................................179

Стандартные потоки ввода-вывода........................................180

Перенаправление потоков ввода-вывода

в файлы и программы .....................................................181

Перенаправление потоков

и взаимодействие с пользователем....................................187

Перенаправление потоков в объекты Python.......................192

Вспомогательные классы io.StringIO и io.BytesIO...............196

Перехват потока stderr....................................................197

Возможность перенаправления с помощью функции print____197

Другие варианты перенаправления:

еще раз об os.popen и subprocess........................................198

Глава 4. Инструменты для работы с файлами и каталогами.... 206

«Как очистить свой жесткий диск за пять простых шагов».......206

Инструменты для работы с файлами ..................................... 206

Модель объекта файла в Python 3.X..................................207

Использование встроенных объектов файлов ...................... 209

Двоичные и текстовые файлы........................................... 220

Низкоуровневые инструменты в модуле os

для работы с файлами ..................................................... 233

Сканеры файлов.............................................................239

Инструменты для работы с каталогами .................................. 243

Обход одного каталога.....................................................243

Обход деревьев каталогов ................................................249

Обработка имен файлов в Юникоде

в версии 3.X: listdir, walk, glob.........................................254

Глава 5. Системные инструменты

параллельного выполнения..................................................258

«Расскажите обезьянам, что им делать» ................................258

Ветвление процессов ........................................................... 260

Комбинация fork/exec.....................................................264

Потоки выполнения............................................................270

Модуль _thread..............................................................274

Модуль threading............................................................ 287

Модуль queue.................................................................293

Графические интерфейсы и потоки выполнения:

предварительное знакомство ............................................ 298

Подробнее о глобальной блокировке

интерпретатора (GIL)......................................................302

Завершение программ ......................................................... 306

Завершение программ средствами модуля sys.....................306

Завершение программ средствами модуля os......................307

Коды завершения команд оболочки...................................308

Код завершения процесса

и совместно используемая информация.............................312

Код завершения потока

и совместно используемая информация.............................314

Взаимодействия между процессами.......................................316

Анонимные каналы........................................................318

Именованные каналы (fifo)..............................................331

Сокеты: первый взгляд .................................................... 335

Сигналы ........................................................................ 340

Пакет multiprocessing.........................................................343

Зачем нужен пакет multiprocessing?.................................. 344

Основы: процессы и блокировки.......................................346

Инструменты IPC: каналы, разделяемая память и очереди ... 349

Запуск независимых программ ......................................... 357

И многое другое..............................................................359

Зачем нужен пакет multiprocessing? Заключение ................ 361

Другие способы запуска программ.........................................362

Семейство функций os.spawn...........................................362

Функция os.startfile в Windows........................................366

Переносимый модуль запуска программ ................................368

Другие системные инструменты............................................ 374

Глава 6. Законченные системные программы........................376

«Ярость поиска».................................................................376

Игра: «Найди самый большой файл Python»...........................377

Сканирование каталога стандартной библиотеки................377

Сканирование дерева каталогов стандартной библиотеки.....378

Сканирование пути поиска модулей..................................379

Сканирование всего компьютера....................................... 382

Вывод имен файлов с символами Юникода.........................387

Разрезание и объединение файлов.........................................390

Разрезание файлов переносимым способом......................... 391

Соединение файлов переносимым образом .........................395

Варианты использования................................................. 399

Создание веб-страниц для переадресации ............................... 403

Файл шаблона страницы .................................................404

Сценарий генератора страниц...........................................405

Сценарий регрессивного тестирования ................................... 408

Запускаем тестирование..................................................411

Копирование деревьев каталогов...........................................417

Сравнение деревьев каталогов ..............................................422

Поиск расхождений между каталогами.............................422

Поиск различий между деревьями....................................425

Запускаем сценарий........................................................428

Проверка резервных копий..............................................431

Отчет о различиях и другие идеи ...................................... 433

Поиск в деревьях каталогов..................................................435

grep, glob и find..............................................................436

Создание собственного модуля find...................................437

Удаление файлов с байт-кодом .........................................442

Visitor: обход каталогов «++»...............................................448

Редактирование файлов в деревьях каталогов (Visitor).........454

Глобальная замена в деревьях каталогов (Visitor) ...............456

Подсчет строк исходного программного кода (Visitor)..........458

Копирование деревьев каталогов

с помощью классов (Visitor).............................................460

Другие примеры обходчиков (внешние).............................462

Проигрывание медиафайлов................................................. 464

Модуль webbrowser.........................................................468

Модуль mimetypes..........................................................470

Запускаем сценарий........................................................473

Автоматизированный запуск программ (внешние примеры) ..... 473

Часть III. Программирование графических интерфейсов .... 477

Глава 7. Графические интерфейсы пользователя....................479

«Я здесь, я смотрю на тебя, детка» ........................................479

Темы программирования GUI...........................................479

Запуск примеров ............................................................ 481

Различные возможности создания GUI в Python......................483

Обзор tkinter......................................................................490

Практические преимущества tkinter ................................490

Документация tkinter ..................................................... 492

Расширения для tkinter...................................................492

Структура tkinter...........................................................495

Взбираясь по кривой обучения

программированию графических интерфейсов ........................ 497

«Hello World» в четыре строки (или меньше)......................497

Основы использования tkinter..........................................498

Создание виджетов ......................................................... 499

Менеджеры компоновки .................................................. 500

Запуск программ с графическим интерфейсом....................501

Альтернативные приемы использования tkinter ................. 502

Основы изменения размеров виджетов............................... 504

Настройка параметров графического элемента

и заголовка окна............................................................506

Еще одна версия в память о былых временах......................508

Добавление виджетов без их сохранения ............................ 508

Добавление кнопок и обработчиков.......................................511

Еще раз об изменении размеров виджетов: растягивание......512

Добавление пользовательских обработчиков...........................514

lambda-выражения как обработчики событий.....................515

Отложенные вызовы с применением

инструкций lambda и ссылок на объекты...........................516

Проблемы с областями видимости обработчиков.................518

Связанные методы как обработчики событий......................525

Объекты вызываемых классов как обработчики событий.....527

Другие протоколы обратного вызова в tkinter.....................528

Связывание событий.......................................................529

Добавление нескольких виджетов.........................................530

Еще раз об изменении размеров: обрезание.........................531

Прикрепление виджетов к фреймам..................................532

Порядок компоновки и прикрепление к сторонам ............... 533

Снова о параметрах expand и fill компоновки.....................534

Использование якорей вместо растягивания....................... 536

Настройка виджетов с помощью классов................................537

Стандартизация поведения и внешнего вида.......................538

Повторно используемые компоненты и классы........................ 540

Прикрепление классов компонентов ................................. 542

Расширение классов компонентов..................................... 544

Автономные классы-контейнеры ...................................... 546

Завершение начального обучения ......................................... 549

Соответствие между Python/tkinter и Tcl/Tk..........................551

Глава 8. Экскурсия по tkinter, часть 1......................................553

«Виджеты, гаджеты,

графические интерфейсы... Бог мой!» ....................................553

Темы этой главы.................................................................554

Настройка внешнего вида виджетов......................................554

Окна верхнего уровня..........................................................558

Виджеты Toplevel и Tk....................................................560

Протоколы окна верхнего уровня......................................561

Диалоги ............................................................................ 566

Стандартные (типичные) диалоги.....................................567

Модуль диалогов в старом стиле.......................................580

Пользовательские диалоги............................................... 581

Привязка событий..............................................................585

Другие события, доступные с помощью метода bind.............590

Виджеты Message и Entry....................................................592

Message ......................................................................... 592

Entry ............................................................................ 593

Компоновка элементов ввода в формах..............................595

«Переменные» tkinter и альтернативные способы

компоновки форм...........................................................599

Флажки, переключатели и ползунки ..................................... 602

Флажки ........................................................................ 602

Переключатели..............................................................607

Ползунки......................................................................614

Три способа использования графических интерфейсов.............618

Прикрепление к фреймам................................................619

Независимые окна .......................................................... 624

Запуск программ............................................................626

Изображения.................................................................633

Развлечения с кнопками и картинками ............................. 637

Отображение и обработка изображений с помощью PIL............641

Основы PIL....................................................................641

Отображение других типов графических

изображений с помощью PIL ............................................ 643

Отображение всех изображений в каталоге.........................645

Создание миниатюр изображений

с помощью пакета PIL.....................................................647

Глава 9. Экскурсия по tkinter, часть 2......................................659

«Меню дня: Spam, Spam и еще раз Spam» ...............................659

Меню................................................................................660

Меню окон верхнего уровня ............................................. 660

Меню на основе виджетов Frame и Menubutton...................665

Окна с меню и панелью инструментов................................670

Виджеты Listbox и Scrollbar.................................................676

Программирование виджетов списков ............................... 678

Программирование полос прокрутки................................. 680

Компоновка полос прокрутки...........................................681

Виджет Text.......................................................................683

Программирование виджета Text......................................685

Операции редактирования текста.....................................689

Юникод и виджет Text....................................................695

Более сложные операции с текстом и тегами ......................707

Виджет Canvas...................................................................709

Базовые операции с виджетом Canvas................................710

Программирование виджета Canvas..................................711

Прокрутка холстов.........................................................715

Холсты с поддержкой прокрутки

и миниатюр изображений................................................718

События холстов ............................................................ 722

Сетки................................................................................726

В чем преимущества размещения по сетке? .......................727

Основы работы с сеткой: еще раз о формах ввода.................728

Сравнение методов grid и pack..........................................729

Сочетание grid и pack......................................................731

Реализация возможности растягивания виджетов,

размещаемых по сетке..................................................... 734

Создание крупных таблиц с помощью grid ......................... 738

Инструменты синхронизации,

потоки выполнения и анимация............................................ 747

Использование потоков выполнения

в графических интерфейсах tkinter...................................750

Использование метода after.............................................752

Простые приемы воспроизведения анимации ..................... 755

Другие темы, связанные с анимацией ................................ 762

Конец экскурсии ................................................................ 764

Другие виджеты и их параметры ...................................... 764

Глава 10. Приемы программирования

графических интерфейсов....................................................766

«Создание улучшенной мышеловки».....................................766

GuiMixin: универсальные

подмешиваемые классы ....................................................... 767

Функции создания виджетов............................................ 768

Вспомогательные подмешиваемые классы ......................... 769

GuiMaker: автоматизация создания меню

и панелей инструментов.......................................................773

Протоколы подклассов....................................................778

Классы GuiMaker...........................................................779

Программный код самотестирования GuiMaker..................779

BigGui: клиентская демонстрационная программа .............. 781

ShellGui: графические интерфейсы

к инструментам командной строки........................................785

Обобщенный графический интерфейс

инструментов оболочки...................................................785

Классы наборов утилит....................................................788

Добавление графических интерфейсов

к инструментам командной строки ...................................789

GuiStreams: перенаправление

потоков данных в виджеты................................................... 797

Использование перенаправления

сценариев архивирования ...............................................802

Динамическая перезагрузка обработчиков............................. 803

Обертывание интерфейсов окон верхнего уровня.....................805

Графические интерфейсы, потоки выполнения и очереди.........810

Помещение данных в очередь...........................................813

Помещение обработчиков в очередь...................................817

Другие способы добавления GUI

к сценариям командной строки.............................................825

Вывод окон графического интерфейса по требованию..........826

Реализация графического интерфейса в виде отдельной

программы: сокеты (вторая встреча) .................................830

Реализация графического интерфейса в виде

отдельной программы: каналы.........................................835

Запускающие программы PyDemos и PyGadgets......................845

Панель запуска PyDemos.................................................846

Панель запуска PyGadgets...............................................852

Глава 11. Примеры законченных программ

с графическим интерфейсом.................................................857

«Python, открытое программное обеспечение и Camaro»...........857

Примеры в других главах................................................858

Стратегия данной главы..................................................859

PyEdit: программа/объект текстового редактора.....................862

Запуск PyEdit ................................................................ 863

Изменения в версии PyEdit 2.0 (третье издание)..................872

Изменения в версии PyEdit 2.1 (четвертое издание) ............874

Исходный программный код PyEdit..................................888

PyPhoto: программа просмотра и изменения

размеров изображений........................................................917

Запуск PyPhoto..............................................................918

Исходный программный код PyPhoto................................922

PyView: слайд-шоу для изображений и примечаний................929

Запуск PyView...............................................................929

Исходный программный код PyView.................................935

PyDraw: рисование и перемещение графики ........................... 941

Запуск PyDraw ............................................................... 941

Исходный программный код PyDraw.................................943

PyClock: виджет аналоговых/цифровых часов........................951

Краткий урок геометрии..................................................951

Запуск PyClock...............................................................957

Исходный программный код PyClock................................961

PyToe: виджет игры в крестики-нолики.................................969

Запуск PyToe.................................................................969

Исходный программный код PyToe (внешний) ...................971

Что дальше........................................................................974

Алфавитный указатель.........................................................976

Предисловие


«А теперь нечто совершенно иное...»

В этой книге исследуются способы применения языка программирования Python в типичных прикладных областях и в реально возникающих задачах. Эта книга рассказывает, что можно делать с языком Python после того, как вы овладели его основами.

Эта книга предполагает, что читатель еще только начинает знакомиться с рассматриваемыми в книге прикладными областями - графическими интерфейсами, Интернетом, базами данных, системным программированием и так далее, - и представляет каждую из них, начиная с самых азов, выполняя роль учебника. Попутно книга ставит своей целью познакомить читателя с часто используемыми инструментами и библиотеками, но не с основами языка. Таким образом, данная книга является ресурсом, позволяющим читателю получить более глубокое понимание роли языка Python в практике программирования.

Дополнительно в этой книге исследуется пригодность языка Python на роль инструмента разработки программного обеспечения, в отличие от просто инструмента «создания сценариев». Многие примеры, представленные в этой книге, подобраны в соответствии с этой целью - среди них вы найдете примеры постепенной разработки клиентов электронной почты, опирающиеся на тысячи строк программного кода. Создание подобных крупномасштабных приложений всегда будет непростым делом, но мы покажем, насколько быстрее и проще создаются такие приложения, когда они разрабатываются на языке Python.

Это четвертое издание было дополнено представлением новых особенностей языка, библиотек и практических приемов программирования для Python 3.X. В частности, примеры, представленные в книге, выполняются под управлением интерпретатора версии Python 3.1 - наиболее свежей версии Python на момент написания этих строк. Непосредственно перед публикацией книги все основные примеры были опробованы под третьей альфа-версией Python 3.2, но вообще говоря, они должны сохранить свою работоспособность при использовании любой версии Python из линейки 3.X. Кроме того, это издание было реорганизовано с целью упорядочить прежний материал и добавить описание новых инструментов и тем.

Поскольку среди читателей этой книги будут и те, кому в руки первым попало это издание, и те, кто знаком с предыдущими изданиями, я хочу в этом предисловии более подробно остановиться на целях и задачах этой книги, прежде чем перейти к программному коду.


Об этой книге

Данная книга является учебником по применению языка Python для решения наиболее типичных задач в различных прикладных областях. В ней рассказывается о применении языка Python в системном администрировании, для создания графических интерфейсов и вебприложений и исследуются приемы программирования сетевых взаимодействий, взаимодействий с базами данных, обработки текста, создания интерфейсов для сценариев и во многих других областях. Несмотря на то, что на протяжении всей книги используется язык Python, тем не менее основное внимание будет уделяться не основам языка, а приемам решения практических задач.


Экосистема этой книги

Диапазон тем, обсуждаемых в этой книге, позволяет ее рассматривать, как второй том двухтомника, который должен быть дополнен третьим томом. Важно помнить, что эта книга описывает особенности разработки приложений и является продолжением книги «Изучаем Python»1, рассматривающей основы языка, знание которых совершенно необходимо для чтения этой книги. Поясним, как связаны эти книги между собой:

•    «Изучаем Python» - подробно описывает основы программирования на языке Python. Основное внимание уделяется базовым особенностям языка Python, знание которых является необходимой предпосылкой для чтения этой книги.

•    «Программирование на Python» - эта книга охватывает практические приемы программирования на языке Python. Основное внимание в этой книге уделяется библиотекам и инструментам, и предполагается, что читатель уже знаком с основами языка.

•    «Python Pocket Reference» - краткий справочник, в котором охватываются некоторые подробности, отсутствующие в данной книге. Этот справочник не может использоваться в качестве учебника, но он позволит вам быстро отыскивать описание тех или иных особенностей.

В некотором смысле, эта книга является аналогом «Изучаем Python», но в ней раскрываются не основы языка, а основы прикладного программирования. Это последовательный учебник, в котором не делается никаких предположений об уровне вашей подготовки, и каждая тема начинает рассматриваться с самых азов. Например, при изучении темы разработки веб-приложений вы приобретете все необходимые знания, которые позволят вам создавать простые веб-сайты, и получите представление о более развитых фреймворках и инструментах, которые пригодятся вам с ростом ваших потребностей. Обсуждение особенностей конструирования графических интерфейсов также идет по нарастающей - от простого к сложному.

Дополнением к этой книге может служить книга «Python Pocket Reference» (карманный справочник по языку Python), где предоставляется дополнительная информация о некоторых особенностях, не рассматриваемых в данной книге, и которая может служить отличным справочником. Книга «Python Pocket Reference» является всего лишь справочником, и в ней практически отсутствуют примеры и пояснения, но она может служить отличным дополнением к книгам «Изучаем Python» и «Программирование на Python». Поскольку текущее четвертое издание «Python Pocket Reference» содержит информацию об обеих основных версиях Python, 2.X и 3.X, оно также может использоваться читателями, выполняющими переход между этими двумя версиями Python (подробнее об этом чуть ниже)2.


Чего нет в этой книге

Из-за распределения областей рассмотрения по книгам, о которых упоминалось выше, область применения этой книги имеет следующие ограничения:

•    Она не раскрывает основы языка Python

•    Она не предназначалась для использования в качестве справочника по особенностям языка

Первый из этих двух пунктов отражает тот факт, что освещение основ языка является исключительной прерогативой книги «Изучаем Python», и если вы совершенно не знакомы с языком Python, я рекомендую сначала прочитать книгу «Изучаем Python», прежде чем приниматься за эту книгу, так как здесь предполагается знание основ языка.

Конечно, некоторые приемы языка демонстрируются в примерах этой книги, а в крупных примерах иллюстрируется, как можно объединять базовые концепции в действующие программы. Особенности объектноориентированного программирования, например, часто лучше всего демонстрировать на примерах крупных программ, которые и будут представлены здесь. Однако формально эта книга предполагает, что вы уже знакомы с основами языка Python в достаточной степени, чтобы понять примеры программного кода. Основное внимание здесь будет уделяться библиотекам и инструментам. Поэтому если программный код, который будет демонстрироваться, покажется вам непонятным, -справляйтесь в других источниках.

Второй из этих двух пунктов отражает тот факт, что за многие годы об этой книге сложились неверные представления (вероятно, стоило назвать эту книгу «Применение Python», будь мы чуть более прозорливы в далеком 1995 году). Я хочу ясно обозначить: эта книга - не справочник. Это учебник. Здесь вы можете найти некоторые подробности, воспользовавшись оглавлением или алфавитным указателем, но данная книга не предназначалась для использования именно в таких целях. Краткий справочник вы найдете в книге под названием «Python Pocket Reference», который вы найдете весьма полезным, как только начнете самостоятельно писать нетривиальный программный код. Существуют и другие источники справочной информации, в том числе другие книги и собственный набор справочных руководств по языку Python. Цель этой книги - постепенное обучение применению языка Python для решения типичных задач, а не подробное описание мельчайших особенностей.


О четвертом издании

Если это первое издание книги, которое вы видите, последние изменения, вероятно, вас будут интересовать меньше всего, поэтому вы можете просто перейти к следующему разделу. Для тех, кто читал предыдущие издания, можно отметить, что четвертое издание этой книги содержит три важных изменения:

•    Оно охватывает только Python 3.X.

•    Оно было сокращено, чтобы сделать книгу еще направленнее и освободить место для новых тем.

•    В него было добавлено обсуждение новых тем и инструментов, появившихся в мире Python.

Первый из этих пунктов является, пожалуй, наиболее важным - это издание опирается на Python 3.X, на стандартную библиотеку для этой версии и на распространенные приемы программирования, используемые его пользователями. Однако чтобы объяснить, как это и два других изменения отразились на данном издании, я должен поведать о некоторых деталях.


Изменения в этом издании

Предыдущие версии книги получили весьма широкое распространение, поэтому ниже я приведу некоторые из наиболее заметных изменений в этом издании:

Существовавший ранее материал был сжат, чтобы освободить место для новых тем

Предыдущее издание книги также имело объем около 1600 страниц, что не позволило выделить достаточно места для рассмотрения новых тем (одна только ориентированность Python 3.X на использование Юникода предполагает массу нового материала). К счастью, недавние изменения в мире Python позволили нам без особого ущерба выкинуть часть существующего материала и освободить место для новых тем.

Глубина обсуждения оставшихся тем при этом не пострадала - эта книга остается такой же основательной, как и прежде. Тем не менее одной из основных задач, стоявших при подготовке этого издания, было не допустить дальнейшего роста его объема, а множество других изменений и сокращений, о которых я упомяну ниже, были сделаны отчасти для того, чтобы включить новые темы.

Рассматривается только Python 3.X

Примеры и пояснения были изменены с учетом того, что теперь эта книга охватывает только версию Python 3.X. Версия Python 2.X больше не поддерживается, за исключением тех особенностей, которые перекочевали из версии 2.X в версию 3.X без изменений. Несмотря на то, что таких особенностей достаточно много, благодаря чему читатели могут использовать версию 2.X, тем не менее формально книга поддерживает только версию 3.X.

В свою очередь это обстоятельство явилось основным фактором, обеспечившим сохранение объема этого издания на прежнем уровне. Ограничившись поддержкой только версии Python 3.X - несовместимой с версией Python 2.X, которую следует рассматривать как будущее языка Python, - нам удалось избежать дублирования описания особенностей, отличающихся в этих двух версиях Python. Такое ограничение поддерживаемых версий особенно важно для такой книги, как эта, где приводится множество расширенных примеров, так как это позволяет демонстрировать примеры только для одной версии.

Для тех, кто по-прежнему пытается удержаться в обоих мирах,

2.X и 3.X, я подробнее расскажу об изменениях в Python 3.X ниже, в этом же предисловии. Самым важным, пожалуй, изменением в версии 3.X, из тех, что описываются в книге, является усовершенствованная поддержка интернационализации в примерах программ PyEdit и PyMailGUI. Несмотря на то, что в версии 2.X также имеется поддержка Юникода, тем не менее усовершенствованная ее реализация в версии 3.X вынуждает заново пересмотреть реализацию подобных систем, прежде ориентированных на работу с кодировкой ASCII.

Включение недавно появившихся библиотек и инструментов

С момента выхода предыдущего издания появилось или получило дальнейшее развитие множество новых библиотек и инструментов, и они также упоминаются здесь. В их число входят новые инструменты стандартной библиотеки языка Python, такие как модули subprocess (рассматривается в главах 2 и 3) и multiprocessing (рассматривается в главе 5), а также новые веб-фреймворки, созданные сторонними разработчиками, и инструменты ORM (Object-Relational Mapping - объектно-реляционное отображение) для работы с базами данных. Большинство из них рассматриваются не очень подробно (многие популярные расширения сами по себе являются сложными системами и гораздо подробнее описаны в соответствующей литературе), но для них дается по крайней мере краткое описание в виде резюме.

Например, новая библиотека виджетов Tk tkinter.ttk рассматривается в главе 7, но весьма кратко. Как правило, в этом издании мы предпочитали упоминать подобные расширения по ходу дела, вместо того чтобы представлять примеры программного кода без внятного пояснения.

Это предисловие было ужато

Я удалил все инструкции по запуску и использованию примеров программ. Поэтому теперь за инструкциями по использованию обращайтесь к файлам README, входящим в состав дистрибутива с комплектом примеров. Кроме того, я убрал большую часть благодарностей, потому что они повторяют благодарности из книги «Изучаем Python», - так как знакомство с книгой «Изучаем Python» теперь считается необходимой предпосылкой, дублирование одного и того же материала здесь ничем не оправдано. Кроме того, было убрано описание содержимого книги - чтобы ознакомиться со структурой книги, обращайтесь к оглавлению.

Была убрана вводная глава с обзором языка Python

Я удалил «организаторскую» главу, присутствовавшую в предыдущем издании, где описывались сильные стороны языка Python, представлялись наиболее видные пользователи, рассматривались различные философии и так далее. Обращение в свою веру играет важную роль в любой сфере, где вопрос «почему» задается менее часто, чем должен бы. В действительности, если бы мастера Python не занимались его популяризацией, все мы, вероятно, использовали бы сейчас Perl или языки командных оболочек!

Однако присутствие здесь такой главы стало совершенно излишним из-за наличия сходной главы в книге «Изучаем Python». Поскольку книга «Изучаем Python» должна предшествовать этой книге, я решил не расходовать книжное пространство на повторную агитацию «Питонистов» («Pythonista»). В этой книге предполагается, что вы уже знаете, почему стоит использовать Python, поэтому мы сразу же перейдем к его применению.

Было убрано заключительное послесловие

Заключительное послесловие к этой книге было написано еще для первого издания, и теперь ему исполнилось уже 15 лет. Естественно, что оно отражает взгляды на Python, в большей степени характерные для того времени. Например, использование языка Python для разработки гибридных приложений казалось более значимым в 1995 году, чем в 2010. В современном, более обширном мире Python большинству пользователей не приходится иметь дело со связанным программным кодом на языке C.

В предыдущих изданиях я добавлял новое послесловие для каждого издания, чтобы уточнить и дополнить идеи, представленные в заключении к книге. Теперь я убрал эти послесловия, заменив их коротким примечанием. Само заключение я решил оставить, потому что оно по-прежнему актуально для многих читателей и имеет некоторую историческую ценность. Да к тому же удачные шутки...

Было убрано вступительное слово

По похожим причинам, представленным в двух предыдущих пунктах, я убрал разделы со вступительным словом к предыдущим изданиям. Те, кому это будет интересно, историческую справку о вкладе Гвидо ван Россума (Guido van Rossum), создателя Python, в развитие языка смогут найти в Интернете. Если вам интересно, как за эти годы изменился язык Python с технической точки зрения, смотрите документ «What’s New» («Что нового»), входящий в состав стандартного комплекта руководств по языку Python (доступен по адресу http://www.python.org/doc и устанавливается вместе с Python в Windows и на других платформах).

Раздел, посвященный интеграции с языком C, был сокращен до одной

главы

Я сократил раздел, посвященный созданию расширений на языке C и встраиванию сценариев на языке Python в программы на языке C, до одной короткой главы в конце части, посвященной инструментальным средствам, где коротко описываются основные понятия, связанные с этой темой. Проблемы связывания программ на языке Python с библиотеками на языке C на сегодняшний день волнуют лишь незначительную часть пользователей, а те, кому эти навыки действительно необходимы, найдут более полный пример интеграции в исходных текстах самого языка Python. Имеющиеся возможности перечислены здесь достаточно полно, но значительный объем программного кода на языке C был вырезан в расчете на то, что более представительные примеры вы найдете в программном коде реализации самого языка Python.

Часть, посвященная системному программированию, была сокращена

и переработана

Прежние две главы с большими примерами использования Python в системном программировании были объединены в одну, более короткую главу с новыми или значительно переработанными примерами. Фактически эта часть (часть II) подверглась самым значительным изменениям. Она включает описание новых инструментов, таких как модули subprocess и multiprocessing, знакомит читателей с сокетами, а кроме того, из нее были удалены устаревшие сведения и примеры, унаследованные из прежних изданий. Честно признаться, несколько примеров работы с файлами были созданы еще в 1990-х годах и оказались сильно устаревшими. Начальная глава в этой части была разбита на две, чтобы упростить чтение материала (описание контекста командной оболочки, включая потоки ввода-вывода, было вынесено в отдельную главу), а несколько листингов крупных программ (включая запускающие сценарии с автоматической настройкой) теперь вынесены за пределы книги и включены в состав дистрибутива с комплектом примеров.

Некоторые крупные примеры были исключены из книги (но остались

в составе дистрибутива с комплектом примеров)

Точно так же из книги были исключены два крупных примера, демонстрирующих создание графического интерфейса, PyTree и PyForm. Однако их обновленная реализация доступна в комплекте примеров. В этом издании вы все еще сможете найти упоминание и описание множества крупных примеров, включая примеры реализации полноценных клиентов электронной почты с графическим и веб-интерфейсом, а также программы для просмотра изображений, калькуляторы, часы, текстовые редакторы с поддержкой Юникода, простые графические редакторы, сценарии регрессивного тестирования и многие другие. Однако, так как программный код примеров не добавляет ничего важного к раскрытию темы и вообще эти примеры предлагались в основном для самостоятельного изучения, - я перевел их в разряд дополнительной информации и исключил их из текста этого издания книги.

Обширная глава с описанием тем, касающихся Интернета, была заменена кратким обзором

Я полностью убрал обширную главу с описанием тем, касающихся Интернета, оставив лишь краткий обзор в начале части (с акцентом на возможностях создания графического интерфейса, который приводится в начале третьей части «Программирование GUI»). Здесь вы найдете все ранее включавшиеся в рассмотрение инструменты, такие как веб-фреймворк ZOPE, объектная модель COM, технологии Windows Active Scripting и ASP, HTMLgen, Python Server Pages (PSP), Jython и уже серьезно устаревшая система Grail. Некоторые из этих инструментов по-прежнему заслуживают положительных оценок, но в этом издании никакие из них не рассматриваются подробно. В обзор были добавлены новые инструменты (включая многие из тех, что перечислены в следующем абзаце), но, опять же, весьма краткие и без примеров программного кода.

Несмотря на все попытки автора угадать направления развития вебтехнологий в будущем, печатное издание не способно полностью соответствовать эволюции сферы развития Интернета. Например, в настоящее время появились веб-фреймворки, такие как Django, Google App Engine, TurboGears, pylons и web2py, соперничающие по своей популярности с ZOPE. Точно так же фреймворк .NET Framework во многих приложениях вытеснил объектную модель Windows COM. Реализация IronPython теперь способна обеспечить такую же тесную интеграцию с .NET, как и Jython с Java. А механизм Active Scripting в значительной степени может быть замещен клиентскими фреймворками, основанными на JavaScript и использующими технологию AJAX, такими как Flex, Silverlight и pyjamas (которые часто называют средствами разработки полнофункциональных вебприложений). Кроме собственно исторической ценности, примеры, ранее представленные в этой категории, не давали возможности изучить описываемые инструменты или хотя бы судить об их достоинствах.

Вместо того чтобы включать в книгу неполное (и практически бесполезное) описание инструментов, которые в течение предполагаемого срока жизни этого издания могут как уйти далеко вперед в своем развитии, так и исчезнуть со сцены, я решил предоставить лишь краткие обзоры наиболее важных тем, касающихся Интернета, и предлагаю читателям самим постараться найти необходимые подробности в Интернете. По сути, основная цель книги, которую вы читаете, состоит в том, чтобы дать углубленные знания об основах Интернета, которые позволят вам воспользоваться более сложными системами, когда вы будете готовы сделать шаг вперед.

Единственное исключение: описание XML, присутствовавшее ранее в этой главе, было дополнено и перемещено в главу, посвященную обработке текста (где оно и должно было бы находиться). Точно так же было сохранено описание объектно-ориентированной базы данных ZODB, поддерживаемой фреймворком ZOPE, хотя и в сильно урезанном виде, чтобы получить возможность добавить описание механизмов ORM, таких как SQLObject и SQLAlchemy (также в краткой форме).

Задействованы, современные инструменты, доступные в версии 3.X

К моменту написания этих строк Python 3.X все еще находился на этапе внедрения, и некоторые из инструментов сторонних разработчиков, которые использовались в примерах в предыдущих изданиях этой книги, по-прежнему доступны только в версиях для Python 2.X. Чтобы обойти этот временный недостаток, я изменил некоторые примеры, задействовав в них альтернативные инструменты, обеспечивающие поддержку версии Python 3.X.

Наиболее заметным в этом смысле является раздел, посвященный базам данных SQL, - теперь в нем вместо интерфейса доступа к серверу MySQL, присутствующего в Python 2.X, используется библиотека SQLite поддержки баз данных, встраиваемых в приложения, которая в версии 3.X стала стандартной частью Python. К счастью, переносимый прикладной интерфейс Python позволяет сценариям взаимодействовать с обоими механизмами практически одинаково, поэтому такое изменение является весьма незначительной жертвой.

Отдельно следует отметить использование расширения PIL для отображения изображений в формате JPEG в части, посвященной созданию графического интерфейса. Это расширение было адаптировано Фредриком Лундом (Fredrik Lundh) для версии Python 3.1 как раз к моменту подготовки этого издания. Когда я сдавал в издательство окончательный вариант рукописи этой книги в июле 2010 года, эта версия расширения официально еще не была выпущена, но она должна была вскоре выйти; поэтому в качестве временной меры заплаты для этой библиотеки, обеспечивающие поддержку Python 3.X, были включены в комплект примеров.

Было исключено описание дополнительных возможностей языка

Все дополнительные особенности языка Python, такие как дескрипторы, свойства, декораторы, метаклассы и поддержка Юникода, являются частью языка Python. Поэтому их описание было перемещено в четвертое издание книги «Изучаем Python». Например, улучшенная поддержка Юникода и ее влияние на приемы работы с файлами, именами файлов, сокетами и со многими другими инструментами обсуждаются в этой книге, но основы Юникода здесь не рассматриваются. Некоторые темы из этой категории определенно имеют прикладной характер (или, по крайней мере, представляют интерес для разработчиков инструментальных средств и архитекторов прикладных интерфейсов), но наличие их описания в книге «Изучаем Python» позволило избежать дальнейшего увеличения объема этой книги. Ищите подробное обсуждение этих тем в книге «Изучаем Python».

Прочие незначительные изменения

Естественно, что попутно было внесено множество мелких изменений. Например, для размещения элементов форм теперь вместо метода pack используется метод grid из библиотеки tkinter, потому что он обеспечивает более непротиворечивый способ размещения элементов в платформах, где размер шрифта в подписях не соответствует высоте полей ввода (включая ОС Windows 7 netbook, установленную на ноутбуке, использовавшемся для работы над этим изда-

нием). Кроме того, по всей книге были добавлены новые сведения, включая новое описание механизма переадресации потоков ввода-вывода в сокеты, в части с описанием приемов работы в Интернете; новый многопоточный диалог поиска по регулярным выражениям с поддержкой Юникода и изменения в тестах для примера PyEdit; множество других изменений, которые, вероятно, вам будет лучше раскрывать по ходу дела, чем читать о них в предисловии.

Наконец, некоторые блоки комментариев, начинающиеся с символа «#» и расположенные в начале файлов с исходными текстами, я заменил строками документирования (и, для единообразия, даже в сценариях, которые не предназначены для импортирования, хотя отдельные строки «#» остались в крупных примерах, где они отделяют текст). Я также заменил несколько устаревших операторов «while 1» на «while True»; чаще стал использовать оператор +=; внес другие изменения, исправив некоторые устаревшие шаблоны программирования. Старые привычки бывает сложно искоренять, но подобные изменения делают примеры не только более функциональными, но и более полно отражающими современные приемы программирования.

Несмотря на добавление новых тем, в общей сложности было удалено четыре главы (нетехническое введение, одна из глав с примерами по системному программированию, глава с расширенными темами, касающимися Интернета, и одна объединительная глава). Были урезаны несколько дополнительных примеров и сопутствующий им материал (включая PyForm и PyTree), а также, специально для экономии пространства, книга ограничивается представлением только версии Python 3.X и описанием лишь самых основ разработки приложений.


Что же осталось?

В результате изменений, обозначенных выше, это издание получилось более кратким и более четко отражающим основную его роль - учебное руководство по применению языка Python для решения типичных задач программирования. Однако, учитывая объем книги, можно смело утверждать, что это по-прежнему обстоятельная и всесторонняя книга, предназначенная быть первой ступенью на пути овладения мастерством разработки приложений на языке Python.

Вопреки последним настроениям (и рискуя получить клеймо еретика) я совершенно уверен, что книги, подобные этой, должны подтягивать своих читателей вверх, а не опускаться до их уровня. Снижение интеллектуальной планки вредит не только читателям, но и той области знаний, в которой они предполагают действовать. В этой книге вы не найдете множество забавных рисунков, как в некоторых, и она не будет поражать вас развлекательным повествованием вместо технической глубины. Цель моих книг состоит в том, чтобы передать сложные поня-

тия убедительным и надежным способом и снабдить вас инструментами, которые потребуются вам в разработке программного обеспечения.

Разумеется, существует множество типов обучающихся, и ни одна книга не сможет работать на любую аудиторию. Фактически именно по этой причине изначальная версия этой книги позднее была разделена на две, и описание основ языка было делегировано отдельной книге «Изучаем Python». Кроме того, и программистов можно поделить на две категории - тех, кто желает получить глубокие знания в области разработки программного обеспечения, и скриптеров, не испытывающих такой потребности. Некоторым вполне достаточно иметь элементарные знания, позволяющие дорабатывать системы или библиотеки и решать текущие проблемы. Но это пока они не начнут вторгаться в область разработки полномасштабных приложений - порог, за которым в худшем случае может наступить разочарование, а в лучшем - лучшее понимание сложной природы этой области.

Неважно, к какому лагерю вы принадлежите, важно понять основное назначение этой книги. Если вы ищете кратчайший путь к мастерству, вам едва ли понравится эта книга (как и разработка программного обеспечения в целом). Однако если вы стремитесь научиться хорошо программировать на языке Python и одновременно получить удовольствие от процесса изучения, эта книга наверняка станет для вас важной вехой на пути обретения опыта.

В конечном счете, овладеть навыками программирования далеко не так просто, как пытаются представить некоторые. Однако если вы приложите необходимые усилия, то обнаружите, что это стоит затраченного времени. Это особенно верно для тех, кто вооружает себя удобным инструментом программирования, таким как язык Python. Ни одна книга и ни одни курсы не превратят вас в «Повелителя Вселенной» Python, тем не менее цель этой книги состоит в том, чтобы помочь вам добиться этого, сократив начальный этап освоения и обеспечив надежный фундамент в наиболее типичных областях применения Python.


Влияние Python 3.X на эту книгу

Как уже упоминалось выше, это издание теперь охватывает только версию Python 3.X. Версия языка Python 3.X несовместима с версией 2.X. В своей основе язык версии 3.X очень напоминает Python 2.X, но имеет множество существенных отличий, кроющихся как в самом языке, так и в стандартной библиотеке. Читатели, не имеющие опыта работы с версией 2.X, могут пропустить описание этих отличий, но появившиеся изменения очень сильно повлияли на содержание этого издания. Для широкого круга пользователей Python 2.X в этом разделе описываются наиболее значимые изменения, относящиеся к этой категории.

Если вам интересно поближе познакомиться с отличиями от версии 2.X, я предлагаю дополнительно найти четвертое издание книги «Python Pocket Reference», упомянутое выше. Там приводятся описания основных структур языка обеих версий, 2.X и 3.X, встроенных функций и исключений, а также перечисляется большинство модулей из стандартной библиотеки и инструментов, используемых в этой книге. Хотя четвертое издание книги «Изучаем Python» не является справочником по различиям между версиями, оно охватывает обе версии,

2.X и 3.X, и, как уже отмечалось, знакомство с ней является обязательным условием для усвоения материала этой книги. Цель этого издания книги «Программирование на Python», ориентированного только на версию 3.X, заключается вовсе не в том, чтобы оставить за бортом огромное количество пользователей версии 2.X, а в том, чтобы помочь читателям перейти на новую версию и избежать увеличения размеров и без того массивной книги.


Изменения, связанные с включением поддержки версии 3.X

К счастью, многие отличия между версиями 2.X и 3.X, обусловившие необходимость внесения изменений в эту книгу, достаточно тривиальны. Например, библиотека tkinter, широко используемая в этой книге для построения графических интерфейсов, присутствует в версии 3.X под именем tkinter и имеет структуру пакета - ее прежняя инкарнация в версии 2.X, в виде модуля Tkinter, в этой книге не описывается. Это отличие приводит, в основном, к необходимости использовать отличные инструкции импортирования, но здесь приводятся только инструкции для версии Python 3. Аналогично с целью соблюдения соглашений об именовании модулей, принятых в версии 3.X, модули для версии 2.X anydbm, Queue, thread, StringIO.StringIO и urllib.open превратились в Python 3.X и в этом издании в модули dbm, queue, _thread, io. StringIO и urllib. request.urlopen соответственно. Точно так же были переименованы и другие инструменты.

С другой стороны, переход к версии 3.X предполагает более широкие идиоматические изменения, которые, конечно же, являются более радикальными. Например, усовершенствованная поддержка Юникода в Python 3.X подтолкнула к созданию для этого издания примеров полностью интернационализированных версий текстового редактора PyEdit и клиента электронной почты PyMailGUI (подробнее об этом чуть ниже). Кроме того, замена модуля os.popen2 модулем subprocess потребовала включения новых примеров; отказ от модуля os.path.walk в пользу модуля os.walk позволил сократить некоторые примеры; новое разделение на файлы и строки Юникода и двоичные файлы и строки потребовало изменения группы дополнительных примеров и описания; кроме того, появились новые модули, такие как multiprocessing, предлагающие новые возможности, которые необходимо было описать в этом издании.

Помимо изменений в библиотеке, в примерах этого издания также отражены изменения в языке Python 3. Например, здесь учтены все изменения, коснувшиеся функций из версии 2.X print, raw_input, keys, has_key, map и apply. Кроме того, новая модель импортирования относительно пакетов, появившаяся в версии 3.X, нашла отражение в некоторых примерах, таких как mailtools и анализаторы выражений, а отличия в поведении операторов деления вынудили внести небольшие изменения в примеры создания графического интерфейса, такие как PyClock, PyDraw и PyPhoto.

Замечу также, что я не стал заменять все выражения форматирования строк на основе оператора % новым методом str.format, потому что оба способа форматирования поддерживаются в Python 3.1, и похоже, оба они будут поддерживаться еще очень долго, если не всегда. Фактически если воспользоваться поиском с помощью регулярных выражений, который мы реализуем в примере текстового редактора PyEdit в главе 11, можно обнаружить, что этот оператор встречается более 3000 раз в программном коде библиотеки для Python 3.1. Я не могу с абсолютной точностью предсказать, как будет развиваться Python в будущем, поэтому обращайтесь к первой главе, где подробнее рассказывается об этом, если когда-нибудь вам потребуется внести изменения.

Кроме того, из-за того, что это издание охватывает только версию 3.X, оказалось невозможным использовать некоторые сторонние пакеты, существующие только в версии для Python 2.X, о чем уже говорилось выше. В их число входят интерфейс к MySQL, ZODB, PyCrypto и другие. Как уже упоминалось выше, для работы под управлением Python 3.1 была адаптирована библиотека PIL с целью использования в этой книге, но для этого потребовалось наложить специальные исправления, а официальная версия, поддерживающая Python 3.X, до сих пор не вышла. Многие из недостающих модулей для версии 3.X могут появиться к моменту, когда вы будете читать эти строки, либо в виде адаптированных версий для Python 2.X, либо в виде совершенно новых версий, специально для Python 3.X.


Особенности языка и библиотека: Юникод

Поскольку эта книга посвящена изучению принципов разработки приложений, а не основ языка программирования, изменения в языке не обязательно должны отслеживаться здесь. В действительности, оглядываясь на книгу «Изучаем Python», можно сказать, что изменения в языке, связанные с переходом на версию 3.X скорее касаются ее, а не данной книги. В большинстве случаев изменения в примерах к этой книге были обусловлены необходимостью сделать их более понятными или более функциональными, а не включением поддержки версии 3.X.

С другой стороны, переход на версию Python 3.X оказывает влияние на значительную часть программного кода, и иногда это влияние может оказаться весьма тонким. Тем не менее читатели с опытом использования Python 2.X обнаружат, что если отличия в версии 3.X языка чаще всего легко преодолимы, то отличия в стандартной библиотеке для версии 3.X преодолеть иногда оказывается гораздо сложнее.

Но к наиболее широким последствиям привело главное изменение в Python 3.X - улучшенная поддержка строк Юникода. Будем честными: поддержка Юникода в версии 3.X иногда может существенно осложнять жизнь тех, кто всю жизнь сталкивался только с кодировкой ASCII! Как мы увидим далее в этой книге, эта поддержка оказывает существенное влияние при работе с содержимым файлов, с именами файлов, с дескрипторами каналов, с сокетами, при выводе текста в графическом интерфейсе, при работе с такими протоколами Интернета, как FTP и email, при разработке сценариев CGI и даже при использовании некоторых инструментов хранения данных. Плохо это или хорошо, но как только мы войдем в мир разработки приложений, описываемый в этой книге, Юникод перестанет быть необязательной темой для многих, если не для большинства программистов на языке Python 3.X.

Конечно, если уж на то пошло, никому и никогда не следовало бы рассматривать использование Юникода, как дополнительную и необязательную возможность. Далее мы увидим, что некоторые приемы, которые, казалось бы, работают в Python 2.X, на самом деле нельзя признать удовлетворительными - работа с текстом как с простым набором байтов может порождать различные проблемы, такие как ошибки при сравнении строк в различных кодировках (утилита grep, реализованная в составе текстового редактора PyEdit в главе 11, является ярким примером программного кода, который должен терпеть неудачу при отказе от использования Юникода). Python 3.X обнажает подобные проблемы, делая их более заметными для программиста.

Однако перенос нетривиального программного кода на версию 3.X не является неразрешимой задачей. Кроме того, многие читатели этого издания имеют шикарную возможность начать использовать язык Python сразу же с версии 3.X и не иметь дела с существующим программным кодом для версии 2.X. Если вы принадлежите к их числу, вы увидите, что Python 3.X является надежным языком широкого применения для разработки сценариев и приложений, стремящимся устранить многие проблемы, которые когда-то скрывались в версии 2.X.


Ограничения Python 3.1: электронная почта, CGI

Здесь необходимо сделать одно важное замечание, касающееся основных примеров в книге. Чтобы их реализация представляла интерес для как можно более широкой аудитории, основные примеры в этой книге имеют отношение к электронной почте и обеспечивают поддержку интернационализации и Юникода. К этой категории относятся примеры PyMailGUI (в главе 14) и PyMailCGI (в главе 16), а также все предшествующие примеры, используемые этими приложениями, в число которых входит текстовый редактор PyEdit, поддерживающий Юникод при работе с файлами, при отображении и поиске текста.

В этом есть свои плюсы и минусы. Плюс в том, что в конечном итоге мы получим полнофункциональный и интернационализированный клиент электронной почты PyMailGUI, использующий существующий ныне пакет email. Это приложение будет поддерживать любые кодировки для содержимого и заголовков электронных писем и обеспечит возможность не только их просмотра, но и составления. Минус заключается в том, что при этом придется приложить определенные усилия, чтобы обойти сложности, связанные с особенностями реализации пакета email в Python 3.1.

К сожалению, как будет показано в главе 13, в пакете email в Python 3.1 имеется ряд проблем, связанных с обработкой типов str/bytes в Python 3.X. Например, отсутствует простой способ определить кодировку для преобразования текста письма, возвращаемого модулем poplib в виде объекта типа bytes, в тип str, который ожидает получить парсер email. Кроме того, в настоящее время пакет email не в состоянии обрабатывать некоторые виды сообщений, а поддержка некоторых типов сообщений реализована неправильно или слишком специфично.

Эта ситуация носит, скорее всего, временный характер. Исправление некоторых из проблем, с которыми нам придется столкнуться в этой книге, уже запланировано разработчиками. (Фактически из-за одного из таких исправлений, выполненных в версии 3.2, в последний момент потребовалось внести изменения в один из примеров в главе 13.) Кроме того, ведется разработка новой версии пакета email, в которой будут учтены все особенности поддержки Юникода и типа bytes в версии 3.X, но новая версия пакета будет выпущена значительно позже выхода этой книги, и она может оказаться несовместимой с API текущей версии пакета, как и сам Python 3.X. По этой причине в книге приводятся не только обходные решения, но и попутно делаются некоторые предположения. Настоятельно рекомендую регулярно посещать веб-сайт книги (описывается ниже), где будут приводиться сведения об изменениях в будущих версиях Python. Один из плюсов этой ситуации состоит в том, что детальное описание проблемы отражает существующие реалии разработки приложений - основной темы этой книги.

Проблемы, наблюдающиеся в пакете email, также в значительной степени были унаследованы реализацией модуля cgi в версии 3.1. Сценарии CGI - это одна из простейших технологий, на смену которой приходят веб-фреймворки, тем не менее они по-прежнему могут служить целям обучения основам Веб и все еще составляют основу множества крупных наборов инструментальных средств. Скорее всего, эти недостатки версии 3.1 также будут исправлены в будущем, но нам придется приложить определенные усилия, чтобы реализовать в сценариях CGI выгрузку текстовых файлов в главах 15 и 16 и вложений в сообщениях электронной почты в приложении PyMailCGI. Казалось бы, спустя два года после выхода версии Python 3.0 такое положение дел как минимум недопустимо, но такова жизнь в динамическом мире разработки программного обеспечения и в мире книг, которые стремятся вести за собой, а не плестись в хвосте.


Использование примеров из книги

Поскольку примеры составляют основную часть содержимого этой книги, я должен сказать несколько слов о них.


Где искать примеры и обновления

Как и прежде, примеры, обновления, исправления и дополнения к этой книге можно найти на веб-сайте автора, по адресу:

http://www.rmi.net/~lutz/about-pp4e.html

На этой странице веб-сайта моей книги будут даваться ссылки на дополнительную информацию, имеющую отношение к данной версии книги. Однако я не являюсь владельцем этого доменного имени, поэтому если указанная ссылка перестанет работать на протяжении срока жизни этого издания, попробуйте обратиться на другой сайт, по адресу:

http://learning-python.com/books/about-pp4e.html (альтернативная страница)

Если перестанут действовать обе ссылки, попробуйте выполнить поиск в Интернете (не сомневаюсь, что большинство читателей предпримут именно этот шаг).

На веб-сайте книги (а также на сайте издательства O’Reilly, о котором говорится в следующем разделе), где бы он ни находился, вы сможете загрузить дистрибутив с комплектом примеров - файл архива, содержащий все примеры, которые приводятся в книге, а также несколько дополнительных примеров, которые упоминаются, но отсутствуют в самой книге. Чтобы иметь возможность опробовать примеры и избавить себя от необходимости вводить их вручную, загрузите архив, распакуйте его и ознакомьтесь с содержимым файла README.txt, где приводятся инструкции по использованию. Порядок именования файлов примеров и структуру дерева каталогов пакета я опишу ниже, когда мы перейдем к опробованию первого сценария в первой главе.

Как и в случае с первыми тремя изданиями, я буду поддерживать на этом веб-сайте неофициальный «блог», где будут описываться изменения в языке Python, а также будут даваться пояснения и обновления в книге, который вы можете рассматривать, как дополнительное приложение.

Кроме того, на веб-сайте издательства O’Reilly, о котором говорится ниже, имеется система регистрации сообщений об опечатках, где вы сможете сообщить об обнаруженных ошибках. Аналогичная страница присутствует на моем сайте книги. Я стараюсь обновлять веб-сайты моих книг как можно чаще, тем не менее может так получиться, что страница на сайте издательства O’Reilly со списком опечаток будет содержать более свежую информацию. В любом случае в качестве официального источника информации об ошибках и исправлениях следует рассматривать объединение этих двух списков.


Переносимость примеров

Примеры для этой книги разрабатывались, тестировались и запускались под управлением ОС Windows 7 и Python 3.1. Непосредственно перед передачей книги в печать все основные примеры успешно прошли тестирование под управлением грядущей версии Python 3.2 (третья альфа-версия), то есть все, о чем рассказывается в этой книге, в равной степени относится и к Python 3.2. Кроме того, программный код на языке C из главы 20 и ряд примеров параллельного программирования были опробованы в Windows под управлением оболочки Cygwin, имитирующей окружение Unix.

Несмотря на то, что Python и стандартная библиотека, вообще говоря, нейтральны в отношении используемой платформы, тем не менее в некоторые примеры необходимо будет внести незначительные изменения, чтобы их можно было опробовать на других платформах, таких как Mac OS X, Linux и в других разновидностях ОС Unix. Примеры с графическим интерфейсом на основе пакета tkinter, а также некоторые примеры из раздела, посвященного системному программированию, могут быть особенно чувствительны к различиям между платформами. Часть проблем, связанных с переносимостью, будут отмечаться в ходе обсуждения примеров, но некоторые проблемы могут не упоминаться явно.

Так как у меня не было ни времени, ни возможности протестировать примеры на всех возможных платформах, какие только могут иметься в распоряжении читателей, адаптацию этих примеров для конкретной платформы предлагается рассматривать, как упражнения для самостоятельного решения. Если вы обнаружите какую-либо зависимость от типа платформы и пожелаете поделиться своими исправлениями, обращайтесь на мой сайт книги, указанный выше, - я буду рад донести до других читателей ваши исправления для любых платформ.


Сценарии запуска демонстрационных программ

В состав пакета с примерами, который был описан выше, также входят сценарии PyDemos и PyGadgets запуска демонстрационных программ. Они позволяют быстро ознакомиться с некоторыми основными примерами с графическим и веб-интерфейсом. Запускающие их сценарии, находящиеся на верхнем уровне дерева каталогов пакета с примерами, предназначены для настройки пути поиска модулей в запускаемых ими программах и могут использоваться непосредственно на совместимых платформах, включая Windows. Более подробную информацию об этих сценариях вы найдете в файлах README, а также в кратких обзорах, которые приводятся в конце главы 6 и 10.


Политика повторного использования программного кода

Мы прерываем предисловие, чтобы вставить несколько слов от имени юридического отдела. Данная книга призвана оказать вам помощь в решении ваших задач. Вообще вы можете свободно использовать примеры программного кода из этой книги в своих приложениях и в документации. Вам не нужно обращаться в издательство за разрешением, если вы не собираетесь воспроизводить существенные части программного кода. Например, если вы разрабатываете программу и используете в ней несколько отрывков программного кода из книги, вам не нужно обращаться за разрешением. Однако в случае продажи или распространения компакт-дисков с примерами из этой книги вам необходимо получить разрешение от издательства O’Reilly. Для цитирования данной книги или примеров из нее, при ответе на вопросы не требуется получение разрешения. При включении существенных объемов программного кода примеров из этой книги в вашу документацию вам необходимо будет получить разрешение издательства.

Мы приветствуем, но не требуем добавлять ссылку на первоисточник при цитировании. Под ссылкой на первоисточник мы подразумеваем указание названия книги, авторов, издательства и ISBN. Например: «Programming Python, Fourth Edition, by Mark Lutz (O’Reilly). Copyright 2011 Mark Lutz, 978-0-596-15810-1».


Как связаться с издательством O'Reilly

В предыдущем разделе я описал свои собственные сайты, где можно найти примеры и обновления. В дополнение к тем сайтам вы можете обратиться на сайт издательства с вопросами и предложениями, касающимися этой книги:

O’Reilly Media

1005 Gravenstein Highway North

Sebastopol, CA 95472

800-998-9938 (в Соединенных Штатах Америки или в Канаде)

707-829-0515 (международный)

707-829-0104 (факс)

Как уже говорилось выше, на сайте издательства O’Reilly поддерживается веб-страница для этой книги, где можно найти список опечаток, файлы с примерами и другую дополнительную информацию:

http://www.oreilly.com/catalog/9780596158101

Свои пожелания и вопросы технического характера отправляйте по адресу:

bookquestions@oreilly.com

Дополнительную информацию о книгах, обсуждения, программное обеспечение, Центр ресурсов издательства O’Reilly вы найдете на сайте:

http://www.oreilly.com


Типографские соглашения

В этой книге приняты следующие соглашения:

Курсив

Курсив применяется для выделения имен файлов и каталогов, новых терминов и некоторых комментариев в примерах программного кода.

Моноширинный шрифт

Применяется для представления листингов программ, а также для выделения в обычном тексте программных элементов, таких как имена модулей, методов, параметров, классов, функций, инструкций, программ, объектов и тегов HTML.

Моноширинный жирный

Используется для выделения команд или текста, который должен быть введен пользователем.

Моноширинный курсив

Обозначает элементы в программном коде, которые должны быть замещены, исходя из конкретной ситуации.

Так выделяются примечания, имеющие отношение к текущему обсуждению.

Так выделяются предупреждения или предостережения, имеющие отношение к текущему обсуждению.

Благодарности

Я благодарен всем, кто перечислен в предисловии к четвертому изданию книги «Изучаем Python», вышедшему меньше года тому назад. Так как знакомство с книгой «Изучаем Python» является обязательным условием для чтения этой книги, а также потому что люди, помогавшие мне в создании обеих книг, - одни и те же, я не стал повторять весь список здесь. Но, как бы то ни было, я хотел бы выразить благодарность:

•    Издательству O’Reilly за продвижение Python и публикацию серьезных и содержательных книг, имеющих отношение к программному обеспечению с открытыми исходными текстами

•    Сообществу Python, составляющему большую часть моего мира начиная с 1992 года

•    Тысячам студентов, прошедших через 250 курсов обучения языку Python, которые я провел начиная с 1997 года

•    Сотням тысяч читателей, прочитавшим 12 изданий всех трех моих книг о Python, которые вышли с 1995 года

•    Монти Пайтону (Monty Python), тезке Python, за множество интересных ситуаций, в которых есть чему поучиться (подробнее об этом - в следующей главе)

Книгу пишет обычно один человек, но многие идеи рождаются в сообществе. Я благодарен за отклики, которые мне повезло получить в течение последних 18 лет от моих студентов и читателей. Студенты - лучшие учителя учителей.

С личной стороны я хотел бы сказать спасибо моим братьям и сестре за старые добрые времена, а также моим детям, Майклу (Michael), Саманте (Samantha) и Роксане (Roxanne), за возможность гордиться ими.

А особенная благодарность моей супруге Вере (Vera), которой так или иначе удалось внести немало хорошего в этот в каком-то смысле неизменяемый объект.

Марк Лутц (Mark Lutz), июль 2010

Так что же такое Python?

Как уже говорилось выше, в этой книге не будет уделяться большое внимание основам Python, и мы отложим абстрактные рассуждения о роли Python до заключительной главы, то есть до того момента, когда вы познакомитесь с этим языком программирования на практике. Тем не менее если вы хотите получить исчерпывающее определение темы этой книги, извольте:

Python - это язык программирования общего назначения, распространяемый с открытыми исходными текстами (open source). Он оптимизирован для создания качественного программного обеспечения, высокой производительности труда разработчиков, переносимости программ и интеграции компонентов. Язык Python используется сотнями тысяч разработчиков по всему миру в таких областях, как создание веб-сценариев, системное программирование, создание пользовательских интерфейсов, настройка программных продуктов под пользователя, численное программирование и в других. Как считают многие, один из самых используемых языков программирования в мире.

Как популярный язык, обеспечивающий сокращение времени, затрачиваемого на разработку программ, Python используется для создания широкого круга программ в самых разных областях. В число пользователей Python в настоящее время входят Google, YouTube, Industrial Light & Magic, ESRI, системы BitTorrent обмена файлами, Jet Propulsion Lab в NASA, игра Eve Online и National Weather Service (национальная метеорологическая служба, США). Язык Python используется в самых разных областях, от администрирования систем, разработки веб-сайтов, создания сценариев для мобильных устройств и обучения, до тестирования аппаратуры, анализа капиталовложений, компьютерных игр и управления космическими кораблями.

Среди прочих достоинств, Python отличается удивительной простотой, удобочитаемостью и простым синтаксисом; он легко интегрируется с внешними компонентами, написанными на других языках программирования; имеет мультипарадигменную архитектуру и поддерживает объектно-ориентированное, функциональное и модульное программирование; обладает обширной коллекцией уже запрограммированных интерфейсов и утилит. Набор встроенных инструментальных средств делает его необычайно гибким и динамичным языком программирования, идеально подходящим не только для быстрого решения тактических задач, но и для разработки перспективных стратегических решений. Несмотря на свое общее назначение, Python часто называют языком сценариев, так как он позволяет легко и просто использовать другие программные компоненты и управлять ими.


Самым большим достоинством Python является, пожалуй, то, что с его помощью разработка программного обеспечения становится более быстрой и приятной. Есть такая категория людей, для которых программирование является самоцелью. Они наслаждаются самим процессом. Пишут программы исключительно для собственного удовольствия, а коммерческие или карьерные выгоды рассматривают лишь, как вторичное следствие. Именно такие люди в значительной степени причастны к появлению Интернета, движения за распространение программного обеспечения с открытыми исходными текстами (open source) и Python. Эти же люди исторически являются основными читателями этой книги. От них часто можно услышать, что с таким инструментом, как Python, программирование превращается в настоящее развлечение.

Чтобы понять, как это происходит, читайте дальше. Как побочный эффект, значительная часть этой книги представляет собой демонстрацию воплощения идеалов Python в действующий программный код. Как мы увидим далее, особенно когда будем знакомиться с инструментальными средствами, используемыми для создания графического интерфейса пользователя, веб-сайтов, в системном программировании и так далее, Python способствует продвижению новых технологий.


Об авторе

Марк Лутц (Mark Lutz) является ведущим специалистом в области обучения языку программирования Python, автором самых ранних и наиболее популярных публикаций и известен в сообществе пользователей Python своими новаторскими идеями.

Марк является автором книг «Изучаем Python», «Программирование на Python» и «Python Pocket Reference», выпущенных издательством O’Reilly, каждая из которых претерпела уже четыре издания. Он использует Python и занимается его популяризацией начиная с 1992 года; книги о Python начал писать в 1995 году; преподаванием этого языка программирования стал заниматься с 1997 года. На начало 2010 года Марк провел 250 курсов, обучил более 3500 студентов, написал книги по языку Python, суммарный тираж которых составил примерно четверть миллиона копий и которые были переведены более чем на десять языков.

Обладает степенями бакалавра и магистра в области информатики, закончи* университет штата Висконсин (США). На протяжении последних 25 лет занимался разработкой компиляторов, инструментальных средств программиста, приложений и разнообразных систем в архитектуре клиент/сервер. Связаться с Марком можно через веб-сайт книги http://rmi.net/~lutz и веб-сайт курсов, которые он ведет: http://learning-python.com.

I

Начало

Эта часть книги запускает повествование, предлагая краткий экскурс, в котором рассматриваются фундаментальные понятия языка Python и представляются некоторые из наиболее типичных приемов его использования.

Глава 1

Эта глава начинает рассказ с простого примера - записи информации о людях, - что позволит коротко представить некоторые из основных областей применения языка Python, которые мы будем изучать в этой книге. Мы заставим этот пример существовать в самых разных ситуациях. По пути мы встретимся с базами данных, графическими интерфейсами, веб-сайтами и так далее. Эта своего рода демонстрационная глава задумывалась с целью возбудить в вас интерес. Здесь мы не будем исследовать все аспекты, но у нас будет возможность увидеть Python в действии, прежде чем мы погрузимся в детали. Данная глава служит также обзором некоторых базовых идей языка, с которыми вы должны быть знакомы, прежде чем приступать к чтению этой книги, - такими как представление данных и объектно-ориентированное программирование (ООП).

Назначение этой части книги не в том, чтобы дать вам всесторонний обзор языка Python, а в том, чтобы познакомить вас с примером его применения и предоставить краткий обзор определенного круга задач, решаемых с помощью языка Python.

1

Предварительный обзор


«Программирование на Python»: краткий очерк

Если вы берете в руки книгу такого размера, как эта, то вам, как и большинству людей, перед тем как засучить рукава, наверняка захочется немного узнать о том, что вы собираетесь изучать. Именно об этом рассказывает данная глава - в ней приводятся несколько примеров, которые позволят вам оценить возможности языка Python, прежде чем вы перейдете к изучению подробностей. Здесь вы найдете лишь краткие пояснения, поэтому если у вас появится желание получить подробное описание инструментов и приемов, использованных в этой главе, вам придется прочитать последующие части книги. Цель этой главы состоит в том, чтобы возбудить у вас аппетит кратким обзором основ языка Python и ознакомлением с некоторыми темами, рассматриваемыми далее.

Для этого я возьму довольно простое приложение, конструирующее базу данных, и проведу вас через различные этапы его создания: моделирование в интерактивном режиме, использование инструментов командной строки, создание интерфейса командной строки, создание графического интерфейса и создание простого веб-интерфейса. Попутно мы познакомимся с такими понятиями, как представление данных, сохранение объектов и объектно-ориентированное программирование (ООП); исследуем несколько альтернативных решений, к которым вернемся позднее; и рассмотрим некоторые основные идеи языка Python, которые вы должны знать, прежде чем продолжать чтение этой книги. В конечном итоге мы получим базу данных, хранящую экземпляры класса, которые можно будет просматривать и изменять с использованием различных интерфейсов.

Конечно, далее я затрону и дополнительные темы, но приемы, которые будут представлены здесь, применимы к некоторым прикладным областям, которые мы будем исследовать далее. Замечу также, что если что-то в программах из этой главы для вас останется непонятным, не волнуйтесь, так и должно быть - пока, по крайней мере. Здесь просто демонстрируются возможности Python. С недостающими подробностями вы познакомитесь достаточно скоро. А теперь начнем с небольшого развлечения.

Читатели четвертого издания книги «Изучаем Python» могут заметить в примере из этой главы знакомые черты - здесь участвуют те же персонажи, что и в главе про ООП в книге «Изучаем Python», а последние версии примера, основанные на использовании классов, по сути являются вариациями на ту же тему. Не боясь обвинений в избыточности, я здесь возвращаюсь к этому примеру по трем причинам: он вполне может использоваться для обзора основных возможностей языка; некоторые читатели этой книги не читали «Изучаем Python»; здесь этот пример получает дальнейшее развитие за счет добавления графического и веб-интерфейсов. Таким образом, эта глава начинается с того места, где закончилась книга «Изучаем Python», и помещает этот пример использования основных возможностей языка в контекст действующего приложения, что в общих чертах соответствует цели этой книги.


Постановка задачи

Представьте, что по некоторым причинам вам необходимо хранить информацию о людях. Это может быть адресная книга на вашем компьютере или, возможно, информация о служащих вашей фирмы. По каким-либо причинам вам требуется написать программу, которая сохраняла бы информацию об этих людях. Другими словами, вам требуется организовать сохранение записей в базе данных, чтобы обеспечить возможность долговременного хранения в компьютере списка людей с информацией о них.

Естественно, существует множество уже готовых программ для работы с базами данных. Однако, написав свою программу, вы получите полный контроль над тем, как она действует. Вы сможете добавлять в нее обработку специальных случаев, которые, возможно, не предусматриваются стандартным программным обеспечением. Вам не придется устанавливать базу данных и учиться пользоваться ею. И вам не придется ждать, пока поставщик программного обеспечения исправит ошибки или добавит новые особенности. Итак, вы решили написать на языке Python программу, управляющую информацией о людях.


Шаг 1: представление записей

Коль скоро мы собрались сохранять записи в базе данных, на самом первом этапе нам необходимо решить, как будут выглядеть эти записи. В языке Python имеется масса способов представления информации о людях. Зачастую для этих целей вполне достаточно бывает использовать объекты встроенных типов, такие как списки и словари, особенно если изначально не требуется предусматривать обработку сохраняемых данных.


Списки

Списки, например, позволяют сохранять информацию о людях упорядоченным способом. Запустите интерпретатор Python в интерактивном режиме и введите следующие две инструкции:

>>> bob = [‘Bob Smith', 42, 30000, 'software']

>>> sue = [‘Sue Jones', 45, 40000, ‘hardware']

Мы только что создали две простые записи, представляющие информацию о Бобе (Bob) и Сью (Sue) (мои извинения, если вас действительно зовут Боб или Сью3). Каждая запись является списком с четырьмя элементами: имя, возраст, оклад и должность. Чтобы получить доступ к этим элементам, достаточно просто использовать операцию индексирования. Результат в примере ниже заключен в круглые скобки потому, что он является кортежем из двух результатов:

>>> bob[0], sue[2]    # получить имя и оклад

(‘Bob Smith’, 40000)

В таком представлении записи легко обрабатывать - достаточно просто использовать операции над списками. Например, можно получить фамилию человека, разбив поле с именем по пробельному символу и отобрав последнюю часть, точно так же можно повысить оклад, изменив соответствующий список:

>>> bob[0].split()[-1]    # получить фамилию Боба

‘Smith’

>>> sue[2] *= 1.25    # повысить оклад Сью на 25%

>>> sue

[‘Sue Jones’, 45, 50000.0, ‘hardware’]

Выражение, извлекающее фамилию в этом примере, обрабатывается интерпретатором слева направо: сначала извлекается строка с именем и фамилией Боба, затем строка разбивается по пробелам и преобразуется в список подстрок, а операция индексирования возвращает фамилию (разбейте это выражение на несколько операций, чтобы увидеть, как это происходит).

Первые замечания

Поскольку это первый пример программного кода в книге, необходимо сделать несколько практических замечаний:

•    Этот программный код можно ввести в среде IDLE с графическим интерфейсом; после ввода команды python в командной строке (или той же команды с указанием полного пути к ней, если она не находится в системном списке путей поиска выполняемых файлов) и так далее.

•    Символы >>> characters - это приглашение к вводу интерпретатора Python (эти символы не нужно вводить).

•    Информационные строки, которые интерпретатор Python выводит при запуске, я опустил ради экономии места.

•    Все примеры из этой книги я запускал под управлением Python 3.1; результаты работы примеров во всех версиях линейки 3.X должны быть одинаковыми (разумеется, исключая непредвиденные случаи внесения существенных изменений в Python).

•    Большая часть примеров в этой книге, за исключением некоторых из них, демонстрирующих приемы системного программирования и интеграции с программным кодом на языке C, выполнялись в ОС Windows 7. Однако, благодаря переносимости Python, не имеет значения, в какой операционной системе будут опробоваться примеры, если иное не указано явно.

Если прежде вам не доводилось выполнять программный код на языке Python подобным способом, тогда обращайтесь за справочной информацией к вводным материалам, таким как книга «Изучаем Python». Далее в этой главе я сделаю несколько замечаний, касающихся запуска программного кода, хранящегося в файлах сценариев.

База данных в виде списка

К настоящему моменту мы всего создали всего лишь две переменных, но не базу данных. Чтобы объединить информацию о Бобе и Сью, мы могли бы просто включить ее в другой список:

>>> people = [bob, sue]    # ссылки в списке списков

>>> for person in people: print(person)

[‘Bob Smith’, 42, 30000, ‘software’]

[‘Sue Jones’, 45, 50000.0, ‘hardware’]

Теперь нашу базу данных представляет список people. Мы можем извлекать из него отдельные записи в соответствии с их позициями в списке и обрабатывать их в цикле:

>>> people[1][0]

‘Sue Jones’

>>> for person in people:

print(person[0].split()[-1])    # вывести фамилию

person[2] *= 1.20    # увеличить оклад на 20%

Smith

Jones

>>> for person in people: print(person[2]) # проверить новый размер оклада

36000.0

60000.0

Теперь, когда у нас имеется список, мы можем организовать выборку значений полей из записей с помощью более мощных инструментов выполнения итераций, присутствующих в языке Python, таких как генераторы списков, функция map и выражения-генераторы:

>>> pays = [person[2] for person in people] # выбрать все оклады >>> pays

[36000.0, 60000.0]

>>> pays = map((lambda x: x[2]), people) # то же самое (в версии 3.X

>>> list(pays)    # функция map возвращает генератор)

[36000.0, 60000.0]

>>> sum(person[2] for person in people) # выражение-генератор,

96000.0    # sum - встроенная функция

Для добавления новых записей в базу данных вполне достаточно использовать обычные операции над списками, такие как append и extend:

>>> people.append(['Tom', 50, 0, None])

>>> len(people)

3

>>> people[-1][0]

‘Tom’

Списки неплохо подходят для реализации нашей базы данных, и их возможностей вполне достаточно для некоторых программ, но они страдают рядом существенных недостатков. Во-первых, информация о Бобе и Сью в настоящий момент хранится в виде объектов в оперативной памяти, и она будет утеряна сразу после завершения работы интерпретатора Python. Во-вторых, всякий раз, когда потребуется извлечь фамилию человека или повысить ему оклад, нам придется повторно вводить программный код, который мы только что видели. Это может вызвать определенные проблемы, если когда-нибудь поменяется алгоритм выполнения этих операций, - нам может потребоваться изменить наш программный код во многих местах. Мы вернемся к этим проблемам через несколько минут.

Обращение к полям по именам

Самый, пожалуй, существенный недостаток при использовании списков заключается в необходимости запоминать позиции полей: как иначе можно утверждать, что программный код, обращающийся к элементу записи с таинственным индексом 2, извлекает размер оклада? С точки зрения очевидности программного кода было бы лучше, если бы можно было присвоить каждому полю осмысленное имя.

Мы могли бы связать имена с позициями полей в записи, используя встроенную функцию range, которая генерирует набор последовательных целых чисел при использовании в контексте итераций (таких как операция присваивания последовательности ниже):

>>> NAME, AGE, PAY = range(3)    # 0, 1 и 2

>>> bob = ['Bob Smith’, 42, 10000]

>>> bob[NAME]

‘Bob Smith’

>>> PAY, bob[PAY]

(2, 10000)

Это решает проблему читаемости программного кода: три имени переменных, состоящих из заглавных символов, по сути, превратились в имена полей. Однако такое решение делает программный код зависящим от инструкции присваивания позиций именам полей, - мы должны помнить о необходимости обновлять ее при любом изменении структуры записи. Поскольку имена полей и записи никак не связаны между собой, они могут перестать соответствовать друг другу, и тогда возникнет необходимость вмешательства в программный код.

Кроме того, так как имена полей являются независимыми переменными, между записью в виде списка и именами полей отсутствует обратная связь. Имея одну только запись в виде списка, например, нельзя реализовать форматированный вывод значений полей с их именами. В случае с предыдущей записью без дополнительных ухищрений невозможно получить имя AGE из значения 42: вызов bob.index(42) вернет 1, значение переменной AGE, но не само имя AGE.

Можно было бы попробовать представлять записи в виде списков кортежей, где кортежи хранят не только значения полей, но их имена. Но еще лучше было бы использовать списки списков, что позволило бы изменять поля (кортежи относятся к категории неизменяемых объектов). Ниже демонстрируется воплощение этой идеи на примере простых записей:

>>> bob = [['name', ‘Bob Smith'], ['age', 42], ['pay', 10000]]

>>> sue = [['name', ‘Sue Jones'], ['age', 45], ['pay', 20000]]

>>> people = [bob, sue]

Однако на самом деле этот прием не решает проблему, потому что для извлечения полей все равно необходимо использовать числовые индексы:

>>> for person in people:

print(person[0][1], person[2][1])    # имя, оклад

Bob Smith 10000 Sue Jones 20000

>>> [person[0][1] for person in people] # выборка имен [‘Bob Smith’, ‘Sue Jones’]

>>> for person in people:

print(person[0][1].split()[-1]) # получить фамилию person[2][1] *= 1.10    # повысить оклад на 10%

Smith

Jones

>>> for person in people: print(person[2])

[‘pay’, 11000.0]

[‘pay’, 22000.0]

Все, чего мы добились, - добавили еще один уровень индексирования. Для достижения желаемого мы могли бы просматривать имена полей в цикле, отыскивая необходимые (в следующем цикле используется операция присваивания кортежа для распаковывания пар имя/значение):

>>> for person in people:

for (name, value) in person:

if name == ‘name': print(value) # поиск требуемого поля

Bob Smith Sue Jones

Еще лучше было бы реализовать функцию, выполняющую всю работу за нас:

>>> def field(record, label):

for (fname, fvalue) in record:

if fname == label:    # поиск поля по имени

return fvalue

>>> field(bob, ‘name')

‘Bob Smith’

>>> field(sue, ‘pay')

22000.0

>>> for rec in people:    # вывести возраст всех людей

print(field(rec, ‘age'))    # в базе данных

42

45

Если двигаться дальше по этому пути, в конечном итоге можно получить набор функций, осуществляющих интерфейс к записям, отображающих имена полей в их значения. Однако если прежде вам приходилось программировать на языке Python, вы наверняка знаете, что существует более простой способ реализации такого рода ассоциаций, и вы уже наверняка догадались, в какую сторону мы направимся в следующем разделе.


Словари

Реализация записей на основе списков, как было показано в предыдущем разделе, вполне работоспособна, хотя и за счет некоторой потери производительности из-за необходимости выполнять поиск полей по именам (если вас волнуют потери, измеряемые миллисекундами). Однако если вы уже имеете некоторое знакомство с языком Python, вы должны знать, что существуют более эффективные и более удобные способы связывания имен полей с их значениями. Встроенные объекты словарей выглядят естественно:

>>> bob = {‘name': ‘Bob Smith', ‘age': 42, ‘pay': 30000, ‘job': ‘dev'}

>>> sue = {‘name': ‘Sue Jones', ‘age': 45, ‘pay': 40000, ‘job': ‘hdw'}

Теперь bob и sue - это объекты, автоматически отображающие имена полей в их значения, и их использование делает программный код более простым и понятным. Нам не требуется запоминать, что означают числовые индексы, и мы даем интерпретатору возможность использовать его высокоэффективный алгоритм индексации словарей, чтобы отыскивать значения полей, ассоциированные с их именами:

>>> bob[‘name'], sue[‘pay'] # в отличие от bob[0], sue[2]

(‘Bob Smith’, 40000)

>>> bob[‘name'].split()[-1]

‘Smith’

>>> sue[‘pay'] *= 1.10

>>> sue[‘pay']

44000.0

Поскольку теперь при обращении к полям используются символические имена, программный код выглядит более осмысленным для всех, кто будет читать его (включая и вас).

Другие способы создания словарей

Словари являются настолько удобными объектами при программировании на языке Python, что было предусмотрено еще несколько способов их создания, отличающихся от традиционного синтаксиса литералов, продемонстрированного выше, - например, вызовом конструктора с именованными аргументами, при этом все ключи будут строками:

>>> bob = dict(name='Bob Smith', age=42, pay=30000, job='dev')

>>> sue = dict(name='Sue Jones', age=45, pay=40000, job='hdw')

>>> bob

{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’}

>>> sue

{‘pay’: 40000, ‘job’: ‘hdw’, ‘age’: 45, ‘name’: ‘Sue Jones’}

заполнением словаря поле за полем (напомню, что для ключей словаря не предусматривается какой-то определенный порядок следования):

>>> sue = {}

>>> sue[‘name'] = ‘Sue Jones'

>>> sue[‘age'] = 45 >>> sue[‘pay'] = 40000 >>> sue[‘job'] = ‘hdw'

>>> sue

{‘job’: ‘hdw’, ‘pay’: 40000, ‘age’: 45, ‘name’: ‘Sue Jones’}

объединением двух списков, содержащих имена и значения:

>>> names = [‘name', ‘age', ‘pay', ‘job']

>>> values = [‘Sue Jones', 45, 40000, ‘hdw']

>>> list(zip(names, values))

[(‘name’, ‘Sue Jones’), (‘age’, 45), (‘pay’, 40000), (‘job’, ‘hdw’)]

>>> sue = dict(zip(names, values))

>>> sue

{‘job’: ‘hdw’, ‘pay’: 40000, ‘age’: 45, ‘name’: ‘Sue Jones’}

Словари можно даже создавать из последовательностей ключей и необязательного начального значения для всех ключей (этот способ удобно использовать для инициализации пустых словарей):

>>> fields = (‘name', ‘age', ‘job', ‘pay')

>>> record = dict.fromkeys(fields, ‘?')

>>> record

{‘job’: ‘?’, ‘pay’: ‘?’, ‘age’: ‘?’, ‘name’: ‘?’}

Списки словарей

Независимо от способа создания словарей, нам все еще необходимо собрать словари-записи в базу данных. Здесь также можно использовать список, при условии, что нам не требуется обеспечить доступ по ключу на верхнем уровне:

>>> bob

{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’}

>>> sue

{‘job’: ‘hdw’, ‘pay’: 40000, ‘age’: 45, ‘name’: ‘Sue Jones’}

>>> people = [bob, sue]    # ссылки в списке

>>> for person in people:

print(person[‘name'], person[‘pay'], sep=', ‘) # все имена, оклады

Bob Smith, 30000 Sue Jones, 40000

>>> for person in people:

if person[‘name'] == ‘Sue Jones':    # оклад Сью

print(person[‘pay'])

40000

Здесь точно так же используются инструменты итераций, но вместо таинственных числовых индексов используются ключи (в терминах баз данных генератор списков и функция map в следующем примере возвращают проекцию базы данных по полю «name»):

>>> names = [person[‘name'] for person in people] # выбирает имена >>> names

[‘Bob Smith’, ‘Sue Jones’]

>>> list(map((lambda x: x[‘name']), people))    # то же самое

[‘Bob Smith’, ‘Sue Jones’]

>>> sum(person[‘pay'] for person in people)    # сумма всех окладов

70000

Интересно, что такие инструменты, как генераторы списков и выражения-генераторы, способны по своему удобству приблизиться к запросам в языке SQL, с тем отличием, что они манипулируют объектами в памяти:

>>> [rec[‘name'] for rec in people if rec[‘age'] >= 45]    # SQL-подобный

[‘Sue Jones’]    # запрос

>>> [(rec[‘age'] ** 2 if rec[‘age'] >= 45 else rec[‘age']) for rec in people]

[42, 2025] >>> G = (rec[‘name'] for rec in people if rec[‘age'] >= 45)

>>> next(G)

‘Sue Jones’

>>> G = ((rec[‘age'] ** 2 if rec[‘age'] >= 45 else rec[‘age']) for rec in people)

>>> G.__next__()

42

А так как словари являются обычными объектами, к этим записям можно также обращаться с использованием привычного синтаксиса:

>>> for person in people:

print(person[‘name'].split()[-1])    # фамилия

person[‘pay'] *= 1.10    # повышение на 10%

Smith

Jones

>>> for person in people: print(person[‘pay'])

33000.0

44000.0

Вложенные структуры

В предыдущих примерах мы могли бы при желании избежать необходимости писать дополнительный программный код, извлекающий фамилии, еще больше структурировав наши записи. Поскольку в языке все объекты составных типов данных могут вкладываться друг в друга сколь угодно глубоко, мы легко можем конструировать чрезвычайно сложные структуры данных, используя простой синтаксис объектов, а Python сам позаботится о создании компонентов, связывании структур в памяти и освобождении памяти позднее. Это одно из значительных преимуществ таких языков сценариев, как Python.

В следующем примере демонстрируется более структурированная запись, содержащая вложенный словарь, список и кортеж внутри другого словаря:

>>> bob2 = {‘name': {‘first': ‘Bob', ‘last': ‘Smith'},

‘age': 42,

‘job': [‘software', ‘writing'],

‘pay': (40000, 50000)}

Эта запись содержит вложенные структуры, поэтому для доступа к более низкому уровню мы просто будем использовать двойные индексы:

>>> bob2[‘name']    # полное имя Боба

{‘last’: ‘Smith’, ‘first’: ‘Bob’}

>>> bob2[‘name'][‘last']    # фамилия Боба

‘Smith’

>>> bob2[‘pay'][1]    # верхний предел оклада Боба

50000

Поле name здесь - это еще один словарь, поэтому вместо того чтобы разбивать строку для извлечения фамилии, мы просто используем операцию индексирования. Кроме того, сотрудники могут занимать несколько должностей, а также иметь верхний и нижний предел оклада. Фактически в подобных ситуациях Python превращается в своеобразный язык запросов - мы можем извлекать и изменять вложенные значения с применением обычных операций над объектами:

>>> for job in bob2[‘job']: print(job) # все должности, занимаемые Бобом

software

writing

>> bob2[‘job'][-1]    # последняя должность Боба

‘writing’

>>> bob2[‘job'].append(‘janitor') # Боб получает новую должность >>> bob2

{‘job’: [‘software’, ‘writing’, ‘janitor’], ‘pay’: (40000, 50000), ‘age’: 42, ‘name’: {‘last’: ‘Smith’, ‘first’: ‘Bob’}}

В расширении вложенного списка с помощью метода append нет ничего необычного, потому что в действительности он является независимым объектом. Такие вложенные конструкции могут пригодиться в более сложных приложениях. Однако, чтобы не усложнять примеры, мы сохраним прежнюю, плоскую структуру записей.

Словари словарей

И еще один поворот в реализации нашей базы данных с информацией о людях: мы можем расширить область применения словарей, задействовав еще один словарь для представления самой базы данных. То есть мы можем создать словарь словарей - внешний словарь будет играть роль базы данных, а вложенные словари - роль записей. В отличие от простого списка записей, база данных, представленная в виде словаря, позволит нам сохранять и извлекать записи с помощью символических ключей:

>>> bob = dict(name='Bob Smith', age=42, pay=30000, job='dev')

>>> sue = dict(name='Sue Jones', age=45, pay=40000, job='hdw')

>>> bob

{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’}

>>> db = {}

>>> db[‘bob'] = bob    # ссылки на словари в словаре

>>> db[‘sue'] = sue

>>>

>>> db[‘bob'][‘name']    # извлечь имя Боба

‘Bob Smith’

>>> db[‘sue'][‘pay'] = 50000    # изменить оклад Сью

>>> db[‘sue'][‘pay']    # извлечь оклад Сью

50000

Обратите внимание, что такая организация позволяет нам обращаться к записям непосредственно, без необходимости выполнять поиск в цикле - мы получаем непосредственный доступ к записи с информацией о Бобе за счет использования ключа bob. Это действительно словарь словарей, хотя это и не заметно, если не вывести всю базу данных сразу (для подобных целей удобно использовать модуль pprint форматированного вывода):

>>> db

{‘bob’: {‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’}, ‘sue’: {‘pay’: 50000, ‘job’: ‘hdw’, ‘age’: 45, ‘name’: ‘Sue Jones’}}

>>> import pprint >>> pprint.pprint(db)

{‘bob’: {‘age’: 42, ‘job’: ‘dev’, ‘name’: ‘Bob Smith’, ‘pay’: 30000},

‘sue’: {‘age’: 45, ‘job’: ‘hdw’, ‘name’: ‘Sue Jones’, ‘pay’: 50000}}

Если же возникнет необходимость последовательно обойти все записи в базе данных, можно воспользоваться итераторами словарей. В последних версиях Python реализован итератор словаря, который на каждой итерации в цикле for воспроизводит по одному ключу (для совместимости с более ранними версиями в циклах for можно также вместо простого имени db использовать явный вызов метода db. keys, но, так как в Python 3 метод keys возвращает генератор, конечный результат будет тот же самый):

>>> for key in db:

print(key, ‘=>', db[key][‘name'])

bob => Bob Smith sue => Sue Jones

>>> for key in db:

print(key, ‘=>', db[key][‘pay'])

bob => 30000 sue => 50000

В процессе обхода доступ к отдельным записям можно получать с использованием операции индексирования по ключу:

>>> for key in db:

print(db[key][‘name'].split()[-1]) db[key][‘pay'] *= 1.10

Smith

Jones

или напрямую, организовав обход значений словаря:

>>> for record in db.values(): print(record[‘pay'])

33000.0

55000.0

>>> x = [db[key][‘name'] for key in db]

>>> x

[‘Bob Smith’, ‘Sue Jones’]

>>> x = [rec[‘name'] for rec in db.values()]

>>> x

[‘Bob Smith’, ‘Sue Jones’]

А чтобы добавить новую запись, достаточно просто выполнить операцию присваивания по новому ключу. В конце концов - это всего лишь словарь:

>>> db[‘tom'] = dict(name='Tom', age=50, job=None, pay=0)

>>>

>>> db[‘tom']

{‘pay’: 0, ‘job’: None, ‘age’: 50, ‘name’: ‘Tom’}

>>> db[‘tom'][‘name']

‘Tom’

>>> list(db.keys())

[‘bob’, ‘sue’, ‘tom’]

>>> len(db)

3

>>> [rec[‘age'] for rec in db.values()]

[42, 45, 50]

>>> [rec[‘name'] for rec in db.values() if rec[‘age'] >= 45] # SQL-подобный [‘Sue Jones’, ‘Tom’]    # запрос

Наша база данных по-прежнему является объектом, хранящимся в оперативной памяти. Но, как оказывается, такой формат словаря словарей в точности соответствует формату, который используется системой сохранения объектов в файлах, - модулем shelve (с точки зрения грамматики английского языка этот модуль должен был бы называться shelf, но в Python термин shelve, обозначающий сохранение объектов, одновременно служит и названием соответствующего ему модуля). О том, как это делается, вы узнаете в следующем разделе.


Шаг 2: сохранение записей на длительное время

К настоящему моменту мы остановились на представлении нашей базы данных в виде словаря и попутно рассмотрели некоторые способы реализации структур данных в языке Python. Однако, как уже упоминалось выше, объекты, с которыми мы имели дело до сих пор, - временные объекты; они располагаются в оперативной памяти и исчезают бесследно после завершения работы интерпретатора Python или программы, создавшей их. Чтобы обеспечить долговременное хранение базы данных, ее необходимо сохранить в каком-нибудь файле.


Текстовые файлы

Один из способов обеспечить сохранность данных между запусками программы заключается в сохранении всех данных в простом файле, в виде отформатированного текста. Выработав определенную договоренность о формате представления данных, которая будет использоваться инструментами чтения и записи, мы сможем реализовать любую схему хранения.

Тестовый сценарий создания данных

Чтобы не продолжать дальнейшую работу в интерактивном режиме, напишем сценарий, инициализирующий базу данных, которую требуется сохранить (если вы уже имеете опыт работы с языком Python, то должны знать, что как только вы начинаете писать программный код, занимающий больше одной строки, работа в интерактивном режиме становится утомительной). Сценарий в примере 1.1 создает несколько записей и словарь базы данных, с которыми мы работали до сих пор, но так как он одновременно является модулем, мы сможем импортировать его и избавиться от необходимости всякий раз вводить программный код вручную. В некотором смысле этот модуль и является базой данных, но его реализация не поддерживает возможность изменения данных автоматически или конечным пользователем.

Пример 1.1. PP4E\Preview\initdata.py

#    инициализировать данные для последующего сохранения в файлах

#    записи

bob =    {‘name’:    ‘Bob Smith’,    ‘age’:    42,    ‘pay’:    30000,    ‘job’:    ‘dev’}

sue =    {‘name’:    ‘Sue Jones’,    ‘age’:    45,    ‘pay’:    40000,    ‘job’:    ‘hdw’}

tom =    {‘name’:    ‘Tom’,    ‘age’:    50,    ‘pay’:    0,    ‘job’:    None}

#    база данных db = {}

db[‘bob’] = bob db[‘sue’] = sue db[‘tom’] = tom

if __name__ == ‘__main__’:    # если запускается, как сценарий

for key in db:

print(key, ‘=>\n ‘, db[key])

Как обычно, проверка переменной__name__в конце примера 1.1 возвра

щает true, только если файл был запущен как самостоятельный сценарий, а не был импортирован как модуль. Если запустить пример как самостоятельный сценарий (например, из командной строки, щелчком на ярлыке или из среды IDLE), будет выполнен программный код теста в теле условной инструкции, который выведет содержимое базы данных в поток стандартного вывода (напомню, что функция print использует этот поток по умолчанию).

Ниже приводится пример запуска сценария из командной строки в ОС Windows. В окне Командная строка (Command Prompt) выполните команду cd, чтобы перейти в каталог со сценарием. На других платформах используйте аналогичную программу-консоль:

...\PP4E\Preview> python initdata.py

bob =>

{‘job’: ‘dev’, ‘pay’: 30000, ‘age’: 42, ‘name’: ‘Bob Smith’}

sue =>

{‘job’: ‘hdw’, ‘pay’: 40000, ‘age’: 45, ‘name’: ‘Sue Jones’} tom =>

{‘job’: None, ‘pay’: 0, ‘age’: 50, ‘name’: ‘Tom’}

Соглашения об именовании файлов

Это наш первый файл (он же «сценарий») с исходными текстами, поэтому здесь необходимо сделать три замечания по использованию примеров из книги:

•    Текст .. .\PP4E\Preview> в первой строке предыдущего примера обозначает приглашение к вводу в командной строке и может отличаться в разных платформах. Вам необходимо ввести лишь текст, следующий за этим приглашением (python initdata.py).

•    Во всех примерах в этой книге текст системной подсказки к вводу содержит путь к каталогу в загружаемом пакете с примерами, внутри которого должна запускаться указанная команда. При запуске сценария из командной строки убедитесь, что текущим рабочим каталогом является каталог PP4E\Preview. Это может иметь значение для примеров, использующих файлы в рабочем каталоге.

•    Кроме того, подписи, предшествующие примерам с листингами программного кода из файлов, сообщают, где находится файл в пакете с примерами. Так, подпись к примеру 1.1 выше сообщает, что полное имя сценария в дереве каталогов имеет вид PP4E\Preview\ initdata.py.

Мы будем пользоваться этими соглашениями на протяжении всей книги - в предисловии описано, как получить примеры, если вы собираетесь работать с ними. Иногда, особенно в части книги о системном программировании, я буду указывать в приглашении к вводу более полный путь к каталогу, если это будет необходимо, чтобы уточнить контекст выполнения (например, префикс «С:\» в Windows или дополнительные имена каталогов).

Замечания по поводу сценариев

Выше я уже давал несколько замечаний по поводу интерактивного режима. Теперь, когда мы начинаем использовать файлы сценариев, я также хочу дать несколько общих замечаний по использованию сценариев Python:

•    На некоторых платформах может потребоваться вводить полный путь к каталогу с программой на языке Python. Если путь к выполняемому файлу интерпретатора Python отсутствует в системном пути поиска, замените в Windows, например, команду python на C:\ Python31\python (здесь предполагается, что вы пользуетесь версией Python 3.1).

•    В большинстве систем Windows вообще не обязательно вводить команду python - чтобы запустить сценарий, вполне достаточно ввести только имя файла, поскольку интерпретатор Python обычно регистрируется, как программа для открытия файлов с расширением

«.py».

•    Кроме того, файлы сценариев можно запускать в стандартной среде IDLE (откройте файл и запустите его, воспользовавшись меню Run (Запустить) в окне редактирования файла) или похожим способом в любой другой среде разработки программ на языке Python IDE (например, в Komodo, Eclipse, NetBeans или Wing IDE).

•    Если вы собираетесь запускать файл в Windows щелчком мыши на ярлыке, не забудьте добавить вызов функции input() в конец сценария, чтобы окно с выводом программы не закрылось после ее завершения. В других системах, чтобы обеспечить возможность запуска сценария щелчком на ярлыке, может потребоваться добавить в его начало строку #! и сделать файл выполняемым с помощью команды chmod.

Далее я буду исходить из предположения, что вы в состоянии запустить программный код на языке Python каким-либо способом. Однако если вы столкнетесь с трудностями, за полной информацией о способах запуска программ на языке Python обращайтесь к другим книгам, таким как «Изучаем Python».

Сценарий записи/чтения данных

Теперь осталось лишь реализовать сохранение всех данных из памяти в файле. Добиться этого можно несколькими способами. Самый простой из них заключается в том, чтобы сохранять записи по одной и вставлять между ними разделители, которые можно будет использовать при загрузке данных, чтобы отделять записи друг от друга. В примере 1.2 показан один из способов воплощения этой идеи.

Пример 1.2. PP4E\Preview\make_db_file.py

Сохраняет в файл базу данных, находящуюся в оперативной памяти, используя собственный формат записи; предполагается, что в данных отсутствуют строки ‘endrec.’, ‘enddb.’ и ‘=>’; предполагается, что база данных является словарем словарей; внимание: применение функции eval может быть опасным - она выполняет строки как программный код; с помощью функции eval() можно также реализовать сохранение словарей-записей целиком; кроме того, вместо вызова print(key,file=dbfile) можно использовать вызов dbfile.write(key + ‘\n’);

dbfilename = ‘people-file’

ENDDB = ‘enddb.’

ENDREC = ‘endrec.’

RECSEP = ‘=>’

def storeDbase(db, dbfilename=dbfilename):

"сохраняет базу данных в файл” dbfile = open(dbfilename, ‘w’) for key in db:

print(key, file=dbfile)

for (name, value) in db[key].items():

print(name + RECSEP + repr(value), file=dbfile) print(ENDREC, file=dbfile) print(ENDDB, file=dbfile) dbfile.close()

def loadDbase(dbfilename=dbfilename):

"восстанавливает данные, реконструируя базу данных” dbfile = open(dbfilename) import sys sys.stdin = dbfile db = {} key = input() while key != ENDDB: rec = {} field = input() while field != ENDREC:

name, value = field.split(RECSEP) rec[name] = eval(value) field = input() db[key] = rec key = input() return db

if__name__== ‘__main__’:

from initdata import db storeDbase(db)

Это достаточно сложная программа, отчасти потому, что в ней реализованы обе операции, сохранения и загрузки, а отчасти потому, что эти операции реализованы не самым простым способом. Как будет показано ниже, существуют более простые способы сохранения объектов в файл и чтения их из файла по сравнению с форматированием и парсингом данных вручную.

Однако для реализации простых задач такой подход вполне оправдан. При запуске примера 1.2 как сценария база данных будет сохранена в файл. Он ничего не выводит на экран, но мы можем проверить содержимое файла базы данных в интерактивной оболочке после выполнения сценария внутри IDLE или в окне консоли (так как файл базы данных появится в текущем рабочем каталоге):

...\PP4E\Preview> python make_db_file.py

...\PP4E\Preview> python

>>> for line in open(‘people-file'):

... print(line, end='')

bob

job=>’dev’

pay=>30000

age=>42

name=>’Bob Smith’

endrec.

sue

job=>’hdw’

pay=>40000

age=>45

name=>’Sue Jones’

endrec.

tom

job=>None

pay=>0

age=>50

name=>’Tom’

endrec.

enddb.

Этот файл хранит содержимое базы данных с дополнительными элементами форматирования. Сами данные берутся из тестовой базы данных, созданной модулем, представленным в примере 1.1, который импортируется программным кодом самопроверки в примере 1.2. С точки зрения практического применения, пример 1.2 сам мог бы импортироваться и использоваться для сохранения различных баз данных.

Обратите внимание, что форматирование сохраняемых данных выполняется с помощью функции repr, а обратное преобразование прочитанных данных - с помощью функции eval, которая интерпретирует входную строку как программный код на языке Python. Это позволяет сохранять и воссоздавать такие виды данных, как объект None, но этот способ небезопасен. Не следует использовать функцию eval, если нет уверенности, что база данных не содержит злонамеренный программный код. Однако в нашем случае нет причин для волнений.

Вспомогательные сценарии

Ниже приводятся дополнительные сценарии, которые можно использовать для тестирования. Сценарий в примере 1.3 выполняет загрузку базы данных из файла.

Пример 1.3. PP4E\Preview\dump_db_file.py

from make_db_file import loadDbase db = loadDbase() for key in db:

print(key, ‘=>\n ‘, db[key]) print(db[‘sue’][‘name’])

А сценарий в примере 1.4 загружает базу данных, вносит в нее изменения и сохраняет ее обратно в файл.

Пример 1.4. PP4E\Preview\update_db_file.py

from make_db_file import loadDbase, storeDbase db = loadDbase() db[‘sue’][‘pay’] *= 1.10 db[‘tom’][‘name’] = ‘Tom Tom’ storeDbase(db)

Ниже приводится пример запуска сценариев dump_db_file.py и update_ db_file.py из командной строки, где видно, что между запусками сценария dump_db_file.py изменяются оклад Сью и имя Тома. Обратите внимание, что после завершения каждого из сценариев данные сохраняются, - это обусловлено тем, что наши объекты просто загружаются и сохраняются в текстовом файле:

...\PP4E\Preview> python dump_db_file.py

bob =>

{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’}

sue =>

{‘pay’: 40000, ‘job’: ‘hdw’, ‘age’: 45, ‘name’: ‘Sue Jones’} tom =>

{‘pay’: 0, ‘job’: None, ‘age’: 50, ‘name’: ‘Tom’}

Sue Jones

...\PP4E\Preview> python update_db_file.py ...\PP4E\Preview> python dump_db_file.py

bob =>

{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’} sue =>

{‘pay’: 44000.0, ‘job’: ‘hdw’, ‘age’: 45, ‘name’: ‘Sue Jones’}

tom =>

{‘pay’: 0, ‘job’: None, ‘age’: 50, ‘name’: ‘Tom Tom'}

Sue Jones

В настоящий момент, чтобы внести в базу данных какие-либо изменения, необходимо написать отдельный программный код, в виде сценария или в интерактивной оболочке (далее в этой главе будут представлены более удачные решения с универсальным интерфейсом командной строки, графическим интерфейсом и веб-интерфейсом). Но, как бы то ни было, наш текстовый файл является базой данных, содержащей записи. Однако, как мы увидим в следующем разделе, мы только что проделали массу ненужной работы.


Модуль pickle

Реализация базы данных на основе текстового файла, представленная в предыдущем разделе, вполне работоспособна, но она имеет ряд существенных ограничений. Во-первых, чтобы получить доступ всего к одной записи, необходимо загрузить всю базу данных из файла, а после некоторых изменений обратно в файл необходимо записывать всю базу данных. Данное ограничение можно обойти, если каждую запись сохранять в отдельном файле, но это еще больше осложнит реализацию программы.

Во-вторых, решение на основе текстового файла предполагает, что символы, выполняющие роль разделителей записей, не должны появляться в самих данных: если, к примеру, данные могут содержать последовательность символов =>, предложенное решение окажется непригодным. Мы могли бы обойти это ограничение, сохраняя записи в формате XML, а для загрузки данных используя инструменты для работы с форматом XML, входящие в состав Python, с которыми мы познакомимся далее в этой книге. Использование тегов XML позволило бы избежать конфликтов с фактическими данными в текстовом виде, но необходимость создания и парсинга XML снова привела бы к усложнению программы.

Хуже всего, пожалуй, то, что решение на основе текстового файла оказывается слишком сложным, не будучи при этом достаточно универсальным: оно привязано к организации базы данных в виде словаря словарей и не способно работать с базами данных, имеющими другую структуру, без существенного расширения реализации. Было бы здорово, если бы существовал универсальный инструмент, способный преобразовывать любые типы данных Python в формат, который можно было бы сохранять в файл за один шаг.

Именно для этого разработан модуль pickle. Этот модуль преобразует объект Python, находящийся в оперативной памяти, в последовательность или в строку байтов, которую можно записать в любой объект, подобный файлу. Кроме того, модуль pickle знает, как восстановить оригинальный объект в памяти, получив последовательность байтов, то есть мы получаем обратно тот же самый объект. В некотором смысле модуль pickle позволяет избежать необходимости разрабатывать специальные форматы представления данных - последовательный формат, реализованный в этом модуле, достаточно универсален и эффективен для большинства применений. При использовании модуля pickle отпадает необходимость вручную преобразовывать объекты перед сохранением и анализировать сложный формат представления данных, чтобы получить исходные объекты. Прием, основанный на использовании модуля pickle, напоминает прием, основанный на использовании формата XML, но он является не только более характерным для Python, но и более простым в реализации.

Другими словами, применение модуля pickle позволит нам сохранять и извлекать объекты Python за один шаг, благодаря чему мы сможем обрабатывать записи, используя привычный синтаксис языка Python. Невзирая на сложность реализации, модуль pickle удивительно прост в использовании. В примере 1.5 демонстрируется, как с помощью этого модуля можно сохранять записи в файле.

Пример 1.5. PP4E\Preview\make_db_pickle.py

from initdata import db import pickle

dbfile = open(‘people-pickle’, ‘wb’) # в версии 3.X следует использовать pickle.dump(db, dbfile)    # двоичный режим работы с файлами, так как

dbfile.close()    # данные имеют тип bytes, а не str

Если запустить этот сценарий, он сохранит всю базу данных (словарь словарей, который создается сценарием из примера 1.1) в файл с именем people-pickle в текущем рабочем каталоге. В процессе работы модуль pickle преобразовывает объект в строку. В примере 1.6 демонстрируется, как можно реализовать доступ к сохраненной базе данных после ее создания, - достаточно просто открыть файл и передать его модулю pickle, который восстановит объект из последовательного представления.

Пример 1.6. PP4E\Preview\dump_db_pickle.py

import pickle

dbfile = open(‘people-pickle’, ‘rb’) # в версии 3.X следует использовать db = pickle.load(dbfile)    # двоичный режим работы с файлами

for key in db:

print(key, ‘=>\n ‘, db[key]) print(db[‘sue’][‘name’])

Ниже приводится пример запуска этих двух сценариев из командной строки. Естественно, эти сценарии можно запустить и в среде IDLE, чтобы в интерактивном сеансе открыть и исследовать файл, созданный модулем pickle:

...\PP4E\Preview> python make_db_pickle.py ...\PP4E\Preview> python dump_db_pickle.py

bob =>

{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’}

sue =>

{‘pay’: 40000, ‘job’: ‘hdw’, ‘age’: 45, ‘name’: ‘Sue Jones’} tom =>

{‘pay’: 0, ‘job’: None, ‘age’: 50, ‘name’: ‘Tom’}

Sue Jones

Внесение изменений в базу данных, сохраненную с помощью модуля pickle, выполняется точно так же, как и при использовании текстового файла, созданного вручную, за исключением того, что все необходимые преобразования выполняются стандартным модулем. Как это делается, демонстрирует пример 1.7.

Пример 1.7. PP4E\Preview\update-db-pickle.py

import pickle

dbfile = open(‘people-pickle’, ‘rb’) db = pickle.load(dbfile) dbfile.close()

db[‘sue’][‘pay’] *= 1.10 db[‘tom’][‘name’] = ‘Tom Tom’

dbfile = open(‘people-pickle’, ‘wb’) pickle.dump(db, dbfile) dbfile.close()

Обратите внимание, что после изменения записи в файл сохраняется вся база данных целиком, как и при использовании простого текстового файла; это может занимать продолжительное время, если база данных имеет значительный объем, но мы пока не будем беспокоиться об этом. Ниже приводится пример запуска сценариев dump_db_pickle.py и update_db_pickle.py - как и в предыдущем разделе, измененный оклад Сью и имя Тома сохраняются между вызовами сценариев, потому что записываются обратно в файл (но на этот раз с помощью модуля pickle):

...\PP4E\Preview> python update_db_pickle.py ...\PP4E\Preview> python dump_db_pickle.py

bob =>

{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’}

sue =>

{‘pay’: 44000.0, ‘job’: ‘hdw’, ‘age’: 45, ‘name’: ‘Sue Jones’} tom =>

{‘pay’: 0, ‘job’: None, ‘age’: 50, ‘name’: ‘Tom Tom'}

Sue Jones

Как мы узнаем в главе 17, модуль pickle поддерживает объекты практически любых типов - списки, словари, экземпляры классов, вложенные структуры и многие другие. Там же мы узнаем о текстовых и двоичных протоколах преобразования сохраняемых данных. В Python 3 для представления сохраненных данных все протоколы используют объекты типа bytes, чем обусловлена необходимость открывать файлы pickle в двоичном режиме, независимо от используемого протокола. Кроме того, как будет показано далее в этой главе, модуль pickle и его формат представления данных используется модулем shelve и базами данных ZODB, а в случае экземпляров классов сохраняются не только данные в объектах, но и их поведение.

Модуль pickle фактически является гораздо более универсальным, чем можно было бы заключить из представленных примеров. Поскольку сериализованные данные принимаются любыми объектами, поддерживающими интерфейс, совместимый с файлами, методы dump и load модуля pickle могут использоваться для передачи объектов Python через различные среды распространения информации. С помощью сетевых сокетов, например, можно организовать передачу сериализованных объектов Python по сети и тем самым обеспечить альтернативу более тяжелым протоколам, таким как SOAP и XML-RPC.


Работа модуля pickle с отдельными записями

Как упоминалось выше, один из потенциальных недостатков примеров, представленных в этом разделе до настоящего момента, состоит в том, что они могут оказаться слишком медленными при работе с очень большими базами данных: так как для изменения единственной записи необходимо загружать и сохранять базу данных целиком, при таком решении значительная часть времени будет тратиться впустую. Мы могли бы избежать этого, предусмотрев сохранение каждой записи базы данных в отдельном файле. Следующие три примера демонстрируют, как это можно реализовать, - сценарий из примера 1.8 сохраняет каждую запись в отдельном файле, где в качестве имени файла используется уникальный ключ записи, к которому добавляется расширение .pkl (он создает файлы bob.pkl, sue.pkl и tom.pkl в текущем рабочем каталоге).

Пример 1.8. PP4E\Preview\make_db_pickle_recs.py

from initdata import bob, sue, tom import pickle

for (key, record) in [(‘bob’, bob), (‘tom’, tom), (‘sue’, sue)]: recfile = open(key + ‘.pkl’, ‘wb’) pickle.dump(record, recfile) recfile.close()

Следующий сценарий, представленный в примере 1.9, выводит содержимое всей базы данных, используя модуль glob для подстановки имен файлов и тем самым для выбора всех файлов с расширением .pkl, присутствующих в текущем каталоге. Чтобы загрузить единственную запись, мы открываем файл этой записи и выполняем преобразование содержимого файла с помощью модуля pickle. Теперь, чтобы получить одну запись, нам необходимо загрузить файл единственной записи, а не всю базу данных.

Пример 1.9. PP4E\Preview\dump_db_pickle_recs.py

import pickle, glob

for filename in glob.glob(‘*.pkl’):    # для ‘bob’,’sue’,’tom’

recfile = open(filename, ‘rb’) record = pickle.load(recfile) print(filename, ‘=>\n ‘, record)

suefile = open(‘sue.pkl’, ‘rb’)

print(pickle.load(suefile)[‘name’]) # извлечь имя Сью

Наконец, сценарий в примере 1.10 обновляет содержимое базы данных, извлекая запись из ее файла, изменяя объект в памяти и затем сохраняя его обратно в файл с помощью модуля pickle. На этот раз для внесения изменений необходимо извлечь и сохранить единственную запись, а не всю базу данных.

Пример 1.10. PP4E\Preview\update_db_pickle_recs.py

import pickle

suefile = open(‘sue.pkl’, ‘rb’)

sue = pickle.load(suefile)

suefile.close()

sue[‘pay’] *= 1.10

suefile = open(‘sue.pkl’, ‘wb’)

pickle.dump(sue, suefile)

suefile.close()

Ниже приводится пример запуска сценариев, реализующих решение, когда каждая запись сохраняется в отдельном файле. Результаты получаются те же, что и прежде, но теперь уникальные ключи в базе данных играют роль имен файлов. При такой реализации файловая система превращается в своеобразный словарь верхнего уровня - имена файлов обеспечивают прямой доступ к отдельным записям.

...\PP4E\Preview> python make_db_pickle_recs.py ...\PP4E\Preview> python dump_db_pickle_recs.py

bob.pkl =>

{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’} sue.pkl =>

{‘pay’: 40000, ‘job’: ‘hdw’, ‘age’: 45, ‘name’: ‘Sue Jones’} tom.pkl =>

{‘pay’: 0, ‘job’: None, ‘age’: 50, ‘name’: ‘Tom’}

Sue Jones

...\PP4E\Preview> python update_db_pickle_recs.py ...\PP4E\Preview> python dump_db_pickle_recs.py

bob.pkl =>

{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’} sue.pkl =>

{‘pay’: 44000.0, ‘job’: ‘hdw’, ‘age’: 45, ‘name’: ‘Sue Jones’} tom.pkl =>

{‘pay’: 0, ‘job’: None, ‘age’: 50, ‘name’: ‘Tom’}

Sue Jones


Модуль shelve

Сохранение объектов в файлах, как было показано в предыдущем разделе, является оптимальным решением для многих приложений. Фактически некоторые приложения используют модуль pickle для передачи объектов Python через сетевые сокеты, как более простую альтернативу сетевым протоколам веб-служб, таким как SOAP и XML-RPC (они также поддерживаются в Python, но являются более тяжеловесными по сравнению с модулем pickle).

Кроме того, если допустить, что файловая система способна хранить необходимое нам количество файлов, реализация, сохраняющая каждую запись в отдельном файле, устраняет необходимость загрузки и сохранения всей базы данных при каждом изменении. Однако, когда действительно необходимо иметь возможность доступа к записям по ключу, можно использовать более высокоуровневый инструмент: модуль shelve.

Модуль shelve автоматически сохраняет и загружает объекты из хранилища, обеспечивающего доступ по ключу. Хранилища напоминают словари; их необходимо открывать, и они автоматически сохраняются после завершения программы. Поскольку хранилища обеспечивают доступ к хранимым записям по ключу, отпадает необходимость создавать отдельные файлы для каждой записи - модуль shelve автоматически разделяет записи и извлекает и обновляет только те записи, к которым осуществляется доступ или которые изменяются. Таким образом модуль shelve обеспечивает решение, напоминающее решение, сохраняющее каждую запись в отдельном файле, но более простое в использовании.

Интерфейс модуля shelve так же прост, как и интерфейс модуля pickle: хранилища, создаваемые модулем shelve, идентичны словарям с дополнительными методами open и close. В программном коде объекты хранилищ действительно выглядят, как словари, содержимое которых сохраняется после завершения программы. А все операции по отображению содержимого хранилища в файл и из файла выполняются интерпретатором Python. Например, сценарий в примере 1.11 демонстрирует, как можно сохранить объекты из словаря в хранилище, созданном с помощью модуля shelve.

Пример 1.11. PP4E\Preview\make_db_shelve.py

from initdata import bob, sue import shelve

db = shelve.open(‘people-shelve’) db[‘bob’] = bob db[‘sue’] = sue db.close()

Этот сценарий создаст в текущем каталоге один или более файлов, имена которых начинаются с префикса people-shelve (в ОС Windows, под управлением Python 3.1, сценарий создаст файлы people-shelve.bak, people-shelve.dat и people-shelve.dir). Вы не должны удалять эти файлы (они составляют вашу базу данных!), а чтобы получить доступ к этому хранилищу в других сценариях, необходимо использовать то же самое имя базы. Сценарий в примере 1.12, например, повторно открывает хранилище и последовательно извлекает хранящиеся в нем записи.

Пример 1.12. PP4E\Preview\dump_db_shelve.py

import shelve

db = shelve.open(‘people-shelve’) for key in db:

print(key, ‘=>\n ‘, db[key]) print(db[‘sue’][‘name’]) db.close()

Мы по-прежнему имеем словарь словарей, но на этот раз словарем верхнего уровня является хранилище, отображаемое в файл. Всякий раз, когда происходит обращение к элементу в хранилище, модуль shelve выполняет необходимые операции с файловой системой, обеспечивающей доступ по ключу, и использует модуль pickle для сериализации и десериализации объектов. Однако, с точки зрения программиста, хранилище - это всего лишь словарь, обладающий возможностью сохраняться между вызовами программы. Сценарий в примере 1.13 демонстрирует, как можно реализовать изменение данных в хранилище.

Пример 1.13. PP4E\Preview\update_db_shelve.py

from initdata import tom import shelve

db = shelve.open(‘people-shelve’)

sue = db[‘sue’]    # извлекает объект sue

sue[‘pay’] *= 1.50

db[‘sue’] = sue    # изменяет объект sue

db[‘tom’] = tom    # добавляет новую запись

db.close()

Обратите внимание, что в этом примере сначала по ключу извлекается объект sue, затем он изменяется в памяти и снова сохраняется в хранилище по ключу. Так действуют хранилища по умолчанию, однако более совершенные системы хранения, такие как ZODB, о которой рассказывается в главе 17, могут действовать иначе. Как мы узнаем позднее, метод shelve.open в подобных системах имеет дополнительный именованный аргумент writeback. Если в этом аргументе передать значение True, все загруженные записи будут сохраняться в кэше и автоматически записываться обратно в файл при закрытии хранилища. Благодаря этому не требуется вручную записывать изменения обратно в хранилище, но при этом увеличивается потребление памяти, а сама операция закрытия может занимать продолжительное время.

Обратите также внимание на необходимость явного закрытия хранилища. Нам не требуется указывать флаги режимов в вызове метода shelve. open (по умолчанию он создает новое хранилище, если это необходимо, и открывает существующее хранилище для чтения и записи), однако некоторые механизмы, обеспечивающие доступ к содержимому файлов по ключу, требуют вызова метода close, чтобы сбросить на диск выходные буферы с изменениями.

Наконец, ниже приводится пример запуска сценариев, опирающихся на использование модуля shelve, которые создают, изменяют и извлекают записи. Записи по-прежнему реализованы как словари, но сама база данных теперь является хранилищем, напоминающим словарь, который автоматически сохраняет свое содержимое в файле:

...\PP4E\Preview> python make_db_shelve.py

...\PP4E\Preview> python dump_db_shelve.py

bob =>

{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’}

sue =>

{‘pay’: 40000, ‘job’: ‘hdw’, ‘age’: 45, ‘name’: ‘Sue Jones’}

Sue Jones

...\PP4E\Preview> python update_db_shelve.py

...\PP4E\Preview> python dump_db_shelve.py

bob =>

{‘pay’: 30000, ‘job’: ‘dev’, ‘age’: 42, ‘name’: ‘Bob Smith’}

sue =>

{‘pay’: 60000.0, ‘job’: ‘hdw’, ‘age’: 45, ‘name’: ‘Sue Jones’}

tom =>

{‘pay': 0, ‘job': None, ‘age': 50, ‘name': ‘Tom'}

Sue Jones

После выполнения сценариев update_db_shelve.py и dump_db_shelve.py можно заметить, что была добавлена новая запись с ключом tom и на 50 процентов был увеличен оклад Сью. Эти изменения сохраняются между запусками сценариев, потому что записи-словари отображаются модулем shelve во внешний файл хранилища. (Этот сценарий особенно хорош для Сью - у нее могло бы появиться желание почаще запускать этот сценарий с помощью планировщика cron в Unix или поместив его в папку Автозагрузка (Startup) с помощью msconfig в Windows...)

Что в имени тебе моем?

Удивительно, но часто остается тайной, что свое название язык Python получил благодаря британскому телевизионному комедийному сериалу «Monty Python’s Flying Circus», появившемуся на экранах в 1970-х годах. Фольклор сообщества Python утверждает, что Гвидо ван Россум (Guido van Rossum), создатель Python, смотрел повторные показы этого сериала как раз в то время, когда подбирал название для нового языка программирования, который он разрабатывал. И, как говорят в шоу-бизнесе: «остальное уже история».

Такая наследственность часто является причиной появления в примерах и обсуждениях ссылок на комедийную игру. Например, в сценариях часто используется имя Brian; словами spam (консервированный фарш), lumberjack (лесоруб) и shrubbery (кустарник), получившими специальное значение, называют пользователей Python; а презентации иногда называют «испанской инквизицией». Как правило, когда пользователь Python начинает произносить фразы, не имеющие отношения к реальности, они оказываются заимствованными из сериала или фильмов с участием персонажа Monty Python. Некоторые из этих фраз могут встретиться даже в этой книге. Конечно, чтобы писать программы на языке Python, необязательно бежать и брать в прокате «The Meaning of Life» или «The Holy Grail», но и хуже от этого не будет.

Имя «Python» быстро прижилось, тем не менее его заимствование стало причиной интересных курьезов. Например, когда в 1994 году возникла телеконференция по Python, comp.lang.python, первые несколько недель она практически полностью была оккупирована желающими обсуждать темы, касающиеся телевизионной постановки. Позднее специальное приложение к журналу «Linux Journal», касающееся Python, стало сопровождаться фотографией Гвидо, облаченного в обязательную «красную форму».

В списке рассылки Python все еще появляются случайные письма от поклонников сериала. Например, в одном письме невинно предлагалось обмениваться сценариями с участием персонажа Monty Python. Если бы автор письма понимал назначение форума, то хотя бы указал, могут ли они выполняться в разных операционных системах или нет.



Шаг 3: переход к ООП

Давайте отвлечемся на минутку и посмотрим, куда мы пришли. Итак, в настоящий момент у нас имеется две реализации базы данных: на


основе модуля shelve и на основе модуля pickle, в которой каждая запись сохраняется в отдельном файле, - этого вполне достаточно для хранения простых данных. Наши записи представлены простыми словарями, которые обеспечивают более простой и понятный доступ к полям, чем списки (то есть не по числовым индексам, а по ключам). Однако словари имеют некоторые ограничения, которые могут оказаться существенными по мере разработки программы.

Во-первых, у нас не предусмотрено место для централизованного хранения логики обработки записей. Операции извлечения фамилии и увеличения оклада, например, могут выполняться так:

>>> import shelve

>>> db = shelve.open(‘people-shelve')

>>> bob = db[‘bob']

>>> bob[‘name'].split()[-1] # вернет фамилию Боба ‘Smith’

>>> sue = db[‘sue']

>>> sue[‘pay'] *= 1.25    # увеличит оклад Сью

>>> sue[‘pay']

75000.0

>>> db[‘sue'] = sue >>> db.close()

Такое решение вполне пригодно для небольших программ. Но если когда-нибудь потребуется изменить логику извлечения фамилии или увеличения оклада, нам, возможно, придется обновить подобный программный код во множестве мест в программе. На практике даже просто отыскать все такие фрагменты может оказаться достаточно сложным делом - копирование одинаковых фрагментов программного кода рано или поздно обязательно даст знать о себе.

Подобные фрагменты предпочтительнее скрывать, то есть инкапсулировать. Эти операции можно было бы реализовать в виде функций в одном модуле и тем самым избежать избыточности, но при таком подходе функции никак не будут связаны с самими записями. Нам же желательно связать логику обработки с данными, хранящимися в базе данных, чтобы ее проще было понимать, отлаживать и многократно использовать.

Другой недостаток использования словарей для представления записей заключается в том, что со временем их становится трудно расширять. Например, представьте, что имеется набор полей данных или различные процедуры, увеличивающие оклад для разных сотрудников по-разному (например, кто-то может получать ежегодную прибавку, а кто-то - нет). Если нам когда-нибудь потребуется расширить программу, сделать это будет очень сложно, так как нет простого и естественного способа расширить простые словари. С учетом дальнейшего роста нам

хотелось бы, чтобы наше программное обеспечение предусматривало возможность расширения и настройки естественными способами.

Если вы уже погружались в изучение Python, то, наверное, знаете, что это тот случай, когда начинает проявляться привлекательность ООП:

Структурирование

Благодаря ООП появляется возможность связать логику обработки записей с самими записями - классы представляют собой программные единицы, объединяющие логику и данные, а наследование позволяет легко избежать избыточности программного кода.

Инкапсуляция

Благодаря ООП можно скрыть детали реализации таких операций, как обработка имени или увеличение оклада, внутри методов - то есть в дальнейшем мы легко сможем изменять реализацию методов, не влияя на работоспособность программного кода, использующего их.

Специализация

Применение ООП обеспечивает естественный способ дальнейшего расширения. Классы могут расширяться и специализироваться за счет создания новых подклассов, без изменения или нарушения работоспособности существующего программного кода.

Таким образом, в объектно-ориентированном программировании мы специализируем и повторно используем программный код, а не переписываем его заново. ООП в Python является дополнительной возможностью, которая, честно признаться, лучше подходит для решения стратегических, а не тактических задач. ООП лучше подходит, когда у вас имеется время для предварительного планирования, что может оказаться непозволительной роскошью, когда ваши пользователи уже начали штурмовать ворота.

Преимущества структурирования и повторного использования программного кода в крупных системах, которые продолжают развиваться в течение длительного времени, перевешивают затраты на изучение ООП и способны существенно сократить время разработки. Даже в нашем простом случае возможность специализации и снижения избыточности, которую дают классы, может оказаться решающим преимуществом.


Использование классов

ООП в Python отличается простотой использования, в значительной степени благодаря динамической модели типов. Фактически программировать в объектно-ориентированном стиле настолько просто, что я сразу же перейду к примеру: пример 1.14 реализует наши записи уже не в виде словарей, а в виде экземпляров класса.

Пример 1.14. PP4E\Preview\person_start.py

class Person:

def __init__(self, name, age, pay=0, job=None):

self.name = name self.age = age self.pay = pay self.job = job

if__name__== ‘__main__’:

bob = Person(‘Bob Smith’, 42, 30000, ‘software’) sue = Person(‘Sue Jones’, 45, 40000, ‘hardware’) print(bob.name, sue.pay)

print(bob.name.split()[-1]) sue.pay *= 1.10 print(sue.pay)

Это очень простой класс - он содержит единственный метод-конструктор, заполняющий экземпляр класса данными, переданными в виде аргументов при обращении к имени класса. Тем не менее этого вполне достаточно для представления записи, а кроме того, сюда уже можно добавить такие элементы, как значения по умолчанию для полей pay и job, чего нельзя сделать в словарях. Программный код самотестирования в конце этого файла создает два экземпляра класса (две записи) и обращается к их атрибутам (полям). Ниже приводится вывод, полученный в результате запуска этого сценария в среде IDLE (при запуске из командной строки результаты получаются такими же):

Bob Smith 40000

Smith

44000.0

Это еще не база данных, но мы могли бы, как и прежде, вставить эти объекты в список или в словарь, чтобы объединить их в одно целое:

>>> from person_start import Person >>> bob = Person(‘Bob Smith', 42)

>>> sue = Person(‘Sue Jones', 45, 40000)

>>> people = [bob, sue]    # список "базы данных”

>>> for person in people:

print(person.name, person.pay)

Bob Smith 0 Sue Jones 40000

>>> x = [(person.name, person.pay) for person in people]

>>> x

[(‘Bob Smith’, 0), (‘Sue Jones’, 40000)]

>>> [rec.name for rec in people if rec.age >= 45]    # SQL-подобный запрос

[‘Sue Jones’]

>>> [(rec.age ** 2 if rec.age >= 45 else rec.age) for rec in people]

[42, 2025]

Обратите внимание, что для Боба был установлен оклад (поле pay) по умолчанию, равный 0, потому что при создании записи мы не указали сумму в соответствующем аргументе (может быть, Сью его поддерживает?). Мы также могли бы реализовать класс, представляющий базу данных, возможно, как подкласс списка или словаря, добавив в него методы вставки и удаления, реализующие особенности функционирования базы данных. Однако пока мы откажемся от этого, потому что гораздо полезнее реализовать сохранение записей в хранилище, которое уже обладает методами записи и чтения. Но прежде чем попытаться использовать хранилище, добавим в наши записи немного логики.


Добавляем поведение

Пока что наш класс - это всего лишь данные: он заменил ключи словаря атрибутами объекта, но не добавляет ничего нового сверх того, что у нас было прежде. Чтобы задействовать всю мощь классов, необходимо добавить реализацию поведения. Заключая реализацию поведения в методы класса, мы сможем изолировать клиентов от влияния изменений в будущем. А объединяя методы в единое целое с данными, мы обеспечиваем естественное место, где другие будут искать наш программный код. В некотором смысле классы объединяют в себе записи и программы, обрабатывающие эти записи, - методы реализуют логику интерпретации и изменения данных (этот стиль программирования потому и называется объектно-ориентированным, что при таком подходе всегда обрабатываются данные объектов).

Например, в примере 1.15 добавляется логика получения фамилии и увеличения оклада в виде методов. Для доступа к обрабатываемому экземпляру (записи) методы используют аргумент self.

Пример 1.15. PP4E\Preview\person.py

class Person:

def __init__(self, name, age, pay=0, job=None):

self.name = name self.age = age self.pay = pay self.job = job def lastName(self):

return self.name.split()[-1] def giveRaise(self, percent): self.pay *= (1.0 + percent)

if__name__== ‘__main__’:

bob = Person(‘Bob Smith’, 42, 30000, ‘software’) sue = Person(‘Sue Jones’, 45, 40000, ‘hardware’) print(bob.name, sue.pay)

print(bob.lastName())

sue.giveRaise(.10)

print(sue.pay)

Если запустить этот сценарий, он выведет те же результаты, что и предыдущий, но теперь эти результаты возвращаются методами, а не извлекаются с помощью жестко зашитой логики, которая всегда оказывается избыточной, когда бы она ни применялась:

Bob Smith 40000

Smith

44000.0


Добавляем наследование

И наконец, рассмотрим еще одно расширение наших записей, прежде чем перейти к реализации их сохранения: поскольку теперь записи реализованы в виде класса, они обретают естественную возможность специализации через механизм наследования в Python. Пример 1.16 специализирует предыдущий класс Person, предусматривая 10-процентную надбавку, добавляемую при увеличении оклада менеджеров (любые совпадения с реальными примерами из жизни являются случайными).

Пример 1.16. PP4E\Preview\manager.py

from person import Person

class Manager(Person):

def giveRaise(self, percent, bonus=0.1): self.pay *= (1.0 + percent + bonus)

if__name__== ‘__main__’:

tom = Manager(name=’Tom Doe’, age=50, pay=50000)

print(tom.lastName())

tom.giveRaise(.20)

print(tom.pay)

Если запустить этот сценарий, он выведет следующее:

Doe

65000.0

Здесь объявление класса Manager находится в отдельном модуле, но это объявление точно так же можно поместить в модуль person (Python не требует создавать отдельные модули для каждого класса). Он наследует конструктор и метод lastName от своего суперкласса и специализирует метод giveRaise (как будет показано позднее, существуют различные способы реализации этого расширения). Поскольку данное дополнение было оформлено в виде нового подкласса, оно никак не отразится на работе экземпляров оригинального класса Person. Экземпляры, представляющие информацию о Бобе и Сью, например, унаследуют оригинальную логику увеличения оклада, а экземпляр, представляющий информацию о Томе, получит специализированную версию, потому что он является экземпляром другого класса. В ООП программы разрабатываются за счет специализации программного кода, а не его изменения.

Фактически программа, использующая наши объекты, вообще ничего не должна знать об особенностях реализации метода giveRaise - объект сам определяет порядок выполнения той или иной операции, опираясь на класс, экземпляром которого он является. Пока объект поддерживает ожидаемый интерфейс (в данном случае метод giveRaise), он будет совместим с вызывающей программой, независимо от конкретного типа объекта и даже независимо от особенностей реализации этого метода, которая может действовать совершенно иначе.

Если вам уже приходилось изучать язык Python, возможно, вы знаете, что такое поведение называется полиморфизмом. Это одно из основных свойств языка, и оно объясняет значительную долю гибкости программного кода. Результат вызова метода giveRaise в следующем фрагменте зависит от того, к какому классу принадлежит обрабатываемый объект obj, - Том получит 20-процентное повышение оклада, а не 10-процентное, потому что соответствующий ему экземпляр является экземпляром специализированного класса Manager:

>>> from person import Person >>> from manager import Manager

>>> bob = Person(name='Bob Smith', age=42, pay=10000)

>>> sue = Person(name='Sue Jones', age=45, pay=20000)

>>> tom = Manager(name='Tom Doe', age=55, pay=30000)

>>> db = [bob, sue, tom]

>>> for obj in db:

obj.giveRaise(.10) # метод по умолчанию или специализированный >>> for obj in db:

print(obj.lastName(), ‘=>', obj.pay)

Smith => 11000.0 Jones => 22000.0 Doe => 36000.0


Реструктуризация программного кода

Прежде чем двинуться дальше, рассмотрим еще несколько альтернативных вариантов реализации. Большинство из них подчеркивают преимущества модели ООП в Python и рассматриваются здесь для краткого знакомства.

Расширение методов

Во-первых, обратите внимание на некоторую избыточность в примере 1.16: расчет увеличения оклада производится в двух местах (в двух классах). Мы могли бы реализовать специализированный класс Manager, не замещая унаследованный метод giveRaise новой реализацией, а расширяя его:

class Manager(Person):

def giveRaise(self, percent, bonus=0.1):

Person.giveRaise(self, percent + bonus)

Вся хитрость заключается в непосредственном вызове версии метода суперкласса и явной передаче ему аргумента self. При таком подходе мы также переопределяем метод, но на этот раз мы просто вызываем универсальную версию после добавления 10-процентной надбавки (предусмотренной по умолчанию) к указанному значению в процентах. Этот прием позволяет уменьшить избыточность программного кода (оригинальная логика метода giveRaise находится в одном только месте, что упрощает возможность ее изменения в будущем), и его особенно удобно использовать при переопределении методов-конструкторов суперклассов.

Если вы уже знакомы с особенностями ООП в Python, то должны знать, что этот прием работает благодаря возможности вызова методов как относительно экземпляра, так и относительно имени класса. Вообще говоря, следующие два вызова являются эквивалентными, и можно использовать обе формы:

instance.method(arg1, arg2) class.method(instance, arg1, arg2)

В действительности, первая форма отображается во вторую - при вызове метода относительно экземпляра интерпретатор Python отыскивает в дереве наследования ближайший класс, в котором имеется требуемый метод, и вызывает его, автоматически передавая экземпляр в первом аргументе. В любом случае, внутри метода giveRaise аргумент self будет ссылаться на экземпляр, являющийся объектом вызова.

Формат отображения

Чтобы получить дополнительное удовольствие от использования ООП, мы могли бы добавить в наши классы несколько методов перегрузки операторов. Например, метод__str__, реализованный здесь, возвраща

ет отформатированную строку для отображения наших объектов при печати объектов целиком - такое представление выглядит гораздо лучше, чем предусмотренное по умолчанию:

class Person:

def __str__(self):

return ‘<%s => %s>’ % (self.__class__.__name__, self.name)

tom = Manager(‘Tom Jones’, 50)

print(tom)    # выведет: <Manager => Tom Jones>

Здесь атрибут__class__содержит ссылку на ближайший класс, экземпляром которого является объект self, даже при том, что метод__str__

может оказаться унаследованной версией. Метод__str__позволяет вы

водить экземпляры непосредственно, вместо того чтобы выводить отдельные атрибуты. В метод__str__можно было бы добавить цикл, выполняющий обход словаря атрибутов__dict__экземпляра и отобража

ющий все атрибуты. Но это лишь краткий обзор, поэтому оставим это предложение для самостоятельного упражнения.

Мы могли бы даже реализовать метод__add__, чтобы оператор + авто

матически вызывал метод giveRaise. Нужно ли это - другой вопрос. Использование оператора + для увеличения оклада может быть истолковано неправильно теми, кто впоследствии будет читать наш программный код.

Специализация конструктора

Наконец, обратите внимание, что в примере 1.16 при создании экземпляра класса Manager мы не передаем конструктору аргумент job. При необходимости мы могли бы передавать это значение в виде именованного аргумента, как показано ниже:

tom = Manager(name=’Tom Doe’, age=50, pay=50000, job=’manager’)

Причина, по которой мы в примере не включили передачу аргумента job, заключается в том, что в этом нет необходимости: если создается новый экземпляр класса Manager, занимаемая должность уже подразумевается классом. Тем не менее, чтобы не оставлять поле job пустым, возможно, имеет смысл явно реализовать конструктор для класса Manager, который будет заполнять это поле автоматически:

class Manager(Person):

def __init__(self, name, age, pay):

Person.__init__(self, name, age, pay, ‘manager’)

Теперь при создании экземпляра класса Manager его поле job будет заполняться автоматически. Вся хитрость заключается в явном вызове версии метода суперкласса, так же, как мы делали при реализации метода giveRaise выше. Единственное отличие здесь - необычное имя метода-конструктора.

Альтернативные классы

В последующих примерах мы не будем использовать ни одно из трех расширений, представленных в этом разделе, но для демонстрации их в действии соберем все эти идеи в примере 1.17, где представлены альтернативные реализации классов Person и Manager.

Пример 1.17. PP4E\Preview\person_alternative.py

Альтернативные реализации классов Person и Manager с данными, методами и с перегрузкой операторов (не используется в объектах, предусматривающих возможность сохранения)

class Person:

универсальное представление человека: данные+логика

def __init__(self, name, age, pay=0, job=None):

self.name = name self.age = age self.pay = pay self.job = job def lastName(self):

return self.name.split()[-1] def giveRaise(self, percent): self.pay *= (1.0 + percent)

def __str__(self):

return (‘<%s => %s: %s, %s>’ %

(self.__class__.__name__, self.name, self.job, self.pay))

class Manager(Person):

класс со специализированным методом giveRaise, наследующий обобщенные методы lastName и __str__

def __init__(self, name, age, pay):

Person.__init__(self, name, age, pay, ‘manager’)

def giveRaise(self, percent, bonus=0.1):

Person.giveRaise(self, percent + bonus)

if__name__== ‘__main__’:

bob = Person(‘Bob Smith’, 44)

sue = Person(‘Sue Jones’, 47, 40000, ‘hardware’)

tom = Manager(name=’Tom Doe’, age=50, pay=50000)

print(sue, sue.pay, sue.lastName())

for obj in (bob, sue, tom):

obj.giveRaise(.10) # вызовет метод giveRaise объекта obj print(obj)    # вызовет обобщенную версию метода __str__

Обратите внимание на полиморфизм в цикле for, находящемся в программном коде самопроверки этого модуля: все три объекта используют один и тот же конструктор, метод lastName и методы вывода, но при обращении к методу giveRaise вызывается версия в зависимости от класса, на основе которого был создан экземпляр. Если запустить сце-

нарий из примера 1.17, он выведет в стандартный поток вывода приведенные ниже строки; поле job в экземпляре класса Manager заполняется конструктором, форматированный вывод наших объектов осуществляется с помощью нового метода__str__, а новая версия метода giveRaise

в классе Manager действует точно так же, как и прежде:

<Person => Sue Jones: hardware, 40000> 40000 Jones <Person => Bob Smith: None, 0.0>

<Person => Sue Jones: hardware, 44000.0>

<Manager => Tom Doe: manager, 60000.0>

Такая реструктуризация программного кода часто применяется по мере роста и развития иерархий классов. Фактически мы никак не сможем увеличить оклад тем, у кого он оказался равным нулю (Бобу явно не повезло), поэтому нам, вероятно, необходимо предусмотреть возможность прямого изменения оклада, но оставим это усовершенствование до следующей версии. Самое приятное, что гибкость и удобочитаемость, присущие языку Python, существенно упрощают реструктуризацию программного кода - вы легко и просто сможете реструктурировать свои программы. Если прежде вы не пользовались языком Python, то со временем обнаружите, что разработка программ на Python выполняется быстро, поэтапно и в интерактивном режиме, что хорошо подходит для постоянно изменяющихся потребностей реальных проектов.


Добавляем возможность сохранения

Пришло время продолжить. Теперь у нас имеются реализации записей, поддающиеся специализации и включающие логику их обработки, в форме классов. Осталось сделать последний маленький шаг и реализовать сохранение наших записей, основанных на классах. Мы могли бы снова сохранять каждую запись в отдельном файле с помощью модуля pickle, но модуль shelve предоставляет точно такую же возможность, а кроме того, его гораздо проще использовать. Как это сделать, демонстрируется в примере 1.18.

Пример 1.18. PP4E\Preview\make_db_classes.py

import shelve

from person import Person

from manager import Manager

bob = Person(‘Bob Smith’, 42, 30000, ‘software’) sue = Person(‘Sue Jones’, 45, 40000, ‘hardware’) tom = Manager(‘Tom Doe’, 50, 50000)

db = shelve.open(‘class-shelve’)

db[‘bob’] = bob

db[‘sue’] = sue

db[‘tom’] = tom

db.close()

Этот сценарий создает три экземпляра (два экземпляра оригинального класса и один - его специализированной версии) и присваивает их ключам вновь созданного хранилища. Другими словами, сценарий создает хранилище с экземплярами классов. Для нашего программного кода база выглядит в точности, как словарь экземпляров классов, с той лишь разницей, что словарь верхнего уровня отображается в файл хранилища, как и прежде. Убедиться, что все работает, поможет сценарий в примере 1.19, который читает содержимое хранилища и выводит значения полей записей.

Пример 1.19. PP4E\Preview\dump_db_classes.py

import shelve

db = shelve.open(‘class-shelve’) for key in db:

print(key, ‘=>\n ‘, db[key].name, db[key].pay)

bob = db[‘bob’]

print(bob.lastName())

print(db[‘tom’].lastName())

Обратите внимание, что в этом примере нам не требуется импортировать класс Person, чтобы извлекать экземпляры из хранилища или вызывать их методы. Когда экземпляры сохраняются с помощью модуля shelve или pickle, используемая этими модулями система сохранения записывает в файл не только значения атрибутов экземпляров, но и дополнительную информацию, позволяющую позднее автоматически определить местоположение классов при извлечении экземпляров (модули с определениями классов просто должны находиться в пути поиска модулей при выполнении операции загрузки). Это сделано специально, потому что определение класса и его экземпляры в хранилище сохраняются отдельно; вы можете изменить класс, чтобы изменить порядок интерпретации экземпляров при загрузке (подробнее об этом рассказывается далее в книге). Ниже приводятся результаты запуска сценария dump_db_classes.py сразу после создания хранилища с помощью сценария make_db_classes.py:

bob =>

Bob Smith 30000 sue =>

Sue Jones 40000 tom =>

Tom Doe 50000 Smith Doe

Как показано в примере 1.20, изменение информации в базе данных выполняется так же просто, как и прежде (сравните с примером 1.13), но на этот раз вместо ключей словарей используются атрибуты экземпляров, а жестко зашитую логику изменений заменили вызовы методов классов. Обратите внимание, что нам по-прежнему необходимо извлечь запись, изменить ее и вновь присвоить тому же самому ключу.

Пример 1.20. PP4E\Preview\update_db_classes.py

import shelve

db = shelve.open(‘class-shelve’)

sue = db[‘sue’] sue.giveRaise(.25) db[‘sue’] = sue

tom = db[‘tom’] tom.giveRaise(.20) db[‘tom’] = tom db.close()

И наконец, ниже приводятся результаты повторного запуска сценария dump_db_classes.py после запуска сценария update_db_classes.py. Том и Сью теперь имеют новые оклады, потому что теперь соответствующие объекты сохраняются в хранилище. Кроме того, мы могли бы открыть и исследовать содержимое хранилища в интерактивной оболочке Python - несмотря на свою долговечность, хранилище является всего лишь объектом Python, содержащим другие объекты Python.

bob =>

Bob Smith 30000 sue =>

Sue Jones 50000.0 tom =>

Tom Doe 65000.0

Smith

Doe

Том и Сью получили прибавку к окладу, потому что теперь эти объекты - объекты, сохраненные в базе данных. Хотя модуль shelve также способен сохранять объекты более простых типов, таких как списки и словари, однако классы позволяют нам объединять данные и поведение в единые сохраняемые элементы. В некотором смысле атрибуты экземпляров и методы классов равносильны записям и обрабатывающим их программам, используемым в более традиционных решениях.


Другие разновидности баз данных

К настоящему моменту мы создали вполне функциональную базу данных: наши классы одновременно реализуют хранение данных записей и их обработку и заключают в себе реализацию поведения. А модули pickle и shelve обеспечивают простой способ сохранения нашей базы данных между запусками программы. Это не реляционная база данных (она хранит объекты, а не таблицы, и запросы имеют вид программного кода на языке Python, обрабатывающего объекты), но ее вполне достаточно для многих видов программ.

Если потребуются более широкие функциональные возможности, мы сможем перевести это приложение на использование более мощных инструментов. Например, если нам потребуется полноценная поддержка запросов на языке SQL, мы сможем использовать библиотеки, позволяющие сценариям на языке Python путем переноса взаимодействовать с реляционными базами данных, такими как MySQL, PostgreSQL и Oracle.

Механизмы ORM (Object Relational Mapper - объектно-реляционное отображение), такие как SQLObject и SqlAlchemy, предлагают иной подход, сохраняющий представление записей в виде объектов Python, но преобразуя их в и из представления таблиц в реляционных базах данных, в некотором смысле обеспечивая сочетание лучших черт обоих миров - с синтаксисом классов Python сверху и надежными базами данных внутри.

Кроме того, существует открытая система ZODB, реализующая более функциональную объектную базу данных для программ на языке Python, с поддержкой особенностей, отсутствующих в хранилищах shelve, включая параллельное изменение записей, подтверждение и откат транзакций, автоматическое обновление компонентов, изменившихся в оперативной памяти, и многие другие. Мы познакомимся с этими, более совершенными инструментами, созданными сторонними разработчиками, в главе 17. А теперь перейдем к созданию лица нашей системы.

Автобусы признаны опасными

На протяжении многих лет Python пользовался мощной и добровольной поддержкой и отдельных лиц, и организаций. В настоящее время конференции и другие некоммерческие мероприятия в сообществе Python проходят при содействии некоммерческой организации Python Software Foundation (PSF). Организации PSF предшествовала организация PSA - группа, которая первоначально была образована в ответ на когда-то давно возникшее в телеконференции Python обсуждение полусерьезного вопроса: «Что будет, если Гвидо попадет под автобус?»

В настоящее время создатель языка Python, Гвидо ван Россум (Guido van Rossum), по-прежнему является верховным арбитром при поступлении предложений о внесении изменений в Python. Он официально был помазан на пост Великодушного Пожизненного Диктатора (Benevolent Dictator for Life, BDFL) на первой же конференции Python, и по-прежнему окончательное решение о принятии


изменений в языке остается за ним (и многие изменения, ведущие к несовместимости, за исключением версии 3.0, несовместимость которой была предусмотрена заранее, он обычно отклоняет: это хорошая черта для языков программирования, потому что Python должен изменяться достаточно медленно и изменения не должны нарушать обратную совместимость).

Но, как бы то ни было, огромное количество пользователей Python помогает поддерживать язык, работает над расширениями, исправляет ошибки и так далее. Это по-настоящему совместный проект. Фактически разработка Python сейчас является совершенно открытым процессом - любой желающий сможет получить самые свежие файлы с исходными текстами или отправить свои исправления, посетив веб-сайт проекта (подробности вы найдете по адресу http://www.python.org).

Разработка Python, как пакета открытого программного обеспечения, в действительности находится в руках большого количества программистов, разбросанных по всему свету. Поэтому, даже если Великодушный Пожизненный Диктатор когда-нибудь передаст факел преемнику, Python практически наверняка продолжит пользоваться поддержкой своих пользователей. По своей природе открытые проекты, хотя и не без отступлений, обычно отражают потребности сообществ их пользователей в большей степени, чем потребности отдельных личностей или учредителей.

С учетом популярности Python нападение со стороны автобуса уже не кажется таким опасным, как раньше. Впрочем, Гвидо может считать иначе.



Шаг 4: добавляем интерфейс командной строки

Пока что наша программа состоит из экземпляров классов, реализованных в предыдущем разделе, которые сохранены в файле хранилища. В качестве хранилища она делает достаточно, но для доступа к содержимому и работы с ним нам потребуется запускать другие сценарии или вводить программный код в интерактивной оболочке. Улучшить ситуацию достаточно просто: необходимо написать универсальную программу, которая будет взаимодействовать с пользователем либо с помощью окна консоли, либо с помощью графического интерфейса.

Интерфейс командной строки к хранилищу

Начнем с наиболее простого варианта. В самом простом виде интерфейс к базе данных должен давать пользователям возможность вводить ключи и значения в окне консоли (вместо того чтобы писать программный код на языке Python). Сценарий в примере 1.21 реализует простейший цикл интерактивных взаимодействий, позволяя пользователю запрашивать объекты, имеющиеся в хранилище.

Пример 1.21. PP4E\Preview\peopleinteract_query.py

# интерактивные запросы import shelve

fieldnames = (‘name’, ‘age’, ‘job’, ‘pay’) maxfield = max(len(f) for f in fieldnames) db = shelve.open(‘class-shelve’)

while True:

key = input(‘\nKey? => ‘)    # ключ или пустая строка, возбуждает исключение

# при вводе EOF

if not key: break try:

record = db[key] # извлечь запись по ключу и вывести except:

print(‘No such key “%s”!’ % key) else:

for field in fieldnames:

print(field.ljust(maxfield), ‘=>’, getattr(record, field))

Для извлечения значений атрибутов в этом сценарии используется встроенная функция getattr, а для форматирования вывода используется строковый метод ljust, выравнивающий строку по левому краю (значение maxfield, порожденное выражением-генератором, представляет длину наибольшего имени поля). После запуска сценария он входит в цикл, предлагает пользователю ввести ключ (со стандартного потока ввода, который обычно соответствует окну консоли) и отображает извлеченную запись поле за полем. Ввод пустой строки завершает сеанс работы со сценарием. Предположим, хранилище находится в том же состоянии, в каком мы его оставили ближе к концу предыдущего раздела:

...\PP4E\Preview> dump_db_classes.py

bob =>

Bob Smith 30000 sue =>

Sue Jones 50000.0 tom =>

Tom Doe 65000.0 Smith Doe

Мы сможем использовать наш новый сценарий для запроса объектов из базы данных в интерактивном режиме:

...\PP4E\Preview> peopleinteract_query.py

Key? => sue name => Sue Jones

age => 45 job => hardware pay => 50000.0

Key? => nobody No such key “nobody”!

Key? =>

Сценарий в примере 1.22 является следующим шагом к созданию интерфейса, позволяющим вносить изменения в интерактивном режиме. Для заданного ключа он позволяет ввести значения для каждого из полей и либо изменяет существующую запись, либо создает новую, после чего сохраняет ее с указанным ключом.

Пример 1.22. PP4E\Preview\peopleinteract_update.py

# интерактивные изменения

import shelve

from person import Person

fieldnames = (‘name’, ‘age’, ‘job’, ‘pay’)

db = shelve.open(‘class-shelve’) while True:

key = input(‘\nKey? => ‘) if not key: break if key in db:

record = db[key]    # изменить существующую

else:    # или создать новую запись

record = Person(name=’?’, age=’?’) # для eval: строки в кавычках for field in fieldnames:

currval = getattr(record, field)

newtext = input(‘\t[%s]=%s\n\t\tnew?=>’ % (field, currval)) if newtext:

setattr(record, field, eval(newtext)) db[key] = record db.close()

Обратите внимание, что для преобразования введенных значений в этом сценарии используется функция eval (это позволяет вводить любые объекты Python, но это означает, что строки при вводе должны заключаться в кавычки). Функция setattr присваивает значение атрибуту, имя которого задается строкой. Этот сценарий позволит добавлять или изменять любое количество записей - чтобы сохранить прежнее значение поля в записи, достаточно просто нажать клавишу Enter в ответ на просьбу ввести новое значение:

Key? => tom

[name]=Tom Doe new?=>

[age]=50

new?=>56

[job]=None

new?=>'mgr'

[pay]=65000.0

new?=>90000

Key? => nobody [name]=?

new?=>'John Doh'

[age]=?

new?=>55

[job]=None

new?=>

[pay]=0

new?=>None

Key? =>

Этот сценарий все еще очень прост (в нем, например, не предусмотрена обработка ошибок), но пользоваться им гораздо удобнее, чем вручную открывать и вносить изменения в хранилище в интерактивной оболочке Python, особенно для тех, кто не занимается программированием. Запустим сценарий peopleinteract_query.py, чтобы проверить изменения, которые мы внесли (если кому-то такой подход покажется утомительным, он сможет объединить оба сценария в один, ценой дополнительного программного кода и повышения сложности для пользователя):

Key? => tom name => Tom Doe age => 56 job => mgr pay => 90000

Key? => nobody name => John Doh age => 55 job => None pay => None

Key? =>


Шаг 5: добавляем графический интерфейс

Интерфейс командной строки, реализованный в предыдущем разделе, вполне работоспособен, и для некоторых пользователей его вполне достаточно, если предположить, что им не досаждает ввод команд в окне консоли. Однако, приложив небольшое количество усилий, мы сможем добавить современный графический интерфейс, более простой в использовании, снижающий риск ошибок и определенно более привлекательный.


Основы графических интерфейсов

Как будет показано далее в этой книге, программистам, использующим язык Python, доступно множество разнообразных инструментов создания графических интерфейсов: tkinter, wxPython, PyQt, PythonCard, Dabo и многие другие. Из них в составе Python поставляется только tkinter, который де-факто считается стандартным инструментом.

tkinter - это легковесный инструмент, который прекрасно интегрируется с языками сценариев, такими как Python. Его легко использовать для реализации простых графических интерфейсов, а дополнительные расширения к нему и применение приемов объектно-ориентированного программирования позволяют без особых затрат реализовать более сложные интерфейсы. Кроме того, реализация графического интерфейса на базе tkinter способна без каких-либо модификаций работать в Windows, Linux/Unix и Macintosh - достаточно просто перенести файлы с исходными текстами на компьютер, где предполагается использовать программу с графическим интерфейсом. В tkinter отсутствуют разнообразные «бантики и рюшечки», имеющиеся в более развитых инструментах, таких как wxPython или PyQt, но это же является основной причиной его относительной простоты, что делает его идеальным инструментом для тех, кто только начинает создавать программы с графическим интерфейсом.

Инструмент tkinter разработан для использования в сценариях, поэтому программирование графических интерфейсов с его применением организовано с достаточной очевидностью. Далее в этой книге мы поближе познакомимся со всеми его понятиями и возможностями. А в качестве первого примера рассмотрим программу, использующую tkinter, которая содержит всего несколько строк программного кода, как видно из примера 1.23.

Пример 1.23. PP4E\Preview\tkinter001.py

from tkinter import *

Label(text=’Spam’).pack()

mainloop()

Импортировав модуль tkinter (на самом деле, в Python 3 - пакет модулей), мы получаем возможность обращаться к различным экранным конструкциям (или «виджетам»), таким как Label; методам менеджера геометрии, таким как pack; предварительно установленным комплектам настроек виджетов, таким как TOP и RIGHT, определяющим край для выравнивания компонентов и используемых при вызове метода pack; и к функции mainloop, запускающей цикл обработки событий.

Это не самый полезный сценарий с графическим интерфейсом из когда-либо созданных, но он демонстрирует основы использования tkinter и создает полнофункциональное окно, как показано на рис. 1.1, -всего тремя строками программного кода. Изображение окна, как и всех других графических интерфейсов в этой книге, было получено в Windows 7. Окно действует одинаково и на других платформах (таких как Mac OS X, Linux и в более старых версиях Windows), но при этом имеет внешний вид, характерный для той платформы, на которой запускается сценарий.

Рис. 1.1. Окно сценария tkinter001.py

Вы можете запустить этот сценарий из среды IDLE, из командной строки или щелчком на ярлыке - то есть точно так же, как любой другой сценарий на языке Python. Сам инструмент tkinter является стандартной частью Python и работает, что называется, «из коробки», на всех платформах, включая Windows. Однако в некоторых операционных системах может потребоваться выполнить дополнительные шаги по установке и настройке (подробнее об этом будет рассказываться далее в книге).

Совсем немного усилий требуется приложить, чтобы создать графический интерфейс, откликающийся на действия пользователя: сценарий в примере 1.24 реализует графический интерфейс с кнопкой, щелчок на которой приводит к вызову функции reply.

Пример 1.24. PP4E\Preview\ tkinter101.py

from tkinter import *

from tkinter.messagebox import showinfo

def reply():

showinfo(title=’popup’, message=’Button pressed!’) window = Tk()

button = Button(window, text=’press’, command=reply)

button.pack()

window.mainloop()

Этот пример также достаточно прост. Он явно создает главное окно Tk приложения, которое будет служить контейнером для кнопки, и воспроизводит на экране простое окно, как показано на рис. 1.2 (при создании нового виджета в модуле tkinter принято передавать контейнерные элементы в первом аргументе; который по умолчанию ссылается на главное окно). Но на этот раз при каждом щелчке на кнопке с надписью press программа будет откликаться вызовом программного кода, который выводит диалог, как показано на рис. 1.3.

Рис. 1.2. Главное окно сценария tkinter101.py

Обратите внимание, что окно диалога выглядит в Windows 7 (на платформе, где сделан снимок с экрана) именно так, как и должно выглядеть. Повторюсь, что модуль tkinter обеспечивает внешний вид создаваемых элементов, характерный для той платформы, на которой запускается сценарий. Мы можем изменять самые разные аспекты графического интерфейса (например, цвет и шрифт, текст и ярлык в заголовке окна, помещать на кнопки изображения вместо текста), но одно из преимуществ использования модуля tkinter - в том, что нам необходимо будет изменять лишь те параметры, которые требуется изменить.

Рис. 1.3. Типичное окно диалога, созданное сценарием tkinter101.py


ООП при разработке графических интерфейсов

Все примеры реализации графического интерфейса, представленные до сих пор, имели вид самостоятельных сценариев, включающих функцию обработки событий. В крупных программах часто более полезно бывает оформить программный код, создающий графический интерфейс, в виде подкласса виджета Frame из библиотеки tkinter - контейнера для других виджетов. В примере 1.25 приводится переработанная версия предыдущего сценария с одной кнопкой, в которой графический интерфейс реализован в виде класса.

Пример 1.25. PP4E\Preview\tkinter102.py

from tkinter import *

from tkinter.messagebox import showinfo

class MyGui(Frame):

def __init__(self, parent=None):

Frame.__init__(self, parent)

button = Button(self, text=’press’, command=self.reply) button.pack() def reply(self):

showinfo(title=’popup’, message=’Button pressed!’)

if__name__== ‘__main__’:

window = MyGui()

window.pack()

window.mainloop()

Обработчик событий от кнопки - это связанный метод self. reply, то есть объект, хранящий в себе значение self и ссылку на метод reply. Данный пример воспроизводит то же самое окно и диалог, что и сценарий в примере 1.24 (рис. 1.2 и 1.3). Но теперь графический интерфейс реализован как подкласс класса Frame и потому автоматически становится присоединяемым компонентом - то есть мы сможем добавить все виджеты, создаваемые этим классом, как единый пакет в любой другой графический интерфейс; достаточно просто присоединить экземпляр этого класса к графическому интерфейсу. Как это делается, показано в примере 1.26.

Пример 1.26. PP4E\Preview\attachgui.py

from tkinter import * from tkinter102 import MyGui

#    главное окно приложения mainwin = Tk()

Label(mainwin, text=__name__).pack()

#    окно диалога popup = Toplevel()

Label(popup, text=’Attach’).pack(side=LEFT)

MyGui(popup).pack(side=RIGHT)    # присоединить виджеты

mainwin.mainloop()

Этот сценарий присоединяет наш графический интерфейс с одной кнопкой к другому окну popup типа Toplevel, которое передается импортированному приложению через вызов конструктора, как родительский компонент (кроме того, вы получаете доступ к главному окну Tk - как будет показано позже, вы всегда сможете получить к нему доступ, независимо от того, создается оно явно или нет). На этот раз наш пакет виджетов, содержащий единственную кнопку, присоединяется к правому краю контейнера. Если запустить этот пример, вы увидите картину, изображенную на рис. 1.4, где кнопка с надписью press - это наш подкласс класса Frame.

Рис. 1.4. Присоединение интерфейсных элементов

Кроме того, так как наш графический интерфейс MyGui оформлен в виде класса, мы получаем возможность настраивать его за счет использования механизма наследования - достаточно просто определить подкласс, реализующий необходимые отличия. Например, можно изменить реализацию метода reply, чтобы он делал что-то особенное, как показано в примере 1.27.

Пример 1.27. PP4E\Preview\customizegui.py

from tkinter import mainloop

from tkinter.messagebox import showinfo

from tkinter102 import MyGui

class CustomGui(MyGui):    # наследует метод __init__

def reply(self):    # замещает метод reply

showinfo(title=’popup’, message=’Ouch!’)

if__name__== ‘__main__’:

CustomGui().pack()

mainloop()

Если запустить этот сценарий, он создаст то же главное окно с кнопкой, что и оригинальный класс MyGui. Но щелчок на кнопке сгенерирует иной ответ, как показано на рис. 1.5, потому что будет вызвана другая версия метода reply.

Несмотря на свою простоту, эти графические интерфейсы иллюстрируют несколько важных идей. Как будет показано далее в этой книге, использование приемов ООП, таких как наследование и присоединение в данных примерах, позволяет повторно использовать пакеты виджетов в наших программах - калькуляторы, текстовые редакторы и подобные им интерфейсы легко могут настраиваться и добавляться в дру-

Рис. 1.5. Изменение графического интерфейса

гие графические интерфейсы как компоненты, если они реализованы в виде классов. Кроме того, подклассы виджетов способны обеспечить единство внешнего вида или стандартизованное поведение всех своих экземпляров, что в иных терминах может быть названо стилями и темами графического интерфейса. Это обычный побочный продукт Python и ООП.


Получение ввода от пользователя

В примере 1.28 приводится заключительный пример вводного сценария, демонстрирующий, как получить ввод пользователя с помощью виджета Entry и вывести его в диалоге. Использованная здесь инструкция lambda откладывает вызов функции reply до момента, когда ввод пользователя можно будет передать дальше, - это типичный прием программирования при работе с модулем tkinter. Без инструкции lambda функция reply была бы вызвана в момент создания кнопки, а не в момент щелчка на ней (мы могли бы использовать глобальную переменную ent внутри функции reply, но это делает функцию менее универсальной). Кроме того, этот пример демонстрирует, как изменить ярлык и текст в заголовке окна верхнего уровня. В данном случае файл ярлыка находится в том же каталоге, что и сценарий (если в вашей системе вызов метода iconbitmap терпит неудачу, попробуйте закомментировать этот вызов - к сожалению, в разных платформах работа с ярлыками выполняется по-разному).

Пример 1.28. PP4E\Preview\tkinter103.py

from tkinter import *

from tkinter.messagebox import showinfo

def reply(name):

showinfo(title=’Reply’, message=’Hello %s!’ % name)

top = Tk() top.title(‘Echo’)

top.iconbitmap(‘py-blue-trans-out.ico’)

Label(top, text=”Enter your name:”).pack(side=TOP)

ent = Entry(top)

ent.pack(side=TOP)

btn = Button(top, text=”Submit”, command=(lambda: reply(ent.get()))) btn.pack(side=LEFT)

top.mainloop()

В этом примере к главному окну Tk присоединяются всего три виджета. Далее вы узнаете, как использовать вложенные контейнеры Frame виджетов для достижения различных схем размещения этих трех виджетов. На рис. 1.6 изображены главное окно и окно диалога, появляющееся после щелчка на кнопке Submit. Нечто похожее мы увидим далее в этой главе, но реализованное на языке разметки HTML - для отображения в веб-броузере.

Рис. 1.6. Получение ввода пользователя

Программный код, представленный выше, демонстрирует множество особенностей программирования графических интерфейсов, но модуль tkinter обладает намного более широкими возможностями, чем можно было бы заключить из этих примеров. В модуле tkinter реализованы более 20 виджетов и еще множество способов дать пользователю возможность вводить данные, включая элементы ввода многострочного текста, «холсты» для рисования, раскрывающиеся меню, радиокнопки и флажки, полосы прокрутки, а также механизмы управления размещением виджетов и обработки событий. Помимо модуля tkinter в состав стандартной библиотеки языка Python входят также расширения, такие как PMW, и инструменты Tix и ttk, которые добавляют дополнительные виджеты для использования в графических интерфейсах, построенных на базе tkinter, и позволяющие придать интерфейсу еще более профессиональный внешний вид. Чтобы в общих чертах продемонстрировать имеющиеся возможности, давайте задействуем модуль tkinter для создания интерфейса к нашей базе данных.


Графический интерфейс к хранилищу

Первое, что необходимо сделать для нашего приложения баз данных, - это создать графический интерфейс для просмотра хранящихся данных (форму с именами и значениями полей) и реализовать способ извлечения записей по ключу. Также было бы полезно иметь возможность изменять значения полей в записях и добавлять новые записи, заполняя пустую форму. Для простоты мы реализуем единый графический интерфейс, позволяющий решать все эти задачи. На рис. 1.7 изображено окно, которое мы создадим, отображенное в Windows 7, с содержимым записи, полученной по ключу sue (здесь снова используется хранилище в том состоянии, в каком мы его оставили в последний раз). Данная запись в действительности является экземпляром нашего класса, сохраненным в файле хранилища, но пользователю это должно быть безразлично.

Рис. 1.7. Главное окно сценария peoplegui.py

Реализация графического интерфейса

Кроме того, чтобы не усложнять пример, допустим, что все записи в базе данных имеют один и тот же набор полей. Было бы совсем несложно создать более универсальную реализацию, способную работать с любыми наборами полей (и при этом создать универсальный инструмент конструирования форм с графическим интерфейсом), но мы отложим реализацию до следующих глав в этой книге. Сценарий в примере 1.29 реализует графический интерфейс, изображенный на рис. 1.7.

Пример 1.29. PP4E\Preview\peoplegui.py

Реализация графического интерфейса для просмотра и изменения экземпляров класса, хранящихся в хранилище;

хранилище находится на том же компьютере, где выполняется сценарий в виде одного или более локальных файлов;

from tkinter import *

from tkinter.messagebox import showerror

import shelve

shelvename = ‘class-shelve’

fieldnames = (‘name’, ‘age’, ‘job’, ‘pay’)

def makeWidgets(): global entries window = Tk()

window.title(‘People Shelve’) form = Frame(window) form.pack() entries = {}

for (ix, label) in enumerate((‘key’,) + fieldnames): lab = Label(form, text=label) ent = Entry(form) lab.grid(row=ix, column=0) ent.grid(row=ix, column=1) entries[label] = ent

Button(window, text=”Fetch”, command=fetchRecord).pack(side=LEFT) Button(window, text=”Update”, command=updateRecord).pack(side=LEFT) Button(window, text=”Quit”, command=window.quit).pack(side=RIGHT) return window

def fetchRecord():

key = entries[‘key’].get() try:

record = db[key]    # извлечь запись по ключу, отобразить в форме

except:

showerror(title=’Error’, message=’No such key!’) else:

for field in fieldnames:

entries[field].delete(0, END)

entries[field].insert(0, repr(getattr(record, field)))

def updateRecord():

key = entries[‘key’].get() if key in db:

record = db[key]    # изменяется существующая запись

else:

from person import Person # создать/сохранить новую запись

record = Person(name=’?’, age=’?’) # eval: строки должны

# заключаться в кавычки

for field in fieldnames:

setattr(record, field, eval(entries[field].get())) db[key] = record

db = shelve.open(shelvename) window = makeWidgets() window.mainloop()

db.close() # в эту точку программа попадает при щелчке на кнопке Quit # или при закрытии окна

Для размещения надписей и полей ввода в этом сценарии вместо метода pack используется метод grid. Как мы увидим далее, этот метод располагает виджеты по столбцам и строкам сетки, что обеспечивает более естественное для форм выравнивание надписей и полей ввода по горизонтали. Далее мы также увидим, что неплохого размещения виджетов на форме можно добиться и с помощью метода pack, добавив вложенные контейнеры для размещения виджетов по горизонтали и надписи фиксированной длины. Пока графический интерфейс никак не обрабатывает изменение размеров окна (для этого необходимы параметры настройки, которые мы будем исследовать позже), тем не менее объем программного кода реализации такой возможности при использовании любого из методов, grid или pack, будет примерно один и тот же.

Обратите внимание, что в конце сценария сначала открывается хранилище, как глобальная переменная, а затем запускается графический интерфейс - хранилище остается открытым на протяжении всего времени работы графического интерфейса (функция mainloop возвращает управление только после закрытия главного окна). Как будет показано в следующем разделе, такое удержание хранилища в открытом состоянии существенно отличает графический интерфейс от веб-интерфейса, где каждая операция обычно является автономной программой. Обратите также внимание, что использование глобальных переменных делает программный код более простым, но непригодным для использования вне контекста нашей базы данных; подробнее об этом мы поговорим ниже.

Пользование графическим интерфейсом

Построенный нами графический интерфейс достаточно прост, но он позволяет просматривать и изменять содержимое файла хранилища без ввода программного кода. Чтобы извлечь запись из хранилища и отобразить ее в графическом интерфейсе, необходимо ввести ключ в поле key (ключ) и щелкнуть на кнопке Fetch (Извлечь). Чтобы изменить запись, необходимо изменить содержимое полей записи после ее извлечения из хранилища и щелкнуть на кнопке Update (Изменить) - значения из полей ввода будут сохранены в базе данных. А чтобы добавить новую запись, необходимо заполнить все поля ввода новыми значениями и щелкнуть на кнопке Update (Изменить) - в хранилище будет добавлена новая запись с указанным ключом и значениями полей.

Другими словами, поля ввода служат одновременно и для отображения, и для ввода. На рис. 1.8 изображена форма после добавления новой записи (щелчком на кнопке Update (Изменить)), а на рис. 1.9 - диалог с сообщением об ошибке, когда пользователь попытался извлечь запись с ключом, отсутствующим в хранилище.

Рис. 1.8. Интерфейсpeoplegui.py после добавления нового объекта


Рис. 1.9. Диалог peoplegui.py с сообщением об ошибке

Обратите внимание, что для отображения значений полей, извлеченных из хранилища, здесь также используется функция repr, а для преобразования значений полей в объекты Python, перед тем как они будут записаны в хранилище, - функция eval. Как уже упоминалось выше, это потенциально опасно, так как открывает возможность ввести в поля ввода злонамеренный программный код, но мы пока не будем касаться этой проблемы.

Однако имейте в виду, что при такой реализации строковые значения, вводимые в поля ввода, должны заключаться в кавычки, так как содержимое всех полей ввода, кроме поля key (ключ), интерпретируется как программный код на языке Python. Фактически, чтобы определить

новое значение, в поле ввода можно ввести произвольное выражение на языке Python. Например, если в поле name (имя) ввести выражение Tom'*3, после щелчка на кнопке Update (Изменить) в записи будет сохранено имя TomTomTom. Чтобы убедиться в этом - извлеките запись из хранилища.

Несмотря на наличие графического интерфейса, позволяющего просматривать и изменять записи, мы по-прежнему можем проконтролировать свои действия, открыв и проверив файл хранилища в интерактивном режиме или запустив сценарий, такой как dump_db_classes. py, представленный в примере 1.19. Не забывайте: несмотря на то, что теперь мы можем просматривать записи с помощью графического интерфейса, сама база данных является файлом хранилища, содержащим объекты Python экземпляров классов, поэтому обращаться к ним может любой программный код на языке Python. Ниже приводятся результаты запуска сценария dump_db_classes.py после изменения существующих и добавления новых объектов с помощью графического интерфейса:

...\PP4E\Preview> python dump_db_classes.py

sue =>

Sue Jones 50000.0 bill => bill 9999 nobody =>

John Doh None tomtom =>

Tom Tom 40000 tom =>

Tom Doe 90000 bob =>

Bob Smith 30000 peg =>

1 4 Smith Doe

Пути усовершенствования

Построенный нами графический интерфейс справляется со своей задачей, тем не менее в него можно внести множество разных усовершенствований:

• На настоящий момент графический интерфейс представляет собой набор функций, использующих глобальный список полей (entries) ввода и глобальное хранилище (db). Вместо этого мы могли бы передать db в вызов функции makeWidgets и организовать передачу обоих этих объектов обработчикам событий в виде аргументов, воспользовавшись приемом с инструкцией lambda из предыдущего раздела. Хотя для таких маленьких сценариев это и не так важно, стоит иметь в виду, что явное определение подобных внешних зависимостей делает программный код более простым для понимания и повторного использования в других контекстах.

•    Этот графический интерфейс можно было бы реализовать в виде класса, чтобы обеспечить поддержку возможности специализации и присоединения к другим графическим интерфейсам (глобальные переменные в этом случае могли бы стать атрибутами экземпляра), хотя повторное использование столь специфического интерфейса маловероятно.

•    Полезнее было бы передавать функциям в виде параметра кортеж fieldnames, чтобы в будущем их можно было использовать с другими типами записей. Программный код в конце сценария также можно было бы оформить в виде функции, принимающей имя файла хранилища, а в функцию updateRecord можно было бы передавать функцию, создающую новую запись, чтобы она могла сохранять не только экземпляры класса Person. Эти усовершенствования выходят далеко за рамки данного краткого обзора, но их реализация была бы для вас неплохим упражнением. Позднее я познакомлю вас с еще одним дополнительным примером, входящим в комплект примеров к книге, PyForm, в котором используется иной подход к созданию универсальных форм ввода.

•    Чтобы сделать этот графический интерфейс более дружественным по отношению к пользователю, можно было бы добавить окно со списком всех ключей, имеющихся в базе данных, и тем самым упростить просмотр содержимого базы данных. Полезно было бы предусмотреть проверку данных перед сохранением, а кроме того, легко можно было бы добавить клавиши Delete (Удалить) и CLear (Очистить). Тот факт, что введенные данные интерпретируются как программный код на языке Python, может доставить массу беспокойств - реализация простейшей схемы ввода могла бы повысить безопасность. (Я не буду явно предлагать реализовать эти усовершенствования в качестве самостоятельного упражнения, но это было бы полезно.)

•    Мы могли бы также реализовать поддержку изменения размеров окна (как мы узнаем позднее, виджеты могут растягиваться и сжиматься вместе с окном) и предоставить возможность вызова методов, которыми обладают сохраняемые экземпляры классов (в том смысле, что графический интерфейс позволяет изменить значение поля pay, но не позволяет вызвать метод giveRaise).

•    Если бы мы планировали распространять этот графический интерфейс, мы могли бы упаковать его в самостоятельную выполняемую программу - скомпилированный двоичный файл (frozen binary) -с использованием сторонних инструментов, таких как Py2Exe, PyInstaller и других (дополнительную информацию ищите в Интернете). Такие программы можно запускать, не устанавливая Python на компьютер клиента, потому что интерпретатор байт-кода включается непосредственно в выполняемый файл.

Я оставлю все эти расширения для дальнейшего обдумывания и вернусь к некоторым из них далее в этой книге.

Два примечания, прежде чем двинуться дальше. Во-первых, я должен упомянуть, что программистам на языке Python доступно множество пакетов создания графических интерфейсов. Например, если вам потребуется реализовать графический интерфейс, состоящий не только из простых окон, вы сможете воспользоваться виджетом Canvas из библиотеки tkinter, поддерживающим возможность создания произвольной графики. Сторонние расширения, такие как Blender, OpenGL, VPython, PIL, VTK, Maya и PyGame, предоставляют еще более совершенные инструменты создания графических изображений, визуализации и воспроизведения анимационных эффектов для использования в сценариях на языке Python. Кроме того, возможности модуля tkinter могут быть расширены с помощью библиотек виджетов PMW, Tix и ttk, упоминавшихся ранее. Описание библиотек Tix и ttk вы найдете в руководстве по стандартной библиотеке Python, а также попробуйте поискать сторонние графические расширения на сайте PyPI или в Интернете.

Из уважения к поклонникам других инструментов создания графических интерфейсов, таких как wxPython и PyQt, я должен заметить, что существуют и другие средства разработки графических интерфейсов, выбор которых иногда зависит от личных предпочтений. Модуль tkinter был продемонстрирован здесь потому, что он является достаточно зрелым, надежным, полностью открытым, хорошо документированным, эффективно поддерживаемым и легковесным инструментом, входящим в состав стандартной библиотеки Python. По большому счету, он является стандартом для построения переносимых графических интерфейсов на языке Python.

Другие инструменты создания графических интерфейсов для сценариев на языке Python обладают своими достоинствами и недостатками, которые будут обсуждаться далее в книге. Например, за использование более богатых наборов виджетов приходится платить некоторым усложнением программного кода. Библиотека wxPython, к примеру, обладает более богатыми возможностями, но она значительно сложнее в использовании. Однако другие инструменты в значительной степени являются лишь вариациями на ту же тему - изучив один инструмент создания графического интерфейса, вы легко и просто овладеете и другими. Поэтому в этой книге мы не будем рассматривать примеры применения разных инструментов, а сосредоточимся на том, чтобы полностью освоить один инструмент.

Хотя программы с традиционными графическими интерфейсами, построенными, например, с помощью tkinter, при необходимости могут поддерживать доступ из сети, обычно они выполняются на единствен-

ном, отдельном компьютере. Иногда даже веб-страницы считаются разновидностью графического интерфейса, но, чтобы составить свое собственное мнение, вам необходимо прочитать следующий и последний раздел этой главы.


На досуге...

Конечно, библиотека обладает гораздо более широкими возможностями, чем было продемонстрировано в этом предварительном обзоре, и мы подробно будем знакомиться с ними далее в этой книге. В качестве еще одного небольшого примера, для демонстрации некоторых дополнительных возможностей библиотеки tkinter, ниже приводится сценарий fungui.py. В этом сценарии используется модуль random из библиотеки Python - для организации выбора из списка; конструктор Toplevel - для создания нового независимого окна; и функция обратного вызова after - для повторного вызова метода через указанное количество миллисекунд:

from tkinter import * import random fontsize = 30

colors = [‘red’, ‘green’, ‘blue’, ‘yellow’, ‘orange’, ‘cyan’, ‘purple’]

def onSpam():

popup = Toplevel()

color = random.choice(colors)

Label(popup, text=’Popup’, bg=’black’, fg=color).pack(fill=BOTH) mainLabel.config(fg=color)

def onFlip():

mainLabel.config(fg=random.choice(colors)) main.after(250, onFlip)

def onGrow():

global fontsize fontsize += 5

mainLabel.config(font=(‘arial’, fontsize, ‘italic’)) main.after(100, onGrow)

main = Tk()

mainLabel = Label(main, text=’Fun Gui!’, relief=RAISED) mainLabel.config(font=(‘arial’, fontsize, ‘italic’), fg=’cyan’,bg=’navy’)

mainLabel.pack(side=TOP, expand=YES, fill=BOTH)

Button(main, text=’spam’, command=onSpam).pack(fill=X)

Button(main, text=’flip’, command=onFlip).pack(fill=X)

Button(main, text=’grow’, command=onGrow).pack(fill=X) main.mainloop()


Запустите этот сценарий, чтобы посмотреть, как он работает. Он создает главное окно с надписью внутри и тремя кнопками - щелчок на первой кнопке приводит к появлению нового окна с меткой, цвет которой выбирается случайным образом. Щелчок на двух других кнопках приводит к запуску независимых циклов вызовов методов обработчиков по таймеру, один из которых постоянно изменяет цвет надписи в главном окне, а другой постепенно увеличивает размер главного окна и шрифта надписи в нем. Но будьте внимательны, когда будете щелкать на последней кнопке, - изменение размеров выполняется со скоростью 10 раз в секунду, поэтому не упустите возможность закрыть окно, пока оно не убежало от вас. Эй, я же предупредил вас!



Шаг 6: добавляем веб-интерфейс

Графические интерфейсы проще в использовании, чем командная строка, и зачастую это все, что нам требуется, чтобы упростить доступ к данным. Однако, обеспечивая доступ к нашей базе данных из Интернета, мы открываем ее для более широкого круга пользователей. Любой, кто обладает выходом в Интернет и имеет броузер, сможет получить доступ к данным, независимо от того, где он находится и какой операционной системой пользуется. Годится любое устройство, от рабочей станции до сотового телефона. Кроме того, при наличии веб-интерфейса требуется только веб-броузер - чтобы получить доступ к данным, не нужно устанавливать Python, за исключением установки на сервере. Традиционные веб-интерфейсы обычно уступают в удобстве и скорости графическим интерфейсам, однако их переносимость может иметь решающее значение.

Как будет показано далее в этой книге, существуют различные способы реализации интерактивных веб-страниц, обеспечивающих доступ к нашим данным. Для решения простых задач, как наша, CGI-сценариев, выполняющихся на стороне сервера, будет более чем достаточно. Поскольку это, пожалуй, самый простой подход и к тому же являющийся основой для более совершенных технологий, разработка CGI-сценариев хорошо подходит для изучения основ разработки веб-приложений.

Для создания более сложных приложений существует богатое многообразие инструментальных средств и фреймворков для Python - включая Django, TurboGears, Google App Engine, pylons, web2py, Zope, Plone, Twisted, CherryPy, Webware, mod_python, PSP и Quixote, - упрощающих решение типичных задач и предоставляющих инструменты, которые в противном случае может потребоваться реализовать самостоятельно. Новейшие технологии, такие как Flex, Silverlight и pyjamas (версия фреймворка Google Web Toolkit, перенесенная на язык Python, и компилятор с языка Python на язык JavaScript), предлагают дополнительные пути создания интерактивных и динамических пользовательских интерфейсов веб-страниц и открывают дверь к использованию Python в разработке полнофункциональных интернет-приложений (Rich Internet Applications, RIA).

Я еще вернусь к этим инструментам позднее, а пока не будем усложнять задачу и напишем CGI-сценарий.


Основы CGI

Писать CGI-сценарии на языке Python достаточно просто, если уже имеется опыт работы с формами HTML, адресами URL и имеется некоторое представление об особенностях модели клиент/сервер Интернета (все эти темы мы будем обсуждать далее в этой книге). Вы можете знать или не знать все подробности, но в основном модель взаимодействий вам должна быть знакома.

В двух словах: пользователь приходит на веб-сайт и получает форму HTML для заполнения в броузере. После отправки формы на сервере запускается сценарий, указанный либо в самой форме, либо в адресе URL сервера, который в ответ воспроизводит другую страницу HTML. В такой схеме взаимодействий данные обычно проходят через три программы: от броузера клиента они передаются веб-серверу, затем CGI-сценарию и возвращаются обратно броузеру. Это естественная модель взаимодействия с базами данных, которой мы будем следовать, - пользователь будет отправлять серверу ключ в базе данных и в ответ будет получать соответствующую страницу с записью.

Далее в книге мы подробнее познакомимся с основами CGI, а пока, в качестве первого примера, создадим простой интерактивный сценарий, который будет запрашивать имя пользователя и возвращать его обратно веб-броузеру. Первая страница в этом примере - это просто форма ввода, реализованная в виде разметки HTML, как показано в примере 1.30. Этот файл HTML хранится на веб-сервере и передается вебброузеру, выполняющемуся на компьютере клиента.

Пример 1.30. PP4E\Preview\egi101.html

<html>

<title>Interactive Page</title>

<body>

<form method=POST action=”cgi-bin/cgi101.py”>

<P><B>Enter your name:</B>

<P><input type=text name=user>

<P><input type=submit>

</form>

</body></html>

Обратите внимание, что в атрибуте action этой формы определяется сценарий, который будет обрабатывать ее. Эта страница будет возвращаться при обращении по ее адресу URL. После получения клиентом эта форма в окне броузера будет выглядеть, как показано на рис. 1.10 (в данном случае, в Internet Explorer).

Рис. 1.10. Форма ввода на странице egi101.html

После отправки формы клиентом веб-сервер получит запрос (подробнее о веб-сервере чуть ниже) и запустит CGI-сценарий на языке Python, представленный в примере 1.31. Как и файл HTML, этот сценарий также находится на веб-сервере - он выполняется на стороне сервера, обрабатывает введенные данные и воспроизводит ответ, который отправляется броузеру на стороне клиента. Сценарий использует модуль cgi, чтобы извлечь данные из формы и вставить их в поток разметки HTML ответа с соответствующим экранированием. Модуль cgi обеспечивает интерфейс к полям ввода формы, отправленной броузером, напоминающий интерфейс словаря, и передачу разметки HTML, которую выводит сценарий, броузеру для отображения в виде следующей страницы. В мире CGI поток стандартного вывода соединен с клиентом посредством сокета.

Пример 1.31. PP4E\Preview\egi-bin\egi101.py

#!/usr/bin/python import cgi

form = cgi.FieldStorage()    # парсинг данных формы

print(‘Content-type: text/html\n’) # http-заголовок плюс пустая строка print('<title>Reply Page</title>’) # html-разметка ответа if not ‘user’ in form:

print(‘<h1>Who are you?</h1>’) else:

print(‘<h1>Hello <i>%s</i>!</h1>' % cgi.escape(form[‘user’].value))

И если все пройдет как надо, мы получим в ответ страницу, изображенную на рис. 1.11, которая, по сути, просто выводит данные, введенные на странице с формой ввода. Страница на этом рисунке была воспроизведена разметкой HTML, которую вывел CGI-сценарий на стороне сервера. В данном случае имя пользователя сначала было передано со стороны клиента на сервер, а затем обратно, возможно преодолев по пути многие сети и километры. Безусловно, это очень небольшой веб-сайт, но одни и те же принципы действуют для любого сайта, выводит он просто введенные данные или является полноценным сайтом электронного бизнеса.

Рис. 1.11. Страница ответа, воспроизведенная сценарием egi101.py в ответ на получение формы ввода

Если у вас возникли проблемы с организацией этих взаимодействий в Unix-подобной системе, попробуйте изменить путь к интерпретатору Python в строке #!, находящейся в начале сценария, и дать файлу сценария право на выполнение командой chmod, хотя во многом это зависит от вашего веб-сервера (подробнее о сервере мы поговорим чуть ниже).

Обратите также внимание, что CGI-сценарий в примере 1.31 выводит не полную разметку HTML: здесь отсутствуют теги <html> и <body>, которые можно увидеть в примере 1.30. Строго говоря, эти теги следовало бы вывести, но веб-броузеры спокойно воспринимают их отсутствие, да и цель этой книги состоит вовсе не в том, чтобы обучить вас формальному языку разметки HTML, - более подробную информацию об HTML ищите в других источниках.

Графические и веб-интерфейсы

Прежде чем двинуться дальше, имеет смысл потратить минуту, чтобы сравнить этот простой пример CGI-сценария с простым графическим интерфейсом, реализованным в примере 1.28 и изображенным на рис. 1.6. В данном случае сценарии выполняются на стороне сервера и генерируют разметку HTML, которая отображается веб-броузером. В реализации графического интерфейса мы вызываем функции, конструирующие интерфейс на экране и реагирующие на события, которые выполняются в рамках единого процесса и на одном и том же компьютере. Графический интерфейс состоит из нескольких программных уровней, но целиком реализован в единственной программе. Реализация на основе CGI, напротив, имеет распределенную архитектуру - сервер, броузер и, возможно, сам CGI-сценарий выполняются как отдельные программы, обычно взаимодействующие друг с другом по сети.

Учитывая эти различия, автономная модель реализации графического интерфейса выглядит более простой и непосредственной: в ней отсутствует промежуточный сервер; чтобы получить ответ, не требуется вызывать новую программу; не требуется генерировать разметку HTML и в нашем распоряжении имеется вся мощь инструмента создания графического интерфейса. С другой стороны, веб-интерфейс может отображаться в любом броузере, на любом компьютере и для его работы необходимо установить Python только на сервере.

Еще больше ситуацию запутывает то обстоятельство, что графический интерфейс также может использовать сетевые инструменты из стандартной библиотеки Python для получения и отображения данных, хранящихся на удаленном сервере (то есть так же, как это делают броузеры). Некоторые новейшие фреймворки, такие как Flex, Silverlight и pyjamas, предоставляют инструменты реализации более полнофункциональных пользовательских интерфейсов в веб-страницах (полнофункциональных интернет-приложений, упоминавшихся выше), хотя и ценой сложности программного кода и большего количества программных уровней. Далее в книге мы еще вернемся к обсуждению различий между графическим интерфейсом и CGI, потому что на сегодняшний день это и есть основной выбор. А теперь рассмотрим несколько практических проблем, связанных с работой механизма CGI, прежде чем применить его к нашей базе данных.


Запуск веб-сервера

Для запуска CGI-сценариев нам потребуется веб-сервер, который будет обслуживать наши страницы HTML и запускать сценарии на языке Python по запросам. Сервер является необходимым промежуточным звеном между броузером и CGI-сценарием. Если у вас нет учетной записи на компьютере, где уже установлен такой веб-сервер, вам придется запустить собственный веб-сервер. Мы могли бы настроить полноценный веб-сервер промышленного уровня, такой как свободно распространяемый веб-сервер Apache (в котором, кстати, можно настроить поддержку Python с помощью расширения mod_python). Однако для данной главы я написал на языке Python собственный простой веб-сервер, программный код которого приводится в примере 1.32.

Мы еще вернемся к инструментам, использованным в этом примере, далее в книге. Тем не менее замечу, что в стандартной библиотеке Python уже имеется реализация некоторых типов сетевых серверов, благодаря чему мы можем реализовать CGI-совместимый и переносимый веб-сервер, написав всего 8 строк программного кода (точнее, 16, если учесть комментарии и пустые строки).

Далее в этой книге мы увидим, насколько просто создать собственный сетевой сервер, используя низкоуровневые функции для работы с сокетами в Python, однако в стандартной библиотеке уже имеются реализации многих наиболее распространенных типов серверов. Модуль sock-etserver, например, поддерживает многопоточные и ветвящиеся версии серверов TCP и UDP. Еще большее количество реализаций можно найти в сторонних системах, таких как Twisted. Модули из стандартной библиотеки, использованные в примере 1.32, предоставляют все, что необходимо для обслуживания нашего веб-содержимого.

Пример 1.32. PP4E\Preview\webserver.py

Реализация веб-сервера на языке Python, способная запускать серверные CGI-сценарии на языке Python; обслуживает файлы и сценарии в текущем рабочем каталоге; сценарии на языке Python должны находиться в каталоге webdir\cgi-bin или webdir\htbin;

import os, sys

from http.server import HTTPServer, CGIHTTPRequestHandler

webdir = ‘.’    # место, где находятся файлы html и подкаталог cgi-bin

port = 80    # по умолчанию http://localhost/, иначе используйте

# http://localhost:xxxx/

os.chdir(webdir)    # перейти в корневой каталог HTML

srvraddr = (“”, port)    # имя хоста и номер порта

srvrobj = HTTPServer(srvraddr, CGIHTTPRequestHandler) srvrobj.serve_forever()    # запустить как бесконечный фоновый процесс

Классы, используемые сценарием, предполагают, что обслуживаемые файлы HTML находятся в текущем рабочем каталоге, а запускаемые CGI-сценарии находятся в подкаталоге cgi-bin или htbin. Как следует из имени файла в примере 1.31, для сценариев мы будем использовать подкаталог cgi-bin. Некоторые веб-серверы определяют CGI-сценарии по расширению в именах файлов, однако мы будем считать CGI-сценариями все файлы, находящиеся в определенном каталоге.

Чтобы запустить веб-сервер, достаточно запустить этот сценарий (из командной строки, щелчком на ярлыке или иным способом). Он будет выполняться бесконечно, ожидая запросы от броузеров и других клиентов. Сервер ожидает запросы, направляемые на компьютер, где он выполняется, прослушивая стандартный порт HTTP с номером 80. Чтобы использовать этот сценарий для обслуживания других веб-сайтов, необходимо либо запустить его из другого каталога, содержащего файлы HTML и подкаталог cgi-bin со сценариями CGI, либо изменить значение переменной webdir, записав в нее имя корневого каталога сайта (сценарий автоматически перейдет в этот каталог и будет обслуживать файлы, находящиеся в нем).

Но где в киберпространстве фактически выполняется сценарий сервера? Если посмотреть внимательнее, на рисунках в предыдущем разделе можно заметить, что в адресной строке броузера (в верхней части окна, сразу после последовательности символов http://) всегда используется имя сервера localhost. Чтобы не усложнять, я запустил веб-сервер на том же компьютере, где запускается веб-броузер, а это означает, что сервер будет иметь имя «localhost» (и соответствующий IP-адрес «127.0.0.1»). То есть клиент и сервер - это один и тот же компьютер: клиент (вебброузер) и сервер (веб-сервер) - это просто разные процессы, одновременно выполняющиеся на одном и том же компьютере.

Хотя этот веб-сервер не может использоваться в промышленных целях, тем не менее он отлично подходит для тестирования CGI-сценариев - вы можете разрабатывать их на том же самом компьютере без необходимости перемещать программный код на удаленный сервер после каждого изменения. Просто запустите этот сценарий из каталога, где находятся файлы HTML и подкаталог cgi-bin с CGI-сценариями, и затем вводите в броузере адрес http://localhost/..., чтобы получить доступ к своим HTML-страницам и сценариям. Ниже приводится вывод, полученный от сценария веб-сервера в окне консоли в ОС Windows, который был запущен на том же компьютере, что и веб-броузер, из каталога, где находятся файлы HTML:

...\PP4E\Preview> python webserver.py

mark-VAIO - - [28/Jan/2010 18:34:01] “GET /cgi101.html HTTP/1.1” 200 -mark-VAIO - - [28/Jan/2010 18:34:12] “POST /cgi-bin/cgi101.py HTTP/1.1” 200 -mark-VAIO - - [28/Jan/2010 18:34:12] command: C:\Python31\python.exe -u C:\ Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\Preview\cgi-bin\cgi101.py “” mark-VAIO - - [28/Jan/2010 18:34:13] CGI script exited OK mark-VAIO - - [28/Jan/2010 18:35:25] “GET /cgi-bin/cgi101.py?user=Sue+Smith HTTP/1.1” 200 -

mark-VAIO - - [28/Jan/2010 18:35:25] command: C:\Python31\python.exe -u C:\ Users\mark\Stuff\Books\4E\PP4E\dev\Examples\PP4E\Preview\cgi-bin\cgi101.py mark-VAIO - - [28/Jan/2010 18:35:26] CGI script exited OK

Здесь следует сделать одно замечание: на некоторых платформах, чтобы запустить сервер, прослушивающий порт по умолчанию с номером 80, вам могут потребоваться привилегии администратора, поэтому узнайте, как в вашей системе запустить такой сервер, или попробуйте использовать порт с другим номером. Чтобы задействовать порт с другим номером, измените значение переменной port в сценарии и указывайте его явно в адресной строке броузера (например, http://localhost:8888/). Подробнее об этом соглашении будет рассказываться далее в книге.

Чтобы запустить этот сервер на удаленном компьютере, выгрузите файлы HTML и подкаталог с CGI-сценариями на удаленный компьютер, запустите там этот сценарий, а в адресной строке броузера вместо имени «localhost» используйте доменное имя или IP-адрес удаленного компьютера (например, http://www.myserver.com/). При использовании удаленного сервера все взаимодействия будут протекать, как показано здесь, но при этом запросы и ответы будут передаваться не между приложениями на одном и том же компьютере, а автоматически будут направляться через сетевые соединения.

Чтобы покопаться в реализации серверных классов нашего веб-сервера, обращайтесь к файлам с исходными текстами в стандартной библиотеке Python (C:\Python31\Lib для версии Python 3.1). Одно из основных преимуществ открытых систем, таких как Python, состоит в том, что мы всегда можем заглянуть «под капот». В главе 15 мы расширим пример 1.32 возможностью указывать имя каталога и номер порта из командной строки.


Использование строки запроса и модуля urllib

В простом CGI-сценарии, представленном выше, мы запускали сценарий на языке Python, заполняя и отправляя форму, содержащую имя сценария. На практике серверные CGI-сценарии могут вызываться разными способами - либо за счет отправки формы, как было показано выше, либо за счет отправки серверу строки URL (интернет-адреса), содержащей входные данные в конце. Такую строку URL можно отправить не только с помощью броузера, то есть минуя традиционный путь, лежащий через форму ввода.

Например, на рис. 1.12 изображена страница, сгенерированная в ответ на ввод адреса URL в адресной строке броузера (символ + здесь означает пробел):


http://localhost/cgi-bin/cgi101.py?user=Sue+Smith

Рис. 1.12. Ответ сценария cgi101.py на запрос GET с параметрами

Входные данные здесь, известные как параметры, запроса, находятся в конце строки URL, после символа ?. Они не были введены в поля формы. Строку URL с дополнительными входными данными иногда называют GET-запросом. Наша оригинальная форма отправляет запрос методом POST, в котором входные данные отправляются отдельно. К счастью, в CGI-сценариях на языке Python не требуется различать эти два вида запросов - парсер входных данных в модуле cgi автоматически обрабатывает все различия между методами отправки данных.

Вполне возможно и часто даже полезно иметь возможность отправлять входные данные в строке URL в виде параметров запроса вообще без помощи веб-броузера. Пакет urllib из стандартной библиотеки Python, например, позволяет читать ответ, сгенерированный сервером для любого допустимого адреса URL. Фактически он позволяет посещать вебстраницы или вызывать CGI-сценарии из другого сценария - ваш программный код на языке Python будет играть роль веб-клиента. Ниже демонстрируется пример использования пакета в интерактивной оболочке:

>>> from urllib.request import urlopen

>>> conn = urlopen('http://localhost/cgi-bin/cgi101.py?user=Sue+Smith')

>>> reply = conn.read()

>>> reply

b’<title>Reply Page</title>\n<h1>Hello <i>Sue Smith</i>!</h1>\n'

>>> urlopen('http://localhost/cgi-bin/cgi101.py').read()

b’<title>Reply Page</title>\n<h1>Who are you?</h1>\n’

>>> urlopen('http://localhost/cgi-bin/cgi101.py?user=Bob').read()

b’<title>Reply Page</title>\n<h1>Hello <i>Bob</i>!</h1>\n’

Пакет urllib реализует интерфейс получения ответов от сервера для заданной строки URL, напоминающий интерфейс файлов. Обратите внимание, что ответ, который мы получаем от сервера, представляет собой простую разметку HTML (обычно отображается броузером). Мы можем обрабатывать этот текст с помощью любых инструментов обработки текста, входящих в состав Python, включая:

•    Строковые методы поиска и разбиения

•    Модуль re, позволяющий выполнять сопоставление с регулярными выражениями

•    Развитую поддержку парсинга разметки HTML и XML в стандартной библиотеке, включая модуль html.parser, а также SAX-, DOM-и ElementTree-подобные инструменты парсинга разметки XML.

В сочетании с такими инструментами пакет urllib естественным образом подходит для применения в интерактивном тестировании вебсайтов, в собственных клиентских графических интерфейсах, в программах, извлекающих содержимое веб-страниц, и в автоматизированных регрессионных тестах для тестирования удаленных CGI-сценариев.


Форматирование текста ответа

Еще одно, последнее примечание: так как для взаимодействия с клиентами CGI-сценарии используют текст, они должны форматировать его, следуя определенному набору правил. Например, обратите внимание, что в примере 1.31 между заголовком ответа и разметкой HTML присутствует пустая строка в виде явного символа перевода строки (\n), в дополнение к символу перевода строки, который автоматически выводится функцией print, - это обязательный разделитель.

Кроме того, обратите внимание, что текст, добавляемый в разметку HTML ответа, передается через вызов функции cgi.escape (она же html. escape в Python 3.2 - смотрите примечание в разделе «Инструменты экранирования HTML и URL в языке Python», в главе 15), на тот случай, если он содержит символы, имеющие специальное значение в HTML. Например, на рис. 1.13 изображена страница ответа, полученная в результате ввода имени пользователя Bob </i> Smith, - последовательность символов </i> в середине преобразуется этой функцией в последовательность </i>, благодаря чему исключается влияние этой последовательности на фактическую разметку HTML (воспользуйтесь возможностью просмотра исходного кода страницы, имеющейся в броузерах, чтобы убедиться в этом). Без вызова этой функции остаток имени был бы выведен обычным, некурсивным шрифтом.

Рис. 1.13. Экранирование символов HTML

Экранирование текста, как в данном примере, требуется не всегда, но его следует применять, когда содержимое текста заранее не известно, -сценарии, генерирующие разметку HTML, должны следовать правилам ее оформления. Как мы увидим далее в этой книге, похожая функция urllib.parse.quote применяет правила экранирования к тексту в строке с адресом URL. Кроме того, мы увидим, что крупные фреймворки часто решают задачи форматирования текста автоматически.


Веб-интерфейс к хранилищу с данными

Теперь для создания нашего приложения баз данных на основе технологии CGI, представленной в предыдущем разделе, нам потребуется реализовать более крупную форму ввода и отображения данных. На рис. 1.14 изображена форма, которую мы реализуем для доступа к нашей базе данных.

Реализация веб-сайта

Чтобы обеспечить возможность взаимодействий, создадим разметку HTML начальной формы ввода, а также CGI-сценарий на языке Python, который будет отображать полученные результаты и обрабатывать запросы на изменение данных в хранилище. В примере 1.33 приводится разметка HTML формы ввода, которая создает страницу, изображенную на рис. 1.14.

Рис. 1.14. Форма ввода peoplecgi.html

Пример 1.33. PP4E\Preview\peopleegi.html

<html>

<title>People Input Form</title>

<body>

<form method=POST action=”cgi-bin/peoplecgi.py”>

<table>

<tr><th>Key <td><input type=text name=key> <tr><th>Name<td><input type=text name=name> <tr><th>Age <td><input type=text name=age> <tr><th>Job <td><input type=text name=job> <tr><th>Pay <td><input type=text name=pay> </table>

<p>

<input type=submit value=”Fetch”, name=action> <input type=submit value=”Update”, name=action> </form>

</body></html>

Обработкой формы и других запросов будет заниматься CGI-сценарий на языке Python, представленный в примере 1.34, который будет извлекать и изменять записи в нашем хранилище. Обратно он будет возвращать страницу, похожую на ту, что воспроизводит разметка в примере 1.33, но с полями формы, заполненными значениями атрибутов объекта, извлеченного из хранилища.

Как и в реализации графического интерфейса, для вывода результатов и ввода изменений будет использоваться одна и та же веб-страница. В отличие от графического интерфейса, этот сценарий будет запускаться заново в ответ на каждое действие пользователя и каждый раз снова будет открывать базу данных (атрибут action формы содержит ссылку на сценарий для следующего запроса). Модель CGI не предоставляет возможности сохранения информации о состоянии между запросами, поэтому каждый раз мы вынуждены выполнять все действия с самого начала.

Пример 1.34. PP4E\Preview\egi-bin\peopleegi.py

Реализует веб-интерфейс для просмотра и изменения экземпляров классов в хранилище; хранилище находится на сервере (или на том же компьютере, если используется имя localhost)

import cgi, shelve, sys, os    # cgi.test() выведет поля ввода

shelvename = ‘class-shelve’    # файлы хранилища находятся

#    в текущем каталоге

fieldnames = (‘name’, ‘age’, ‘job’, ‘pay’)

form = cgi.FieldStorage()    # парсинг данных формы

print(‘Content-type: text/html’)    # заголовок + пустая строка для ответа

sys.path.insert(0, os.getcwd())    # благодаря этому модуль pickle

#    и сам сценарий будут способны

#    импортировать модуль person

#    главный шаблон разметки html

replyhtml = """

<html>

<title>People Input Form</title>

<body>

<form method=POST action=”peoplecgi.py”>

<table>

<tr><th>key<td><input type=text name=key value=”%(key)s”>

$ROWS$

</table>

<p>

<input type=submit value=”Fetch”, name=action>

<input type=submit value=”Update”, name=action>

</form>

</body></html>

#    вставить разметку html с данными в позицию $ROWS$

rowhtml = ‘ <tr><th>%is<td><input type=text name=%s value=”%%(%s)s”>\n’ rowshtml = ‘’

for fieldname in fieldnames:

rowshtml += (rowhtml % ((fieldname,) * 3)) replyhtml = replyhtml.replace(‘$ROWS$’, rowshtml)

def htmlize(adict):

new = adict.copy()    # значения могут содержать &, >

for field in fieldnames:    # и другие специальные символы,

value = new[field]    # отображаемые особым образом;

new[field] = cgi.escape(repr(value)) # их необходимо экранировать return new

def fetchRecord(db, form): try:

key = form[‘key’].value record = db[key]

fields = record.__dict__    # для заполнения строки ответа

fields[‘key’] = key    # использовать словарь атрибутоЕ

except:

fields = dict.fromkeys(fieldnames, ‘?’) fields[‘key’] = ‘Missing or invalid key!’ return fields

def updateRecord(db, form): if not ‘key’ in form:

fields = dict.fromkeys(fieldnames, ‘?’) fields[‘key’] = ‘Missing key input!’ else:

key = form[‘key’].value if key in db:

record = db[key]    # изменить существующую запись

else:

from person import Person    # создать/сохранить новую

record = Person(name=’?’, age=’?’) # eval: строки должны быть

# заключены в кавычки

for field in fieldnames:

setattr(record, field, eval(form[field].value)) db[key] = record

fields = record.__dict__

fields[‘key’] = key return fields

db = shelve.open(shelvename)

action = form[‘action’].value if ‘action’ in form else None if action == ‘Fetch’:

fields = fetchRecord(db, form) elif action == ‘Update’:

fields = updateRecord(db, form) else:

fields = dict.fromkeys(fieldnames, ‘?’)    # недопустимое значение

fields[‘key’] = ‘Missing or invalid action!’ # кнопки отправки формы

db.close()

print(replyhtml % htmlize(fields))    # заполнить форму ответа

# из словаря

Сценарий получился таким большим, потому что в его задачу входит обработка ввода пользователя, выполнение операций с базой данных и генерирование разметки HTML для ответа. Тем не менее действует он достаточно прямолинейно и по своему поведению напоминает реализацию графического интерфейса из предыдущего раздела.

Каталоги, форматирование строк и безопасность

Прежде чем двинуться дальше, необходимо сделать несколько важных замечаний. Прежде всего, сценарий веб-сервера, представленный в примере 1.32, должен быть запущен до того, как вы начнете экспериментировать, - он будет перехватывать запросы от броузера и передавать их нашему CGI-сценарию.

Обратите также внимание, что при запуске CGI-сценарий добавляет путь к текущему рабочему каталогу (os.getcwd) в путь поиска модулей sys.path. Не изменяя переменную окружения PYTHONPATH, этот прием позволит модулю pickle и самому сценарию импортировать модуль person, находящийся в том же каталоге, что и сценарий. Из-за нового способа запуска CGI-сценариев, реализованного в Python 3, текущий рабочий каталог не добавляется в список sys.path автоматически, хотя при этом файлы хранилища, находящиеся там, будут обнаруживаться и открываться корректно. Эта особенность в поведении может отличаться, в зависимости от выбранного веб-сервера.

Еще один интересный прием в CGI-сценарии - использование словаря атрибутов записи (__dict__) как источника значений в операции экра

нирования полей внутри выражения форматирования строки, преобразующего строку шаблона HTML в ответ, в последней строке сценария. Напомню, что выражение вида %(key)code заменит ключ key значением этого ключа в словаре:

>>> D = {'say': 5, ‘get’: 'shrubbery'}

>>> D['say']

5

>>> S = '%(say)s => %(get)s' % D

>>> S

‘5 => shrubbery’

Благодаря использованию словаря атрибутов мы можем ссылаться на атрибуты по их именам в форме строк. Фактически часть шаблона ответа генерируется программным кодом. Если его структура кажется вам непонятной, просто вставьте инструкции вывода replyhtml и вызова sys. exit и запустите сценарий из командной строки. Ниже показано, как выглядит разметка HTML таблицы в середине сгенерированного ответа (немного отформатированная здесь для удобочитаемости):

<table>

<tr><th>key<td><input type=text name=key value=”%(key)s”>

<tr><th>name<td><input type=text name=name value=”%(name)s”> <tr><th>age<td><input type=text name=age value=”%(age)s”>

<tr><th>job<td><input type=text name=job value=”%(job)s”>

<tr><th>pay<td><input type=text name=pay value=”%(pay)s”>

</table>

Далее этот текст заполняется значениями ключей из словаря атрибутов записи инструкцией форматирования строки в конце сценария. Эта инструкция выполняется после того, как словарь будет обработан вспомогательной функцией, преобразующей значения в текст с помощью функции repr и экранирующей текст вызовом функции cgi.escape, в соответствии с требованиями языка разметки HTML (опять же, последний шаг не всегда является обязательным, но он никогда не будет лишним).

Эти строки ответа в формате HTML можно было бы жестко определить в программном коде, но генерирование их из кортежа с именами полей обеспечивает более универсальное решение - в будущем мы сможем добавлять новые поля без необходимости изменять шаблон HTML. Инструменты обработки строк в языке Python позволяют это.

Справедливости ради следует заметить, что более новый метод str.for-mat позволяет добиться того же эффекта, что и традиционный оператор % форматирования, используемый в сценарии, и дает возможность использовать синтаксис ссылок на атрибуты объектов, который выглядит более явным по сравнению с приемом использования ключей словаря __dict__:

>>> D = {'say': 5, 'get': 'shrubbery’}

>>> '%(say)s => %(get)s' % D    # выражение: ссылка на ключ

‘5 => shrubbery’

>>> '{say} => {get}'.format(**D)    # метод: ссылка на ключ

‘5 => shrubbery’

>>> from person import Person >>> bob = Person('Bob', 35)

>>> '%(name)s, %(age)s' % bob.__dict__ # выражение: ключи __dict__

‘Bob, 35’

>>> '{0.name} => {0.age}'.format(bob) # метод: синтаксис атрибутов ‘Bob => 35’

Однако из-за того, что нам сначала необходимо экранировать атрибуты, мы не можем использовать синтаксис атрибутов в вызове метода форматирования. Фактически для выбора нам доступен лишь синтаксис ссылок на ключи, представленный выше. (К моменту написания этих строк еще не было очевидно, какой из двух способов форматирования займет доминирующее положение, поэтому мы позволим себе исполь-

зовать в книге оба способа - даже если какой-то из этих способов заменит другой, вы все равно останетесь в выигрыше.)

В интересах безопасности необходимо также напомнить, что прием использования функции eval для преобразования входных данных в объекты языка Python является достаточно мощным, но далеко не безопасным. Эта функция с радостью выполнит любой программный код на языке Python, который в свою очередь сможет выполнить любые системные операции, разрешение на которые будет иметь процесс сценария. Если проблема безопасности имеет для вас значение, то вам придется обеспечить выполнение сценария в ограниченном окружении или использовать более специализированные механизмы преобразования, такие как функции int и float. Вообще говоря, проблема безопасности занимает важное место в мире веб-приложений, где строки запросов могут поступать из самых разных источников. Однако, поскольку все мы здесь считаемся друзьями, мы проигнорируем возможную угрозу.

Пользование веб-сайтом

Несмотря на сложности, связанные с серверами, каталогами и строками, пользоваться веб-интерфейсом ничуть не сложнее, чем графическим интерфейсом. Вдобавок веб-интерфейс имеет дополнительное преимущество - им можно пользоваться в любой операционной системе, где имеется броузер и подключение к Интернету. Чтобы извлечь запись из хранилища, заполните поле Key (Ключ) и щелкните на кнопке Fetch (Извлечь) - сценарий заполнит страницу данными, полученными из атрибутов соответствующего экземпляра класса, извлеченного из хранилища, как показано на рис. 1.15, где была извлечена запись с ключом bob.

На рис. 1.15 показано, что получится, когда ключ передается с помощью формы. Как уже отмечалось выше, CGI-сценарий можно также

вызвать, передав входные данные в виде строки запроса, поместив ее в конец адреса URL. На рис. 1.16 показана страница, полученная в ответ на попытку обратиться по следующему адресу URL:


http://localhost/cgi-bin/peoplecgi.py?action=Fetch&key=sue

Рис. 1.16. Ответ сценария peoplecgi.py на запрос с параметрами

Как вы уже знаете, такую строку URL можно отправить с помощью броузера или сценария, использующего такие инструменты, как пакет urllib. И снова, замените «localhost» на доменное имя своего сервера, если вы запускаете сценарий на удаленном компьютере.

Чтобы изменить запись, извлеките ее по ключу, введите новые значения в поля ввода и щелкните на кнопке Update (Изменить) - сценарий извлечет значения из полей ввода и запишет их в соответствующие атрибуты экземпляра класса в хранилище. На рис. 1.17 показана страница ответа, полученная после изменения записи с ключом sue.

Наконец, операция добавления новой записи выполняется точно так же, как и в графическом интерфейсе: укажите новые значения ключа и полей, щелкните на кнопке Update (Изменить) - CGI-сценарий создаст новый экземпляр класса, запишет в его атрибуты значения соответствующих полей ввода и сохранит его в хранилище с новым ключом. В действительности здесь под покровом веб-страницы выполняются операции с объектом класса, но нам не приходится иметь дело с логикой его создания. На рис. 1.18 изображена запись, добавленная в базу данных таким способом.

В принципе мы точно так же можем изменять и добавлять записи, отправляя соответствующие строки URL - из броузера или из сценария -например:

http://localhost/cgi-bin/

peoplecgi.py?action=Update&key=sue&pay=50000&name=Sue+Smith& ...и далее...

Рис. 1.17. Ответ peoplecgi.py на операцию изменения записи


Рис. 1.18. Ответ peoplecgi.py после добавления новой записи

Однако вводить такую длинную строку URL без использования автоматизированных инструментов существенно сложнее, чем заполнять поля формы. Ниже приводится часть страницы ответа, сгенерированной в ответ на создание записи с ключом «guido» и изображенной на рис. 1.18 (воспользуйтесь возможностью просмотра исходного кода страницы, имеющейся в броузерах, чтобы убедиться в этом). Обратите внимание, что символы < и > были преобразованы функцией cgi.escape в экранированные последовательности HTML, перед тем как они были вставлены в ответ:

<tr><th>key<td><input type=text name=key value=”guido”>

<tr><th>name<td><input type=text name=name value=”’GvR’”>

<tr><th>age<td><input type=text name=age value=”None”> <tr><th>job<td><input type=text name=job value=”’BDFL’”>

<tr><th>pay<td><input type=text name=pay value=”’<shrubbery>’”>

Как обычно, для тестирования нашего CGI-сценария можно использовать пакет urllib из стандартной библиотеки - возвращаемый результат представляет собой простую разметку HTML, которую можно проанализировать с помощью других инструментов, имеющихся в стандартной библиотеке, и использовать в качестве основы для системы регрессионного тестирования серверного сценария, выполняющейся на любой машине, подключенной к Интернету. Мы могли бы даже реализовать анализ ответа сервера, полученного таким способом, и отображать данные в графическом интерфейсе, реализованном с помощью библиотеки tkinter, - графические интерфейсы и веб-страницы не являются взаимоисключающими технологиями. В последнем примере получения данных в интерактивном сеансе демонстрируется фрагмент страницы HTML с сообщением об ошибке, которая была сгенерирована в ответ на отсутствующее или недопустимое входное значение, с разрывами строк, добавленными для удобочитаемости:

>>> from urllib.request import urlopen

>>> url = 'http://localhost/cgi-bin/peoplecgi.py?action=Fetch&key=sue'

>>> urlopen(url).read()

b’<html>\n<title>People Input Form</title>\n<body>\n <form method=POST action=”peoplecgi.py”>\n <table>\n <tr><th>key<td><input type=text name=key value=”sue”>\n <tr><th>name<td><input type=text name=name value=”\’Sue Smith\’”>\n <tr><t ...остальной текст удален...

>>> urlopen('http://localhost/cgi-bin/peoplecgi.py').read()

b’<html>\n<title>People Input Form</title>\n<body>\n <form method=POST action=”peoplecgi.py”>\n <table>\n

<tr><th>key<td><input type=text name=key value=”Missing or invalid action!”>\n <tr><th>name<td><input type=text name=name value=”\’?\’”>\n <tr><th>age<td><input type=text name=age value=”\’?\’”>\n<tr> ...остальной текст удален...

Фактически, если CGI-сценарий выполняется на локальном компьютере «localhost», для просмотра одного и того же хранилища вы сможете использовать и графический интерфейс из предыдущего раздела, и вебинтерфейс из этого раздела - это всего лишь альтернативные интерфейсы доступа к одним и тем же хранимым объектам Python. Для сравнения на рис. 1.19 показано, как выглядит запись в графическом интерфейсе, которую мы видели на рис. 1.18, - это тот же самый объект, но на этот раз мы получили ее, не обращаясь к промежуточному серверу, запускающему другие сценарии или генерирующему разметку HTML.

Как и прежде, мы всегда можем проверить результаты нашей деятельности на сервере, используя интерактивную оболочку или другие сценарии. Мы можем просматривать содержимое базы данных с помощью веб-броузеров или графического интерфейса, но в любом случае это всего лишь объекты Python в файле хранилища:

>>> import shelve

>>> db = shelve.open('class-shelve')

>>> db['sue'].name

‘Sue Smith’

>>> db['guido'].job

‘BDFL’

>>> list(db['guido'].name)

[‘G’, ‘v’, ‘R’]

>>> list(db.keys())

[‘sue’, ‘bill’, ‘nobody’, ‘tomtom’, ‘tom’, ‘bob’, ‘peg’, ‘guido’]

Рис. 1.19. Тот же самый объект, отображаемый в графическом интерфейсе

Ниже приводятся результаты запуска первоначального сценария из примера 1.19, извлекающего информацию из базы данных, который мы написали до того, как перешли к реализации графического и вебинтерфейса, - в языке Python существует масса способов просмотра данных:

...\PP4E\Preview> dump_db_classes.py

sue =>

Sue Smith 60000 bill => bill 9999 nobody =>

John Doh None tomtom =>

Tom Tom 40000 tom =>

Tom Doe 90000 bob =>

Bob Smith 30000 peg =>

1 4 guido =>

GvR <shrubbery>

Smith

Doe

Дальнейшие направления усовершенствования

Естественно, что в этот пример можно было бы внести множество улучшений:

•    Разметка HTML для начальной формы ввода, представленная в примере 1.33, несколько избыточна для сценария в примере 1.34, и ее можно было бы генерировать с помощью другого сценария, используемого как источник совместно используемой информации.

•    Фактически мы вообще можем отказаться от встраивания разметки HTML в наш сценарий, если воспользуемся одним из инструментов-генераторов HTML, с которыми мы познакомимся далее в книге, таким как HTMLgen (система создания разметки HTML из дерева объектов документа) и PSP (Python Server Pages - серверные страницы Python, серверная система шаблонов HTML для Python, напоминающая PHP и ASP).

•    Чтобы упростить обслуживание, можно было бы также вынести разметку HTML для CGI-сценария в отдельный файл, чтобы отделить представление от логики (с разными файлами могли бы работать разные специалисты).

•    Кроме того, если веб-сайтом могут пользоваться сразу несколько человек, мы могли бы добавить возможность блокировки файла хранилища или перейти на использование базы данных, такой как ZODB или MySQL, чтобы обеспечить возможность параллельных изменений. ZODB и другие полноценные системы управления базами данных позволяют также использовать возможность отмены транзакций в случае ошибок. Реализовать простейшую блокировку файла можно с помощью функции os.open и ее флагов.

•    Механизмы ORM (object relational mappers - объектно-реляционного отображения) для Python, такие как SQLObject и SQLAlchemy, упоминавшиеся выше, также способны обеспечить поддержку одновременной работы нескольких пользователей с реляционной базой данных, сохраняя в ней представление данных в виде наших классов Python.

•    Наконец, если размер нашего сайта станет больше, чем несколько интерактивных страниц, мы могли бы перейти от CGI-сценариев к более развитым веб-фреймворкам, таким как упоминавшиеся в начале этого раздела - Django, TurboGears, pyjamas и другие. На случай, если потребуется сохранять информацию между обращениями к страницам, можно было бы использовать такие инструменты, как cookies, скрытые поля ввода, сеансы, поддерживаемые модулем mod_python и FastCGI.

•    Если потребуется хранить на сайте информационное наполнение, производимое его пользователями, мы могли бы перейти на использование Plone. Это популярная и открытая система управления содержимым, написанная на языке Python, использующая сервер приложений Zope, реализующая модель документооборота и делегирующая управление содержимым сайта его авторам.

•    А если на повестке дня встанет поддержка беспроводных или распределенных интерфейсов, мы могли бы перенести нашу систему на сотовые телефоны, используя один из трансляторов с языка Python, доступных, например, для платформы Nokia и Google Android, или на платформу распределенных вычислений, такую как Google App Engine. Язык Python с успехом проникает в области, куда ведет развитие технологий.

Но, тем не менее, и графический, и веб-интерфейс, созданные нами, вполне справляются со своей работой.


Конец демонстрационного примера

На этом мы заканчиваем знакомство с вводным демонстрационным примером использования языка Python. Мы исследовали способы представления данных, ООП, механизмы сохранения объектов, инструменты создания графических интерфейсов и основы разработки веб-сайтов. Ни одну из этих тем мы не рассматривали достаточно глубоко. Тем не менее хотелось бы надеяться, что эта глава пробудила в вас любопытство к программированию приложений на языке Python.

В оставшейся части книги мы углубимся в изучение этих и других инструментов и тем прикладного программирования, чтобы помочь вам включить язык Python в работу в ваших собственных программах. В следующей главе мы начнем наше путешествие с изучения инструментов системного программирования и администрирования, имеющихся в распоряжении программистов на языке Python.

Скрытые «сюрпризы» в Python

На настоящий момент, когда я пишу эти строки в 2010 году, я занимаюсь языком Python уже почти 18 лет, и я видел, как он вырос из никому не известного языка в инструмент, который в том или ином виде используется практически каждой организацией, занимающейся разработкой, и входит в четверку или пятерку наиболее используемых языков программирования в мире. Это были лучшие годы.

Оглядываясь назад, могу сказать, что если в языке Python что-то и осталось действительно неизменным, так это его врожденная


способность заставлять разработчиков акцентировать внимание на качестве программного кода. И это практически неизбежно. Язык, который требует, чтобы разработчик форматировал программный код для большей удобочитаемости, не может не заставить людей поднимать вопросы о выборе наиболее удачных приемов разработки программного обеспечения.

Пожалуй, ничто так не подчеркивает эту сторону жизни языка Python, как модуль this из стандартной библиотеки - своего рода сюрприз, или «пасхальное яйцо» в Python, созданный одним из основных разработчиков Python - Тимом Петерсом (Tim Peters), который хранит в себе список основных принципов, на которых основывается язык. Чтобы увидеть их, запустите интерактивный сеанс интерпретатора Python и импортируйте модуль (естественно, он доступен на всех платформах):

>>> import this

The Zen of Python, by Tim Peters

Beautiful is better than ugly.

Explicit is better than implicit.

Simple is better than complex.

Complex is better than complicated.

Flat is better than nested.

Sparse is better than dense.

Readability counts.

Special cases aren’t special enough to break the rules.

Although practicality beats purity.

Errors should never pass silently.

Unless explicitly silenced.

In the face of ambiguity, refuse the temptation to guess.

There should be one-- and preferably only one --obvious way to do it.

Although that way may not be obvious at first unless you’re Dutch.

Now is better than never.

Although never is often better than *right* now.

If the implementation is hard to explain, it’s a bad idea.

If the implementation is easy to explain, it may be a good idea.

Namespaces are one honking great idea -- let’s do more of those!

>>>

(Перевод:

Дзен языка Python, составлен Тимом Петерсом

Красивое лучше, чем уродливое.

Явное лучше, чем неявное.

Простое лучше, чем сложное.

Сложное лучше, чем запутанное.

Плоское лучше, чем вложенное.

Разреженное лучше, чем плотное.


Удобочитаемость имеет значение.

Особые случаи не настолько особые, чтобы нарушать правила.

При этом практичность важнее безупречности.

Ошибки никогда не должны замалчиваться.

Если не замалчиваются явно.

Встретив двусмысленность, отбрось искушение угадать.

Должен существовать один и, желательно, только один очевидный способ

сделать что-то.

Хотя он поначалу может быть и не очевиден, если вы не голландец.

Сейчас лучше, чем никогда.

Хотя никогда зачастую лучше, чем *прямо сейчас*.

Если реализацию сложно объяснить - идея плоха.

Если реализацию легко объяснить - идея, возможно, хороша.

Пространства имен - отличная штука! Будем делать их побольше!

)1

Особого упоминания заслуживает правило «Явное лучше, чем неявное», которое в мире Python известно, как аббревиатура «EIBTI» («Explicit is better than implicit») - одна из основных идей языка Python, и одно из самых сильных отличий от других языков. Любой, кто проработал на этой ниве более, чем несколько лет, сможет засвидетельствовать, что волшебство и инженерное искусство есть вещи несовместимые. Конечно, сам язык Python не всегда неукоснительно следовал всем этим правилам, но старался придерживаться их как можно ближе. И если Python заставляет людей задумываться о таких вещах, то это уже победа. Кстати, название языка отлично смотрится на футболке.


Перевод взят из Википедии: http://ru.wikipedia.org/wiki/Python - Прим. перев.


Системное

программирование

В этой первой посвященной деталям части книги представлены инструменты Python для системного программирования - интерфейсы к службам операционной системы, а также к контексту выполнения программы. Эта часть книги состоит из следующих глав:

Глава 2

Эта глава обеспечивает полный обзор часто используемых инструментов системного программирования. Она неторопливо знакомит с инструментами и приемами, которые мы будем использовать далее в этой книге, и отчасти может использоваться, как справочник.

Глава 3

Эта глава продолжает тему, начатую в главе 2, и показывает, как работать со стандартными потоками ввода-вывода, аргументами командной строки, переменными окружения и многим другим.

Глава 4

Эта глава продолжает знакомить нас с системными интерфейсами и описывает инструменты и приемы, используемые при работе с файлами и каталогами. В этой главе мы познакомимся с двоичными файлами, с приемами обхода деревьев и так далее.

Глава5

Эта глава служит введением в поддержку библиотекой Python параллельного выполнения программ. Здесь вы найдете описание потоков выполнения, механизма ветвления процессов, каналов, сокетов, сигналов, очередей и тому подобного.

Глава 6

Последняя глава этой части содержит коллекцию типичных примеров системного программирования, основанных на материале первых четырех глав. Сценарии Python, представленные здесь, выполняют реальные задачи; демонстрируют среди прочего, как разрезать и объединять файлы, сравнивать и копировать каталоги, тестировать другие программы, а также отыскивать и запускать выполняемые файлы.

Хотя в этой части книги особое значение придается задачам системного программирования, представленные в ней средства являются универсальными и часто будут использоваться в последующих главах.

2

Системные инструменты


«os.path - дорога к знанию»

В этой главе начинается детальное рассмотрение способов применения Python для решения практических задач программирования. В этой и в последующих главах мы увидим, как использовать Python для разработки системных инструментов, графических интерфейсов пользователя, приложений баз данных, веб-сценариев, веб-сайтов и многого другого. Попутно на практических примерах будут также изучаться важнейшие концепции программирования на Python: повторное использование программного кода, простота сопровождения, объектноориентированное программирование (ООП) и так далее.

В этой части книги мы начнем свое путешествие по программированию на языке Python с исследования области системных приложений - сценариев, работающих с файлами, программами и окружением программ в целом. Хотя примеры из этой области сфокусированы на определенном типе задач, используемые в них приемы окажутся полезными и в последующих частях книги. Иными словами, если вы еще не эксперт по системному программированию на Python, вам следует пускаться в путь именно с этого места.


Зачем здесь нужен Python?

Системные интерфейсы Python обслуживают области приложений, но в последующих пяти главах большинство примеров будет относиться к категории системных инструментов - программ, иногда называемых утилитами командной строки, сценариями командной оболочки, программами системного администрирования, системными программами и другими сочетаниями этих слов. Независимо от того, знакомы

ли вам эти названия, вы, вероятно, уже знакомы со сценариями этого типа: они выполняют такие задачи, как обработка файлов в каталоге, запуск тестовых сценариев и тому подобное. Исторически такие программы писались на непереносимых и синтаксически неочевидных языках оболочек, таких как командные файлы DOS, csh и awk.

Однако даже в этой относительно простой области ярко проявляются лучшие свойства Python. Например, простота использования Python и обширное многообразие встроенных библиотек упрощают (и даже делают приятным) использование развитых системных инструментов, таких как потоки выполнения, сигналы, ветвление процессов, сокеты и аналогичные им; такие инструменты намного сложнее использовать в неясном синтаксисе языков оболочек и в многоэтапных циклах разработки на компилируемых языках. Поддержка в Python таких идей, как ясность программного кода и объектно-ориентированное программирование, способствует созданию таких инструментов оболочки, которые можно читать, сопровождать и повторно использовать. При использовании Python нет необходимости начинать с нуля каждый новый сценарий.

Более того, мы обнаружим, что в Python не только есть все интерфейсы, необходимые для разработки системных инструментов, но он также обеспечивает переносимость сценариев. При использовании стандартной библиотеки Python большинство системных сценариев, написанных на языке Python, автоматически становятся переносимыми на все основные платформы. Например, сценарий для обработки каталогов, написанный под Windows, обычно может выполняться и под Linux безо всякой правки исходных текстов: достаточно просто скопировать сценарий. Для разработки сценариев, обеспечивающих такой уровень переносимости, необходимо прикладывать некоторые усилия, тем не менее при разумном использовании Python может стать единственным средством, которым необходимо владеть для создания системных сценариев.


В следующих пяти главах

Чтобы упростить изучение этой части книги, я поделил ее на пять глав:

•    В этой главе я познакомлю вас с основными системными модулями в виде краткого обзора. Здесь мы впервые встретим некоторые из наиболее часто используемых системных инструментов.

•    В главе 3 мы продолжим исследование основных системных интерфейсов - изучением их роли в терминах системного программирования, таких как потоки ввода-вывода, аргументы командной строки, переменные окружения и так далее.

•    В главе 4 мы сосредоточимся на изучении инструментов Python для работы с файлами, каталогами и деревьями каталогов.

•    В главе 5 мы перейдем к изучению стандартных инструментов Python для реализации параллельной обработки данных - процессов, потоков выполнения, очередей, каналов, сигналов и многих других.

•    В главе 6, завершающей эту часть книги, будет представлена коллекция законченных системных программ. Здесь приводятся более крупные примеры, имеющие практическую ценность; в них для решения практических задач используются инструменты, представленные в четырех предыдущих главах. В состав этой коллекции входят не только универсальные системные сценарии, но и сценарии для обработки каталогов с файлами.

В примерах, представленных в последней главе этой части книги, мы будем уделять большое внимание не только системным интерфейсам, но и принципам разработки программ на языке Python в целом. Попутно будут представлены процедурные и объектно-ориентированные версии одних и тех же примеров, чтобы показать преимущества стратегического мышления.

«Батарейки - в комплекте»

В данной главе и в следующих за ней речь идет одновременно о языке Python и о его стандартной библиотеке - коллекции модулей, написанных на языке Python и C, которые автоматически устанавливаются вместе с интерпретатором. Хотя Python и представляет собой легкий язык сценариев, большая часть операций в реальных разработках на Python выполняется с привлечением этой обширной библиотеки инструментов (по последним подсчетам - несколько сотен модулей), поставляемых вместе с пакетом Python.

В действительности стандартная библиотека обладает настолько широкими возможностями, что нередко можно слышать в отношении Python фразу «batteries included» (батарейки - в комплекте), обычно приписываемую Фрэнку Стаяно (Frank Stajano) и означающую, что все необходимое для практической повседневной деятельности уже присутствует в стандартной библиотеке и может быть импортировано. Несмотря на то, что стандартная библиотека не является частью самого языка, тем не менее она является стандартной частью системы Python, и можно быть уверенным, что она будет доступна везде, где выполняются сценарии. Фактически это одно из наиболее существенных отличий Python от некоторых других языков сценариев - благодаря тому, что в составе Python поставляется огромное количество библиотечных инструментов, для программистов на Python вспомогательные сайты не имеют такого большого значения, как CPAN для программистов на Perl.


Как будет показано далее, стандартные библиотеки выполняют существенную часть задачи программирования на Python. Овладев основами языка, вы заметите, что в основном заняты применением встроенных функций и модулей, поставляемых вместе с системой. С другой стороны, библиотеки обеспечивают все самое интересное. В реальном мире программы становятся интересными, когда в них начинают использоваться службы, находящиеся вне интерпретатора языка: сети, файлы, графические интерфейсы, базы данных и так далее. Все это поддерживается стандартной библиотекой Python.

Помимо стандартной библиотеки для Python существуют дополнительные пакеты, созданные сторонними разработчиками, которые могут быть получены и установлены отдельно. Когда писалась эта книга, большинство таких расширений сторонних разработчиков можно было найти путем поиска в Интернете и по ссылкам на http://www.python.org и на веб-сайте PyPI (ссылка на который также приводится на сайте http://www.python.org). Некоторые сторонние расширения являются крупными системами. Например, расширения NumPy, Djangо и VPython реализуют операции векторной алгебры, обеспечивают конструирование сайтов и предоставляют средства визуализации соответственно.

Если нужно сделать с помощью Python что-либо специальное, есть большая вероятность, что необходимая поддержка уже включена в состав стандартной библиотеки или реализована в виде бесплатного модуля с открытым исходным кодом. Большинство инструментов, используемых в этой книге, входит в стандартный дистрибутив Python, а то, что должно устанавливаться отдельно, будет отмечено особо. Конечно, идиома повторного использования программного кода делает программы зависимыми от используемого в них кода, однако на практике, в чем мы еще не раз убедимся в этой книге, мощные библиотеки с открытыми исходными текстами развиваются очень быстро, не запирая своих пользователей в узком кругу фиксированных возможностей и ограничений.



Знакомство с разработкой системных сценариев

Исследование области системного программирования мы начнем с краткого обзора модулей sys и os из стандартной библиотеки, а затем перейдем к более важным понятиям системного программирования. Из перечня атрибутов этих модулей можно заключить, что это очень крупные модули, - следующий пример интерактивного сеанса был получен в Python 3.1 и в Windows 7 вне среды IDLE:


C:\...\PP4E\System> python

Python 3.1.1 (r311:74483, Aug 17 2009, 17:02:12) [MSC v.1500 32 bit (...)] on win32

Type "help”, "copyright”, "credits” or "license” for more information.

>>> import sys, os

>>> len(dir(sys))    # 65 атрибутов

65

>>> len(dir(os))    # в Windows 122 атрибута, в Unix - больше

122

>>> len(dir(os.path)) # модуль, вложенный в os 52

Содержимое этих двух модулей может отличаться для разных версий Python и платформ. Например, модуль os имеет намного больший размер после сборки Python 3.1 из исходных текстов под Cygwin (Cygwin - система, обеспечивающая Unix-подобную функциональность в Windows; о ней рассказывается во врезке «Подробнее о Cygwin Python для Windows» в главе 5):

$ ./python.exe

Python 3.1.1 (r311:74480, Feb 20 2010, 10:16:52)

[GCC 3.4.4 (cygming special, gdc 0.12, using dmd 0.125)] on cygwin Type "help”, "copyright”, "credits” or "license” for more information.

>>> import sys, os >>> len(dir(sys))

64

>>> len(dir(os))

217

>>> len(dir(os.path))

51

Я не собираюсь представлять все элементы в каждом встроенном модуле, поэтому прежде всего я хочу показать, как самостоятельно получать более подробную информацию. Такая задача может служить формальным оправданием тому, что здесь будут представлены некоторые базовые понятия системного программирования. Попутно мы напишем код первого сценария для форматирования документации.


Системные модули Python

Большинство интерфейсов Python системного уровня находится всего в двух модулях: sys и os. Впрочем, это несколько упрощенное представление - к данной области относятся и другие стандартные модули. В их числе:

glob

Реализует механизм подстановки имен файлов

socket

Обеспечивает возможности создания сетевых соединений и взаимодействий между процессами (Inter-Process Communication, IPC)

threading, _thread, queue

Средства запуска и синхронизации параллельных потоков выполнения

time, timeit

Обеспечивают возможность получения информации о системном времени

subprocess, multiprocessing

Средства запуска и управления параллельными процессами

signal, select, shutil, tempfile и другие Для решения других системных задач

Некоторые сторонние расширения, такие как pySerial (интерфейс к последовательному порту), Pexpect (механизм управления взаимодействиями между программами, напоминающий утилиту Expect) и даже Twisted (сетевой фреймворк), также могут быть отнесены к разряду системных инструментов. Кроме того, некоторые встроенные функции также в действительности являются системными интерфейсами -функция open, например, обеспечивает интерфейс к файловой системе. Но в общем и целом ядро арсенала системных инструментов Python образуют модули sys и os.

В теории, по крайней мере, модуль sys экспортирует компоненты, относящиеся к самому интерпретатору Python (например, путь поиска модулей), a модуль os содержит переменные и функции, соответствующие операционной системе, в которой выполняется Python. На практике это различие может быть не столь отчетливым (например, стандартные потоки ввода и вывода находятся в модуле sys, но можно утверждать, что они связаны с парадигмами операционной системы). Могу вас обрадовать: инструменты, находящиеся в этих модулях, будут использоваться так часто, что их местонахождение прочно отпечатается в вашей памяти.4

Модуль os пытается также предоставить переносимый интерфейс программирования для используемой операционной системы: его функции могут быть по-разному реализованы на различных платформах, но для сценариев на языке Python они выглядят одинаково. Кроме того, модуль os экспортирует вложенный подмодуль os.path, предоставляющий переносимый интерфейс к средствам обработки файлов и каталогов.


Источники документации по модулям

Как можно заключить из предыдущих абзацев, обучение системному программированию на языке Python в основном сводится к изучению системных модулей Python. К счастью, существует множество источников информации, облегчающих эту задачу, - от атрибутов модуля до печатных справочников и книг.

Например, если потребуется узнать, какие элементы экспортирует встроенный модуль, можно прочесть соответствующий раздел руководства по библиотеке, исследовать его исходный код (все-таки Python является открытым программным обеспечением) или получить список атрибутов и строку документации в интерактивном режиме. Давайте импортируем модуль sys в Python 3.1 и посмотрим, что в нем находится:

C:\...\PP4E\System> python >>> import sys >>> dir(sys)

[‘__displayhook__’, ‘__doc__’, ‘__excepthook__’, ‘__name__’, ‘__package__’,

‘__stderr__’, ‘__stdin__’, ‘__stdout__’, ‘_clear_type_cache’, ‘_current_

frames’, ‘_getframe’, ‘api_version’, ‘argv’, ‘builtin_module_names’, ‘byteorder’,’call_tracing’, ‘callstats’, ‘copyright’, ‘displayhook’,

‘dllhandle’, ‘dont_write_bytecode’, ‘exc_info’, ‘excepthook’, ‘exec_ prefix’, ‘executable’, ‘exit’, ‘flags’, ‘float_info’, ‘float_repr_style’, ‘getcheckinterval’, ‘getdefaultencoding’, ‘getfilesystemencoding’,

‘getprofile’, ‘getrecursionlimit’, ‘getrefcount’, ‘getsizeof’, ‘gettrace’, ‘getwindowsversion’, ‘hexversion’, ‘int_info’, ‘intern’, ‘maxsize’,

‘maxunicode’, ‘meta_path’, ‘modules’, ‘path’, ‘path_hooks’, ‘path_importer_ cache’, ‘platform’, ‘prefix’, ‘ps1’, ‘ps2’,

‘setcheckinterval’, ‘setfilesystemencoding’, ‘setprofile’, ‘setrecursionlimit’, ‘settrace’, ‘stderr’, ‘stdin’, ‘stdout’, ‘subversion’, ‘version’, ‘version_ info’, ‘warnoptions’, ‘winver’]

Функция dir просто возвращает список строк с именами всех атрибутов для любого объекта, имеющего атрибуты; это удобная подсказка по содержимому модуля при работе в интерактивном режиме. Мы можем понять, например, что существует нечто с именем sys.version, поскольку имя version присутствует в списке, возвращаемом функцией dir. Если этого недостаточно, всегда можно обратиться к строке __doc__ встроенного модуля:

>>> sys.__doc__

"This module provides access to some objects used or maintained by the\ ninterpreter and to functions that interact strongly with the interpreter.\n\ nDynamic objects:\n\nargv -- command line arguments; argv[0] is the script pathname if known \npath -- module search path; path[0] is the script directory, else ‘’\nmodules -- dictionary of loaded modules\n\ndisplayhook -- called to show results in an i ...далее следует еще много текста...


Постраничный вывод строк документации

Встроенный атрибут __doc__ обычно содержит строку документации, которая может выглядеть несколько странно при отображении в таком виде, - это одна длинная строка с символами перевода строки, выводящимися как \n, а не красивый список строк. Чтобы отформатировать эти строки и придать им более удобочитаемый вид, можно воспользоваться функцией print:

>>> print(sys.__doc__)

This module provides access to some objects used or maintained by the interpreter and to functions that interact strongly with the interpreter.

Dynamic objects:

argv -- command line arguments; argv[0] is the script pathname if known path -- module search path; path[0] is the script directory, else ‘’ modules -- dictionary of loaded modules

...далее следует еще много текста...

Встроенная функция print, в отличие от механизма вывода в интерактивной оболочке, корректно интерпретирует символы перевода строки. К сожалению, функция print не реализует возможность прокрутки или постраничного просмотра и поэтому может быть неудобной на некоторых платформах. Большего успеха можно добиться с помощью других инструментов, таких как встроенная функция help:

>>> help(sys)

Help on built-in module sys:

NAME

sys

FILE

(built-in)

MODULE DOCS

http://docs.python.org/library/sys

DESCRIPTION

This module provides access to some objects used or maintained by the interpreter and to functions that interact strongly with the interpreter.

Dynamic objects:

argv -- command line arguments; argv[0] is the script pathname if known path -- module search path; path[0] is the script directory, else ‘’ modules -- dictionary of loaded modules

...далее следует еще много текста...

Функция help - это один из интерфейсов, предоставляемых системой PyDoc. Она входит в состав стандартной библиотеки, распространяемой вместе с Python, и предназначена для отображения в форматированном виде документации (строк документации, а также дополнительной структурной информации), связанной с объектом. Документация может быть в формате страниц справочного руководства Unix, который используется для вывода с помощью функции help, или в виде HTML-страницы, что еще лучше. Это очень удобный способ получения начальной информации при работе в интерактивном режиме, и это последний шанс разобраться, прежде чем погрузиться в справочники и книги.


Сценарий постраничного вывода

Функция help, с которой мы только что познакомились, также не обладает достаточной гибкостью при отображении информации. Хотя она и пытается в некоторых ситуациях обеспечить постраничный вывод, тем не менее на некоторых компьютерах - из тех, на которых мне приходилось работать, - она неточно выбирает размер страницы. Кроме того, она вообще не обеспечивает постраничный просмотр в графическом интерфейсе IDLE; вместо этого предлагается использовать полосу прокрутки, что весьма неудобно на больших мониторах. Когда мне требуется получить более полный контроль над тем, как функция help будет выводить текст, я обычно использую свой собственный вспомогательный сценарий, представленный в примере 2.1.

Пример 2.1. PP4E\System\more.py

разбивает строку или текстовый файл на страницы для интерактивного просмотра def more(text, numlines=15):

lines = text.splitlines()    # подобно split(‘\n’) но без ‘’ в конце

while lines:

chunk = lines[:numlines] lines = lines[numlines:] for line in chunk: print(line)

if lines and input(‘More?’) not in [‘y’, ‘Y’]: break if __name__ == ‘__main__’:

import sys    # если запускается как сценарий

more(open(sys.argv[1]).read(), 10) # отобразить постранично содержимое

# файла, указанного в командной строке

Главной в этом файле является функция more, и если вы обладаете достаточными знаниями языка Python, чтобы читать эту книгу, вы без труда поймете ее. Она просто разбивает строку по символам перевода строки, а затем извлекается срез и выводится сразу несколько строк (по умолчанию 15), чтобы избежать прокрутки экрана. Выражение извлечения среза lines[:15] вернет первые 15 элементов списка, a выражение lines[15:] - последние. Чтобы изменить размер страницы, передайте требуемое число строк в аргументе numlines (например, в последней строке примера 2.1 в аргументе numlines функции more передается число 10).

Вызов строкового метода splitlines, используемый в этом сценарии, возвращает список подстрок, полученный в результате разбиения исходной строки по символам перевода строки (например, [“line”, “line”,...]). Альтернативный метод split позволяет получить похожий результат, но в последнем элементе массива он возвращает пустую строку, если исходная строка заканчивается символом \n:

>>> line = 'aaa\nbbb\nccc\n'

>>> line.split('\n')

[‘aaa’, ‘bbb’, ‘ccc’, ‘’]

>>> line.splitlines()

[‘aaa’, ‘bbb’, ‘ccc’]

Как будет показано далее в главе 4, символом конца строки в сценариях на языке Python всегда является \n (обозначающий байт с числовым значением 10), вне зависимости от платформы. (Если вы еще не знаете, почему это имеет значение, - символы DOS \r отбрасываются при чтении.)


Основы использования строковых методов

Пример 2.1 - это простая программа на языке Python, но в ней представлены три важные темы, заслуживающие краткого упоминания: она использует строковые методы, осуществляет чтение из файла и предусматривает возможность импортирования или выполнения как самостоятельного сценария. Строковые методы в языке Python как таковые не являются системными инструментами, но их можно встретить в большинстве программ на языке Python. В действительности они будут постоянно встречаться на протяжении этой и последующих глав, поэтому коротко рассмотрим наиболее полезные инструменты, имеющиеся в наборе. В число строковых методов входят вызовы для поиска и замены:

>>> mystr = 'xxxSPAMxxx'

>>> mystr.find('SPAM')    # вернет индекс первого вхождения

3

>>> mystr = 'xxaaxxaa'

>>> mystr.replace('aa', 'SPAM') # замена всех вхождений

‘xxSPAMxxSPAM’

Вызов метода find возвращает смещение первого вхождения подстроки, а метод replace осуществляет глобальный поиск и замену. Как и все строковые операции, метод replace возвращает новую строку, оставляя исходную строку неизменной (напомню, что строки являются неизменяемыми объектами). Для всех этих методов подстроки являются просто строками; в главе 19 будет представлен модуль re, который позволяет использовать шаблоны регулярных выражений при поиске и замене.

В самых последних версиях Python имеется возможность использовать оператор in определения принадлежности, как альтернативу методу find, когда необходимо всего лишь получить ответ «да» или «нет» (он проверяет присутствие подстроки в строке). Существует также несколько методов, удаляющих пробельные символы из концов строки, что особенно полезно при работе с текстовыми строками, извлекаемыми из файла:


>>> mystr = 'xxxSPAMxxx'


>>> 'SPAM' in mystr

# проверка присутствия подстроки в строке

True


>>> 'Ni' in mystr

# если подстрока отсутствует

False


>>> mystr.find('Ni')


-1

>>> mystr = '\t Ni\n'

>>> mystr.strip()    # удалит пробельные символы

‘Ni’

>>> mystr.rstrip()    # то же самое, но только с правого конца

‘\t Ni’

Среди строковых методов имеются также функции, которые могут быть полезны, например, как инструменты преобразования регистра символов, а кроме того, в стандартной библиотеке имеется модуль string, определяющий некоторые полезные константы:

>>> mystr = 'SHRUBBERY'

>>> mystr.lower()    #    преобразует регистр символов

‘shrubbery’

>>> mystr.isalpha()    #    проверяет содержимое

True

>>> mystr.isdigit()

False

>>> import string    # константы, например, для использования в ‘in’

>>> string.ascii_lowercase

‘abcdefghijklmnopqrstuvwxyz’

>>> string.whitespace    #    пробельные символы

‘ \t\n\r\x0b\x0c’

Существуют также методы, позволяющие разбивать строки по подстрокам-разделителям и объединять их вместе, вставляя между ними подстроку. Эти средства будут изучены далее в этой книге, но в качестве знакомства покажем, как они работают:

>>> mystr = 'aaa,bbb,ccc'

>>> mystr.split(',')    # разбить в список подстрок

[‘aaa’, ‘bbb’, ‘ccc’] >>> mystr = 'a b\nc\nd'

>>> mystr.split()    # разделитель по умолчанию: пробельные символы

[‘a’, ‘b’, ‘c’, ‘d’]

>>> delim = 'NI'

>>> delim.join(['aaa', 'bbb', 'ccc'])    # объединить подстроки из списка

‘aaaNIbbbNIccc’

>>> ' '.join(['A', 'dead', 'parrot']) # добавить пробел между подстроками ‘A dead parrot’

>>> chars = list('Lorreta') # преобразовать в список символов >>> chars

[‘L’, ‘o’, ‘r’, ‘r’, ‘e’, ‘t’, ‘a’]

>>> chars.append('!')

>>> ''.join(chars)    # преобразовать в строку: с пустым разделителем

‘Lorreta!’

Эти вызовы оказываются удивительно мощными. Например, строку с колонками данных, разделенными символами табуляции, можно разобрать по колонкам единственным вызовом метода split; сценарий more.py, представленный выше, использует разновидность splitlines этого метода, чтобы разбить строку в список строк. На практике вызов метода replace можно эмулировать с помощью комбинации split/join:

>>> mystr = 'xxaaxxaa'

>>> 'SPAM'.join(mystr.split('aa')) # усложненная версия str.replace! ‘xxSPAMxxSPAM’

Запомните на будущее, что язык Python не предусматривает автоматического преобразования строк в числа и обратно, поэтому если в этом возникнет необходимость, такие преобразования необходимо выполнять явно:

>>> int("42"), eval("42")    # преобразование строки в целое число

(42, 42)

>>> str(42), repr(42)    # преобразование целого числа в строку

(‘42’, ‘42’)

>>> ("%d" % 42), '{:d}'.format(42) # с помощью оператора и метода форматиров. (‘42’, ‘42’)

>>> "42” + str(1), int("42") + 1    # в операциях конкатенации и сложения

(‘421’, 43)

В последней приведенной инструкции первое выражение выполняет конкатенацию строк (так как оба операнда являются строками), а второе выполняет сложение целых чисел (поскольку оба объекта являются числами). Python не делает предположений о том, какое преобразование вы могли иметь в виду, и не выполняет преобразования автоматически. Одно из главных правил интерпретатора Python - где только возможно, избегать закулисных магических действий и попыток что-то угадывать. Более подробно о средствах для работы со строками будет рассказано далее (им посвящена целая глава в пятой части), но, кроме того, стоит посмотреть описание дополнительных строковых инструментов в руководстве по библиотеке.


Другие особенности строк в Python 3.X:


Юникод и тип bytes

Строго говоря, история со строками в Python 3.X гораздо богаче, чем можно было бы заключить из вышесказанного. До сих пор было продемонстрировано, что объекты типа str являются последовательностями символов (точнее - «кодовыми пунктами» Юникода, представляющими «элементы» Юникода), которые могут быть не только символами ASCII, но и многобайтовыми символами Юникода и предусматривают возможность кодирования и декодирования вручную или автоматически при выполнении операций с текстовыми файлами. Строки в программном коде заключаются в кавычки (например, abc) и допускают использование дополнительного синтаксиса для представления символов, не входящих в набор ASCII (например, ‘\xc4\xe8’, ‘\u00c4\u00e8’).

Однако на самом деле в Python 3.X имеется два дополнительных строковых типа, поддерживающих большинство операций, которыми обладает тип str: тип bytes - последовательность коротких целых чисел для представления 8-битовых двоичных данных и тип bytearray - изменяемый вариант типа bytes. Как вы уже знаете, присутствие символа «Ь» перед открывающей кавычкой (например, b’abc’, b’\xc4\xe8’) говорит о том, что вы имеете дело с объектом типа bytes. Как будет показано в главе 4, файлы в Python 3.X также проявляют подобную двойственность: при работе в текстовом режиме используется тип str (при этом предусматриваются преобразования символов конца строки и символов Юникода, в соответствии с указанной кодировкой), а при работе в двоичном режиме используется тип bytes (в этом случае данные при чтении/записи не подвергаются преобразованиям). В главе 5 мы увидим такое же деление при работе с такими инструментами, как сокеты, которые на сегодняшний день работают со строками байтов

Текст Юникода используется в интернационализированных приложениях, и многие инструменты языка Python, ранее ориентированные на работу с двоичными данными, в настоящее время работают со строками байтов. К ним относятся некоторые инструменты для работы с файлами, которые мы встретим далее, такие как функция open, а также инструменты os.listdir и os.walk, которые мы будем изучать в последующих главах. Как будет показано ниже, даже простые инструменты для работы с каталогами должны иметь возможность обрабатывать символы Юникода в содержимом и в именах файлов. Кроме того, инструменты для сериализации объектов и анализа двоичных данных на сегодняшний день ориентированы на работу со строками байтов.

Далее в этой книге мы также увидим, что в настоящее время Юникод используется для представления текста в графических интерфейсах; для обмена данными по сети, в виде последовательностей байтов; в стандартных инструментах Интернета, таких как электронная почта; и даже в некоторых механизмах сохранения объектов, таких как файлы DBM и модуль shelve. Любой интерфейс, предусматривающий работу с текстом, на сегодняшний день обязательно предусматривает работу с Юникодом, потому что тип str представляет строки символов Юникода, а не только ASCII. Как только мы в этой книге доберемся до сферы программирования приложений, для большинства программистов на Python 3.X тема Юникода перестанет быть необязательной.

Мы отложим дальнейшее обсуждение Юникода, пока нам не представится возможность увидеть его в прикладном контексте и в практических программах. Более фундаментальное освещение поддержки текстовых и двоичных данных Юникода в строках и файлах вы найдете в четвертом издании книги «Изучаем Python». Эта книга официально посвящена основам языка, что предполагает углубленное рассмотрение тем и позволило отвести этой теме отдельную главу, занимающую 45 страниц.


Основы операций с файлами

Помимо обработки строк, сценарий more.py также использует файлы - он открывает внешний файл, имя которого задается в командной строке, с помощью встроенной функции open, и целиком считывает его в память с помощью метода read объекта файла. Поскольку объекты файлов, возвращаемые функцией open, являются составной частью самого базового языка Python, я могу предположить, что вы хотя бы бегло знакомы с ними. Но в случае, если эта глава попалась вам в самом начале изучения Python, подскажу, что следующие вызовы загружают в строку содержимое файла, загружают в строку набор байтов фиксированной длины, загружают содержимое файла в список строк и загружают в строку очередную строку файла, соответственно:

open(‘file’).read()    # читает    весь файл    в    строку

open(‘file’).read(N)    # читает    следующие    N    байтов    в    строку

open(‘file’).readlines()    # читает    весь файл    в    массив    строк

open(‘file’).readline()    # читает    следующую    строку, до    символа ‘\n’

Как мы скоро увидим, эти вызовы можно также применять в Python к командам оболочки, чтобы прочитать их вывод. У объектов файлов есть также методы write, которые посылают строки в соответствующий файл. Более глубоко темы, связанные с файлами, раскрываются в главе 4, однако сами операции вывода данных в файл и чтения их обратно в языке Python реализуются очень просто:

>>> file = open('spam.txt', 'w')    # создать файл spam.txt

>>> file.write(('spam' * 5) + '\n')    # записать текст: вернет

21    # число записанных символов >>> file.close()

>>> file = open('spam.txt')    # или open(‘spam.txt’).read()

>>> text = file.read()    # прочитать в строку

>>> text

‘spamspamspamspamspam\n’


Два способа использования программ

Последние несколько строк в сценарии more.py из примера 2.1 знакомят нас с одним из первых важных понятий в программировании инструментов командной строки. Они настраивают файл так, чтобы его можно было использовать двумя способами: как сценарий или как библиотеку.

Напомню, что в каждом модуле Python доступна встроенная переменная__name__, в которую интерпретатор Python записывает значение

__main__, только если файл выполняется как программа, а не импортируется в качестве библиотеки. Благодаря этому функция more в этом файле автоматически выполняется в последней строке файла, когда сценарий запускается, как самостоятельная программа, а не импортируется в какое-либо другое место. Этот простой прием является ключом к созданию многократно используемых сценариев: благодаря реализации логики программы в виде функции, а не в виде программного кода верхнего уровня, ее можно импортировать и повторно использовать в других сценариях.

В результате появляется возможность запускать more.py отдельно или импортировать его и вызывать функцию more из любого другого места. При запуске файла как самостоятельной программы мы передаем ей в командной строке имя файла, который нужно прочесть и выводить постранично: в следующей главе будет полностью описано, как слова, вводимые в команде для запуска программы, появляются во встроенном списке sys.argv. Ниже приводится пример запуска файла сценария для постраничного вывода самого себя (эта команда должна выполняться в каталоге PP4E\System, иначе входной файл не будет найден; причина этого будет пояснена позднее):

C:\...\PP4E\System> python more.py more.py

разбивает строку или текстовый файл на страницы для интерактивного просмотра def more(text, numlines=15):

lines = text.splitlines()    # подобно split(‘\n’), но без ‘’ в конце

while lines:

chunk = lines[:numlines] lines = lines[numlines:] for line in chunk: print(line)

More?y

if lines and input(‘More?’) not in [‘y’, ‘Y’]: break

if__name__== ‘__main__’:

import sys    # если запускается как сценарий

more(open(sys.argv[1]).read(), 10) # отобразить постранично содержимое

# файла, указанного в командной строке

Если мы импортируем файл more.py, мы явно передаем строку в его функцию more; функция more - как раз такая утилита, которая нам нужна для просмотра текста документации. Запуск этой утилиты для просмотра строки документации модуля sys представит информацию о том, какие возможности дает этот модуль сценариям, в виде, пригодном для чтения:

C:\...\PP4E\System> python >>> from more import more >>> import sys >>> more(sys.__doc__)

This module provides access to some objects used or maintained by the interpreter and to functions that interact strongly with the interpreter.

Dynamic objects:

argv -- command line arguments; argv[0] is the script pathname if known path -- module search path; path[0] is the script directory, else ‘’ modules -- dictionary of loaded modules

displayhook -- called to show results in an interactive session excepthook -- called to handle any uncaught exception other than SystemExit To customize printing in an interactive session or to install a custom top-level exception handler, assign other functions to replace these.

stdin -- standard input file object; used by input()

More?

Нажатие клавиши у или Y заставит функцию отобразить несколько следующих строк документации и снова вывести приглашение, если список строк еще не закончился. Попробуйте сделать это у себя, и вы увидите, как выглядит оставшаяся часть строки документации. Кроме того, попробуйте поэкспериментировать, задавая размер окна во втором аргументе, - вызов more(sys.__doc__, 5) будет выводить текст блока

ми по 5 строк.


Руководства по библиотекам Python

Если изложение кажется недостаточно детальным, то полную информацию вы можете получить, обратившись к разделу, посвященному модулю sys, в руководстве по библиотекам Python. Все стандартные руководства Python доступны в Интернете и, кроме того, часто устанавливаются вместе с Python. В Windows стандартные руководства устанавливаются автоматически. Для обращения к руководствам приведу несколько простых указаний:

•    В Windows щелкните на кнопке Пуск (Start), выберите пункт Все программы (ALL Programs), затем выберите пункт Python и пункт Python ManuaLs (Руководства Python). Руководства чудесным образом появятся на вашем экране. Начиная с версии Python 2.4 руководства для Windows поставляются в формате файлов справки, благодаря чему они поддерживают возможность поиска и навигации.

•    В Linux или Mac OS X можно щелкнуть на элементе руководства в менеджере файлов или запустить броузер из командной строки и перейти в каталог, где в вашей системе находятся файлы HTML руководства.

•    Если в вашей системе руководств не обнаружилось, их всегда можно прочесть в Интернете. Перейдите на веб-сайт Python http://www. python.org и найдите ссылки, ведущие к документации. Этот сайт также обеспечивает возможность простого поиска поруководствам.

В любом случае выберите руководство «Library», если вас интересуют такие вещи, как модуль sys. Это руководство содержит описание всех стандартных модулей, встроенных типов данных и функций и многое другое. В комплект стандартных руководств Python входит также краткий учебник, справочник по языку, справочники по расширениям и многое другое.


Коммерческие справочники

Рискуя заслужить упрек за рекламу в книге, я должен упомянуть, что можно приобрести комплект руководств по Python, отпечатанный и переплетенный; подробности и ссылки можно найти на информационной странице по изданиям на сайте http://www.python.org. На сегодняшний день есть также коммерческие печатные справочники по Python, в том числе «Python Essential Reference»5, «Python in a Nutshell», «Python Standard Library» и «Python Pocket Reference». Некоторые из этих книг являются более полными и содержат примеры, при этом последний из перечисленных справочников удобно использовать как «напоминал-ку», после того как вы уже раз-другой изучили библиотеку.6


Модуль sys

Но достаточно разговоров об источниках информации (и основах разработки сценариев) - перейдем к подробностям, касающихся системных модулей. Как говорилось выше, модули sys и os образуют ядро набора инструментов Python для решения системных задач. Сделаем сейчас краткий интерактивный обзор некоторых инструментов, имеющихся в этих двух модулях, прежде чем использовать их в более крупных примерах. Начнем с модуля sys, меньшего из этих двух модулей. Напомню, чтобы получить полный список всех атрибутов модуля sys, вы можете передать его функции dir (или посмотреть на список, полученный нами выше в этой главе).


Платформы и версии

Как и в большинстве модулей, в модуле sys есть атрибуты, содержащие информацию, и функции, выполняющие действия. Например, в его атрибутах можно найти название операционной системы, в которой выполняется программный код, наибольшее целое число, поддерживаемое аппаратной платформой на данном компьютере (хотя в Python 3.X целые числа могут быть произвольной величины), и номер версии интерпретатора Python, выполняющего программный код:

C:\...\PP4E\System> python

>>> import sys

>>> sys.platform, sys.maxsize, sys.version

(‘win32’, 2147483647, ‘3.1.1 (r311:74483, Aug 17 2009, 17:02:12)

...дополнительные строки были удалены... ’)

>>> if sys.platform[:3] == 'win': print('hello windows')

hello windows

Если программный код должен по-разному выполняться на разных компьютерах, просто проверьте строку sys.platform, как сделано в этом примере. Несмотря на то, что Python по большей части независим от платформы, а непереносимые средства обычно заключаются в условные инструкции if, типа той, что здесь приведена, тем не менее далее, например, будет показано, что средства запуска программ и низкоуровневые взаимодействия с консолью различаются в зависимости от платформы. Поэтому проверка значения sys.platform поможет выбрать правильный инструмент для той машины, на которой выполняется сценарий.


Путь поиска модулей

Модуль sys позволяет также проверить путь поиска модулей как в интерактивном режиме, так и из программы на языке Python. Переменная sys.path хранит список строк, представляющих действительный путь поиска в выполняющемся интерпретаторе Python. Когда выполняется операция импортирования модуля, Python просматривает этот путь слева направо, пытаясь отыскать файл модуля в каждом каталоге, указанном в списке. Поэтому данная переменная позволит вам убедиться, что путь поиска действительно задан так, как нужно.7

Список sys.path просто инициализируется при первом запуске интерпретатора из PYTHONPATH с добавлением системных значений по умолчанию и содержимого файлов .pth, находящихся в каталогах со сценариями. В действительности, если заглянуть в список sys.path в интерактивной оболочке, можно обнаружить довольно много каталогов, которые отсутствуют в переменной PYTHONPATH: в него входит также указатель на домашний каталог сценария (пустая строка - назначение которой я объясню далее, после знакомства с функцией os.getcwd) и набор каталогов стандартных библиотек, который может быть различным в каждой установке:

>>> sys.path

[‘’, ‘C:\\PP4thEd\\Examples’, ...плюс каталоги стандартной библиотеки... ]

Как это ни удивительно, но список sys.path можно изменять программным способом. Сценарии могут использовать такие операции над списками, как append, extend, insert, pop и remove, а также использовать инструкцию del, чтобы изменять путь поиска модулей в процессе выполнения, чтобы подключить все каталоги с необходимыми модулями. Python всегда использует для импорта текущее значение sys.path, учитывая все внесенные вами изменения:

>>> sys.path.append(r'C:\mydir')

>>> sys.path

[‘’, ‘C:\\PP4thEd\\Examples’, ...more deleted..., ‘C:\\mydir’]

Такое непосредственное изменение переменной sys.path является альтернативой установке переменной оболочки PYTHONPATH, хотя и не самой лучшей. Изменения в sys.path сохраняются лишь до завершения процесса Python, и их нужно повторно вносить при каждом новом запуске программы или сеанса Python. Однако некоторые типы программ (например, сценарии, выполняющиеся на веб-сервере) не должны зависеть от значения PYTHONPATH. Такие сценарии могут сами настраивать список sys.path при запуске и включать в него все необходимые каталоги с импортируемыми модулями. Более конкретный пример использования sys.path приводится в примере 1.34, в предыдущей главе, где мы вынуждены были предусмотреть динамическую настройку пути поиска, так как применение веб-сервера не позволяет делать какие-либо предположения о путях к импортируемым модулям.

Пути к каталогам в Windows

Обратите внимание, что при настройке списка sys.path в примерах выше были использованы литералы необрабатываемых строк (raw string): поскольку обратный слеш в строке Python обычно означает начало экранированной последовательности, пользователи Windows должны следить за тем, чтобы удваивать символы слеша при использовании в строках с путями к каталогам (например, в строке "C:\\dir" комбинация \\ в действительности является экранированной последовательностью, означающей символ \), или использовать константы необрабатываемых строк, чтобы иметь возможность вставлять символ обратного слеша без всяких ухищрений (например, r"C:\dir").

При просмотре путей к каталогам в Windows (например, при выводе списка sys.path в интерактивной оболочке) Python выводит \\, как один символ \. Формально можно обойтись одним символом \, если за ним следует символ, не воспринимаемый Python как продолжение экранированной последовательности, но использовать удвоение и необрабатываемые строки обычно легче, чем запоминать таблицы экранированных последовательностей.

Обратите также внимание, что большинство библиотечных функций Python в качестве разделителей элементов путей к каталогам принимают как прямой (/), так и обратный (\) слеш, независимо от используемой платформы. Это значит, что / обычно действует и в Windows, что способствует созданию сценариев, переносимых на Unix. Описываемые далее в этой главе инструменты из модулей os и os.path также способствуют переносимости путей в сценариях.



Таблица загруженных модулей

В модуле sys есть также средства, позволяющие подключиться к интерпретатору. Например, переменная sys.modules служит словарем, содержащим записи вида namemodule для каждого модуля, импортированного в сеанс или программу Python (точнее, в вызывающий процесс Python):

>>> sys.modules

{‘reprlib’: <module ‘reprlib’ from ‘c:\python31\lib\reprlib.py’>,

...часть строк удалена...

>>> list(sys.modules.keys())

[‘reprlib’, ‘heapq’, ‘__future__’, ‘sre_compile’, ‘_collections’, ‘locale’,

‘_sre’, ‘functools’, ‘encodings’, ‘site’, ‘operator’, ‘io’, ‘__main__’, ...часть

строк удалена... ]

>>> sys

<module ‘sys’ (built-in)>

>>> sys.modules['sys']

<module ‘sys’ (built-in)>

Мы могли бы использовать эту переменную для создания программ, выводящих или иным образом обрабатывающих все модули, загруженные программой (нужно просто обойти в цикле список ключей sys.modules).

Аналогичным средством подключения к интерпретатору является счетчик ссылок на объекты, доступный через переменную sys.getrefcount, и список имен модулей, встроенных в выполняемый файл интерпретатора Python (sys.builtin_module_names). Более подробные сведения вы найдете в руководстве по библиотеке Python. Подобные переменные главным образом предназначены для получения внутренней информации интерпретатора Python, но иногда они могут иметь большое значение для программистов, создающих инструменты для других программистов.


Сведения об исключениях

Некоторые атрибуты модуля sys позволяют получить все сведения о самом последнем исключении, возбужденном интерпретатором Python. Это удобно, когда требуется реализовать обобщенную процедуру обработки исключений. Например, функция sys.exc_info возвращает кортеж, содержащий тип последнего исключения, его значение и объект с трассировочной информацией. В модели исключений, которая в Python 3 полностью основана на классах, первые два элемента кортежа соответствуют классу последнего исключения и его экземпляру:

>>> try:

...    raise IndexError

... except:

...    print(sys.exc_info())

(<class ‘IndexError’>, IndexError(), <traceback object at 0x019B8288>)

Эту информацию можно использовать для создания собственного сообщения об ошибке, выводимого в окне диалога графического интерфейса или в веб-странице HTML (напомню, что не обработанные исключения по умолчанию завершают программы с выводом сообщения об ошибке). Первые два элемента кортежа, возвращаемого этой функцией, по умолчанию предусматривают вывод полезной информации, а третий элемент, объект с трассировочной информацией, можно обработать с помощью стандартного модуля traceback:

>>> import traceback, sys >>> def grail(x):

...    raise TypeError('already got    one')

>>> try:

...    grail('arthur')

... except:

...    exc_info = sys.exc_info()

...    print(exc_info[0])

...    print(exc_info[1])

...    traceback.print_tb(exc_info[2])

<class ‘TypeError’> already got one

File "<stdin>", line 2, in <module>

File "<stdin>", line 2, in grail

Модуль traceback может также представлять сообщения в виде строк и записывать их в указанный объект файла - более подробную информацию вы найдете в руководстве по библиотеке Python.


Другие элементы, экспортируемые модулем sys

Модуль sys экспортирует и другие часто используемые инструменты, с которыми мы встретимся в контексте более крупных тем и примеров далее в этой части книги. Например:

•    Аргументы командной строки можно получить в виде списка строк под именем sys.argv

•    Стандартные потоки ввода-вывода доступны в виде sys.stdin, sys. stdout и sys.stderr

•    Завершение программы можно вызвать с помощью функции sys. exit

Однако поскольку все эти инструменты ведут к более крупным темам, о них будет рассказано ниже в отдельных разделах.


Модуль os

Как уже говорилось выше, модуль os - более крупный из двух основных системных модулей. В нем содержатся все обычные вызовы операционной системы, с которыми вы могли ранее встречаться в своих программах на языке C и в сценариях оболочки. Его вызовы имеют дело с каталогами, процессами, переменными оболочки и так далее. Формально этот модуль предоставляет инструментальные средства POSIX - переносимого стандарта вызовов операционной системы - вместе с платформонезависимыми средствами работы с каталогами, к которым относится вложенный модуль os.path. Функционально модуль os играет роль переносимого интерфейса к системным вызовам операционной системы: сценарии, написанные с использованием модулей os и os.path, обычно могут выполняться на любой платформе без внесения изменений. На некоторых платформах модуль os включает дополнительные инструменты, доступные только на этой платформе (например, низкоуровневые операции с процессами в Unix). Однако, в общем и целом, этот модуль является кросс-платформенным, насколько это технически возможно.


Инструменты в модуле os

Бросим беглый взгляд на основные интерфейсы в модуле os. В табл. 2.1 приводится список наиболее часто используемых инструментов из модуля os, организованных по функциональному назначению.


Таблица 2.1. Часто используемые инструменты из модуля os


Область применения

Инструменты

Переменные окружения

os.environ

Запуск программ

os.system, os.popen, os.execv, os.spawnv

Порождение дочерних процессов

os.fork, os.pipe, os.waitpid, os.kill

Дескрипторы файлов, блокировки

os.open, os.read, os.write

Обработка файлов

os.remove, os.rename, os.mkfifo, os.mkdir, os. rmdir

Инструменты администрирования

os.getcwd, os.chdir, os.chmod, os.getpid, os.listdir, os.access

Инструменты обеспечения переносимости

os.sep, os.pathsep, os.curdir, os.path.split, os.path.join

Инструменты для работы с путями к каталогам

os.path.exists(‘path’), os.path.isdir(‘path’), o s.p at h.g et s i z e (‘p a t h’)


Если попробовать получить перечень атрибутов этого модуля в интерактивном режиме, получится громадный список имен, который будет различным для разных версий Python. Скорее всего, он будет зависеть от платформы и не будет слишком полезен, если не изучить, что означает каждое имя (я позволил себе немного отформатировать этот список и удалить часть строк для экономии места - запустите эту команду у себя):

>>> import os >>> dir(os)

[‘F_OK’, ‘MutableMapping’, ‘O_APPEND’, ‘O_BINARY’, ‘O_CREAT’, ‘O_EXCL’, ‘O_ NOINHERIT’, ‘O_RANDOM’, ‘O_RDONLY’, ‘ O_RDWR’, ‘ O_SEQUENTIAL’, ‘ O_SHORT_LIVED’,

‘O_TEMPORARY’, ‘O_TEXT’, ‘O_TRUNC’, ‘O_WRONLY’, ‘P_DETACH’, ‘P_NOWAIT’, ‘P_ NOWAITO’, ‘P_OVERLAY’, ‘P_WAIT’, ‘ R_OK’, ‘ SEEK_CUR’, ‘ SEEK_END’, ‘ SEEK_SET’, ‘TMP_MAX’,

...здесь было удалено 9 строк...

‘pardir’, ‘path’, ‘pathsep’, ‘pipe’, ‘popen’, ‘putenv’, ‘read’, ‘remove’, ‘rem ovedirs’, ‘rename’, ‘renames’, ‘rmdir’, ‘sep’, ‘spawnl’, ‘spawnle’, ‘spawnv’, ‘spawnve’, ‘startfile’, ‘stat’, ‘stat_float_times’, ‘stat_result’, ‘statvfs_ result’, ‘strerror’, ‘sys’, ‘system’, ‘times’, ‘umask’, ‘unlink’, ‘urandom’, ‘utime’, ‘waitpid’, ‘walk’, ‘write’]

Помимо этого вложенный модуль os.path экспортирует дополнительные инструменты, большинство из которых связано с обеспечением переносимости при выполнении операций с именами файлов и каталогов:

>>> dir(os.path)

[‘__all__’, ‘__builtins__’, ‘__doc__’, ‘__file__’, ‘__name__’, ‘__package__’, ‘_get_altsep’, ‘_get_bothseps’, ‘_get_colon’, ‘_get_dot’, ‘_get_empty’, ‘_ get_sep’, ‘_getfullpathname’, ‘abspath’, ‘altsep’, ‘basename’, ‘commonprefix’, ‘curdir’, ‘defpath’, ‘devnull’, ‘dirname’, ‘exists’, ‘expanduser’, ‘expandvars’, ‘extsep’, ‘genericpath’, ‘getatime’, ‘getctime’, ‘getmtime’, ‘getsize’,

‘isabs’, ‘isdir’, ‘isfile’, ‘islink’, ‘ismount’, ‘join’, ‘lexists’, ‘normcase’, ‘normpath’, ‘os’, ‘pardir’, ‘pathsep’, ‘realpath’, ‘relpath’, ‘sep’, ‘split’, ‘splitdrive’, ‘splitext’, ‘splitunc’, ‘stat’, ‘supports_unicode_filenames’,

‘sys’]


Средства администрирования

Если вы полагаете, что приведения этих огромных листингов недостаточно, чтобы двинуться дальше, давайте поэкспериментируем с некоторыми из простейших инструментов модуля os в интерактивной оболочке. Как и модуль sys, модуль os содержит набор инструментов для получения информации и администрирования:

>>> os.getpid()

7980

>>> os.getcwd()

‘C:\\PP4thEd\\Examples\\PP4E\\System’

>>> os.chdir(r'C:\Users')

>>> os.getcwd()

‘C:\\Users’

Здесь видно, что функция os.getpid возвращает числовой идентификатор (ID) процесса (уникальный идентификатор выполняющейся программы, определяемый системой), а функция os.getcwd возвращает текущий рабочий каталог. Текущим рабочим каталогом является тот, в котором предполагается нахождение файлов, открываемых сценарием, если в их именах явно не указан путь к каталогу. Вот почему ранее я предлагал запустить следующую команду именно в том каталоге, где находится файл more.py:

C:\...\PP4E\System> python more.py more.py

Аргумент с именем входного файла здесь задан без явного пути к каталогу (хотя его можно указать, чтобы обеспечить постраничный вывод файлов из другого каталога). Если потребуется назначить текущим другой каталог, вызовите функцию os.chdir: ваш программный код будет выполняться относительно нового каталога до конца программы (или нового вызова os.chdir). В следующей главе еще будет говориться

о понятии текущего рабочего каталога и его связи с операцией импортирования модулей, когда мы будем изучать контекст выполнения сценария.


Константы переносимости

Модуль os экспортирует также ряд имен, упрощающих программирование для нескольких платформ. В этот набор входят специфические для платформ настройки символов-разделителей компонентов путей и каталогов, указателей на родительский и текущий каталоги и символов, используемых для завершения строк в используемой системе:

>>> os.pathsep, os.sep, os.pardir, os.curdir, os.linesep

(‘;’, ‘\\’, ‘..’, ‘.’, ‘\r\n’)

Константа os.sep определяет символ, который используется в качестве разделителя компонентов пути к каталогу на платформе, где выполняется Python. Она автоматически получает значение \ в Windows, / -в POSIX-совместимых системах и : - в некоторых версиях Mac. Аналогично константа os.pathsep определяет символ, отделяющий каталоги в списках каталогов. Она получает значение : в POSIX-совместимых системах и ; - в DOS и Windows.

Использование таких атрибутов для составления и разбора относящихся к системе строк делает сценарии полностью переносимыми. Например, вызов вида dirpath.split(os.sep) правильно разберет на составляющие специфические для платформы имена каталогов, даже если dirpath выглядит как dir\dir в Windows, dir/dir в Linux и dir:dir в некоторых версиях на Mac. Как уже говорилось выше, при определении имен открываемых файлов в Windows допускается использовать символы прямого слеша вместо символов обратного слеша, но применение этих констант обеспечивает независимость программного кода, реализующего операции с каталогами, от платформы, на которой он выполняется.

Обратите также внимание, что функция os.linesep в примере выше возвращает последовательность символов \r\n - экранированные последовательности, соответствующие комбинации символов возврата каретки и перевода строки, которая в Windows используется как признак конца строки и на которую обычно никто не обращает внимания при обработке текстовых файлов в Python. Подробнее о преобразовании символов конца строки будет рассказываться в главе 4.


Основные инструменты os.path

Вложенный модуль os.path предоставляет большой набор собственных средств для работы с каталогами. Например, в него входят переносимые функции для таких задач, как проверка типа файла (isdir, isfile и другие), подтверждение существования файла (exists) и получение размера файла по его имени (getsize):

>>> os.path.isdir(r'C:\Users'), os.path.isfile(r'C:\Users')

(True, False)

>>> os.path.isdir(r'C:\config.sys'), os.path.isfile(r'C:\config.sys')

(False, True)

>>> os.path.isdir('nonesuch'), os.path.isfile('nonesuch')

(False, False)

>>> os.path.exists(r'c:\Users\Brian')

False

>>> os.path.exists(r'c:\Users\Default')

True

>>> os.path.getsize(r'C:\autoexec.bat')

24

Функции os.path.isdir и os.path.isfile сообщают нам о том, является ли имя файла каталогом или простым файлом; обе они возвращают False, если указанный файл не существует (то есть отсутствие файла предполагает отрицание). Есть также функции для разбиения или объединения строк путей к каталогам, которые автоматически используют соглашения об именовании каталогов для той платформы, где работает Python:

>>> os.path.split(r'C:\temp\data.txt')

(‘C:\\temp’, ‘data.txt’)

>>> os.path.join(r'C:\temp', 'output.txt')

‘C:\\temp\\output.txt’

>>> name = r'C:\temp\data.txt'    # пути в Windows

>>> os.path.dirname(name), os.path.basename(name)

(‘C:\\temp’, ‘data.txt’)

>>> name = '/home/lutz/temp/data.txt'    # пути в стиле Unix

>>> os.path.dirname(name), os.path.basename(name)

(‘/home/lutz/temp’, ‘data.txt’)

>>> os.path.splitext(r'C:\PP4thEd\Examples\PP4E\PyDemos.pyw')

(‘C:\\PP4thEd\\Examples\\PP4E\\PyDemos’, ‘.pyw’)

Функция os.path.split отделяет имя файла от пути к его каталогу, a os.path.join снова соединяет их вместе, и все это - совершенно переносимым образом, с использованием соглашений по оформлению путей, действующих в той системе, где они вызываются. Функции dirname и base-name возвращают первый и второй элементы, возвращаемые функцией split, и реализованы просто для удобства, a функция splitext отделяет расширение файла (за последним символом ). Тонкое замечание: эти функции по своему действию почти эквивалентны строковым методам split и join, если вызывать их относительно строковой константы os.sep. Почти, но не совсем:

>>> os.sep

‘\\’

>>> pathname = r'C:\PP4thEd\Examples\PP4E\PyDemos.pyw'

>>> os.path.split(pathname)    # отделить имя файла от каталога

(‘C:\\PP4thEd\\Examples\\PP4E’, ‘PyDemos.pyw’)

>>> pathname.split(os.sep)    # разбить путь по символам слеша

[‘C:’, ‘PP4thEd’, ‘Examples’, ‘PP4E’, ‘PyDemos.pyw’]

>>> os.sep.join(pathname.split(os.sep))

‘C:\\PP4thEd\\Examples\\PP4E\\PyDemos.pyw’

>>> os.path.join(*pathname.split(os.sep))

‘C:PP4thEd\\Examples\\PP4E\\PyDemos.pyw’

Последний вызов join требует передачи отдельных аргументов (отсюда и символ *), но он не вставляет первый символ слеша после буквы, обозначающей имя диска в Windows. Если подобные отличия имеют большое значение, используйте предшествующий вызов метода str. join. Функция normpath может пригодиться в ситуациях, когда в путях произвольно смешиваются разделители компонентов пути для Unix и Windows:

>>> mixed

‘C:\\temp\\public/files/index.html’

>>> os.path.normpath(mixed)

‘C:\\temp\\public\\files\\index.html’

>>> print(os.path.normpath(r'C:\temp\\sub\.\file.ext'))

C:\temp\sub\file.ext

В этом модуле имеется также функция abspath, которая переносимым образом возвращает полное имя файла. Она учитывает добавленный текущий каталог, родительский каталог .. и многое другое:

>>> os.chdir(r'C:\Users')

>>> os.getcwd()

‘C:\\Users’

>>> os.path.abspath('') # пустая строка означает тек. раб. каталог (cwd) ‘C:\\Users’

>>> os.path.abspath('temp') # расширяет до полного пути к файлу в тек. кат. ‘C:\\Users\\temp’

>>> os.path.abspath(r'PP4E\dev') # частичный путь относительно тек. раб. кат. ‘C:\\Users\\PP4E\\dev’

>>> os.path.abspath('.') # расширяет относительные пути ‘C:\\Users’

>>> os.path.abspath('..')

‘C:\\’ >>> os.path.abspath(r'..\examples')

‘C:\\examples’

>>> os.path.abspath(r'C:\PP4thEd\chapters') # абсолютные пути не изменяются ‘C:\\PP4thEd\\chapters’

>>> os.path.abspath(r'C:\temp\spam.txt')

‘C:\YtempY\spam.txt’

Поскольку имена файлов считаются относящимися к текущему рабочему каталогу, если не заданы полными путями, функция os.path.ab-spath может пригодиться, если потребуется показать пользователю, какой каталог используется в действительности для сохранения файла. В Windows, например, при запуске программ с графическим интерфейсом щелчком на ярлыках в проводнике или на рабочем столе рабочим каталогом программы является тот, в котором находится запускаемый файл, что не всегда очевидно пользователю. В таких случаях может помочь вывод значения, возвращаемого функцией abspath для файла.


Выполнение команд оболочки из сценариев

Модуль os позволяет также выполнять команды оболочки из сценариев Python. Это понятие переплетается с другими, такими как потоки ввода-вывода, которые не будут освещаться в полной мере до следующей главы, но, поскольку это одно из ключевых понятий, которое будет использоваться на протяжении всей данной части книги, бегло коснемся основ. В модуле os имеются две функции, позволяющие запустить из сценария любую команду, которую можно ввести в окне консоли:

os.system

Запускает команду оболочки из сценария Python

os.popen

Запускает команду оболочки и соединяется с ее потоками ввода или вывода

Кроме того, существует относительно новый модуль subprocess, обеспечивающий более точное управление запускаемыми командами оболочки через потоки ввода-вывода, который может использоваться как альтернатива и даже для реализации двух функций, представленных выше (хотя и за счет некоторой сложности программного кода).

Что такое команда оболочки?

Чтобы понять область действия этих вызовов, нужно сначала ввести несколько терминов. В данной книге под термином оболочка (shell) подразумевается система, которая считывает и выполняет командные строки на вашем компьютере, а под командой оболочки (shell command) подразумевается командная строка, которую вы обычно вводите в ответ на приглашение оболочки.

В Windows, например, можно открыть окно консоли MS-DOS (она же Командная строка (Command Prompt)) и вводить в нем команды DOS, такие как dir для получения списка каталогов, type для просмотра файла, имена программ, которые нужно запустить, и так далее. DOS является системной оболочкой, а команды, такие как dir и type, - командами оболочки. В Linux и Mac OS X можно запустить новый сеанс оболочки, открыв окно терминала, и также вводить в него команды оболочки - ls для вывода списка каталогов, cat для просмотра файлов и так далее. Для Unix существует множество оболочек (например, csh, ksh), но все они читают и выполняют командные строки. Ниже показаны две команды оболочки, введенные и выполненные в окне консоли MS-DOS под Windows:

C:\...\PP4E\System> dir /B ...ввод команды оболочки helloshell.py    ...далее следует вывод этой команды

more.py    ...DOS играет роль оболочки в Windows

more.pyc spam.txt __init__.py

C:\...\PP4E\System> type helloshell.py

#    a Python program print(‘The Meaning of Life’)

Выполнение команд оболочки

Конечно, все это не имеет прямого отношения к Python (несмотря на то, что сценарии командной строки на языке Python иногда ошибочно называют инструментами оболочки). Но, поскольку функции system и popen из модуля os позволяют сценариям Python выполнять любые команды, понятные оболочке системы, мы можем использовать в своих сценариях любые имеющиеся в системе инструменты командной строки, независимо от того, написаны они на Python или нет. Например, ниже приводится некоторый программный код на языке Python, который выполняет две команды оболочки DOS, введенные выше в ответ на приглашение оболочки:

C:\...\PP4E\System> python

>>> import os

>>> os.system('dir /B')

helloshell.py

more.py

more.pyc

spam.txt

__init__.py

0

>>> os.system('type helloshell.py')

#    a Python program print(‘The Meaning of Life’)

0 >>> os.system('type hellshell.py')

The system cannot find the file specified.

1

Нули, которые выводятся по окончании выполнения первых двух команд, являются значениями, возвращаемыми самой функцией system. Функцию system можно использовать для выполнения любой командной строки, которую допускается ввести в ответ на приглашение оболочки (здесь приглашением является C:\...\PP4E\System>). Выводимые командой данные обычно попадают в стандартный поток вывода сеанса Python или программы.

Обмен данными с командами оболочки

Но что если в сценарии потребуется перехватить данные, выводимые командой? Функция os.system просто запускает команду оболочки, тогда как функция os.popen дополнительно соединяется со стандартными потоками ввода-вывода команды, - обратно возвращается объект, подобный файлу, по умолчанию соединенный с выводом команды (если передать функции popen флаг режима w, то вместо этого произойдет подключение к потоку ввода команды). Используя этот объект для чтения данных, выводимых командой, запущенной с помощью popen, можно перехватывать текст, в обычных условиях появляющийся в окне консоли, где вводится команда:

>>> open('helloshell.py').read()

"# a Python program\nprint(‘The Meaning of Life’)\n”

>>> text = os.popen('type helloshell.py').read()

>>> text

"# a Python program\nprint(‘The Meaning of Life’)\n”

>>> listing = os.popen('dir /B').readlines()

>>> listing

[‘helloshell.py\n’, ‘more.py\n’, ‘more.pyc\n’, ‘spam.txt\n’, ‘__init__.py\n’]

Здесь мы получаем содержимое файла сначала обычным способом (средствами Python для работы с файлами), а затем - как вывод команды оболочки type. Чтение вывода команды dir позволяет получить список файлов в каталоге, который затем можно обработать в цикле. В главе 4 будут представлены другие способы получения такого списка, и там же мы познакомимся с итераторами файлов, которые в большинстве программ делают ненужным вызов функции readlines, показанный в примере выше, за исключением отображения списка в интерактивной оболочке, как в данном случае (дополнительная информация по этой теме приводится также во врезке «subprocess, os.popen и итераторы» ниже).

До сих пор мы выполняли простые команды DOS, но поскольку эти функции могут запускать любые допустимые команды, с их помощью можно также запускать другие сценарии Python. Предположим, что каталог с выполняемым файлом интерпретатора Python в вашей системе находится в пути поиска файлов (чтобы можно было использовать короткую команду «python» вместо «C:\Python31\python»):

>>> os.system('python helloshell.py') # запустит программу на языке Python The Meaning of Life 0

>>> output = os.popen('python helloshell.py').read()

>>> output

‘The Meaning of Life\n’

Во всех этих примерах командные строки, передаваемые функциям system и popen, жестко «зашиты» непосредственно в программный код, но нет никаких причин, по которым программы на языке Python не могли бы создавать такие строки на этапе выполнения с помощью обычных строковых операций (+, % и других). Учитывая, что команды могут конструироваться и выполняться динамически, функции system и popen превращают сценарии на языке Python в гибкие и переносимые средства запуска и управления другими программами. Например, тестовый «управляющий» сценарий на языке Python можно использовать для запуска программ, написанных на любых языках программирования (например, C++, Java, Python), и анализа их вывода. Такой сценарий будет рассмотрен в главе 6. В следующей главе мы снова вернемся к функции os.popen, где будем рассматривать ее в соединении с проблемой перенаправления потоков ввода-вывода - как будет показано там, механизм перенаправления также может использоваться для передачи ввода в программы.

Альтернатива на основе модуля subprocess

Как уже говорилось, в последних версиях Python появился модуль subprocess, позволяющий добиться того же эффекта, что и функции os.system и os.popen. Вообще говоря, для этого придется написать дополнительный программный код, но этот модуль обеспечивает более полный контроль над подключением и использованием потоков ввода-вывода. Это особенно полезно для реализации сложных схем связывания потоков ввода-вывода.

Например, чтобы запустить простую команду оболочки, как мы делали это с помощью функции os.system выше, можно воспользоваться функцией call из нового модуля, которая действует похожим образом (чтобы запустить такую команду, как type, встроенную в оболочку Windows, требуется соблюсти дополнительные условия, хотя для запуска обычных выполняемых файлов, таких как python, этого не требуется):

>>> import subprocess

>>> subprocess.call('python helloshell.py')    # напоминает os.system()

The Meaning of Life 0

>>> subprocess.call('cmd /C "type helloshell.py”') # встроенная команда

#    a Python program print(‘The Meaning of Life’)

0

>>> subprocess.call('type helloshell.py', shell=True) # альтернативный способ

#    a Python program    # для встроенных команд

print(‘The Meaning of Life’)

0

Обратите внимание на аргумент shell=True в последнем вызове. Это платформозависимая особенность:

•    Чтобы запустить встроенную команду оболочки в Windows, инструментам модуля subprocess, таким как call и Popen (об этой функции будет рассказываться ниже), необходимо передавать аргумент shell=True. Команды Windows, такие как type, требуют соблюдения дополнительных условий, но для запуска обычных выполняемых файлов, таких как python, этого не требуется.

•    В Unix-подобных платформах, когда аргумент shell принимает значение False (по умолчанию), команда запускается непосредственно вызовом функции os.execvp, с которой мы встретимся в главе 5. Если в этом аргументе передать True, команда будет выполнена с помощью оболочки, при этом вы можете указать используемую оболочку в дополнительном аргументе.

Подробнее о некоторых из этих особенностей мы поговорим ниже, а пока достаточно будет запомнить, что в Unix-подобных системах вам может потребоваться передавать аргумент shell=True в некоторых примерах в этом разделе и в книге, если они предполагают использование таких особенностей оболочки, как путь поиска программ. Поскольку я запускаю примеры в Windows, этот аргумент я часто буду опускать.

Помимо имитации функции os.system, мы точно так же можем использовать этот модуль для имитации функции os.popen, чтобы запускать команды оболочки и получать ее вывод в нашем сценарии:

>>> pipe = subprocess.Popen('python helloshell.py', stdout=subprocess.PIPE)

>>> pipe.communicate()

(b’The Meaning of Life\r\n’, None)

>>> pipe.returncode

0

Здесь мы связали поток стандартного вывода команды оболочки с каналом и вызвали метод communicate, ожидающий завершения команды и принимающий текст, который она выводит в стандартный поток вывода и в стандартный поток ошибок. Код завершения команды доступен в виде атрибута, после того как она будет выполнена. Точно так же мы могли бы использовать отдельную функцию чтения потока стандартного вывода команды и отдельную функцию ожидания ее завершения (которая возвращает код завершения):

>>> pipe = subprocess.Popen('python helloshell.py', stdout=subprocess.PIPE)

>>> pipe.stdout.read()

b’The Meaning of Life\r\n’

>>> pipe.wait()

0

Фактически существует возможность прямой замены вызова os.popen объектом subprocess.Popen:

>>> from subprocess import Popen, PIPE

>>> Popen('python helloshell.py', stdout=PIPE).communicate()[0]

b’The Meaning of Life\r\n’

>>>

>>> import os

>>> os.popen('python helloshell.py').read()

‘The Meaning of Life\n’

Как видите, реализация относительно простых случаев с помощью модуля subprocess требует дополнительной работы. Но ситуация меняется в лучшую сторону, когда возникает необходимость гибкого управления потоками ввода-вывода. Фактически, благодаря возможности обрабатывать стандартные потоки вывода и ошибок команды похожими способами, модуль subprocess в Python 3.X заменил оригинальные функции os.popen2, os.popen3 и os.popen4, имевшиеся в Python 2.X. Теперь эти функции являются лишь частными случаями использования интерфейса объектов модуля subprocess. Поскольку в более сложных случаях использования этого модуля предполагается взаимодействие со стандартными потоками ввода-вывода, мы отложим дальнейшее обсуждение этого модуля, пока не познакомимся с механизмом перенаправления потоков в следующей главе.

Ограничения, присущие командам оболочки

Прежде чем двинуться дальше, вы должны запомнить два ограничения, связанные с функциями system и popen. Во-первых, хотя сами по себе эти функции хорошо переносимы, в действительности их применение переносимо лишь в той мере, в какой это относится к выполняемым ими командам. Предыдущие примеры с командами DOS dir и type, например, работают только в Windows, а для Unix-подобных платформ их следует изменить так, чтобы они выполняли команды ls и cat.

Во-вторых, важно помнить, что запуск файлов Python как самостоятельных программ таким способом очень отличается от импорта программных файлов и вызова функций, объявленных в них, и обычно происходит гораздо медленнее. Когда вызываются функции os.system и os.popen, им приходится запускать совершенно новую и независимую программу, выполняемую операционной системой (как правило, они запускают команды в виде новых процессов). При импорте программы в качестве модуля интерпретатор Python просто загружает и выполня-

ет код файла в том же процессе, генерируя объект модуля. При этом никакие другие программы не запускаются.8

Могут быть веские основания для построения системы в виде отдельных программ, и в следующей главе будут рассмотрены такие темы, как аргументы командной строки и потоки ввода-вывода, которые позволяют программам передавать информацию в обоих направлениях. Но в большинстве случаев операция импортирования является более быстрым и прямым способом построения систем.

Если вы серьезно намерены использовать эти вызовы, то следует знать, что вызов os.system обычно блокирует (то есть приостанавливает) вызывающую программу до завершения запущенной ею команды. В Linux и Unix-подобных платформах имеется возможность заставить команду выполняться независимо и параллельно с вызвавшей ее программой, добавив в конец командной строки оператор & выполнения в фоновом режиме:

os.system("python program.py arg arg &”)

В Windows запуск с помощью команды DOS start обычно также приводит к запуску команды в виде независимого процесса, выполняющегося параллельно:

os.system("start program.py arg arg”)

В действительности, такой способ запуска команд оказался настолько удобным, что в последние версии Python была добавлена функция os.startfile. Эта функция открывает файл с помощью программы, указанной в реестре Windows для файлов этого типа, как если бы был выполнен щелчок мышью на ярлыке этого файла:

os.startfile("webpage.html”) # open file in your web browser os.startfile("document.doc”) # open file in Microsoft Word os.startfile("myscript.py”) # run file with Python

Функция os.popen обычно не блокирует вызывающую программу (она, по определению, должна иметь возможность читать или писать в возвращаемый объект файла). Тем не менее вызывающая программа иногда все же может оказаться заблокированной в любой операционной системе, Windows или Linux, если объект канала будет закрыт до завершения порожденной программы (например, при сборке мусора) или когда канал будет прочитан до исчерпания (например, с помощью метода read() канала). Как будет показано далее в этой части книги, для параллельного исполнения программ без блокирования можно использовать функции os.fork/exec в Unix и os.spawnv - в Windows.

Поскольку функции system и popen из модуля os, а также модуль subprocess попадают еще и в категорию средств запуска программ, перенаправления потоков ввода-вывода и средств взаимодействия процессов, они снова появятся в последующих главах, поэтому пока мы отложим их дальнейшее обсуждение. Если вам уже сейчас необходимы дополнительные подробности, прочитайте раздел, касающийся вопросов перенаправления потоков ввода-вывода в следующей главе, и раздел, описывающий получение списка каталогов, в главе 4.


Другие элементы, экспортируемые модулем os

Мы рассмотрели в модуле os, что смогли. Поскольку все другие инструменты модуля os еще труднее оценить вне контекста более крупных приложений, мы отложим их более пристальное рассмотрение до последующих разделов. Но, чтобы дать вам представление о характере этого модуля, ниже приводится краткий справочный обзор. Среди прочего на вооружении модуля os состоят:

os.environ

Извлекает и устанавливает значения переменных окружения оболочки.

os.fork

Запускает новый дочерний процесс в Unix-подобных системах.

os.pipe

Обеспечивает обмен данными между программами.

os.execlp

Запускает новые программы.

os.spawnv

Запускает новые программы с возможностью низкоуровневого управления.

os.open

Открывает файл с использованием файлового дескриптора.

os.mkdir

Создает новый каталог.

os.mkfifo

Создает новый именованный канал.

os.stat

Получает низкоуровневую информацию о файле.

os.remove

Удаляет файл по строке пути к нему.

os.walk

Применяет функцию или тело цикла ко всем элементам в дереве каталогов.

И так далее. Заранее предупреждаю: модуль os предоставляет группу функций для открытия, чтения и записи файлов, но все они используют доступ к файлам на низком уровне и кардинально отличаются от встроенных файловых объектов Python stdio, создаваемых с помощью встроенной функции open. Обычно во всех случаях следует использовать встроенную функцию open, а не модуль os, кроме очень специфических потребностей обработки файлов (например, когда требуется открыть файл, заблокировав доступ к нему из других программ).

В следующей главе мы будем использовать инструменты из модулей sys и os для решения обычных системных задач, но объем данной книги не позволяет приводить полные списки содержимого встречающихся модулей. Если вы этого еще не сделали, ознакомьтесь с содержимым таких модулей, как os и sys, обратившись к ресурсам, описанным выше. А теперь перейдем к исследованию дополнительных системных инструментов в контексте более широких понятий системного программирования.

subprocess, os.popen и итераторы

В главе 4 мы будем исследовать итераторы файлов, но перед тем, как взять эту книгу, вы наверняка уже ознакомились с основами. Поскольку объекты, возвращаемые функцией os.popen, обладают итераторами, позволяющими читать данные по одной строке за раз, использование метода readlines этих объектов обычно является излишним. Например, ниже приводится пример чтения строк, которые выводятся другой программой, без явного использования методов чтения:

>>> import os

>>> for line in os.popen('dir /B *.py'): print(line, end='')


helloshell.py more.py __init__.py

Интересно, что в Python 3.1 функция os.popen реализована с использованием объекта subprocess.Popen, с которым мы познакомились в этой главе. Вы можете убедиться в этом, заглянув в файл os.py в стандартной библиотеке Python (в Windows вы найдете этот файл в каталоге C:\Python31\Lib). Результатом вызова функции os.popen является объект, управляющий объектом Popen и его каналами:

>>> I = os.popen('dir /B *.py')

>>> I

<os._wrap_close object at 0x013BC750>

Этот объект-обертка канала определяет метод__iter__, поэтому он

поддерживает возможность итераций по строкам, которые могут выполняться автоматически (например, в цикле for, как показано выше) или вручную. Любопытно отметить, что, несмотря на наличие в объекте-обертке поддержки прямого вызова метода__next__,

как если бы этот объект обладал собственным итератором (подобно простым файлам), тем не менее он не поддерживает встроенную функцию next, хотя последняя, вероятно, просто вызывает метод __next__:

>>> I = os.popen('dir /B *.py')

>>> I.__next__()

'helloshell.py\n'

>>> I = os.popen('dir /B *.py')

>>> next(I)

TypeError: _wrap_close object is not an iterator

Причина такого поведения скрыта глубоко в недрах реализации -

прямой вызов метода__next__перехватывается методом__ge-

tattr__, определенном в объекте-обертке канала, и преобразуется

в вызов метода обернутого объекта. Но функция next обращается к механизму перегрузки операторов, который в Python 3.X действует в обход метода__getattr__, когда производится вызов методов со специальными именами, такими как__next__. Поскольку

объект-обертка канала не определяет собственный метод__next__,

обращение к нему не перехватывается и не передается обернутому объекту, что приводит к ошибке при вызове встроенной функции next. Как детально объясняется в книге «Изучаем Python», метод

__getattr__обертки не вызывается по той простой причине, что

в Python 3.X поиск методов начинается не с экземпляра, а с класса.


Такое поведение может быть или не быть ожидаемым, но вам не придется беспокоиться об этом при выполнении итераций по строкам в канале с помощью цикла for, генераторов и других инструментов. Тем не менее, чтобы обеспечить выполнение итераций вручную, необходимо сначала вызвать встроенную функцию iter -

она вызовет метод__iter__объекта-обертки канала и обеспечит

корректную поддержку обоих способов перемещения по строкам:

>>> I = os.popen('dir /B *.py')

>>> I = iter(I)    # так поступает цикл for

>>> I. next ()    # теперь обе формы итераций действуют правильно

'helloshell.py\n'

>>> next(I)

‘more.py\n’


3

Контекст выполнения сценариев


«Ваши аргументы, пожалуйста!»

Сценарии на языке Python выполняются не в вакууме (хотя, возможно, вы слышали что-то иное). В зависимости от платформы и процедуры запуска программы на языке Python выполняются в определенном контексте, то есть обладают информацией, которая автоматически передается операционной системой при запуске программы. Например, у сценариев есть доступ к следующим видам входных данных системного уровня и интерфейсов:

Текущий рабочий каталог

Функция os.getcwd предоставляет доступ к каталогу, из которого запущен сценарий и значение которого неявно используют многие инструменты для работы с файлами.

Аргументы командной строки

sys.argv предоставляет доступ к словам в команде, с помощью которой была запущена программа и которые играют роль входных данных сценария.

Переменные оболочки

os.environ предоставляет интерфейс к переменным окружения, созданным в охватывающей оболочке (или родительской программой) и передаваемым сценарию.

Стандартные потоки ввода-вывода

sys.stdin, stdout и stderr экспортируют три потока ввода-вывода, лежащие в центре инструментов командной оболочки, и могут использоваться в сценариях с помощью функции print, os.popen и модулем

subprocess, представленными в главе 2, с помощью класса io. StringIO и других инструментов.

Такие инструменты могут играть роль входных данных сценариев, параметров настройки и так далее. В этой главе мы исследуем все эти четыре инструмента работы с контекстом - их интерфейсы в Python и типичные области их использования.


Текущий рабочий каталог

Понятие текущего рабочего каталога (current working directory, CWD) оказывается ключевым при выполнении некоторых сценариев: это всегда неявно определенное место в файловой системе, где предполагается размещение обрабатываемых сценарием файлов, если в их именах отсутствует абсолютный путь к каталогу. Как мы уже видели, функция os.getcwd позволяет сценарию получить имя текущего рабочего каталога в явном виде, а функция os.chdir позволяет сценарию переместиться в новый текущий рабочий каталог.

Однако имейте в виду, что для имен файлов, не содержащих полного пути, подразумевается, что они находятся в текущем рабочем каталоге, и это не имеет никакого отношения к переменной окружения PYTHONPATH. С технической точки зрения сценарий всегда запускается из текущего рабочего каталога, а не из каталога, содержащего файл сценария. Напротив, при импортировании модулей поиск всегда начинается в каталоге, содержащем сценарий, а не в текущем рабочем каталоге (если только сценарий не размещен в текущем рабочем каталоге). Поскольку это тонкое различие, на котором часто попадаются новички, изучим его более подробно.


Текущий рабочий каталог, файлы и путь поиска модулей

Если запустить сценарий Python, введя в команду, например, python dir1\dir2\file. py, - текущим рабочим каталогом будет каталог, в котором вы находились при вводе этой команды, но не dirl\dir2. С другой стороны, Python автоматически добавляет путь к каталогу, где находится сценарий, в начало пути поиска модулей, поэтому file.py всегда сможет импортировать другие файлы из dirl\dir2, откуда бы он ни был запущен. Чтобы проиллюстрировать это, напишем простой сценарий, выводящий имя текущего рабочего каталога и путь поиска модулей:

C:\...\PP4E\System> type whereami.py

import os, sys

print(‘my os.getcwd =>’, os.getcwd()) # вывод текущего рабочего каталога print(‘my sys.path =>’, sys.path[:6]) # вывод первых 6 каталогов в пути поиска input()    # ожидает нажатия клавиши

Теперь при запуске этого сценария в том каталоге, где он находится, будет выбран ожидаемый текущий рабочий каталог, и имя этого каталога будет добавлено в начало пути поиска. Мы уже встречались со списком sys.path, содержащим путь поиска модулей, - первый его элемент может быть пустой строкой, обозначающей текущий рабочий каталог при работе в интерактивной оболочке; здесь большая часть пути к текущему рабочему каталогу усекается до «...» при отображении:

C:\...\PP4E\System> set PYTHONPATH=C:\PP4thEd\Examples

C:\...\PP4E\System> python whereami.py

my os.getcwd => C:\...\PP4E\System

my sys.path => [‘C:\\...\\PP4E\\System’, ‘C:\\PP4thEd\\Examples’, ...другие

элементы... ]

Если запускать этот сценарий из других каталогов, вслед за нашим перемещением переместится и текущий рабочий каталог (это каталог, в котором вводятся команды), а Python будет добавлять в начало пути поиска модулей каталог, где находится сам сценарий, что позволит сценарию по-прежнему видеть файлы в своем исходном каталоге. Например, если запустить сценарий, поднявшись на один уровень (..), каталог System будет добавлен в начало списка sys.path и станет первым каталогом, в котором Python станет искать модули, импортируемые сценарием whereami.py: первый элемент списка будет нацеливать импорт обратно на каталог, содержащий выполняемый сценарий. Однако поиск файлов, имена которых не содержат полного пути, будет выполняться относительно текущего рабочего каталога (C:\PP4thEd\Examples\ PP4E), а не в его подкаталоге System:

C:\...\PP4E\System> cd ..

C:\...\PP4E> python System\whereami.py

my os.getcwd => C:\...\PP4E

my sys.path => [‘C:\\...\\PP4E\\System’, ‘C:\\PP4thEd\\Examples’, ...другие

элементы... ]

C:\...\PP4E> cd System\temp

C:\...\PP4E\System\temp> python ..\whereami.py

my os.getcwd => C:\...\PP4E\System\temp

my sys.path => [‘C:\\...\\PP4E\\System’, ‘C:\\PP4thEd\\Examples’, ...]

В результате поиск файлов, имена которых в сценарии не содержат полных путей, будет выполняться в том месте, где была введена команда (os.getcwd), но операции импортирования по-прежнему будут иметь доступ к каталогу, где находится выполняемый сценарий (через первый элемент в списке sys.path). Наконец, если файл запускается щелчком на ярлыке, текущим рабочим каталогом станет каталог, содержащий файл, на котором выполнен щелчок. Например, следующие строки будут выведены в новом окне консоли DOS при двойном щелчке на whereami.py в проводнике Windows:

my os.getcwd => C:\...\PP4E\System

my sys.path => [‘C:\\...\\PP4E\\System’, ...more... ]

В данном случае текущий рабочий каталог, используемый для поиска файлов, и первый каталог в пути поиска модулей совпадают с каталогом, содержащим файл сценария. Обычно все действует так, как предполагается, но здесь вас могут подстерегать две опасности:

•    Имена файлов должны содержать полные пути к каталогам, если заранее не известно, из какого каталога будет запущен сценарий.

•    Сценарии командной строки не всегда могут использовать текущий рабочий каталог, чтобы увидеть импортируемые файлы, не находящиеся в собственных каталогах сценариев, - для доступа к модулям из других каталогов следует использовать переменную окружения PYTHONPATH и пути к импортируемым пакетам.

Например, сценарии из этой книги всегда могут импортировать другие файлы из собственного исходного каталога, не указывая путь к импортируемому пакету (import file here), независимо от того, как они запущены. Но, чтобы отыскать файлы в другом месте в дереве каталогов примеров, путь поиска должен проходить через корень пакета РР4Е (from PP4E.dir1.dir2 import filethere), даже если запускать сценарии из каталога, содержащего нужный внешний модуль. Как обычно, чтобы обеспечить возможность импортирования модулей, имя каталога PP4E\dirl\dir2 можно также добавить в PYTHONPATH, чтобы сделать file-there видимым отовсюду, без указания пути к импортируемому пакету (хотя лишние каталоги в PYTHONPATH увеличивают вероятность конфликта имен). В любом случае импорт всегда осуществляется из исходного каталога сценария или из другого каталога, находящегося в пути поиска Python, а не из текущего рабочего каталога.


Текущий рабочий каталог и командные строки

Это различие между текущим рабочим каталогом и путями поиска модулей объясняет, почему многие сценарии в данной книге, которые должны действовать в текущем рабочем каталоге (а не в том, имя которого передано), запускаются командной строкой вида:

C:\temp> python C:\...\PP4E\Tools\cleanpyc.py обработка cwd

В данном примере сам файл сценария на языке Python находится в каталоге C:\..\PP4E\Tools, но поскольку он запускается из C:\temp, то обрабатывает файлы, содержащиеся в C:\temp (то есть в текущем рабочем каталоге, а не исходном каталоге сценария). Чтобы обработать с помощью такого сценария файлы, находящиеся где-то в другом месте, нужно просто изменить текущий рабочий каталог с помощью команды cd и перейти в каталог, который должен быть обработан:

C:\temp> cd C:\PP4thEd\Examples

C:\PP4thEd\Examples> python C:\...\PP4E\Tools\cleanpyc.py обработка cwd

Поскольку текущий рабочий каталог всегда неявно определен, сценарий узнает, какой каталог должен быть обработан, от команды cd с той же степенью определенности, что и при явной передаче имени каталога сценарию, как показано ниже (примечание, касающееся переносимости: в других примерах команд вам может потребоваться заключить *.py в кавычки, чтобы предотвратить подстановку имен файлов, которая выполняется в некоторых оболочках Unix):

C:\...\PP4E\Tools> python find.py *.py C:\temp обработка указанного

каталога

В этой командной строке текущим рабочим каталогом является каталог, содержащий запускаемый сценарий, который должен быть выполнен (обратите внимание на отсутствие пути в имени файла сценария). Но поскольку этот сценарий обрабатывает каталог, явно указанный в командной строке (C:\temp), в данном случае не имеет никакого значения, какой каталог является текущим рабочим каталогом. Наконец, если потребуется выполнить такой сценарий, расположенный в некотором другом каталоге, для обработки файлов, находящихся в третьем каталоге, можно просто указать пути каталогов для обоих:

C:\temp> python C:\...\PP4E\Tools\find.py *.cxx C:\PP4thEd\Examples\PP4E

В этом случае для импорта будут доступны файлы в исходном каталоге сценария PP4E\Tools, а обрабатываться будут файлы в каталоге, указанном в командной строке, при этом текущим рабочим каталогом будет совершенно другой каталог (C:\temp). Использование последней формы требует больше вводить с клавиатуры, тем не менее в этой книге вам еще не раз встретятся различные текущие рабочие каталоги и командные строки с явными путями к сценариям.


Аргументы командной строки

Модуль sys позволяет также получить те слова, которые были введены в команде, запустившей сценарий на языке Python. Эти слова обычно называются аргументами командной строки и находятся во встроенном списке строк sys.argv. Программисты на C могут заметить сходство с массивом argv в языке C (массивом строк). В интерактивном режиме смотреть особенно не на что, так как для запуска Python в этом режиме ему не требуется передавать аргументы командной строки:

>>> import sys

>>> sys.argv

[‘’]

Чтобы действительно увидеть аргументы, нужно запустить сценарий из командной строки. В примере 3.1 приводится простой до безобразия сценарий, который всего лишь выводит список argv для изучения.

Пример 3.1. PP4E\System\testargv.py

import sys

print(sys.argv)

Если запустить этот сценарий, он выведет список аргументов командной строки. Обратите внимание, что первым элементом всегда является имя самого выполняемого сценария, независимо от способа запуска (смотрите врезку «Выполняемые сценарии в Unix» далее в этой главе):

C:\...\PP4E\System> python testargv.py

[‘testargv.py’]

C:\...\PP4E\System> python testargv.py spam eggs cheese

[‘testargv.py’, ‘spam’, ‘eggs’, ‘cheese’]

C:\...\PP4E\System> python testargv.py -i data.txt -o results.txt

[‘testargv.py’, ‘-i’, ‘data.txt’, ‘-o’, ‘results.txt’]

Последняя команда в этом фрагменте иллюстрирует общепринятое соглашение. Подобно аргументам функции, параметры командной строки иногда передаются по позиции, а иногда по имени с помощью пары «-имя значение». Например, пара -i data.txt означает, что значением ключа -i является data.txt (например, имя входного файла). В качестве параметров командной строки допускается передавать любые слова, но обычно программы накладывают на них некоторые структурные ограничения.

Аргументы командной строки играют в программах такую же роль, как аргументы в функциях: они просто позволяют передать в программу информацию, которая может быть различной для каждого запуска программы. То обстоятельство, что они не определяются жестко в программном коде, позволяет использовать сценарии более универсальными способами. Например, аргументы командной строки могут использоваться для передачи имен файлов сценариям, их обрабатывающим, - взгляните на сценарий more.py в главе 2 (пример 2.1), который был нашим первым примером. Другие сценарии могут принимать флаги режима обработки, адреса Интернета и так далее.


Анализ аргументов командной строки

Однако при регулярном использовании аргументов командной строки вы можете обнаружить, что писать код, который вылавливает в списке слова, неудобно. Обычно при запуске программы преобразуют список аргументов в структуры, более удобные для обработки. Ниже приводится один из способов реализации такого преобразования: сценарий в примере 3.2 просматривает список argv в поисках пар -optionname op-tionvalue и помещает их в словарь, используя в качестве ключей имена параметров, чтобы потом их было легче извлекать.

Пример 3.2. PP4E\System\testargv2.py

"собирает параметры командной строки в словаре”

def getopts(argv): opts = {} while argv:

if argv[0][0] == ‘-’:    # поиск пар "-name value”

opts[argv[0]] = argv[1] # ключами словарей будут имена параметров argv = argv[2:] else:

argv = argv[1:] return opts

if__name__== ‘__main__’:

from sys import argv    # пример клиентского программного кода

myargs = getopts(argv) if ‘-i’ in myargs:

print(myargs[‘-i’])

print(myargs)

Такую функцию можно импортировать и использовать во всех инструментах командной строки. Если запустить этот пример как самостоятельный сценарий, он просто выведет отформатированный словарь аргументов:

C:\...\PP4E\System> python testargv2.py

{}

C:\...\PP4E\System> python testargv2.py -i data.txt -o results.txt

data.txt

{‘-o’: ‘results.txt’, ‘-i’: ‘data.txt’}

Естественно, мы могли бы предусмотреть еще более сложную реализацию механизма работы с аргументами, добавить проверку ошибок и тому подобное. Для обработки более сложных командных строк мы могли бы использовать более развитые инструменты обработки командной строки из стандартной библиотеки Python:

•    Модуль getopt моделирует поведение одноименной утилиты Unix/C

•    Модуль optparse является более современной альтернативой и по общему признанию - более мощной

Оба модуля описаны в руководстве по библиотеке языка Python, и там же вы найдете примеры использования, которые мы не будем приводить здесь ради экономии места. Вообще говоря, чем более широкие возможности по настройке предусматривают ваши сценарии, тем больше труда вам придется вложить в реализацию логики обработки аргументов командной строки.

Выполняемые сценарии в Unix

Пользователям Unix и Linux: текстовые файлы с исходным программным кодом на языке Python можно сделать непосредственно исполняемыми, добавив в их начало особую строку, содержащую путь к интерпретатору Python, и присвоив файлу права на выполнение. Например, сохраните следующий фрагмент в текстовом файле с именем myscript:

#!/usr/bin/python print(‘And nice red uniforms’)

Первая строка будет восприниматься интерпретатором как комментарий (она начинается с #), но при запуске этого файла операционная система будет посылать строки этого файла интерпретатору, указанному после #! в первой строке. Если этот файл сделать непосредственно исполняемым с помощью команды chmod +x myscript, его можно будет запускать непосредственно, не вводя слово python в команде, как если бы это был двоичный файл исполняемой программы:

% myscript a b с And nice red uniforms

При запуске таким способом список sys.argv по-прежнему будет содержать имя сценария в первом элементе: [“myscript”, “a”, “b”, “с”] - в точности, как если бы сценарий был запущен с помощью более явного и переносимого формата команды python myscript a b с. Превращение сценариев в непосредственно исполняемые файлы на самом деле является трюком ОС Unix, а не особенностью Python, но стоит отметить, что можно сделать его несколько менее машинно-зависимым, указав в начале команду Unix env вместо пути к исполняемому файлу Python:

#!/usr/bin/env python print(‘Wait for it...’)

В этом случае операционная система будет отыскивать интерпретатор Python с помощью значений переменных окружения (обычно с помощью переменной PATH). Если один и тот же сценарий потребуется использовать на разных машинах, достаточно будет только изменить на них настройки окружения (а не редактировать исходный программный код сценария). Конечно, файлы Python по-прежнему можно запускать с помощью более явной командной строки:

% python myscript a b с


При этом предполагается, что интерпретатор python находится в системном пути поиска (иначе нужно указывать полный путь к нему), но этот прием действует на любой платформе, где установлен Python и имеется доступ к командной строке. Поскольку это более переносимый способ, я обычно использую его в примерах книги (за дополнительной информацией по этой теме я рекомендую обращаться к страницам руководства Unix). Но, несмотря на это, особые строки #! можно встретить во многих примерах данной книги - на случай, если читателям потребуется запускать их как исполняемые файлы в Unix или Linux; на других платформах они просто игнорируются как комментарии Python.

Обратите внимание, что в последних версиях Windows также можно вводить имя сценария непосредственно (без слова python), чтобы запустить его, и добавлять строку #! в начало сценария не нужно. При установке Python регистрируется в реестре Windows как программа для открытия файлов с расширениями, которые воспринимаются интерпретатором Python (.py и другие). Это также объясняет, почему сценарии могут запускаться в Windows простым щелчком мыши.



Переменные окружения оболочки

Переменные оболочки, которые иногда называют также переменными окружения, доступны в сценариях на языке Python через os.environ -объект Python, напоминающий словарь, в котором для каждой переменной оболочки отводится отдельная запись. Переменные оболочки находятся за пределами интерпретатора Python. Они часто устанавливаются в командной строке, в сценариях начального запуска системы или с помощью панели управления и обычно служат в качестве общесистемных параметров настройки для программ.

На самом деле вам уже должен быть знаком главный пример: путь поиска модулей PYTHONPATH является переменной оболочки, которую Python использует при выполнении операции импортирования модулей. После установки этой переменной в системе ее значение становится доступным для каждой запускаемой программы Python. Переменные оболочки могут также устанавливаться программами, чтобы передавать входные данные другим программам в приложении; поскольку обычно их значения наследуются порожденными программами, они могут служить простым средством связи между процессами.


Получение значений переменных оболочки

В Python окружение оболочки является простым предустановленным объектом, для обращения к которому не требуется использовать специальный синтаксис. Операция индексирования объекта os.environ строками с именами переменных оболочки (например, os.environ[‘USER’ ]) является эквивалентом знака доллара перед именем переменной в большинстве оболочек Unix (например, $USER), использования с двух сторон знака процента в DOS (%USER%) и вызова getenv(“USER”) в программе на языке С. Запустим интерактивный сеанс и поэкспериментируем (следующий сеанс выполнялся в Python 3.1 в Windows 7):

>>> import os

>>> os.environ.keys()

KeysView(<os._Environ object at 0x013B8C70>)

>>> list(os.environ.keys())

[‘TMP’, ‘COMPUTERNAME’, ‘USERDOMAIN’, ‘PSMODULEPATH’, ‘ COMMONPROGRAMFILES’, ...множество строк было удалено...

‘NUMBER_OF_PROCESSORS’, ‘PROCESSOR_LEVEL’, ‘USERPROFILE’, ‘OS’, ‘PUBLIC’, ‘QTJAVA’]

>>> os.environ['TEMP']

‘C:\\Users\\mark\\AppData\\Local\\Temp’

Здесь метод keys возвращает итерируемый объект со списком установленных переменных, а операция индексирования возвращает значение переменной TEMP в Windows. В Linux эти инструкции действуют точно так же, но обычно при запуске Python устанавливаются другие переменные. Поскольку нам знакома переменная PYTHONPATH, посмотрим в Python на ее значение и убедимся в его правильности (когда я писал эти строки, в эту переменную временно был добавлен путь к корневому каталогу с примерами к четвертому изданию книги):

>>> os.environ['PYTHONPATH']

‘C:\\PP4thEd\\Examples;C:\\Users\\Mark\\temp’

>>> for srcdir in os.environ['PYTHONPATH'].split(os.pathsep):

... print(srcdir)

C:\PP4thEd\Examples

C:\Users\Mark\temp

>>> import sys >>> sys.path[:3]

[‘’, ‘C:\\PP4thEd\\Examples’, ‘C:\\Users\\Mark\\temp’]

Переменная PYTHONPATH содержит строку, содержащую список каталогов, разделенных символом, используемым для разделения таких элементов пути на вашей платформе (например, ; в DOS/Windows, : в Unix и Linux). Чтобы разделить эту строку на составляющие, передадим строковому методу split разделитель os.pathsep (переносимая константа, дающая правильный разделитель для соответствующей системы). Как обычно, фактический путь поиска, используемый во время выполнения, хранится в списке sys.path и является объединением пути к текущему рабочему каталогу и содержимого переменной окружения PYTHONPATH.


Изменение переменных оболочки

Как и обычные словари, объект os.environ поддерживает обращение по ключу и присваивание. Операция присваивания, применяемая к словарям, изменяет значение ключа:

>>> os.environ['TEMP']

‘C:\\Users\\mark\\AppData\\Local\\Temp >>> os.environ['TEMP'] = r'c:\temp'

>>> os.environ['TEMP']

‘c:\\temp’

Но в данном случае выполняются некоторые дополнительные действия. Во всех последних версиях Python значения, присваиваемые ключам os.environ таким способом, автоматически экспортируются в другие части приложения. То есть присваивание по ключу изменяет не только объект os.environ в программе, но и соответствующую переменную в окружении оболочки для процесса выполняемой программы. Новое значение переменной становится видимым программе на языке Python, всем связанным с ней модулям на языке C и всем программам, порождаемым процессом Python.

За кулисами при присваивании объекту os.environ по ключу происходит вызов os.putenv - функции, изменяющей переменную окружения за границами интерпретатора Python. Чтобы показать, как это работает, нам потребуется пара сценариев, которые изменяют и получают значения переменных оболочки. Первый из них приводится в примере 3.3.

Пример 3.3. PP4E\System\Environment\setenv.py

import os

print(‘setenv...’, end=’ ‘)

print(os.environ[‘USER’]) # выведет текущее значение переменной оболочки

os.environ[‘USER’] = ‘Brian’ # неявно вызовет функцию os.putenv os.system(‘python echoenv.py’)

os.environ[‘USER’] = ‘Arthur’ # изменение передается порождаемым программам os.system(‘python echoenv.py’) # и связанным с процессом библ. модулям на C

os.environ[‘USER’] = input(‘?’) print(os.popen(‘python echoenv.py’).read())

Данный сценарий setenv.py просто изменяет переменную оболочки USER и запускает другой сценарий, выводящий значение этой переменной, который приводится в примере 2.5.

Пример 3.4. PP4E\System\Environment\echoenv.py

import os

print(‘echoenv...’, end=’ ‘) print(‘Hello,’, os.environ[‘USER’])

Независимо от способа запуска сценарий echoenv.py выводит значение переменной окружения USER. При запуске из командной строки этот сценарий выведет значение, установленное нами в самой оболочке:

C:\...\PP4E\System\Environment> set USER=Bob

C:\...\PP4E\System\Environment> python echoenv.py echoenv... Hello, Bob

Однако при запуске из другого сценария, например, из setenv.py, с помощью функции os.system или os.popen, с которыми мы познакомились ранее, сценарий echoenv.py получит то значение переменной USER, которое было установлено родительской программой:

C:\...\PP4E\System\Environment> python setenv.py

setenv... Bob

echoenv... Hello, Brian

echoenv... Hello, Arthur

?Gumby

echoenv... Hello, Gumby

C:\...\PP4E\System\Environment> echo %USER%

Bob

Точно так же этот механизм действует и в Linux. Вообще говоря, порождаемая программа всегда наследует значения переменных окружения от своих родителей. Порожденными программами являются такие, которые запускаются средствами Python, например os.spawnv, комбинацией os.fork/exec в Unix-подобных системах, и os.popen, os.system или с помощью модуля subprocess на ряде других платформ. Все программы, запущенные таким способом, получают значения переменных окружения, существующие в момент запуска в родительском процессе.9

Подобный способ установки переменных окружения перед запуском новой программы является одним из способов передачи информации в новую программу. Например, можно написать сценарий, изменяющий переменную PYTHONPATH включением в нее пользовательских каталогов, перед запуском других сценариев. Благодаря этому запущенный сценарий получит свой путь поиска модулей в списке sys.path, потому что переменные оболочки передаются потомкам (такой запускающий сценарий будет представлен в конце главы 6).


Особенности переменных оболочки: родители, putenv и getenv

Обратите внимание на последнюю команду в предыдущем примере -после завершения программы верхнего уровня переменная USER получает свое первоначальное значение. Присвоения значений ключам os.environ передаются за пределы интерпретатора вниз по цепочке порожденных программ и никогда не передаются вверх процессам родительских программ (включая системную оболочку). Это относится и к программам на языке C, использующим библиотечный вызов putenv, то есть данная особенность не является ограничением, характерным именно для Python.

Это едва ли вызовет проблемы в сценарии Python, являющемся вершиной приложения. Но помните, что настройки оболочки, сделанные внутри программы, действуют, лишь пока выполняется эта программа и порожденные ею дочерние программы. Если вам потребуется экспортировать настройки окружения, чтобы они действовали после завершения программы на языке Python, вам необходимо будет найти платформозависимые расширения, реализующие такую возможность. Попробуйте поискать их на сайте http://www.python.org и в Интернете.

Другая тонкость: в нынешней реализации изменение значений в os.en-viron автоматически приводит к вызову функции os.putenv, которая вызывает функцию putenv в библиотеке языка C, если она доступна на вашей платформе, чтобы экспортировать измененное значение за пределы интерпретатора Python во все связанные с ним расширения на языке C. Однако, хотя изменения в os.environ приводят к вызову os.putenv, тем не менее прямой вызов функции os.putenv не оказывает влияния на содержимое os.environ. По этой причине для изменения окружения предпочтительнее использовать интерфейс os.environ.

Обратите также внимание, что настройки окружения загружаются в os.environ на этапе запуска программы, а не при каждом обращении к этому объекту. По этой причине изменения, выполненные в расширениях на языке C уже после запуска программы, могут не отражаться в os.environ. В языке Python на самом деле имеется более конкретная функция os.getenv, но она не вызывает функцию getenv из библиотеки языка C, а просто выбирает значения ключей из os.environ в большинстве платформ (во всех в версии 3.X). Для большинства приложений в этом нет ничего плохого, особенно если они содержат программный код только на языке Python. На платформах, где отсутствует функция putenv, для настройки окружения порождаемой программы можно передавать словарь os.environ инструментам запуска программ в виде параметра.


Стандартные потоки ввода-вывода

Модуль sys - это место обитания стандартных потоков ввода, вывода и вывода ошибок для программ на языке Python. Эти потоки обеспечивают еще один распространенный способ организации обмена данными между программами:

>>> import sys

>>> for f in (sys.stdin, sys.stdout, sys.stderr): print(f)

<_io.TextIOWrapper name=’<stdin>’ encoding=’cp437’>

<_io.TextIOWrapper name=’<stdout>’ encoding=’cp437’>

<_io.TextIOWrapper name=’<stderr>’ encoding=’cp437’>

Стандартные потоки - это всего лишь предварительно открытые объекты файлов Python, которые автоматически подключаются к стандартным потокам ввода-вывода программы при запуске. По умолчанию все они связаны с окном консоли, в котором был запущен интерпретатор Python (или программа на языке Python). Поскольку встроенные функции print и input являются не чем иным, как дружественными интерфейсами к стандартным потокам вывода-ввода, по своему действию они аналогичны прямому использованию stdout и stdin в sys:

>>> print('hello stdout world')

hello stdout world

>>> sys.stdout.write('hello stdout world' + '\n')

hello stdout world 19

>>> input('hello stdin world>')

hello stdin world>spam ‘spam’

>>> print('hello stdin world>'); sys.stdin.readline()[:-1]

hello stdin world> eggs

‘eggs’

Стандартные потоки в Windows

Пользователям Windows: при запуске программ на языке Python из проводника Windows щелчком на имени файла с расширением .ру (или с помощью os.system) автоматически появляется окно консоли DOS, служащее стандартным потоком программы. Если программа создает собственные окна, можно избежать открытия окна консоли, дав файлу с исходным текстом программы расширение .pyw, а не .ру. Расширение .pyw означает просто исходный файл .ру программы, для запуска которой не требуется открывать окно DOS в Windows (это обеспечивается настройками в реестре Windows, где файлам с расширением .pyw поставлена в соответствие специализированная версия Python). Файлы с расширением .pyw могут импортироваться, как обычные файлы .py.

Обратите также внимание, что при запуске программы щелчком мыши вывод производится во всплывающее окно DOS, поэтому сценарии, которые просто выводят текст и завершают свою работу, производят странную «вспышку»: при запуске появляется окно консоли DOS, в него производится вывод, а затем окно сразу закрывается (не самое дружественное поведение!). Чтобы сохранить окно DOS открытым и получить возможность ознакомиться с результатами работы сценария, просто добавьте вызов функции input() в конец сценария, который приостановит выполнение до нажатия на клавишу Enter.



Перенаправление потоков ввода-вывода в файлы и программы

Теоретически, текст, который выводится в стандартный поток вывода (и с помощью функции print), отображается в окне консоли, в котором запущена программа, текст в стандартный поток ввода (и возвращаемый функцией input) поступает с клавиатуры, а стандартный поток вывода ошибок обычно выводит сообщения об ошибках Python в окно консоли. По крайней мере, так происходит по умолчанию. Существует также возможность перенаправить эти потоки в файлы или в другие программы системной оболочки, а также в произвольные объекты внутри сценария на языке Python. В большинстве систем возможность перенаправления упрощает повторное использование и комбинирование утилит командной строки общего назначения.

Перенаправление удобно использовать, например, для ввода заранее подготовленных тестовых данных: один и тот же сценарий можно протестировать, используя несколько наборов входных данных, просто перенаправляя стандартный поток ввода при каждом запуске сценария в разные файлы. Аналогично перенаправление стандартного потока вывода позволяет сохранить и впоследствии проанализировать вывод программы. Например, в системах тестирования для обнаружения ошибок может выполняться сравнение сохраненного стандартного вывода сценария с файлом, содержащим ожидаемые результаты.

Несмотря на всю мощь этой парадигмы, сам механизм перенаправления весьма прост в использовании. В качестве примера рассмотрим простой цикл «прочесть-вычислить-вывести», представленный в примере 3.5.

Пример 3.5. PP4E\System\Streams\teststreams.py

“читает числа до символа конца файла и выводит их квадраты”

def interact():

print(‘Hello stream world’) # print выводит в sys.stdout while True: try:

reply = input(‘Enter a number>’) # input читает из sys.stdin except EOFError:

break    # исключение при встрече символа eof

else:    # входные данные в виде строки

num = int(reply)

print(“%d squared is %d” % (num, num ** 2)) print(‘Bye’)

if__name__== ‘__main__’:

interact()    # если выполняется, а не импортируется

Как обычно, функция interact вызывается автоматически, если файл не импортируется, а выполняется как самостоятельный сценарий. По умолчанию запуск этого файла из командной строки вызывает появление стандартного потока в месте, где вводилась команда. Сценарий просто читает числа, пока не достигнет конца файла в стандартном потоке ввода (в Windows конец файла обычно можно ввести комбинацией двух клавиш CtrL+Z; в Unix нужно нажать комбинацию CtrL+D):10

C:\...\PP4E\System\Streams> python teststreams.py

Hello stream world Enter a number>12 12 squared is 144 Enter a number>10 10 squared is 100 Enter a number>^Z Bye

И в Windows, и в Unix-подобных системах стандартный поток ввода можно перенаправить в файл - с помощью синтаксической конструкции < filename оболочки. Ниже приводится сеанс работы в окне консоли DOS под Windows, где сценарий читает входные данные из текстового файла input.txt. То же самое можно проделать и в Linux, только команду DOS type нужно заменить командой Unix cat:

C:\...\PP4E\System\Streams> type input.txt 8 6

C:\...\PP4E\System\Streams> python teststreams.py < input.txt

Hello stream world Enter a number>8 squared is 64 Enter a number>6 squared is 36 Enter a number>Bye

Здесь ввод данных, которые обычно поступают с клавиатуры в интерактивном режиме, автоматизирован с помощью файла input.txt: сценарий читает данные из этого файла, а не с клавиатуры. Точно так же можно перенаправить в файл и стандартный поток вывода - с помощью синтаксической конструкции > filename оболочки. При этом перенаправление ввода и вывода можно объединить в одной команде:

C:\...\PP4E\System\Streams> python teststreams.py < input.txt > output.txt

C:\...\PP4E\System\Streams> type output.txt

Hello stream world

Enter a number>8 squared is 64

Enter a number>6 squared is 36

Enter a number>Bye

На этот раз стандартные потоки ввода и вывода сценария отображаются в текстовые файлы, а не в сеанс интерактивной консоли.

Соединение программ с помощью каналов

В Windows и в Unix-подобных системах имеется возможность направлять стандартный вывод одной программы в стандартный ввод другой, помещая между командами символ |. Обычно это называется операцией создания «канала» или «конвейера»: оболочка создает канал, соединяющий вывод и ввод двух команд. Попробуем отправить вывод сценария на вход программы more, чтобы увидеть, как действует этот прием:

C:\...\PP4E\System\Streams> python teststreams.py < input.txt | more

Hello stream world Enter a number>8 squared is 64 Enter a number>6 squared is 36 Enter a number>Bye

В этом примере данные также поступают в поток стандартного ввода сценария teststreams из файла, но выходные данные (которые выводятся вызовами функции print) посылаются другой программе, а не в файл или окно. Принимающей программой является more - стандартная программа командной строки для постраничного просмотра, имеющаяся в Windows и в Unix-подобных системах. Поскольку Python привязывает сценарии к стандартной модели потоков ввода-вывода, сценарии на языке Python можно использовать с обоих концов канала: вывод одного сценария Python всегда можно отправить на ввод другого:

C:\...\PP4E\System\Streams> type writer.py print(“Help! Help! I’m being repressed!”) print(42)

C:\...\PP4E\System\Streams> type reader.py print(‘Got this: “%s”’ % input()) import sys

data = sys.stdin.readline()[:-1]

print(‘The meaning of life is’, data, int(data) * 2)

C:\...\PP4E\System\Streams> python writer.py Help! Help! I’m being repressed!

42

C:\...\PP4E\System\Streams> python writer.py | python reader.py

Got this: “Help! Help! I’m being repressed!”

The meaning of life is 42 84

На этот раз связь устанавливается между двумя программами на языке Python. Сценарий reader получает входные данные от сценария writer -оба сценария просто используют стандартные функции чтения и записи, не задумываясь о работе механизма потоков. На практике такое соединение программ в цепочку является простой формой организации взаимодействий между программами. Оно облегчает повторное использование утилит, предусматривающих возможность взаимодействий через stdin и stdout, самыми неожиданными способами. Например, программу на языке Python, которая сортирует текст, поступающий из stdin, можно использовать для работы с любым источником данных, в том числе с выводом других сценариев. Рассмотрим сценарии командной строки из примеров 3.6 и 3.7, которые сортируют строки с числами, поступающие в стандартный поток ввода, и складывают их.

Пример 3.6. PP4E\System\Streams\sorter.py

import sys    # или sorted(sys.stdin)

lines = sys.stdin.readlines()    # читает входные строки из stdin,

lines.sort()    # сортирует их

for line in lines: print(line, end=’’) # отправляет результаты в stdout

# для дальнейшей обработки

Пример 3.7. PP4E\System\Streams\adder.py

import sys sum = 0 while True: try:

line = input() # или sys.stdin.readlines() except EOFError:    # или for line in sys.stdin:

break    # input отсекает символы \n в конце строк

else:

sum += int(line) # во 2-м издании использовалась функция sting.atoi() print(sum)

Мы можем использовать эти универсальные инструменты командной строки, чтобы с их помощью сортировать и складывать содержимое произвольных файлов и вывода других программ (примечание для пользователей Windows: на моей предыдущей машине с Windows XP и Python 2.X я должен был вводить команду «python file.py», а не просто «file.py», в противном случае перенаправление не давало ожидаемых результатов; ныне, в Windows 7 и Python 3.X, обе формы команд действуют корректно):

C:\...\PP4E\System\Streams> type data.txt 123 000 999 042

C:\...\PP4E\System\Streams> python sorter.py < data.txt сортировка файла

000

042

123

999

C:\...\PP4E\System\Streams> python adder.py < data.txt вычисление суммы

1164

C:\...\PP4E\System\Streams> type data.txt | python adder.py вычисление суммы 1164    для вывода

команды type

C:\...\PP4E\System\Streams> type writer2.py for data in (123, 0, 999, 42): print(‘%03d’ % data)

C:\...\PP4E\System\Streams> python writer2.py | python sorter.py сортировка 000    вывода сценария

042

123

999

C:\...\PP4E\System\Streams> writer2.py | sorter.py краткая форма записи выводит те же результаты, что и предыдущая команда Windows...

C:\...\PP4E\System\Streams> python writer2.py | python sorter.py | python adder. py

1164

В последней команде составлена цепочка из трех сценариев на языке Python: вывод каждого предыдущего сценария соединяется с вводом последующего с помощью синтаксиса конвейера.

Альтернативные реализации сценариев adder и sorter

Если присмотреться, можно заметить, что сценарий sorter.py читает сразу все данные, имеющиеся в stdin, используя метод readlines, а сценарий adder.py читает данные по одной строке. Если источником входных данных является другая программа, то в некоторых системах соединенные каналом программы выполняются параллельно. В таких системах, особенно если пересылается большой объем данных, лучше производить построчное чтение: читающей программе не придется ждать, пока пишущая программа полностью завершит работу, чтобы заняться обработкой данных. Так как функция input просто читает данные из потока stdin, схему построчного ввода, используемую в adder.py, можно также реализовать прямым обращением к sys.stdin:

C:\...\PP4E\System\Streams> type adder2.py import sys sum = 0 while True:

line = sys.stdin.readline() if not line: break sum += int(line) print(sum)

Данная версия использует тот факт, что функция int допускает наличие пробельных символов вокруг числа (функция readline возвращает строку вместе с символом \n, но мы не должны использовать [:-1] или rstrip() для его удаления). Фактически для достижения того же эффекта можно использовать более современные итераторы файлов - цикл for, например, автоматически извлекает из объекта файла по одной строке в каждой итерации (подробнее об итераторах файлов рассказывается в следующей главе):

C:\...\PP4E\System\Streams> type adder3.py import sys

sum = 0

for line in sys.stdin: sum += int(line) print(sum)

Однако перевод сценария sorte r на построчное чтение едва ли даст большой выигрыш в производительности, потому что метод sort списков требует, чтобы весь список был заполнен. Как будет показано в главе 18, запрограммированные вручную алгоритмы сортировки, скорее всего, будут работать значительно медленнее, чем метод сортировки списка Python.

Интересно отметить, что в версии Python 2.4 и выше эти два сценария можно реализовать более компактно, используя новую встроенную функцию sorted, выражения-генераторы и итераторы файлов. Следующий сценарий действует точно так же, как и оригиналы, но имеет заметно меньший размер:

C:\...\PP4E\System\Streams> type sorterSmall.py import sys

for line in sorted(sys.stdin): print(line, end=’’)

C:\...\PP4E\System\Streams> type adderSmall.py import sys

print(sum(int(line) for line in sys.stdin))

В последнем примере функции sum в виде аргумента передается выражение-генератор, по своему поведению похожее на генератор списков, с той лишь разницей, что оно возвращает результаты по одному значению, а не в виде списка. В результате сценарий получается более компактным. За дополнительной информацией обращайтесь к ресурсам по основам языка, таким как книга «Изучаем Python».


Перенаправление потоков и взаимодействие с пользователем

Выше в этом разделе мы направили вывод сценария teststreams.py на вход стандартной программы командной строки more с помощью следующей команды:

C:\...\PP4E\System\Streams> python teststreams.py < input.txt | more

Но поскольку в предыдущей главе мы уже написали на языке Python собственную утилиту «more» постраничного вывода, почему не сделать так, чтобы она тоже принимала ввод из stdin? Например, если изменить последние три строки в файле more.py, представленном в примере 2.1, на следующие:

if __name__ == ‘__main__’:    # если выполняется, а не импортируется

import sys

if len(sys.argv) == 1:    # вывести данные из stdin, если нет аргументов

more(sys.stdin.read())

else:

more(open(sys.argv[1]).read())

Тогда, похоже, мы сможем перенаправить стандартный вывод сценария teststreams.py на стандартный ввод more.py:

C:\...\PP4E\System\Streams> python teststreams.py < input.txt | python ..\more. py

Hello stream world Enter a number>8 squared is 64 Enter a number>6 squared is 36 Enter a number>Bye

В целом такой прием можно использовать в сценариях на языке Python. Здесь сценарий teststreams.py снова принимает данные из файла. И, как и в предыдущем разделе, вывод одной программы отправляется по каналу на ввод другой - сценарий more.py в родительском (. ) каталоге.

Однако в предыдущей команде more^Y кроется малозаметная проблема. В действительности эта цепочка сработала по чистой случайности: если первый сценарий будет выводить слишком много данных и сценарию more придется спрашивать пользователя о разрешении продолжить вывод, произойдет полный отказ сценария (точнее, когда функция input возбудит исключение EOFError).

Проблема в том, что улучшенный вариант more^ использует stdin для достижения двух различных целей. Он читает ответ пользователя из stdin, вызывая input, но теперь еще и принимает из stdin основные входные данные. Когда поток stdin подключается к входному файлу или каналу, его нельзя использовать для получения ответа от пользователя, потому что он содержит входные данные. Кроме того, поскольку перенаправление потока stdin происходит еще до запуска программы, невозможно узнать, был ли он перенаправлен в командной строке.

Если потребуется принимать входные данные из stdin и использовать консоль для взаимодействия с пользователем, в сценарий нужно будет внести дополнительные изменения: нам придется отказаться от использования функции input и задействовать специальные интерфейсы для чтения ответов пользователя непосредственно с клавиатуры. В Windows такую возможность обеспечивает модуль msvcrt, входящий в состав стандартной библиотеки Python; в большинстве Unix-подобных систем достаточно будет использовать файл устройства /dev/tty.

Поскольку это достаточно редкая ситуация, мы рассмотрим полную реализацию предложенного решения. В примере 3.8 показана модифицированная версия сценария more.py для Windows, которая постранично выводит данные из стандартного потока ввода при вызове без аргументов, а кроме того, в ней используются низкоуровневые и зависящие от платформы средства взаимодействия с пользователем через клавиатуру.

Пример 3.8. PP4E\System\Streams\moreplus.py

обеспечивает постраничный вывод в stdout содержимого строки, файла или потока; если запускается как самостоятельный сценарий, обеспечивает постраничный вывод содержимого потока stdin или файла, имя которого указывается в виде аргумента командной строки; когда входные данные поступают через поток stdin, исключается возможность использовать его для получения ответов пользователя --вместо этого можно использовать платформозависимые инструменты или графический интерфейс;

import sys def getreply():

читает клавишу, нажатую пользователем,

даже если stdin перенаправлен в файл или канал

if sys.stdin.isatty():    # если stdin связан с консолью,

return input(‘?’)    # читать ответ из stdin

else:

if sys.platform[:3] == ‘win’: #    если stdin был перенаправлен,

import msvcrt    #    его нельзя использовать для чтения

msvcrt.putch(b’?’)    #    ответа пользователя

key = msvcrt.getche()    #    использовать инструмент консоли

msvcrt.putch(b’\n’)    #    getch(), которая не выводит символ

return key    #    для нажатой клавиши

else:

assert False, ‘platform not supported’

#для Linux: open(‘/dev/tty’).readline()[:-1]

def more(text, numlines=10):

реализует постраничный вывод содержимого строки в stdout

lines = text.splitlines() while lines:

chunk = lines[:numlines]

lines = lines[numlines:]

for line in chunk: print(line)

if lines and getreply() not in [b’y’, b’Y’]: break

if name == ‘ main ’:    # если выполняется, а не импортируется

if len(sys.argv) == 1:    # если нет аргументов командной строки

more(sys.stdin.read())    # вывести содержимое stdin

else:

more(open(sys.argv[1]).read()) # иначе вывести содержимое файла

Большая часть нововведений этой версии находится в функции getreply Метод файла isatty сообщает, соединен ли stdin с консолью, - если да функция просто считывает ответ из stdin, как и раньше. Конечно, по добная дополнительная логика необходима только в сценариях, предусматривающих возможность взаимодействия с пользователем и получения входных данных из stdin. В приложениях с графическим интерфейсом можно было бы, например, выводить диалог, реализовать обработку событий от клавиатуры в виде функций обратного вызова и так далее (знакомиться с графическими интерфейсами мы будем в главе 7).

Имея на вооружении функцию getreply, можно спокойно запускать утилиту moreplus различными способами. Как и прежде, можно импортировать и непосредственно вызывать функцию этого модуля, передавая ей ту строку, которую требуется вывести постранично:

>>> from moreplus import more

>>> more(open('adderSmall.py').readO)

import sys

print(sum(int(line) for line in sys.stdin))

И так же, как и прежде, при запуске с аргументом командной строки этот сценарий интерактивно будет пролистывать текст указанного файла:

C:\...\PP4E\System\Streams> python moreplus.py adderSmall.py

import sys

print(sum(int(line) for line in sys.stdin))

C:\...\PP4E\System\Streams> python moreplus.py moreplus.py

обеспечивает постраничный вывод в stdout содержимого строки, файла или потока; если запускается как самостоятельный сценарий, обеспечивает постраничный вывод содержимого потока stdin или файла, имя которого указывается в виде аргумента командной строки; когда входные данные поступают через поток stdin, исключается возможность использовать его для получения ответов пользователя - вместо этого можно использовать платформозависимые инструменты или графический интерфейс;

import sys

def getreply():

?n

Но теперь сценарий также правильно выводит постранично текст, перенаправленный в stdin из файла или по конвейеру команд, даже если этот текст слишком велик, чтобы целиком уместиться в одну страницу. В большинстве оболочек входные данные посылаются с помощью перенаправления или операторов конвейера, как показано ниже:

C:\...\PP4E\System\Streams> python moreplus.py < moreplus.py

обеспечивает постраничный вывод в stdout содержимого строки, файла или потока; если запускается как самостоятельный сценарий, обеспечивает постраничный вывод содержимого потока stdin или файла, имя которого указывается в виде аргумента командной строки; когда входные данные поступают через поток stdin, исключается возможность использовать его для получения ответов пользователя - вместо этого можно использовать платформозависимые инструменты или графический интерфейс;

import sys

def getreply():

?n

C:\...\PP4E\System\Streams> type moreplus.py | python moreplus.py

обеспечивает постраничный вывод в stdout содержимого строки, файла или потока; если запускается как самостоятельный сценарий, обеспечивает постраничный вывод содержимого потока stdin или файла, имя которого указывается в виде аргумента командной строки; когда входные данные поступают через поток stdin, исключается возможность использовать его для получения ответов пользователя - вместо этого можно использовать платформозависимые инструменты или графический интерфейс;

import sys

def getreply():

?n

Наконец, если вывод одного сценария отправляется по каналу на ввод другого, все работает как надо, и при этом взаимодействие с пользователем не вызывает нарушений (и вовсе не потому, что нам просто повезло):

......\System\Streams> python teststreams.py < input.txt | python moreplus.py

Hello stream world Enter a number>8 squared is 64 Enter a number>6 squared is 36 Enter a number>Bye

Здесь стандартный вывод одного сценария подается на стандартный ввод другого сценария, находящегося в том же каталоге: moreplus.py читает вывод teststreams.py.

Все перенаправления в таких командах действуют только потому, что сценариям безразлично, чем в действительности являются стандартный ввод и вывод - консолью, файлами или каналами между программами. Например, при запуске moreplus.py как самостоятельного сценария он просто читает поток sys.stdin; командная оболочка (например, DOS в Windows, csh в Linux) прикрепляет такие потоки к источникам, определяемым командой, перед запуском сценария. Для доступа к этим источникам сценарии используют заранее открытые объекты файлов stdin и stdout, независимо от их истинной природы.

Для читателей, ведущих подсчет: мы запускали один сценарий постраничного вывода more четырьмя различными способами - импортируя и вызывая его функцию, передавая имя файла через аргумент командной строки, перенаправляя stdin в файл и передавая вывод команды по каналу в stdin. Благодаря возможности импортировать функции, принимать аргументы командной строки и получать ввод через стандартные потоки системные инструменты Python можно повторно использовать в разнообразных режимах.


Перенаправление потоков в объекты Python

Все приведенные выше способы перенаправления стандартных потоков действуют для программ, написанных на любом языке программирования, который обеспечивает возможность перехватывать стандартные потоки, и зависят скорее от процессора командной строки оболочки, чем от самого интерпретатора. Операции перенаправления в командной строке, такие как < filename и | program, обрабатываются оболочкой, а не интерпретатором Python. Более «питонистый» способ перенаправления можно реализовать в самих сценариях, присваивая переменным sys.stdin и sys.stdout объекты, похожие на файлы.

Это возможно благодаря тому, что любой объект, который набором методов напоминает файл, может выступать в роли стандартного потока. Важен не конкретный тип объекта, а его интерфейс (иногда его называют протоколом). Это означает следующее:

•    Любой объект, обладающий методами чтения, может быть присвоен переменной sys.stdin, в результате чего ввод будет осуществляться через методы чтения этого объекта.

•    Любой объект, обладающий методами записи, может быть присвоен переменной sys.stdout; в результате весь стандартный вывод будет отправляться методам этого объекта.

Так как функции print и input просто вызывают методы write и readline объектов, на которые ссылаются sys.stdout и sys.stdin, мы можем генерировать и перехватывать стандартные текстовые потоки с помощью объектов, реализованных с помощью классов.

Если вам уже приходилось изучать язык Python, вы, вероятно, знаете, что подобная совместимость по интерфейсам обычно называется полиморфизмом - неважно, чем является объект, и неважно, что делают его методы, - важно, чтобы этот объект предоставлял ожидаемый интерфейс. Такое либеральное отношение к типам данных объясняет значительную долю гибкости и выразительности программного кода на языке Python. Ниже демонстрируется способ, позволяющий сценариям переопределять собственные потоки ввода-вывода. В примере 3.9 приводится вспомогательный модуль, демонстрирующий эту идею.

Пример 3.9. PP4E\System\Streams\redirect.py

объекты, похожие на файлы, один из которых сохраняет в строке текст, отправленный в стандартный поток вывода, а другой обеспечивает ввод текста из строки в стандартный поток ввода; функция redirect вызывает переданную ей функцию, для которой стандартные потоки вывода и ввода будут связаны с объектами, похожими на файлы;

import sys    # импортировать встроенный модуль

class Output:    # имитирует выходной файл

def __init__(self):

self.text = ‘’    # при создании строка пустая

def write(self, string):    # добавляет строку байтов

self.text += string

def writelines(self, lines):    # добавляет все строки в список

for line in lines: self.write(line)

class Input:    # имитирует входной файл

def __init__(self, input=’’):    # аргумент по умолчанию

self.text = input    # сохранить строку при создании

def read(self, size=None):    #    необязательный    аргумент

if size == None:    #    прочитать N байт или все

res, self.text = self.text, ‘’ else:

res, self.text = self.text[:size], self.text[size:] return res def readline(self):

eoln = self.text.find(‘\n’)    #    найти смещение    следующего eoln

if eoln == *1:    #    извлечь строку    до eoln

res, self.text = self.text, ‘’ else:

res, self.text = self.text[:eoln+1], self.text[eoln+1:] return res

def redirect(function, pargs, kargs, input): # перенаправляет stdin/out savestreams = sys.stdin, sys.stdout # вызывает объект функции sys.stdin = Input(input)    # возвращает текст в stdout

sys.stdout = Output() try:

result = function(*pargs, **kargs) # вызвать функцию с аргументами output = sys.stdout.text

finally:    # восстановить, независимо от

sys.stdin, sys.stdout = savestreams # того, было ли исключение return (result, output)    # вернуть результат,

# если исключения не было

В этом модуле определены два класса, маскирующиеся под настоящие файлы:

Output

Предоставляет интерфейс (он же протокол) метода записи, предполагаемый у выходных файлов, но сохраняет всю записываемую информацию в строке, хранящейся в памяти.

Input

Предоставляет интерфейс, предполагаемый у входных файлов, но возвращает входные данные по требованию, извлекая их из хранящейся в памяти строки, переданной при создании объекта.

Функция redirect в конце этого файла объединяет эти два объекта, чтобы выполнить единственную функцию, для которой стандартные потоки ввода и вывода будут перенаправлены в объекты Python. Функции, которая вызывается функцией redirect, не требуется ни знать, ни заботиться о том, что вызываемые ею функции print и input или методы stdin и stdout в действительности будут иметь дело с нашими объектами, а не с настоящим файлом, каналом или пользователем.

Чтобы продемонстрировать, как действует эта функция, импортируем и вызовем функцию interact, лежащую в основе сценария teststreams, представленного в примере 3.5, который прежде мы запускали из командной строки (для использования вспомогательной функции перенаправления нужно действовать на языке функций, а не файлов). При непосредственном вызове функция читает данные с клавиатуры и выводит результаты на экран, как если бы она выполнялась как программа без перенаправления:

C:\...\PP4E\System\Streams> python >>> from teststreams import interact >>> interact()

Hello stream world Enter a number>2

2    squared is 4 Enter a number>3

3    squared is 9 Enter a number^Z Bye

>>>

Теперь вызовем эту функцию под управлением функции перенаправления в redirect.py и передадим ей некоторый готовый входной текст. В этом случае на вход функции interact поступит переданная строка ('4\n5\n6\n ’ - три строки с явными символами конца строки), а результатом выполнения функции будет кортеж, содержащий возвращаемое значение и строку с текстом, который был записан в стандартный поток вывода:

>>> from redirect import redirect

>>> (result, output) = redirect(interact, (), {}, '4\n5\n6\n')

>>> print(result)

None

>>> output

‘Hello stream world\nEnter a number>4 squared is 16\nEnter a number>5 squared is 25\nEnter a number>6 squared is 36\nEnter a number>Bye\n’

На выходе получится одна длинная строка, содержащая весь текст, записанный в стандартный поток вывода. Чтобы улучшить внешний вид строки, ее можно передать функции print или разбить на отдельные строки с помощью строкового метода splitlines:

>>> for line in output.splitlines(): print(line)

Hello stream world Enter a number>4 squared is 16 Enter a number>5 squared is 25 Enter a number>6 squared is 36 Enter a number>Bye

Еще лучше повторно использовать модуль more^Y, который мы написали в предыдущей главе (пример 2.1). При этом придется меньше запоминать и вводить с клавиатуры, а качество работы уже проверено нами (ниже, как и во всех примерах, где выполняется импортирование модулей из других каталогов, предполагается, что каталог, содержащий корневой подкаталог PP4E, находится в пути поиска модулей, - измените значение переменной окружения PYTHONPATH, если это необходимо):

>>> from PP4E.System.more import more >>> more(output)

Hello stream world Enter a number>4 squared is 16 Enter a number>5 squared is 25 Enter a number>6 squared is 36 Enter a number>Bye

Конечно, это искусственный пример, но продемонстрированные приемы могут иметь широкое применение. Например, в программу, реализующую интерфейс командной строки для взаимодействия с пользователем, легко добавить графический интерфейс. Нужно просто перехватить стандартный вывод с помощью объекта, такого как экземпляр класса Output, и сбросить текстовую строку в окно. Аналогично стандартный ввод можно перенаправить в объект, который получает текст из графического интерфейса (например, из окна диалога). Поскольку классы могут заменять настоящие файлы, их можно использовать в любых инструментах, работающих с файлами. Обратитесь к модулю перенаправления потоков с графическим интерфейсом guiStreams в главе 10, где вы найдете конкретную реализацию некоторых из описанных идей.


Вспомогательные классы io.StringIO и io.BytesIO

Прием перенаправления потоков ввода-вывода в объекты, о котором рассказывалось в предыдущем разделе, оказался настолько удобным на практике, что в стандартную библиотеку был добавлен модуль, автоматизирующий задачу его применения в большинстве случаев (хотя в некоторых случаях, например, при реализации графического интерфейса, все еще может потребоваться писать свой программный код). В стандартной библиотеке имеется объект, который добавляет интерфейс объектов файлов к объектам строк, хранящимся в памяти. Например:

>>> from io import StringIO

>>> buff = StringIO()    # сохраняет записываемый текст в строке

>>> buff.write('spam\n')

5

>>> buff.write('eggs\n')

5

>>> buff.getvalue()

‘spam\neggs\n’

>>> buff = StringIO('ham\nspam\n') # возвращает входные данные из строки >>> buff.readline()

‘ham\n’

>>> buff.readline()

‘spam\n’

>>> buff.readline()

Экземпляры класса StringIO могут присваиваться переменным sys.stdin и sys.stdout, как демонстрировалось в предыдущем разделе, с целью перенаправить потоки для функций input и print, и использоваться в любом программном коде, выполняющем операции с настоящими объектами файлов. Напомню еще раз, что в языке Python правила игры определяются интерфейсом объекта, а не его конкретным типом:

>>> from io import StringIO

>>> import sys

>>> buff = StringIO()

>>> temp = sys.stdout >>> sys.stdout = buff

>>> print(42, 'spam', 3.141)    # или print(..., file=buff)

>>> sys.stdout = temp    # восстановит оригинальный поток

>>> buff.getvalue()

‘42 spam 3.141\n’

Следует также отметить, что существует класс io. BytesIO, обладающий похожим поведением, но он отображает операции с файлами не на строку типа str, а на буфер байтов типа bytes:

>>> from io import BytesIO >>> stream = BytesIO()

>>> stream.write(b'spam')

>>> stream.getvalue()

b’spam’

>>> stream = BytesIO(b'dpam')

>>> stream.read()

b’dpam’

Из-за существенных различий между текстовыми и двоичными данными в Python 3.X эта альтернатива может оказаться более подходящей для сценариев, имеющих дело с двоичными данными. Подробнее с проблемой различий между текстовыми и двоичными данными мы познакомимся в следующей главе, когда займемся исследованием файлов.


Перехват потока stderr

Мы сосредоточились на перенаправлении stdin и stdout, но поток stderr также можно перенаправлять в файлы, каналы и объекты. Несмотря на то, что некоторые оболочки поддерживают возможность перенаправления этого потока, тем не менее это также можно сделаеть легко и просто в сценарии Python. Например, присвоение переменной sys. stderr экземпляра класса, такого как Output или StringIO из предыдущего примера, позволит сценарию перехватывать также текст, записываемый в стандартный поток ошибок.

Сам интерпретатор Python использует стандартный поток ошибок для вывода сообщений об ошибках (графический интерфейс IDLE перехватывает этот текст и по умолчанию окрашивает его в красный цвет). Однако в языке отсутствуют высокоуровневые инструменты для работы со стандартным потоком ошибок, такие как функции print и input для стандартных потоков вывода и ввода. Если вам потребуется организовать вывод в стандартный поток ошибок, вы можете явно вызвать метод sys.stderr.write() или прочитать следующий раздел, где описывается одна особенность функции print, упрощающая эту возможность.

Операция перенаправления стандартного потока ошибок из командной строки выглядит несколько сложнее и хуже переносится. В большинстве Unix-подобных систем перехватить вывод в поток stderr обычно можно с помощью операции перенаправления вида command > output 2>&1. Однако в некоторых версиях Windows она не действует, и даже в некоторых оболочках для Unix она может иметь другой вид - за дополнительной информацией обращайтесь к страницам справочного руководства по вашей оболочке.


Возможность перенаправления с помощью функции print

Вследствие того, что переназначение атрибутов потоков приобрело большую популярность, встроенная функция print в языке Python также была дополнена возможностью явно указывать файл для вывода. Следующая инструкция:

print(stuff, file=afile) # afile - это объект, а не имя строковой переменной

выведет stuff в afile, а не в поток sys.stdout. По своему действию это напоминает присваивание объекта переменной sys.stdout, но в данном случае отпадает необходимость сохранять и восстанавливать первоначальное значение, чтобы вернуться к использованию оригинального потока вывода (как было показано в разделе, описывающем перенаправление потоков в объекты). Например:

import sys

print(‘spam’ * 2, file=sys.stderr)

выведет текст в объект стандартного потока ошибок, а не в sys.stdout, причем такое перенаправление будет действовать только для данного вызова функции print. Следующий вызов функции print (без аргумента file) выведет текст в стандартный поток вывода, как обычно. Точно так же в качестве выходного файла можно передать свой собственный объект или экземпляр класса из стандартной библиотеки:

>>> from io import StringIO >>> buff = StringIO()

>>> print(42, file=buff)

>>> print('spam', file=buff)

>>> print(buff.getvalue())

42

spam

>>> from redirect import Output >>> buff = Output()

>>> print(43, file=buff)

>>> print('eggs', file=buff)

>>> print(buff.text)

43

eggs


Другие варианты перенаправления: еще раз об os.popen и subprocess

Ближе к концу предыдущей главы мы впервые встретились с функцией os.popen и родственной ей subprocess.Popen, которые предоставляют возможность перенаправления потоков ввода-вывода других команд из программы на языке Python. Как мы видели, эти инструменты могут использоваться для выполнения команд оболочки (например, команд, которые обычно вводятся с клавиатуры в ответ на приглашение DOS или csh), и они возвращают объект Python, похожий на файл, соединенный с потоком вывода команды, - чтение из объекта файла позволяет сценарию принимать вывод другой программы. Однако эти инструменты могут также использоваться для соединения с потоками ввода.

Благодаря этому функцию os.popen и инструменты из модуля subprocess можно рассматривать как еще один способ перенаправления потоков порождаемых программ, родственный только что рассмотренным приемам. Их действие во многом похоже на действие оператора | объединения команд в конвейер (фактически имена этих инструментов означают «pipe open» - «открыть канал»), но они выполняются внутри сценария и предоставляют схожий с файлами интерфейс к потокам данных, связанных каналом. По духу они близки функции redirect, но запускают не функции, а программы, и потоки ввода-вывода обрабатываются в порождающем сценарии как файлы (не привязанные к объектам классов). Эти инструменты перенаправляют потоки ввода-вывода программ, запускаемых сценарием, а не самого сценария.

Перенаправление ввода или вывода с помощью os.popen

Передавая в функцию флаг нужного режима, мы фактически выполняем перенаправление в файл потока ввода или вывода программы, порожденной сценарием, и можем получить код завершения этой программы вызовом метода close (значение None говорит об успешном завершении). Чтобы проиллюстрировать это, рассмотрим следующие два сценария:

C:\...\PP4E\System\Streams> type hello-out.py print(‘Hello shell world’)

C:\...\PP4E\System\Streams> type hello-in.py inp = input()

open(‘hello-in.txt’, ‘w’).write(‘Hello ‘ + inp + ‘\n’)

Эти сценарии могут запускаться из командной строки, как обычно:

C:\...\PP4E\System\Streams> python hello-out.py Hello shell world

C:\...\PP4E\System\Streams> python hello-in.py Brian

C:\...\PP4E\System\Streams> type hello-in.txt Hello Brian

В предыдущей главе мы видели, что сценарии на языке Python могут также читать вывод других программ и подобных им сценариев, как показано ниже:

C:\...\PP4E\System\Streams> python >>> import os

>>> pipe = os.popen('python hello-out.py') # ‘r’ - по умолчанию, чтение stdout

>>> pipe.read()

‘Hello shell world\n’

>>> print(pipe.close())    # код завершения: None - успех

None

Но сценарии на языке Python могут играть роль источников данных, подаваемых в поток стандартного ввода порождаемых программ, -если передать функции os.popen флаг режима «w» вместо «г», подразумеваемого по умолчанию, она вернет объект, подключенный к потоку ввода порожденной программы. Все, что мы запишем в этот объект со стороны родительского сценария, окажется в стандартном потоке ввода запущенной программы:

>>> pipe = os.popen('python hello-in.py', 'w')    # ‘w’- запись в stdin программы

>>> pipe.write('Gumby\n')

6

>>> pipe.close()    # символ \n в конце необязателен

>>> open('hello-in.txt').read()    # вывод был отправлен в файл

‘Hello Gumby\n’

Функция popen достаточно сообразительна, чтобы выполнить команду оболочки как независимый процесс на платформах, поддерживающих такую возможность. Она принимает необязательный третий параметр, который используется для управления буферизацией записываемого текста, но мы пока не будем затрагивать эту тему.

Перенаправление ввода и вывода с помощью модуля subprocess

Еще больший контроль над потоками ввода-вывода порождаемых программ позволяет получить модуль subprocess, представленный в предыдущей главе. Как было показано ранее, с помощью этого модуля можно имитировать функциональность os.popen, но он позволяет добиться большего, например - создавать двунаправленные потоки обмена данными (то есть подключаться к обоим потокам, ввода и вывода, порождаемой программы) и связывать вывод одной программы с вводом другой.

Например, этот модуль позволяет множеством способов запускать программу, подключаться к ее стандартному потоку вывода и получать код завершения. Ниже демонстрируются три наиболее типичных способа использования этого модуля для запуска программы и перенаправления ее потока вывода (напомню, что для опробования примеров из этого раздела в Unix-подобных системах вам может потребоваться передавать функции Popen аргумент shell=True, как отмечалось в главе 2):

C:\...\PP4E\System\Streams> python

>>> from subprocess import Popen, PIPE, call

>>> X = call('python hello-out.py')    # удобно

Hello shell world

>>> X 0

>>> pipe = Popen('python hello-out.py', stdout=PIPE)

>>> pipe.communicate()[0]    #    (stdout, stderr)

b’Hello shell world\r\n’

>>> pipe.returncode    #    код завершения

0

>>> pipe = Popen('python hello-out.py', stdout=PIPE)

>>> pipe.stdout.read()

b’Hello shell world\r\n’

>>> pipe.wait()    #    код завершения

0

Функция call, использованная в первом из этих трех способов, - это всего лишь функция-обертка, реализованная для удобства (существует несколько таких функций, о которых вы сможете прочитать в руководстве по библиотеке языка Python). Функция communicate делает второй способ немного удобнее третьего (она позволяет отправлять данные в stdin; читать данные из stdout, пока не будет достигнут конец файла; и ожидает завершения дочернего процесса).

Перенаправление и подключение к потоку ввода порождаемой программы реализуется так же просто, хотя и немного сложнее, чем при использовании функции os.popen с флагом режима ‘w’, как было показано в предыдущем разделе (как уже упоминалось в предыдущей главе, в настоящее время функция os.popen реализована с применением инструментов из модуля subprocess, и поэтому сама может считаться функцией-оберткой, реализованной для удобства):

>>> pipe = Popen('python hello-in.py', stdin=PIPE)

>>> pipe.stdin.write(b'Pokey\n')

6

>>> pipe.stdin.close()

>>> pipe.wait()

0

>>> open('hello-in.txt').read()    # вывод был отправлен в файл

‘Hello Pokey\n’

С помощью этого модуля мы можем получить доступ к обоим потокам, ввода и вывода, порожденной программы. Для демонстрации воспользуемся простыми сценариями, выполняющими операции чтения и записи, созданными нами ранее:

C:\...\PP4E\System\Streams> type writer.py print("Help! Help! I’m being repressed!”) print(42)

C:\...\PP4E\System\Streams> type reader.py print(‘Got this: “%s”’ % input())

import sys

data = sys.stdin.readline()[:-1]

print(‘The meaning of life is’, data, int(data) * 2)

Следующий программный код демонстрирует возможность чтения и записи в потоки ввода-вывода сценария reader - объект pipe имеет два атрибута с объектами, похожими на файлы, один из которых подключается к потоку ввода, а другой - к потоку вывода (пользователи Python 2.X легко могут узнать в них эквивалент кортежа, возвращаемого функцией os.popen2, ныне исключенной из библиотеки):

>>> pipe = Popen('python reader.py', stdin=PIPE, stdout=PIPE)

>>> pipe.stdin.write(b'Lumberjack\n')

11

>>> pipe.stdin.write(b'12\n')

3

>>> pipe.stdin.close()

>>> output = pipe.stdout.read()

>>> pipe.wait()

0

>>> output

b’Got this: “Lumberjack”\r\nThe meaning of life is 12 24\r\n’

Как будет показано в главе 5, при двунаправленном обмене данными с программами, подобными этим, необходимо проявлять осторожность - механизм буферизации потоков вывода может приводить к взаимоблокировке при чередовании операций записи и чтения и, в конечном счете, к необходимости использовать для решения проблемы такие инструменты, как Pexpect (подробнее об этой функции будет рассказываться далее в книге).

Наконец, модуль subprocess позволяет реализовать еще более экзотические формы управления потоками ввода-вывода - ниже демонстрируется соединение двух программ, где поток вывода одного сценария на языке Python подключается к потоку ввода другого. Сначала демонстрируется подключение с использованием командной оболочки, а затем - с помощью модуля subprocess:

C:\...\PP4E\System\Streams> python writer.py | python reader.py

Got this: “Help! Help! I’m being repressed!”

The meaning of life is 42 84

C:\...\PP4E\System\Streams> python

>>> from subprocess import Popen, PIPE

>>> p1 = Popen('python writer.py', stdout=PIPE)

>>> p2 = Popen('python reader.py', stdin=p1.stdout, stdout=PIPE)

>>> output = p2.communicate()[0]

>>> output

b’Got this: “Help! Help! I\’m being repressed!”\r\nThe meaning of life is 42 84\r\n’ >>> p2.returncode

0

Нечто похожее можно реализовать с помощью функции os.popen, но тот факт, что она может подключать только один из потоков ввода-вывода (но не оба), препятствует возможности перехватить вывод второго сценария:

>>> import os

>>> p1 = os.popen('python writer.py', 'r')

>>> p2 = os.popen('python reader.py', 'w')

>>> p2.write( p1.read() )

36

>>> X = p2.close()

Got this: “Help! Help! I’m being repressed!”

The meaning of life is 42 84 >>> print(X)

None

С точки зрения более широкой перспективы, функция os.popen и модуль subprocess являются переносимыми эквивалентами механизма перенаправления потоков ввода-вывода порождаемых программ, реализованного в командных оболочках для Unix-подобных систем. Однако реализации на языке Python с таким же успехом работают в Windows и предоставляют более универсальный способ запуска других программ из сценариев на языке Python. Строки команд, передаваемые им, могут иметь свои особенности в зависимости от платформы (например, в Unix список содержимого каталога можно получить с помощью команды ls, а в Windows - с помощью команды dir), но сами инструменты могут применяться на всех платформах, поддерживающих Python.

Запуск новых, независимых программ и подключение к их потокам ввода-вывода из родительской программы в Unix-подобных системах можно также реализовать с помощью функций os.fork, os.pipe, os.dup и некоторых функций из семейства os.exec. Кроме того, они обеспечивают еще один способ перенаправления потоков ввода-вывода и являются низкоуровневыми эквивалентами таким инструментам, как os.popen (функция os.fork доступна в Windows, в версии Python для Cygwin).

Однако все перечисленные функции являются более сложными инструментами параллельной обработки данных, поэтому мы отложим дальнейшее их обсуждение до главы 5, где дополнительно будет рассказываться об организации каналов и получении кодов завершения. А к модулю subprocess мы вернемся в главе 6, где на его основе реализуем механизм регрессионного тестирования, перехватывающий все три стандартных потока ввода-вывода тестируемого сценария - потоки ввода, вывода и ошибок.

Но перед этим, в главе 4, мы продолжим наше исследование системных интерфейсов, реализованных в библиотеке языка Python, и познако-

мимся с инструментами для работы с файлами и каталогами. Несмотря на то, что все наше внимание будет сконцентрировано на других вопросах, тем не менее мы увидим, что некоторые инструменты, изученные здесь, могут использоваться, как универсальные инструменты системного программирования. Например, возможность запуска команд оболочки позволяет исследовать содержимое каталогов, а интерфейс объектов файлов, на котором мы подробно остановимся в следующей главе, составляет основу приемов работы с потоками ввода-вывода, обсуждавшихся здесь.


Python и csh

Если вы знакомы с другими распространенными языками сценариев командной оболочки, вам может оказаться полезным сравнить их с языком Python. Ниже приводится простой сценарий на языке командной оболочки csh для Unix, который отправляет по электронной почте все файлы с расширением .py из текущего рабочего каталога (то есть все файлы с исходным программным кодом на языке Python) на фиктивный, как мы надеемся, электронный адрес:

#!/bin/csh foreach x (*.py) echo $x

mail eric@halfabee.com -s $x < $x end

Ниже приводится эквивалентный сценарий на языке Python:

#!/usr/bin/python import os, glob for x in glob.glob(‘*.py’): print(x)

os.system(‘mail eric@halfabee.com -s %s < %s’ % (x, x))

Он выглядит более подробным. Язык Python, в отличие от csh, не предназначен для разработки исключительно сценариев командной строки, поэтому системные интерфейсы необходимо импортировать и вызывать явно. А так как Python не является языком программирования, ориентированным на работу исключительно со строками, строки символов необходимо заключать в кавычки, как в языке C.

И хотя использование языка Python для создания простых сценариев, таких как этот, требует лишних нажатий на клавиши, тем не менее его принадлежность к классу языков программирования общего назначения обеспечивает ему более высокую привлекатель-


ность, как только мы покидаем область тривиальных программ. Мы могли бы, к примеру, расширить предыдущий сценарий, добавив в него такие возможности, как передача файлов по протоколу FTP, предоставление выбора операции с помощью графического интерфейса, содержащего строку состояния, извлечение сообщений из базы данных SQL, использование COM-объектов в Windows, - и все это с применением стандартных инструментов Python.

Кроме того, сценарии на языке Python обычно легко могут переноситься на другие платформы, в отличие от csh. Например, задействовав для отправки электронной почты модуль Python, обеспечивающий интерфейс к SMTP, вместо утилиты mail командной строки, мы сможем использовать этот сценарий на любом компьютере, где установлен Python и имеется подключение к Интернету (как мы узнаем в главе 13, для работы с протоколом SMTP достаточно одних сокетов). Как и в языке C, нам нет необходимости использовать префикс $, чтобы получать значения переменных; что еще можно желать от свободного языка?


Инструменты для работы с файлами и каталогами


«Как очистить свой жесткий диск за пять простых шагов»

Эта глава продолжает исследование системных интерфейсов Python, фокусируясь на инструментах для работы с файлами и каталогами. Как вы увидите в этой главе, благодаря встроенным инструментам и инструментам из стандартной библиотеки операции с файлами и деревьями каталогов реализуются очень просто. Механизмы работы с файлами составляют часть ядра языка Python, поэтому часть материала этой главы посвящена обзору основных сведений о файлах, подробно рассматриваемых в других книгах, таких как четвертое издание «Изучаем Python», к которым мы рекомендуем обратиться за детальными разъяснениями концепций, связанных с файлами. Например, мы будем касаться таких тем, как итерации, менеджеры контекста и поддержка Юникода объектами файлов, но они не будут раскрываться здесь полностью. Цель этой главы - дать достаточный объем сведений, чтобы вы могли начать писать полезные сценарии.


Инструменты для работы с файлами

Внешние файлы - это тот объект, ради которого чаще всего разрабатываются системные утилиты. Например, система тестирования может читать входные данные из одного файла, сохранять результаты программы в другом файле и проверять полученные результаты, загружая третий файл. Даже программы с пользовательским интерфейсом и вебприложения могут загружать двоичные изображения и аудиоклипы из локальных файлов. Это базовая концепция программирования.

В Python главным инструментом, используемым сценариями для доступа к файлам, служит встроенная функция open. Поскольку эта функция является неотъемлемой частью языка Python, вы, вероятно, уже знакомы с тем, как она работает. При вызове функции open возвращается новый объект файла, соединенный с внешним файлом. Объект файла обладает методами для чтения и записи данных и для выполнения различных операций над файлами. Кроме того, функция open предоставляет переносимый интерфейс к используемой файловой системе - она одинаково работает на любой платформе, где выполняется Python.

Другие связанные с файлами модули в Python позволяют, например, выполнять операции над файлами на низком уровне с использованием файловых дескрипторов (модуль os), перемещать файлы и группы файлов (модули os и shutil), сохранять в файлах данные и объекты по ключу (модули dbm и shelve) и обращаться к базам данных SQL (модуль sqlite3 и модули сторонних разработчиков). Последние две категории в большей степени относятся к обсуждению баз данных, которое ведется в главе 17.

В данном разделе мы кратко рассмотрим встроенный объект файла и несколько более сложных тем, относящихся к файлам. Как обычно, более подробное описание и методы, которые мы не имеем возможности разместить здесь, следует искать в руководстве по библиотеке или в справочниках, таких как «Python Pocket Reference». Не забывайте, что краткую справку можно получить в интерактивной оболочке: чтобы ознакомиться со списком атрибутов объекта файла, можно вызвать функцию dir(file) для объекта открытого файла; вызвав функцию help(file), можно получить справку более общего характера; а с помощью вызова help(file.read) - справку о конкретном методе, таком как read, хотя реализация объекта файла в версии 3.1 содержит меньше справочной информации, чем руководство по библиотеке и другие ресурсы.


Модель объекта файла в Python 3.X

Как и в случае со строковыми типами, о которых говорилось в главе 2, поддержка файлов в Python 3.X стала гораздо богаче, чем в предыдущих версиях. Как уже отмечалось ранее, в Python 3.X строки типа str всегда представляют текст Юникода (символы ASCII или многобайтовые символы), а строки типов bytes и bytearray представляют простые двоичные данные. Python 3.X проводит подобные различия между файлами, содержащими текст и двоичные данные:

Текстовые файлы, содержат текст, состоящий из символов Юникода. Содержимое текстовых файлов в сценариях всегда представляется в виде строк типа str - последовательностей символов (точнее, последовательностей «кодовых пунктов» Юникода). Для текстовых файлов автоматически выполняется преобразование символов конца строки, о котором рассказывается в этой главе, а к содержимому файлов автоматически применяются операции кодирования/деко-дирования: данные кодируются в двоичное представление при записи в файл и декодируются обратно в Юникод при чтении из файла, в соответствии с указанной или используемой по умолчанию кодировкой. Кодирование является тривиальной операцией для текста ASCII, но может быть весьма сложной в других случаях.

Двоичные файлы содержат обычные 8-битовые байты. Содержимое двоичных файлов в сценариях всегда представляется в виде строк байтов, обычно в виде объекта типа bytes - последовательности коротких целых чисел, которые поддерживают большинство операций, присущих типу str, и отображаются как последовательности символов ASCII, когда это возможно. Для двоичных файлов не предусматривается никаких преобразований данных при чтении или записи: ни преобразования символов конца строки, ни кодирования/ декодирования в Юникод.

На практике текстовые файлы используются для хранения действительно текстовых данных, а двоичные файлы - для хранения таких элементов, как упакованные двоичные данные, изображения, аудиоданные, выполняемый программный код и так далее. Программно эти два типа файлов различаются с помощью аргумента со строкой режима, который передается функции open: дополнительный символ «Ь» (например, ‘rb’, ‘wb’) означает, что файл содержит двоичные данные. Для создания нового содержимого текстовых файлов используются обычные строки (например, ‘spam’ или bytes.decode()), а для создания нового содержимого двоичных файлов - строки байтов (например, bspam’ или str.encode()).

Если в вашей практике область применения файлов не ограничивается использованием текста в кодировке ASCII, различия между представлением текстовых и двоичных данных в версии 3.X иногда будут сказываться на вашем программном коде. При работе с текстовыми файлами требуется использовать строки типа str, а с двоичными файлами - строки байтов. Поскольку вы не сможете смешивать эти типы в выражениях, вам придется внимательно подходить к вопросу выбора режима открытия файла. Многие встроенные инструменты, которые мы будем использовать в этой книге, делают этот выбор за нас - модули struct и pickle, например, в версии 3.X работают со строками байтов, а пакет xml - с Юникодом. Кроме того, о различиях между текстовыми и двоичными данными в версии 3.X необходимо помнить даже при использовании системных инструментов, таких как дескрипторы каналов и сокеты, потому что на сегодняшний день эти инструменты передают данные в виде строк байтов (впрочем, при необходимости эти данные можно кодировать и декодировать как текст Юникода).

Кроме того, при работе с текстовыми файлами выполняется обязательное декодирование их содержимого в Юникод в соответствии с выбранной кодировкой, поэтому вам придется использовать двоичный режим для чтения содержимого файлов, не поддающихся декодированию, в виде строк байтов (или обрабатывать исключения декодирования в Юникод с помощью инструкций try и пропускать такой файл целиком). Это относится и к собственно двоичным файлам, и к текстовым файлам, для представления текста в которых используется неподдерживаемая или неизвестная кодировка. Как мы увидим далее в этой главе, в версии 3.X строки типа str всегда содержат текст Юникода, поэтому иногда придется использовать строки байтов для представления имен файлов при использовании таких инструментов, как os.listdir, glob.glob и os.walk, если они не могут быть декодированы (передача в виде строки байтов фактически подавляет необходимость декодирования).

На протяжении всей книги мы будем видеть примеры влияния различий между текстовым и двоичным типами str и bytes в инструментах для работы с файлами: в главах 5 и 12, когда будем исследовать сокеты; в главах 6 и 11, когда нам потребуется игнорировать ошибки Юникода при поиске в файлах и каталогах; в главе 12, когда будем знакомиться с модулями поддержки протоколов Интернета на стороне клиента, таких как FTP и протоколы электронной почты, реализованные поверх сокетов, предполагающих определение режимов файлов и кодировок; и и во многих других местах.

Но так же, как и для строковых типов, в данной главе мы не будем углубляться в эту тему, хотя и будем рассматривать практическое влияние некоторых из представленных концепций. Файлы и строки являются базовой частью языка, и знание их является необходимым условием для чтения этой книги. Как упоминалось ранее, поддержке Юникода посвящена 45-страничная глава в четвертом издании книги «Изучаем Python», поэтому я не буду повторять эти сведения в данной книге. Если при чтении следующих разделов вам покажется, что вы вконец запутались в концепциях, связанных с Юникодом, и в различиях между текстовыми и двоичными строками и файлами, я советую обратиться за более полной информацией к указанной выше книге или к другим источникам.


Использование встроенных объектов файлов

Несмотря на различия между текстовыми и двоичными данными в Python 3.X, файлы по-прежнему очень просты в использовании. Для большинства задач обработки файлов в сценариях достаточно знать функцию open. Объект файла, возвращаемый функцией open, обладает методами для чтения данных (read, readline, readlines), записи данных (write, writelines), освобождения системных ресурсов (close), перемещения по файлу (seek), принудительного выталкивания выходных буферов на диск (flush), получения соответствующего дескриптора файла (fileno) и других. Но так как встроенный объект файла очень прост в использовании, давайте сразу рассмотрим несколько интерактивных примеров.

Вывод в файлы

Чтобы создать новый файл, следует вызвать функцию open с двумя аргументами: внешним именем создаваемого файла и строкой режима "w" (от write - запись). Чтобы сохранить данные в файле, нужно вызвать метод write объекта файла со строкой, содержащей данные, которые нужно сохранить, а затем метод close, чтобы закрыть файл. Метод write вернет количество символов или байтов, записанных в файл (о котором мы не всегда будем упоминать для экономии места в книге). Вызов метода close, как мы увидим далее, не является обязательным, если вам требуется открыть и прочитать файл повторно в той же программе или сеансе:

C:\temp> python

>>> file = open('data.txt', 'w')    # откроет файл для вывода: создаст объект

>>> file.write('Heno file world!\n') # запишет строку, как есть

18

>>> file.write('Bye file world.\n') # вернет число символов/байтов

18

>>> file.close()    # закрытие "сборщиком мусора" и выход

Вот и все - вы только что создали на своем компьютере, неважно каком, совершенно новый файл:

C:\temp> dir data.txt /B

data.txt

C:\temp> type data.txt

Hello file world!

Bye file world.

В новом файле нет ничего необычного. Здесь для показа имени файла и отображения его содержимого использованы команды DOS dir и type, но этот файл также будет виден в менеджере файлов с графическим интерфейсом.

Открытие файлов. В вызове функции open, показанном в предыдущем примере, первый аргумент может содержать необязательный полный путь к файлу. Если просто передать имя файла без указания пути, файл окажется в текущем рабочем каталоге Python. To есть он появится в том месте, откуда был запущен программный код, - в данном случае простое имя файла data.txt предполагает использование каталога C:\temp на моем компьютере, поэтому в реальности будет создан файл C:\temp\data.txt. Если быть более точным, в случае отсутствия абсолютного пути в имени файла путь к нему определяется относительно текущего рабочего каталога. Освежить эту тему в памяти можно с помощью раздела «Текущий рабочий каталог» (глава 3).

Обратите также внимание, что при открытии в режиме w Python либо создает новый файл, если он еще не существует, либо стирает текущее содержимое файла, если он уже присутствует (поэтому следует проявлять осторожность - при открытии в этом режиме вы потеряете все, что находилось в файле).

Запись. Обратите внимание, что в строки, записываемые в файл, был явно добавлен символ конца строки \n. В отличие от функции print, метод write объекта файла записывает в точности то, что ему передано, без дополнительного форматирования. Строка, переданная методу write, появляется во внешнем файле «символ в символ». При записи в текстовые файлы может выполняться преобразование символов конца строки или операция кодирования Юникода, о которых упоминалось выше, а когда позднее данные будут читаться из файла, автоматически будут выполнены обратные преобразования.

Для записи в файлы можно также использовать метод writelines, который просто записывает все строки из списка без дополнительного форматирования. Например, ниже приводится вызов writelines, эквивалентный двум вызовам write, показанным ранее:

file.writelines([‘Hello file world!\n’, ‘Bye file world.\n’])

Этот метод используется не очень часто (и может эмулироваться простым циклом for и другими инструментами итераций), но его удобно использовать в сценариях, которые сначала сохраняют выходные данные в списке, а потом записывают его в файл.

Закрытие. Использованный выше метод файла close завершает формирование содержимого файла и освобождает системные ресурсы. Например, закрытие файла влечет выталкивание на диск буферизованных выходных данных. Обычно файлы автоматически закрываются, когда объект файла уничтожается интерпретатором при сборке мусора (то есть когда в сценарии исчезнет последняя ссылка на объект). Под этим подразумеваются все файлы, оставшиеся открытыми к моменту завершения программы или сеанса Python. По этой причине вызов метода close часто является необязательным. На практике часто можно увидеть программный код, обрабатывающий файлы, который использует эту идиому:

open(‘somefile.txt’, ‘w’).write("G’day Bruce\n") # записать во временный файл open(‘somefile.txt’, ‘ r').read()    # прочитать временный файл

Так как в обоих выражениях создаются временные объекты файлов, сразу же осуществляется запись или чтение и при этом не сохраняются ссылки на них, объекты файлов немедленно автоматически закрываются и уничтожаются сразу после выполнения операции. В таких случаях нет никакой необходимости вызывать метод close явно.

Однако в некоторых контекстах вам может потребоваться явно закрывать файлы:

• Во-первых, реализация Jython опирается на механизм сборки мусора в интерпретаторе Java, поэтому вы не всегда можете знать, когда файлы будут закрыты, как вы это знаете при работе со стандартным Python. Если вы собираетесь запускать свой программный код на языке Python под управлением Jython, вам может потребоваться закрывать файлы вручную, если программа создает большое количество объектов файлов за короткое время (например, в цикле), чтобы избежать исчерпания файловых ресурсов в операционных системах, где их количество ограничено.

•    Во-вторых, некоторые среды разработки, такие как стандартный графический интерфейс IDLE, могут удерживать объекты файлов дольше, чем хотелось бы (например, в объектах с трассировочной информацией о предыдущих ошибках), и тем самым препятствовать немедленной их утилизации сборщиком мусора. Выполняя запись в выходной файл в среде IDLE, обязательно закрывайте его явно (или вызывайте метод flush), если вам необходимо обеспечить достоверное чтение информации из этого файла в течение того же сеанса IDLE. В противном случае может получиться, что выходные буферы не будут вытеснены на диск, и при чтении вы получите неполные данные.

•    И хотя это и кажется маловероятным, тем не менее такая особенность, как автоматическое закрытие файлов, в будущем может измениться. Технически это особенность реализации объектов файлов, которая с течением времени может перестать рассматриваться как часть определения языка.

По этим причинам закрытие файлов вручную - хороший навык для нетривиальных программ, даже если с технической точки зрения это не требуется. Закрытие файлов вообще не только не нанесет вреда, но и считается полезной практикой, которой стоит следовать.

Гарантированное закрытие файлов: обработчики исключений и менеджеры контекста

Операция закрытия файла вручную оформляется в программном коде очень просто, но как гарантировать закрытие файла в случае исключения, которое может отбросить поток выполнения программы далеко от того места, где находится вызов метода close? Прежде всего, не забывайте, что файлы закрываются сами, когда соответствующие им объекты файлов утилизируются сборщиком мусора, причем это произойдет в любом случае, даже если возникнет исключение.

Однако если необходимо обеспечить явное закрытие файла в любом случае, у вас есть два пути: наиболее типичный - использование инструкции try с предложением finally, потому что оно позволяет реализовать выполнение заключительных операций для любых типов исключений:

myfile = open(filename, ‘w’) try:

...обработка myfile... finally:

myfile.close()

В последних версиях Python появилась инструкция with, обеспечивающая более краткий способ реализации заключительных операций для объектов определенных типов, включая закрытие файлов:

with open(filename, ‘w’) as myfile:

... обработка myfile, закрывается автоматически после выхода...

Данная инструкция опирается на использование менеджера контекста объекта файла: программного кода, который автоматически вызывается при входе и при выходе из инструкции, независимо от того, возникло ли исключение. Поскольку реализация выхода в объекте файла автоматически закрывает файл, этот прием гарантирует закрытие файла в любом случае, независимо от возникновения исключения.

Решение на основе инструкции with выглядит заметно короче (на 3 строки), чем альтернативное решение на основе конструкции try/finally, но оно является менее универсальным - инструкция with может применяться только к объектам, поддерживающим протокол менеджеров контекста, тогда как конструкция try/finally позволяет реализовать произвольные заключительные операции для произвольных контекстов исключений. Область применения инструкции with ограничена, несмотря на то, что у некоторых типов объектов также имеются менеджеры контекста (например, у блокировок потоков). Если вам хочется помнить только один вариант реализации заключительных операций, то конструкция try/finally выглядит наиболее объемлющей. При этом инструкция with позволяет уменьшить объем программного кода для файлов, которые должны быть закрыты в любом случае, и прекрасно справляется с этой конкретной задачей. Она позволяет сэкономить строку программного кода, когда обработка исключений не предусматривается (хотя и за счет добавления в логику обработки файла еще одного уровня вложенности и отступов):

myfile = open(filename, ‘w’)    # традиционная форма

...обработка myfile...

myfile.close()

with open(filename) as myfile: # с применением менеджера контекста ... обработка myfile...

В версии Python 3.1 и выше эта инструкция позволяет указывать несколько (то есть вложенные) менеджеров контекста - в инструкции можно перечислить через запятую любое количество менеджеров контекста, и она будет действовать, как набор вложенных друг в друга инструкций with. В общем случае в версии 3.1 и выше следующий программный код:

with A() as a, B() as b:

...инструкции...

действует так же, как программный код ниже, который можно использовать в версиях 3.1, 3.0 и 2.6:

with A() as a: with B() as b:

...инструкции...

Например, когда программа выходит из следующего блока инструкции with, автоматически выполняются действия по закрытию обоих файлов, независимо от того, возникло исключение или нет:

with open(‘data’) as fin, open(‘results’, ‘w’) as fout: for line in fin:

fout.write(transform(line))

В последние годы такой программный код, опирающийся на использование менеджеров контекста, становится все более привычным, причем отчасти благодаря приходу новых программистов из языков, требующих вручную закрывать файлы в любых случаях. В большинстве ситуаций нет никакой необходимости обертывать инструкциями with программный код обработки файлов - часто бывает вполне достаточно того, что объекты файлов автоматически закрываются при утилизации, а для других ситуаций достаточно вручную вызывать метод close. Приемы, основанные на использовании инструкций with и try, описанные выше, следует использовать только в случае необходимости явно закрывать файлы и только, когда существует вероятность исключений. Поскольку стандартная реализация C Python автоматически закрывает файлы при утилизации объектов, во многих (если не в большинстве) ситуациях ни один из приведенных вариантов не является необходимым.

Чтение из файлов

Чтение данных из внешних файлов осуществляется столь же просто, как запись, но при этом доступно большее количество методов, позволяющих загружать данные в разнообразных режимах. Входные текстовые файлы открываются с флагом режима "r" (от «read» - читать) либо вообще без флага режима ("r" - значение по умолчанию, и параметр часто пропускается). После открытия текстового файла его строки можно читать с помощью метода readlines:

C:\temp> python

>>> file = open('data.txt') # открыть входной файл: ‘r’ - по умолчанию

>>> lines = file.readlines() # прочитать в список строк

>>> for line in lines:    # НО! использовать итератор файла!

... print(line, end='')    # строки оканчиваются символом ‘\n’

Hello file world!

Bye file world.

Метод readlines загружает содержимое файла в память целиком и передает его сценарию в виде списка строк, который можно обойти в цикле. В действительности существует много способов чтения входного файла:

file.read()

Возвращает строку, содержащую все символы (или байты), хранящиеся в файле.

file.read(N)

Возвращает строку, содержащую очередные N символов (или байтов) из файла.

file.readline()

Читает содержимое файла до ближайшего символа \n и возвращает строку.

file.readlines()

Читает файл целиком и возвращает список строк.

Попробуем воспользоваться этими методами для чтения файлов, строк и символов из файлов - вызов метода seek(0) перед каждой попыткой чтения переустанавливает текущую позицию чтения в начало файла (подробнее об этом методе рассказывается чуть ниже):

>>> file.seek(0)    # перейти в начало файла

>>> file.read()    # прочитать в строку файл целиком

‘Hello file world!\nBye file world.\n’

>>> file.seek(0)

>>> file.readlines()    # прочитать файл целиком в список строк

[‘Hello file world!\n’, ‘Bye file world.\n’]

>>> file.seek(0)

>>> file.readline()    # читать по одной строке

‘Hello file world!\n’

>>> file.readline()

‘Bye file world.\n’

>>> file.readline()    # конец файла - возвращается пустая строка

>>> file.seek(0)    # прочитать N (или оставшиеся) символы/байты

>>> file.read(1), file.read(8) # конец файла - возвращается пустая строка (‘H’, ‘ello fil’)

Все эти методы ввода позволяют определить, сколько данных должно быть получено. Ниже приводится несколько практических правил выбора метода:

• read() и readlines() загружают в память сразу весь файл. Это удобно, когда желательно получить содержимое файла, написав более короткий программный код. Кроме того, эти методы действуют очень быстро, но для больших файлов их применение накладно: загрузка гигабайтных файлов - обычно не самое лучшее решение (а кроме того, на некоторых компьютерах - просто невозможное).

•    С другой стороны, вызовы readline() и read(N) возвращают лишь часть файла (очередную строку или блок из N символов или байтов), поэтому они надежнее для потенциально больших файлов, но не так удобны и обычно работают медленнее. Оба метода возвращают пустую строку по достижении конца файла. Если скорость для вас важна, а ваши файлы не слишком велики, методы read и readlines могут оказаться лучшим выбором.

•    Кроме того, смотрите обсуждение итераторов файлов в следующем разделе. Как мы увидим, итераторы объединяют в себе удобство метода readlines() и экономное отношение к памяти метода readline(), и на сегодняшний день являются наиболее предпочтительным способом построчного чтения текстовых файлов.

Часто встречающийся здесь вызов seek(0) означает «вернуться в начало файла». В нашем примере этот вызов является альтернативой повторному открытию файла перед очередной попыткой. Все операции чтения и записи в файлах происходят в текущей позиции. Обычно при открытии текущая позиция в файле устанавливается со смещением 0 и перемещается вперед по мере передачи данных. Метод seek просто позволяет переместиться в новую позицию для очередной операции передачи данных. Подробнее об этом методе будет рассказываться ниже, когда мы перейдем к исследованию возможности произвольного доступа к файлам.

Чтение строк с помощью итераторов файлов

В прежних версиях Python принятым способом построчного чтения информации из файла в цикле for было чтение файла в список, а затем обход этого списка в цикле:

>>> file = open('data.txt')

>>> for line in file.readlines(): # НЕ ДЕЛАЙТЕ ТАК БОЛЬШЕ!

... print(line, end='')

Если вы уже изучили основы языка с помощью других книг, таких как «Изучаем Python», возможно, вы знаете, что того же результата можно добиться с меньшими усилиями - и для вас, и для вашего компьютера. В последних версиях Python объект файла включает итератор, который при каждом обращении извлекает только одну строку из файла в любых итерационных контекстах, включая циклы for и генераторы списков. Практическая выгода заключается в том, что теперь нет необходимости вызывать метод readlines в цикле for, чтобы построчно просканировать содержимое файла, - итератор читает строки автоматически:

>>> file = open('data.txt')

>>> for line in file:    # нет необходимости вызывать readlines

...    print(line, end='')    # итератор каждый раз читает следующую строку

Hello file world!

Bye file world.

Более того - теперь файл можно открывать непосредственно в инструкции цикла, как временный, который будет автоматически закрыт сборщиком мусора после выхода из цикла (так как часто цикл - это единственная ссылка на объект файла):

>>> for line in open('data.txt'): # еще короче: временный объект файла

... print(line, end='')    # будет закрыт при утилизации автоматически

Hello file world!

Bye file world.

Кроме того, такая форма обхода строк в файле не вызывает загрузку всего содержимого файла в список строк, поэтому она более экономно расходует память при работе с большими текстовыми файлами. По этой причине данный способ построчного чтения файлов является наиболее предпочтительным на сегодняшний день. Если вам интересно узнать, что же в действительности происходит внутри цикла for, вы можете попробовать использовать итератор вручную. Итератор - это всего лишь метод__next__(вызываемый встроенной функцией next), который сво

им поведением напоминает метод readline, за исключением того, что по достижении конца файла методы чтения возвращают пустую строку, а итератор возбуждает исключение, чтобы прервать итерации:

>>> file = open('data.txt') # методы чтения: пустая строка в конце файла >>> file.readline()

‘Hello file world!\n’

>>> file.readline()

‘Bye file world.\n’

>>> file.readline()

>>> file = open('data.txt')    # итераторы: исключение в конце файла

>>> file. next ()    # не нужно предварительно вызывать iter(file),

‘Hello file world!\n’    # потому что файлы имеют собственные итераторы

>>> file.__next__()

‘Bye file world.\n’

>>> file.__next__()

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

StopIteration

Интересно отметить, что итераторы автоматически используются во всех итерационных контекстах, включая конструктор списка, генераторы списков, функцию map и оператор in проверки на вхождение:

>>> open('data.txt').readlines()    # всегда читает строки

[‘Hello file world!\n’, ‘Bye file world.\n’]

>>> list(open('data.txt'))    # выполняет обход строк

[‘Hello file world!\n’, ‘Bye file world.\n’]

>>> lines = [line.rstrip() for line in open('data.txt')] # генераторы >>> lines

[‘Hello file world!’, ‘Bye file world.’]

>>> lines = [line.upper() for line in open('data.txt')] # произв. действия >>> lines

[‘HELLO FILE WORLD!\n’, ‘BYE FILE WORLD.\n’]

>>> list(map(str.split, open('data.txt')))    # применение функции

[[‘Hello’, ‘file’, ‘world!’], [‘Bye’, ‘file’, ‘world.’]]

>>> line = 'Hello file world!\n'

>>> line in open('data.txt')    # проверка на вхождение

True

На первый взгляд итераторы могут показаться не слишком представительными, но они предоставляют множество способов, упрощающих жизнь разработчиков программ на языке Python.

Другие режимы открытия файлов

Помимо режимов открытия файлов "w" и "r" (по умолчанию) большинством платформ поддерживается строка режима открытия "а", означающая «append» (дополнение). В этом режиме вывода методы записи добавляют данные в конец файла, и вызов функции open не уничтожает текущее содержимое файла:

>>> file = open('data.txt', 'a')    # для дополнения: содержимое не стирается

>>> file.write('The Life of Brian’) # добавит в конец существующих данных >>> file.close()

>>>

>>> open('data.txt').read()    # открыть и прочитать весь файл

‘Hello file world!\nBye file world.\nThe Life of Brian’

Хотя в большинстве случаев для открытия файлов применяются уже рассмотренные нами формы вызовов, но кроме того, функция open может принимать дополнительные аргументы, позволяющие более точно определить потребности обработки файла. Чаще всего в практике используются первые три аргумента - имя файла, режим открытия и размер буфера. Все они, кроме первого, являются необязательными: если они опущены, принимается режим открытия по умолчанию "r" (ввод) и разрешается полная буферизация. Ниже приводятся некоторые сведения об этих трех аргументах функции open, которые вам следует знать:

Имя файла

Как уже говорилось, имена файлов могут включать путь к каталогу, что дает возможность ссылаться на файлы, находящиеся на компьютере в произвольном месте; если полный путь в имени файла отсутствует, считается, что путь к файлам указывается относительно текущего рабочего каталога (который описывался в предыдущей главе). В целом, любой формат имени файла, который можно ввести в системной оболочке, можно использовать и в вызове функции open. Например, аргумент имени файла r\.\temp\spam.txt’ в Windows соответствует файлу spam.txt в подкаталоге temp, находящемся в родительском каталоге текущего рабочего каталога, - на один шаг вверх и затем вниз в каталог temp.

Режим открытия

Функция open может принимать и другие режимы, часть из которых мы увидим далее в этой главе, "r+", "w+" и "a+", которые используются, чтобы открыть файл для чтения и записи, и "b" - для обозначения двоичного режима. В частности, режим "r+" означает, что файл доступен как для чтения, так и для записи, при этом содержимое существующих файлов сохраняется; "w+", позволяет выполнять операции чтения и записи, но создает файл заново, уничтожая прежнее его содержимое; режимы "rb" и "wb" разрешают читать и записывать данные в двоичном режиме без выполнения автоматических преобразований; наконец, режимы "wb+" и "r+b" объединяют возможность чтения и записи с двоичным режимом. Проще говоря, по умолчанию используется режим для чтения "r", но вы можете использовать режим "w" для записи и "a" для дополнения, можете добавлять символ +, чтобы обеспечить возможность изменения содержимого файла, а также указывать b и t, чтобы задать двоичный или текстовый режим. Порядок следования спецификаторов в строке режима не имеет значения.

Как будет показано ниже в этой главе, режимы со спецификатором + часто используются совместно с методом seek, обеспечивающим возможность произвольного доступа к файлам. Независимо от режима содержимым файлов в программах Python всегда являются строки - методы чтения возвращают строку, и строку мы передаем методам записи. Однако тип используемой строки зависит от выбранного режима: str - для текстового режима, и bytes или другие типы строк байтов - для двоичного режима.

Размер буфера

Функция open также принимает необязательный третий аргумент с размером буфера, позволяющий управлять буферизацией файла -способом размещения данных в очереди, позволяющим повысить производительность. Значение 0 в этом аргументе означает отсутствие буферизации (данные передаются немедленно, но это значение допустимо только для двоичных режимов), значение 1 означает построчную буферизацию, а любое другое положительное число означает использование режима полной буферизации (который используется по умолчанию, если третий аргумент отсутствует в вызове функции).

Как обычно, полные сведения о дополнительных аргументах функции

open, помимо этих трех, вы найдете в руководстве по библиотеке языка

Python и в справочных изданиях. Например, функция open может принимать дополнительные аргументы, управляющие поведением отображения конца файла и определяющие кодировку символов, автоматически применяемую к содержимому текстовых файлов. Обе эти концепции будут обсуждаться в следующем разделе, поэтому двинемся дальше.


Двоичные и текстовые файлы

Во всех предыдущих примерах обрабатываются простые текстовые файлы, но сценарии на языке Python могут также открывать и обрабатывать файлы, содержащие двоичные данные - изображения JPEG, аудиоклипы, упакованные двоичные данные, произведенные программами на языке FORTRAN и C, кодированный текст и все остальное, что может храниться в файлах в виде последовательностей байтов. Главное отличие для программного кода заключается в аргументе режима, передаваемом встроенной функции open:

>>> file = open('data.txt', 'wb') # откроет двоичный файл для записи >>> file = open('data.txt', 'rb') # откроет двоичный файл для чтения

После открытия двоичных файлов таким способом можно читать и записывать их содержимое с помощью представленных выше методов: read, write и так далее. Методы readline и readlines, как и построчные итераторы файлов, по-прежнему будут работать с текстовыми файлами, открытыми в двоичном режиме, но нет никакого смысла применять их к действительно двоичным данным, которые не имеют построчной организации (байты, обозначающие конец строки в текстовых данных, не имеют такого смысла в двоичных данных, да и вообще их может не быть в файле).

Во всех случаях данные, перемещаемые между файлами и программами, представляются в сценариях в виде строк Python, даже если они являются двоичными. Однако для файлов, открытых в двоичном режиме, содержимое файла будет представлено в виде строк байтов. Продолжим предыдущий пример:

>>> open('data.txt').read()    # текстовый режим: тип str

‘Hello file world!\nBye file world.\nThe Life of Brian’

>>> open('data.txt', 'rb').read() # двоичный режим: тип bytes b’Hello file world!\r\nBye file world.\r\nThe Life of Brian’

>>> file = open('data.txt', 'rb’)

>>> for line in file: print(line)

b’Hello file world!\r\n’ b’Bye file world.\r\n’ b’The Life of Brian’

Это обусловлено тем, что в Python 3.X содержимое текстовых файлов интерпретируется, как последовательность символов Юникода, которая автоматически декодируется при чтении и кодируется при записи.

Содержимое файлов, открытых в двоичном режиме, напротив, доступно в виде простых строк байтов, для которых никаких промежуточных преобразований не выполняется, - они содержат именно то, что хранится в файле. В Python 3.X строки типа str всегда содержат символы Юникода, поэтому для представления двоичных данных потребовалось ввести специальный строковый тип bytes, представляющий последовательность однобайтовых целых чисел, которые могут иметь любые 8-битовые значения. Обычные строки и строки байтов обладают практически идентичными наборами операций, поэтому различия между ними в большинстве случаев незаметны, но имейте в виду, что действительно двоичные файлы для чтения должны открываться в двоичном режиме, потому что они могут содержать данные, которые невозможно будет декодировать в текст Юникода.

Точно так же при выводе в двоичные файлы необходимо использовать строки байтов, потому что обычные строки интерпретируются не как двоичные данные, а как декодированные символы Юникода (то есть кодовые пункты), которые должны быть закодированы в двоичное представление при записи в файл в двоичном или текстовом режиме:

>>> open('data.bin', 'wb').write(b'Spam\n')

5

>>> open('data.bin', 'rb').read()

b’Spam\n’

>>> open('data.bin', 'wb').write('spam\n')

TypeError: must be bytes or buffer, not str

(TypeError: аргумент должен иметь тип bytes или buffer, но не str)

Но обратите внимание, что в данном примере строки завершаются символом \n вместо последовательности \r\n, принятой в Windows, которая присутствовала в предыдущем примере работы с текстовым файлом в двоичном режиме. Строго говоря, двоичный режим запрещает не только кодирование символов Юникода, но и автоматическое преобразование символов конца строки, которые по умолчанию выполняются для файлов, открытых в текстовом режиме. Но прежде чем нам удастся полностью с этим разобраться, необходимо познакомиться с двумя основными отличиями между текстовыми и двоичными файлами.

Кодирование символов Юникода в текстовых файлах

Как упоминалось выше, объекты файлов, открытые в текстовом режиме, при передаче данных между программой и внешним файлом всегда преобразуют данные в соответствии с кодировкой по умолчанию или указанной явно. При записи в файл выполняется кодирование данных, а при чтении - декодирование. Для файлов, открытых в двоичном режиме, никаких преобразований не выполняется, потому что именно это и требуется для действительно двоичных данных. Например, взгляните на следующую строку, содержащую символ Юникода, двоичное представление которого выходит за рамки 7-битового диапазона представления символов ASCII:

>>> data = 'sp\xe4m'

>>> data

‘spam’

>>> 0xe4, bin(0xe4), chr(0xe4)

(228, ‘0b11100100’, ‘a’)

Эту строку можно закодировать вручную в соответствии с той или иной кодировкой, и для разных кодировок будут получаться различные двоичные представления строки:

>>> data.encode('latin1') # 8-битовые символы: ascii + дополнительные b’sp\xe4m’

>>> data.encode('utf8')    # 2 байта отводится только

b’sp\xc3\xa4m’    # для специальных символов

>>> data.encode('ascii')    # кодирование в ascii невозможно

UnicodeEncodeError: ‘ascii’ codec can’t encode character ‘\xe4’ in position 2: ordinal not in range(128)

(UnicodeEncodeError: кодек ‘ascii’ не может преобразовать символ ‘\xe4’ в позиции 2: число выходит за пределы range(128) )

Интерпретатор Python отображает печатаемые символы в таких строках как обычно, а непечатаемые - в виде шестнадцатеричных экранированных значений \xNN, количество которых увеличивается при использовании некоторых более сложных схем кодирования (cp500 в следующем примере - это кодировка EBCDIC):

>>> data.encode('utf16')    # по 2 байта на символ плюс преамбула

b’\xff\xfes\x00p\x00\xe4\x00m\x00’

>>> data.encode('cp500')    # кодировка ebcdic: двоичное представление

b’\xa2\x97C\x94’    # строки существенно отличается

Результат кодирования здесь отражает двоичное представление строки, которое будет записано в файл при сохранении. Однако выполнять кодирование вручную обычно не требуется, потому что для текстовых файлов кодирование выполняется автоматически при передаче данных - данные декодируются при чтении и кодируются при записи, в соответствии с именем указанной кодировки (или с использованием кодировки, используемой на текущей платформе: смотрите описание функции sys.getdefaultencoding). Продолжим интерактивный сеанс:

>>> open('data.txt', 'w', encoding='latin1').write(data)

4

>>> open('data.txt', 'r', encoding='latin1').read()

‘spam’

>>> open('data.txt', 'rb').read()

b’sp\xe4m’

Если файл открыть в двоичном режиме, никаких преобразований производиться не будет - последняя инструкция в предыдущем примере предъявляет в точности то, что хранится в файле. Чтобы увидеть отличия при использовании других кодировок, сохраним эту строку еще раз:

>>> open('data.txt', 'w', encoding='utf8').write(data) # кодировка utf8 4

>>> open('data.txt',    'r',    encoding='utf8').read()    # декодирование: отменяет

‘spam’    #    кодирование

>>> open('data.txt',    'rb').read()    #    преобразование

b’sp\xc3\xa4m’    #    не производится

На этот раз двоичное содержимое файла получилось другим, но в результате автоматического декодирования, которое выполняется при чтении файла в текстовом режиме, возвращается та же самая строка. В действительности, кодировка имеет значение для строк, только когда они находятся в файлах, - сразу после загрузки в память строки превращаются в простые последовательности символов Юникода («кодовые пункты»). Этот этап преобразования желателен для текстовых файлов, но не для двоичных. При использовании двоичных режимов этап преобразования содержимого пропускается, поэтому при работе с истинно двоичными данными необходимо использовать эти режимы. Если вам нужны доказательства, попробуйте сами: попытка записать или прочитать недекодируемые данные в текстовом режиме приведет к появлению ошибки:

>>> open('data.txt', 'w’, encoding='ascii').write(data)

UnicodeEncodeError: ‘ascii’ codec can’t encode character ‘\xe4’ in position 2: ordinal not in range(128)

(UnicodeEncodeError: кодек ‘ascii’ не может преобразовать символ '\xe4' в позиции 2: число выходит за пределы range(128) )

>>> open(r'C:\Python31\python.exe', 'r').read()

UnicodeDecodeError: ‘charmap’ codec can’t decode byte 0x90 in position 2: character maps to <undefined>

(UnicodeDecodeError: кодек ‘charmap’ не может преобразовать байт 0x90 в позиции 2: символ отображается в символ <undefined> )

Двоичный режим можно также рассматривать, как последний шанс прочитать текстовый файл, если он не может быть декодирован с использованием кодировки по умолчанию, а кодировка файла неизвестна. Следующий программный код воссоздает оригинальные строки, когда кодировка известна, но терпит неудачу, когда она неизвестна, если только не использовать двоичный режим (такие ошибки могут возникать как при чтении данных, так и при записи, но в любом случае программный код терпит неудачу):

>>> open('data.txt', 'w’, encoding='cp500').writelines(['spam\n', 'ham\n'])

>>> open('data.txt', 'r’, encoding='cp500').readlines() [‘spam\n’, ‘ham\n’]

>>> open('data.txt', 'r').readlines()

UnicodeDecodeError: ‘charmap’ codec can’t decode byte 0x81 in position 2: character maps to <undefined>

(UnicodeDecodeError: кодек ‘charmap’ не может преобразовать байт 0x81 в позиции 2: символ отображается в символ <undefined> )

>>> open('data.txt', 'rb').readlines()

[b’\xa2\x97\x81\x94\r%\x88\x81\x94\r%’]

>>> open('data.txt', 'rb').read()

b’\xa2\x97\x81\x94\r%\x88\x81\x94\r%’

Если вы имеете дело только с текстом ASCII, вы можете пропустить все, что связано с кодировками, - данные в файлах будут один-в-один отображаться в символы в строках, потому что ASCII является подмножеством большинства кодировок, используемых по умолчанию. Если вам приходится обрабатывать файлы, созданные с применением других кодировок, и, возможно, на других платформах (например, файлы, полученные из Интернета), вам может потребоваться использовать двоичный режим, если кодировка заранее не известна. Однако имейте в виду, что текст в кодированном двоичном представлении не может обрабатываться так, как вам хотелось бы: текст, закодированный с применением определенной кодировки, не может сравниваться или объединяться с текстом, закодированным с применением других кодировок.

И снова за дополнительной информацией о Юникоде обращайтесь к другим ресурсам. Мы еще не раз будем возвращаться к теме Юникода в этой книге: в главе 9 будет показано, какое влияние оказывает Юникод на виджет Text из библиотеки tkinter, а в части IV, охватывающей вопросы программирования для Интернета, мы узнаем, как это отражается на данных, доставляемых по сети с использованием протоколов FTP, электронной почты и в Интернете в целом. Текстовые файлы обладают еще одной особенностью, отсутствующей у двоичных файлов: преобразование символов конца строки, что является темой следующего раздела.

Преобразование символов конца строки в Windows

По историческим причинам конец строки текста в файле представляется на разных платформах различными символами. В Unix и Linux -это одиночный символ \n, а в Windows - это последовательность из двух символов \r\n. В результате файлы, перемещаемые между Linux и Windows, могут после передачи странно выглядеть в текстовом редакторе - они могут сохранить окончание строки, принятое на исходной платформе.

Например, большинство текстовых редакторов для Windows обрабатывает текст в формате Unix, но Блокнот (Notepad) составляет заметное исключение - текстовые файлы, скопированные из Unix или Linux, обычно выглядят в Блокноте, как одна большая строка со странными символами внутри (\n). Точно так же при копировании файлов из Windows в Unix в двоичном режиме в них сохраняется символ \r (который в текстовых редакторах часто отображается как ^M).

Сценариям на языке Python это обычно безразлично, потому что объекты файлов автоматически отображают последовательность DOS \r\n в одиночный символ \n. При выполнении сценариев в Windows это действует так:

•    Для файлов, открытых в текстовом режиме, при чтении \r\n преобразуется в \n.

•    Для файлов, открытых в текстовом режиме, при записи \n преобразуется в \r\n.

•    Для файлов, открытых в двоичном режиме, никакие преобразования не производятся.

В Unix-подобных системах преобразование не производится в любом режиме, потому что в файлах используется символ \n. Следует запомнить два важных следствия из этих правил. Во-первых, почти всегда во всех сценариях на языке Python символ конца строки представляется одиночным \n, независимо от способа его сохранения во внешних файлах на соответствующей платформе. Путем соответствующего преобразования \n при чтении и записи Python скрывает присущие платформам различия.

Второе следствие из этого преобразования более тонкое: при обработке двоичных файлов использование двоичного режима (например, rb, wb) отключает механизм преобразования символов конца строки. Если выбрать неправильный режим, указанные преобразования вполне могут повредить данные, как при чтении, так и при записи, - случайно оказавшиеся среди двоичных данных байты \r могут быть ошибочно отброшены при чтении или ошибочно добавлены к байтам \n при записи. В итоге двоичные данные окажутся искаженными, что, вероятно, совсем не то, что вам хотелось бы получить при работе с изображениями или аудиоклипами!

В Python 3.X эта проблема ушла на задний план, потому что мы в принципе не можем использовать двоичные данные с файлами, открытыми в текстовом режиме, из-за того, что текстовый режим предполагает автоматическое применение кодировок Юникода к содержимому файлов. Операции чтения и записи просто будут терпеть неудачу, если данные не смогут быть декодированы при чтении или закодированы при записи. Использование двоичного режима позволяет избежать ошибок, связанных с преобразованием Юникода, и автоматически запрещает преобразование символов конца строки как таковое (ошибки, связанные с преобразованием Юникода, можно было бы перехватывать в инструкции try). Итак, стоит запомнить как отдельный факт, что двоичный ре-

жим предохраняет двоичные данные от искажения в результате преобразования символов конца строки, особенно если вы работаете только с текстовыми данными ASCII, когда можно смело забыть обо всех проблемах, связанных с Юникодом.

Ниже демонстрируется действие механизма преобразования символов конца строки в Python 3.1 в Windows - объект файла, открытого в текстовом режиме, выполняет преобразование символов конца строки и обеспечивает переносимость наших сценариев:

>>> open('temp.txt', 'w').write('shrubbery\n') # запись в текстовом режиме:

10    # \n -> \r\n

>>> open('temp.txt', 'rb').read()    # чтение двоичных данных:

b’shrubbery\r\n’    # фактические байты из файла

>>> open('temp.txt', 'r').read()    # проверка чтением: \r\n -> \n

‘shrubbery\n’

При записи в двоичном режиме, напротив, предотвращаются любые преобразования, как и предполагалось, даже когда данные содержат байты, которые в текстовом режиме интерпретировались бы как часть символов конца строки (при выводе строк байтов отдельные байты выводятся как символы ASCII, если они соответствуют печатаемым символам, и как экранированные шестнадцатеричные последовательности в противном случае):

>>> data = b'a\0b\rc\r\nd'    # 4 байта, 4 обычных символа

>>> len(data)

8

>>> open('temp.bin', 'wb').write(data) # запись двоичных данных как есть 8

>>> open('temp.bin', 'rb').read() # чтение двоичных данных: b’a\x00b\rc\r\nd’    # без преобразования

Но при чтении двоичных данных в текстовом режиме, неважно, случайно или нет, механизм преобразования символов конца строки может повредить данные (предполагая, что вообще при декодировании не возникло ошибок, как с нашими ASCII-данными на платформе Windows):

>>> open('temp.bin', 'r').read() # чтение в текстовом режиме: искажены \r! ‘a\x00b\nc\nd’

Тот же эффект может возникнуть при записи двоичных данных в текстовом режиме - могут изменяться или вставляться байты, значения которых совпадают с символами конца строки (и снова имеются в виду данные, успешно прошедшие этап кодирования в кодировку по умолчанию для текущей платформы):

>>> open('temp.bin', 'w').write(data) # в текстовом режиме должна TypeError: must be str, not bytes # передаваться строка типа str

#    используйте bytes.decode()

#    для преобразования типа

>>> data.decode()

‘a\x00b\rc\r\nd’

>>> open('temp.bin', 'w').write(data.decode())

8

>>> open('temp.bin', 'rb').read() # запись в текстовом режиме: добавит \r b’a\x00b\rc\r\r\nd’

>>> open('temp.bin', 'r').read()    # опять искажение, изменит \r

‘a\x00b\nc\n\nd’

Проще говоря, запомните, что во всех текстовых файлах при определении конца строки следует ориентироваться на символ \n, а двоичные файлы всегда должны открываться в двоичном режиме, чтобы предотвратить преобразование символов конца строки и кодирование/ декодирование символов Юникода. Вообще тип содержимого файла определяется режимом его открытия, а режимы открытия определяют способы обработки содержимого, что в точности соответствует нашим желаниям.

Однако следует понимать, что в особых случаях может потребоваться открывать текстовые файлы в двоичном режиме. Так, в примерах из главы 6 мы иногда будем использовать двоичный режим для текстовых файлов, чтобы избежать возможных ошибок декодирования в Юникод при работе с файлами, созданными на разных платформах, где могут использоваться различные кодировки. Данный прием позволяет избежать ошибок кодирования, но при этом некоторые операции с текстом могут выполняться не так, как предполагается, - поиск в таком двоичном тексте не всегда может давать точные результаты, потому что искомый ключ также придется преобразовать в строку байтов в соответствии с некоторой кодировкой, возможно, несовместимой с кодировкой текста в файле.

В примере текстового редактора PyEdit, в главе 11, нам также потребуется перехватывать исключения, вызванные ошибками преобразования Юникода в утилите поиска «grep» по файлам в каталоге, и мы пойдем еще дальше, позволив пользователю определять кодировку символов содержимого файлов для целого дерева каталогов. Кроме того, когда необходимо явно выполнить преобразование символов конца строки в соответствии с соглашениями для двух разных платформ, может потребоваться прочитать текст в двоичном режиме, чтобы сохранить оригинальное представление концов строк, - при открытии в текстовом режиме они могут оказаться преобразованными в \n к моменту, когда данные попадут в сценарий.

Имеется также возможность запретить преобразование символов конца строки в текстовом режиме с помощью дополнительных аргументов функции open, которые мы не будем рассматривать здесь. Подробности ищите в описании аргумента newline в справочной документации по функции open, но, в двух словах: если в этом аргументе передать пустую строку, это предотвратит преобразование символов конца строки и сохранит остальные особенности поведения текстового режима. Теперь обратимся к следующим двум типичным случаям использования двоичных файлов: работа с упакованными двоичными данными и произвольный доступ.

Работа с упакованными двоичными данными с помощью модуля struct

Используя символ b в аргументе режима функции open, вы получаете возможность открывать двоичные файлы с данными платформонезависимым способом, а также читать и записывать их содержимое с помощью обычных методов объекта файла. Но как обрабатывать двоичные данные после того, как они будут прочитаны? Эти данные будут возвращены сценарию в виде простой строки байтов, большая часть из которых наверняка будет соответствовать непечатаемым символам.

Если нужно лишь переписать двоичные данные в другой файл или передать другой программе, тогда все просто - достаточно записать строку байтов в другой файл, открытый в двоичном режиме. Если потребуется извлечь некоторое количество байтов из определенной позиции, можно воспользоваться операцией извлечения среза строки. При необходимости можно даже использовать битовые операции. Кроме того, имеется мощный инструмент, позволяющий получить двоичные данные в структурированном виде или сконструировать их, - модуль struct из стандартной библиотеки.

В модуле struct имеются функции для упаковывания и распаковывания двоичных данных, как если бы данные были созданы с помощью объявления struct языка C. Имеется возможность при упаковывании и распаковывании данных учитывать прямой или обратный порядок следования байтов (порядок следования байтов определяет, где будут находиться старшие значимые биты в двоичном представлении чисел, - слева или справа). Создание двоичного файла с данными, например, - достаточно простая задача: нужно упаковать значения языка Python в строку байтов и записать ее в файл. Строка формата в вызове pack ниже определяет: прямой порядок следования байтов (>), целое число, 4-символьную строку, короткое целое число и вещественное число:

>>> import struct

>>> data = struct.pack('>i4shf', 2, 'spam’, 3, 1.234)

>>> data

b’\x00\x00\x00\x02spam\x00\x03?\x9d\xf3\xb6’

>>> file = open('data.bin’, 'wb’)

>>> file.write(data)

14

>>> file.close()

Обратите внимание, что модуль struct возвращает строку байтов: сейчас мы находимся в царстве двоичных данных, а не текста, и для сохранения должны использовать двоичные файлы. Как обычно, интерпретатор отображает большую часть байтов с упакованными двоичными данными, которые не соответствуют печатаемым символам, в виде шестнадцатеричных экранированных последовательностей \xNN. Чтобы выполнить обратное преобразование этих данных, нужно прочитать их из файла и передать модулю struct с той же строкой формата, как и при создании, - в результате получится кортеж значений, полученных в результате анализа строки байтов и преобразованных в объекты языка Python:

>>> import struct

>>> file = open('data.bin’, 'rb’)

>>> data = file.read()

>>> values = struct.unpack('>i4shf’, data)

>>> values

(2, b’spam’, 3, 1.2339999675750732)

Анализируемые строки - это также строки байтов, и к ним допускается применять строковые и битовые операции для более глубокого анализа:

>>> bin(values[0] | 0b1)    # доступ к битам и байтам

‘0b11’

>>> values[1], list(values[1]), values[1][0]

(b’spam’, [115, 112, 97, 109], 115)

Обратите также внимание, что здесь может пригодиться операция извлечения среза. 4-символьную строку из середины только что прочитанных упакованных двоичных данных легко получить, используя операцию извлечения среза. Числовые значения также можно извлекать подобным способом и передавать функции struct.unpack для преобразования:

>>> data

b’\x00\x00\x00\x02spam\x00\x03?\x9d\xf3\xb6’

>>> data[4:8]

b’spam’

>>> number = data[8:10]

>>> number

b’\x00\x03’

>>> struct.unpack('>h’, number)

(3,)

Упакованные двоичные данные бывают получены из самых разных контекстов, включая некоторые виды сетевых взаимодействий и представление данных другими языками программирования. Однако все это не относится к разряду повседневных задач программирования, поэтому оставим описание подробностей за разделом с описанием модуля struct в руководстве по стандартной библиотеке Python.

Произвольный доступ к данным в файлах

При работе с двоичными файлами часто также применяется операция произвольного доступа. Ранее упоминалось, что добавление символа + в строку режима открытия файла позволяет выполнять обе операции, чтения и записи. Этот режим обычно используется вместе с методом seek объектов файлов, позволяющим выполнять чтение/запись произвольных участков файла. Такие гибкие режимы обработки файлов позволяют читать байты из одного места, записывать в другое и так далее. При объединении этих режимов с двоичным режимом появляется возможность извлекать и изменять произвольные байты в файле.

Выше для перехода в начало файла вместо операций закрытия файла и повторного его открытия использовался метод seek. Как уже упоминалось, операции чтения и записи всегда выполняются в текущей позиции в файле. При открытии файлов текущая позиция обычно устанавливается в смещение 0 от начала файла и перемещается вперед по мере чтения/записи данных. Метод seek позволяет переместить текущую позицию для следующей операции чтения/записи в другое место, для чего ему достаточно передать величину смещения в байтах.

Метод seek в языке Python принимает также второй необязательный аргумент, который определяет физический смысл первого аргумента и может принимать одно из трех значений: 0 - абсолютная позиция в файле (по умолчанию), 1 - смещение относительно текущей позиции и 2 - смещение относительно конца файла. Когда методу seek передается только аргумент смещения 0, это соответствует операции перемотки файла в начало (rewind): текущая позиция перемещается в начало файла. Вообще, метод seek поддерживает произвольный доступ на уровне смещения в байтах. Используя в качестве множителя размер записи в двоичном файле, можно организовать доступ к записям по их относительным позициям.

Метод seek можно использовать и без спецификатора + в строке режима для функции open (например, чтобы просто обеспечить произвольное чтение данных), но наибольшая гибкость достигается при работе с файлами, открытыми для чтения и записи. Возможность произвольного доступа поддерживается и для файлов, открытых в текстовом режиме. Но выполняющиеся в текстовом режиме операции кодирования/деко-дирования Юникода и преобразования символов конца строки сильно осложняют вычисление абсолютных смещений в байтах и длин, необходимых методам позиционирования и чтения, - представление ваших данных может значительно измениться при сохранении в файл. Кроме того, применение текстового режима может также ухудшить переносимость данных между платформами, где по умолчанию используются различные кодировки, если только вы не предполагаете всегда явно указывать кодировку файлов. Метод seek лучше подходит для работы с двоичными файлами; исключение составляет простой некодируемый текст ASCII, в котором отсутствуют символы конца строки.

Для демонстрации создадим файл в режиме “w+b” (эквивалент режима ‘wb+’) и запишем в него некоторые данные - этот режим позволяет читать из файла и писать в него и создает новый пустой файл, если он существовал прежде (это относится ко всем режимам “w”). После записи данных мы вернемся в начало файла и прочитаем его содержимое (несколько целочисленных значений, возвращаемых вызовами методов в этом примере, было опущено ради экономии места):

>>> records = [bytes([char] * 8) for char in b’spam’]

>>> records

[b’ssssssss’, b’pppppppp’, b’aaaaaaaa’, b’mmmmmmmm’]

>>> file = open('random.bin’, 'w+b’)

>>> for rec in records:    # запиcать четыре записи

...    size = file.write(rec)    # bytes означает двоичный режим

>>> file.flush()

>>> pos = file.seek(0)    # прочитать файл целиком

>>> print(file.read())

b’ssssssssppppppppaaaaaaaammmmmmmm’

Теперь повторно откроем файл в режиме “r+b” - он также позволяет читать из файла и писать в него, но не очищает файл при открытии. На этот раз мы будем выполнять позиционирование и чтение с учетом размеров элементов данных («записей»), чтобы показать возможность получения и изменения записей в произвольном порядке:

c:\temp> python

>>> file = open('random.bin’, 'r+b’)

>>> print(file.read())    # прочитать файл целиком

b’ssssssssppppppppaaaaaaaammmmmmmm’

>>> record = b’X’ * 8

>>> file.seek(0)    # изменить первую запись

>>> file.write(record)

>>> file.seek(len(record) * 2)    # изменить третью запись

>>> file.write(b’Y’ * 8)

>>> file.seek(8)

>>> file.read(len(record))    # извлечь вторую запись

b’pppppppp’

>>> file.read(len(record))    # извлечь следующую (третью) запись

b’YYYYYYYY’

>>> file.seek(0)    # прочитать файл целиком

>>> file.read()

b’XXXXXXXXppppppppYYYYYYYYmmmmmmmm’

c:\temp> type random.bin    # посмотреть файл за пределами Python

XXXXXXXXppppppppYYYYYYYYmmmmmmmm

Наконец, имейте в виду, что метод seek можно использовать, даже если файл открыт только для чтения. Следующий пример демонстрирует возможность чтения произвольных записей фиксированной длины. Обратите внимание, что при этом используется текстовый режим “г”: поскольку данные представляют собой простой текст ASCII, где каждый символ представлен одним байтом, и текст не содержит символов конца строки, на данной платформе текстовый и двоичный режимы действуют одинаково:

c:\temp> python

>>> file = open('random.bin’, 'r’)    # текстовый режим можно использовать, если

#    не выполняется кодирование и отсутствуют

#    символы конца строки

>>> reclen = 8

>>> file.seek(reclen * 3)    # извлечь четвертую запись

>>> file.read(reclen)

‘mmmmmmmm’

>>> file.seek(reclen * 1)    # извлечь вторую запись

>>> file.read(reclen)

‘pppppppp’

>>> file = open('random.bin’, 'rb’) # в данном случае двоичный режим действует

#    точно так же

>>> file.seek(reclen * 2)    # извлечь третью запись

>>> file.read(reclen)    # вернет строку байтов

b’YYYYYYYY’

Но в общем случае текстовый режим не следует использовать, если вам требуется произвольный доступ к записям (за исключением файлов с простым некодируемым текстом, подобным ASCII, не содержащим символов конца строки). Символы конца строки могут преобразовываться в Windows, а применение кодировок Юникода может вносить различные искажения - оба эти преобразования существенно осложняют возможность позиционирования по абсолютному смещению. Например, в следующем фрагменте соответствие между строкой Python и ее кодированным представлением в файле нарушается сразу же за первым не-ASCII символом:

>>> data = 'sp\xe4m’    # данные в сценарии

>>> data, len(data)    # 4 символа Юникода,

(‘spam’, 4)    # 1 символ не-ASCII

>>> data.encode('utf8’), len(data.encode('utf8’)) # байты для записи в файл (b’sp\xc3\xa4m’, 5)

>>> f = open('test’, mode=’w+’, encoding=’utf8’) # текст. режим, кодирование >>> f.write(data)

>>> f.flush()

>>> f.seek(0); f.read(1)    # работает для байтов ascii

‘s’

>>> f.seek(2); f.read(1)    # 2-байтовый не-ASCII

‘ a’

>>> data[3]    # а в смещении 3 - не ‘m’ !

‘m’

>>> f.seek(3); f.read(1)

UnicodeDecodeError: ‘utf8’ codec can’t decode byte 0xa4 in position 0: unexpected code byte

(UnicodeDecodeError: кодекutf8’ не может преобразовать байт 0xa4 в позиции 0: неопознанный код)

Как видите, режимы открытия файлов в Python обеспечивают необходимую гибкость при работе с файлами в программах. А модуль os предлагает еще более широкие возможности для обработки файлов, которые представлены в следующем разделе.


Низкоуровневые инструменты в модуле os для работы с файлами

Модуль os содержит дополнительный набор функций для работы с файлами, отличных от инструментов, которыми располагают встроенные объекты файлов, демонстрировавшиеся в предыдущих примерах. Например, ниже приводится неполный список функций в модуле os, имеющих отношение к файлам:

os.open(path, flags, mode)

Открывает файл, возвращает его дескриптор

os.read(descriptor N)

Читает не более N байтов и возвращает строку байтов

os.write(descriptor, string)

Записывает в файл байты из строки байтов string os.lseek(descriptor, position, how)

Перемещается в позицию position в файле

С технической точки зрения, функции из модуля os обрабатывают файлы по их дескрипторам, которые представляют собой целочисленные коды или «описатели» (handles), идентифицирующие файлы в операционной системе. Файлы, представленные дескрипторами, интерпретируются как обычные двоичные файлы, к которым не применяются ни преобразование символов конца строки, ни кодирование текста, о которых рассказывалось в предыдущем разделе. Фактически, за исключением отдельных особенностей, таких как буферизация, операции с файлами, представленными дескрипторами, мало чем отличаются от операций, поддерживаемых объектами файлов для двоичного режима. При работе с такими файлами мы также читаем и пишем строки типа bytes, а не str. Однако так как инструменты для работы с файлами с использованием дескрипторов, представленные в модуле os, - более низкого уровня и более сложны в применении, чем встроенные объекты файлов, создаваемые с помощью встроенной функции open, то следует использовать последние во всех ситуациях, за исключением отдельных случаев специальной обработки файлов.11

Использование файлов, возвращаемых os.open

Чтобы дать вам общее представление об этом наборе инструментов, проведем несколько интерактивных экспериментов. Встроенные объекты файлов и файловые дескрипторы модуля os обрабатываются различными наборами инструментов, но в реальности они связаны между собой - объекты файлов просто добавляют дополнительную логику поверх дескрипторов файлов.

Метод fileno объекта файла возвращает целочисленный дескриптор, ассоциированный со встроенным объектом файла. Например, объекты файлов стандартных потоков ввода-вывода имеют дескрипторы 0, 1 и 2; вызов функции os.write для отправки данных в stdout по дескриптору дает тот же эффект, что и вызов метода sys.stdout.write:

>>> import sys

>>> for stream in (sys.stdin, sys.stdout, sys.stderr):

... print(stream.fileno())

0

1

2

>>> sys.stdout.write('Hello stdio world\n’) # записать с помощью метода Hello stdio world    # объекта файла

18

>>> import os

>>> os.write(1, b’Hello descriptor world\n’) # записать с помощью модуля os Hello descriptor world 23

Поскольку объекты файлов, открываемые явно, ведут себя точно так же, с одинаковым успехом для обработки конкретного внешнего файла на компьютере можно использовать встроенную функцию open, инструменты из модуля os или и то и другое вместе:

>>> file = open(r’C:\temp\spam.txt’, 'w’)    # создать внешний файл, объект

>>> file.write('Hello stdio file\n’)    # записать с помощью объекта файла

>>> file.flush()    # или сразу - функции os.write

>>> fd = file.fileno()    # получить дескриптор из объекта

>>> fd 3

>>> import os

>>> os.write(fd, b’Hello descriptor file\n’) # записать с помощью модуля os >>> file.close()

C:\temp> type spam.txt    # строки, записанные

Hello stdio file    # двумя способами

Hello descriptor file

Флаги режима os.open

Зачем же нужны дополнительные файловые средства в модуле os? Если вкратце, то они обеспечивают более низкоуровневое управление обработкой файлов. Встроенная функция open проста в использовании, но она ограничена возможностями файловой системы, которую использует, и добавляет некоторые дополнительные особенности, которые могут быть нежелательны. Модуль os позволяет сценариям быть более точными; например, следующий фрагмент открывает дескриптор файла в двоичном режиме для чтения-записи, выполняя битовую операцию «ИЛИ» над двумя флагами режима, экспортируемыми модулем os:

>>> fdfile = os.open(r’C:\temp\spam.txt’, (os.O_RDWR | os.O_BINARY))

>>> os.read(fdfile, 20)

b’Hello stdio file\r\nHe’

>>> os.lseek(fdfile, 0, 0)    # вернуться в начало файла

>>> os.read(fdfile, 100)    # в двоичном режиме сохраняются “\r\n”

b’Hello stdio file\r\nHello descriptor file\n’

>>> os.lseek(fdfile, 0, 0)

>>> os.write(fdfile, b’HELLO’) # перезаписать первые 5 байтов 5

C:\temp> type spam.txt

HELLO stdio file Hello descriptor file

В данном случае эквивалентный режим открытия с помощью встроенной функции open определяется строками “rb+” и “r+b”:

>>> file = open(r’C:\temp\spam.txt’, 'rb+’) # то же самое, но с помощью open >>> file.read(20)    # и объектов файлов

b’HELLO stdio file\r\nHe’

>>> file.seek(0)

>>> file.read(100)

b’HELLO stdio file\r\nHello descriptor file\n’

>>> file.seek(0)

>>> file.write(b’Jello’)

5

>>> file.seek(0)

>>> file.read()

b’Jello stdio file\r\nHello descriptor file\n’

В некоторых системах флаги для функции os.open позволяют указывать более сложные режимы - например, исключительный доступ (O_EXCL) и неблокирующий режим (O_NONBLOCK). Некоторые из этих флагов не переносимы между платформами (еще одна причина в пользу встроенных объектов файлов). Найти полный список других флагов открытия можно в руководстве по библиотеке или вызвав на своем компьютере функцию dir(os).

И последнее замечание: в Python использование функции os.open с флагом O_EXCL на сегодняшний день является наиболее переносимым способом исключить возможность параллельного изменения файла или обеспечить синхронизацию с другими процессами. Где может использоваться эта особенность, мы увидим в следующей главе, когда приступим к исследованию инструментов параллельной обработки данных. Программам, параллельно выполняющимся на сервере, к примеру, может потребоваться устанавливать блокировку на файлы, прежде чем изменять их, если подобные изменения могут одновременно запрашиваться несколькими потоками выполнения или процессами.

Обертывание дескрипторов объектами файлов

Ранее было показано, как перейти от использования объекта файла к использованию дескриптора с помощью метода объекта файла fileno, - получив дескриптор, мы можем использовать инструменты из модуля os для выполнения низкоуровневых операций с файлом. Но можно пойти и обратным путем - функция os.fdopen обертывает дескриптор файла объектом файла. Поскольку преобразования могут выполняться в обоих направлениях, мы можем выбирать любой набор инструментов -объект файла или модуль os:

>>> fdfile = os.open(r’C:\temp\spam.txt’, (os.O_RDWR | os.O_BINARY))

>>> fdfile

3

>>> objfile = os.fdopen(fdfile, 'rb’)

>>> objfile.read()

b’Jello stdio file\r\nHello descriptor file\n’

Фактически мы можем обернуть дескриптор файла любым объектом файла, открытым в текстовом или в двоичном режиме. В текстовом режиме операции чтения и записи будут производить кодирование/деко-дирование Юникода и преобразование символов конца строки, с которыми мы познакомились выше, и для работы с ними необходимо будет использовать строки типа str, а не bytes:

C:\...\PP4E\System> python >>> import os

>>> fdfile = os.open(r’C:\temp\spam.txt’, (os.O_RDWR | os.O_BINARY))

>>> objfile = os.fdopen(fdfile, 'r’)

>>> objfile.read()

‘Jello stdio file\nHello descriptor file\n’

Встроенная функция open в Python 3.X также может принимать дескриптор файла вместо строки с его именем. В этом режиме она действует практически так же, как функция os.fdopen, но обеспечивает более полный контроль. Например, можно использовать дополнительные аргументы, чтобы определить кодировку для текста и подавить операцию закрытия дескриптора, которая выполняется по умолчанию. Однако на практике функция os.fdopen в версии 3.X принимает те же дополнительные аргументы, потому что она была переопределена и теперь вызывает встроенную функцию open (смотрите файл os.py в стандартной библиотеке):

C:\...\PP4E\System> python >>> import os

>>> fdfile = os.open(r’C:\temp\spam.txt’, (os.O_RDWR | os.O_BINARY))

>>> fdfile

3

>>> objfile = open(fdfile, 'r’, encoding=’latin1’, closefd=False)

>>> objfile.read()

‘Jello stdio file\nHello descriptor file\n’

>>> objfile = os.fdopen(fdfile, 'r’, encoding=’latin1’, closefd=True)

>>> objfile.seek(0)

>>> objfile.read()

‘Jello stdio file\nHello descriptor file\n’

Далее в книге мы будем использовать этот прием обертывания в объекты файлов, чтобы упростить работу в текстовом режиме с каналами и другими объектами на основе дескрипторов (например, сокеты обладают методом makefile, позволяющим добиться похожего эффекта).

Другие инструменты для работы с файлами в модуле os

В модуле os имеется также ряд инструментов для работы с файлами, которые принимают строку пути к файлу и выполняют ряд операций, связанных с файлами, таких как переименование (os.rename), удаление (os.remove) и изменение владельца файла и прав доступа к нему (os. chown, os.chmod). Рассмотрим несколько примеров использования этих инструментов:

>>> os.chmod('spam.txt’, 0o777)    # разрешить доступ всем пользователям

Функции os.chmod установки прав доступа к файлу передается строка из девяти битов, состоящая из трех групп, по три бита в каждой. Эти три группы определяют права доступа, слева направо, для пользователя-владельца файла, для группы пользователей, которой принадлежит файл, и для всех остальных. Три бита внутри каждой группы отражают право на чтение, на запись и на выполнение. Если какой-то бит в этой строке равен «1», это означает разрешение на выполнение соответствующей операции. Например, восьмеричное число 0777 является строкой из девяти единичных битов в двоичном представлении и разрешает все три вида доступа для всех трех групп пользователей; восьмеричное число 0600 означает возможность только чтения и записи для пользователя, который владеет файлом (восьмеричное число 0600 в двоичной записи дает 110 000 000).

Эта схема ведет свое происхождение от системы прав доступа в Unix, но работает также в Windows. Если она вас озадачила, посмотрите описание команды chmod в документации по вашей системе (например, в страницах руководства Unix). Идем дальше:

>>> os.rename(r’C:\temp\spam.txt’, r’C:\temp\eggs.txt’) # откуда, куда

>>> os.remove(r’C:\temp\spam.txt’)    # удалить файл?

WindowsError: [Error 2] The system cannot find the file specified: ‘C:\\ temp\\...’

(WindowsError: [Error 2] Системе не удается найти указанный путь: C:\\ temp\\...)

>>> os.remove(r’C:\temp\eggs.txt’)

Использованная здесь функция os.rename изменяет имя файла; функция os.remove удаляет файл, она синонимична функции os.unlink (имя последней - имя, которое имеет эта функция в Unix, но оно не знакомо пользователям других платформ)12. Модуль os также экспортирует системный вызов stat:

>>> open('spam.txt’, 'w’).write('Hello stat world\n’) # +1 для символа \r 17

>>> import os

>>> info = os.stat(r’C:\temp\spam.txt’)

>>> info

nt.stat_result(st_mode=33206, st_ino=0, st_dev=0, st_nlink=0, st_uid=0, st_gid=0, st_size=18, st_atime=1267645806, st_mtime=1267646072, st_ ctime=1267645806)

>>> info.st_mode, info.st_size # через атрибуты именованного кортежа (33206, 18)

>>> import stat

>>> info[stat.ST_MODE], info[stat.ST_SIZE] # через константы в модуле stat

(33206, 18)

>>> stat.S_ISDIR(info.st_mode), stat.S_ISREG(info.st_mode)

(False, True)

Функция os.stat возвращает кортеж величин (в версии 3.X это особая разновидность кортежа, элементы которого имеют имена), представляющих низкоуровневую информацию о файле с указанным именем, а модуль stat экспортирует константы и функции для получения этой информации переносимым способом. Например, значение, получаемое из результата функции os.stat по индексу stat.ST_SIZE, соответствует размеру файла, а вызов функции stat.S_ISDIR с параметром «режим», полученным из результата функции os.stat, позволяет проверить, является ли файл каталогом. Однако, как было показано выше, обе эти операции доступны и в модуле os.path, поэтому на практике редко возникает необходимость использовать функцию os.stat; исключение составляют низкоуровневые запросы:

>>> path = r’C:\temp\spam.txt’

>>> os.path.isdir(path), os.path.isfile(path), os.path.getsize(path)

(False, True, 18)


Сканеры файлов

Прежде чем закончить обзор инструментов для работы с файлами, реализуем более практичную задачу и проиллюстрируем кое-что из того, что мы уже видели. В отличие от некоторых языков командной оболочки, в Python нет неявной процедуры циклического сканирования файла, но написать такую универсальную процедуру, пригодную для многократного использования, несложно. Модуль в примере 4.1 определяет универсальную процедуру сканирования файлов, которая просто применяет переданную в нее функцию к каждой строке внешнего файла.

Пример 4.1. PP4E\System\Filetools\scanfile.py

def scanner(name, function):

file = open(name, ‘r’)    # создать объект файла

while True:

line = file.readline() # вызов методов файла if not line: break # до конца файла function(line)    # вызвать объект функции

file.close()

Функции scanner безразлично, какая функция обработки строк в нее передана, чем и определяется ее универсальность: она готова применить любую функцию одного аргумента, уже существующую или которая может появиться в будущем, ко всем строкам в текстовом файле. Если реализацию этого модуля поместить в каталог, входящий в путь поиска модулей, им можно будет воспользоваться всякий раз, когда потребуется выполнить построчный обход файл. В примере 4.2 приводится клиентский сценарий, выполняющий простое преобразование строк.

Пример 4.2. PP4E\System\Filetools\commands.py

#!/usr/local/bin/python

from sys import argv

from scanfile import scanner

class UnknownCommand(Exception): pass

def processLine(line):    #    определить функцию,

if line[0] == ‘*’:    #    применяемую к каждой строке

print(“Ms.”, line[1:-1]) elif line[0] == ‘+’:

print(“Mr.”, line[1:-1])    # отбросить первый и последний символы

else:

raise UnknownCommand(line)    #    возбудить исключение

filename = ‘data.txt’

if len(argv) == 2: filename = argv[1]    #    аргумент командной строки    с именем

scanner(filename, processLine)    #    файла запускает сканер

Для текстового файла hillbillies.txt:

*Granny +Jethro *Elly May +”Uncle Jed”

наш сценарий commands.py вернет следующие результаты:

C:\...\PP4E\System\Filetools> python commands.py hillbillies.txt

Ms. Granny Mr. Jethro Ms. Elly May Mr. “Uncle Jed”

Все работает, тем не менее существует множество альтернативных способов реализации обоих примеров, и какие-то из них могут предлагать более удачные решения. Например, процессор команд, представленный в примере 4.2, можно было бы реализовать, как показано ниже. Преимущества этой реализации становятся более очевидными с ростом обрабатываемых вариантов - управляемый данными подход может оказаться короче и проще в сопровождении, чем длинная инструкция if с избыточными, по сути, действиями (если вам когда-нибудь потребуется изменить способ вывода строк, в следующей реализации вы сможете сделать это, изменив всего одну строку):

commands = {‘*’: ‘Ms.’, ‘+’: ‘Mr.’} # данные изменять проще, чем код?

def processLine(line): try:

print(commands[line[0]], line[1:-1]) except KeyError:

raise UnknownCommand(line)

Сканер также можно было бы улучшить. Как правило, перемещение обработки из программного кода Python во встроенные инструменты приводит к увеличению скорости. Например, если скорость имеет большое значение, сканер файлов можно было бы сделать быстрее, заменив в примере 4.1 вызов функции readline итератором объекта файла (в эффективности которого вы уже имели возможность убедиться):

def scanner(name, function):

for line in open(name, ‘r’):    # построчное сканирование

function(line)    # вызов объекта функции

Еще больших чудес в примере 4.1 можно достичь с помощью таких инструментов итераций, как встроенная функция map, генераторы списков и выражения-генераторы. Ниже приводится минималистская версия. Цикл for замещается вызовом функции map или генератором, и Python сам закрывает файл на этапе сборки мусора или при выходе из сценария (в процессе обработки во всех реализациях создается список результатов, однако такое неэкономное расходование ресурсов вполне допустимо, за исключением очень больших файлов):

def scanner(name, function):

list(map(function, open(name, ‘r’)))

def scanner(name, function):

[function(line) for line in open(name, ‘r’)]

def scanner(name, function):

list(function(line) for line in open(name, ‘r’))

Фильтры файлов

Предыдущий пример работает, как предполагалось, но как быть, если во время сканирования файла нам потребуется файл изменить? В примере 4.3 показаны два подхода: в одном используются явные файлы, а в другом стандартные потоки ввода-вывода, которые можно перенаправить в командной строке.

Пример 4.3. PP4E\System\Filetools\filters.py

import sys

def filter_files(name, function):    # фильтрация файлов через функцию

input = open(name, ‘r’)    # создать объекты файлов

output = open(name + ‘.out’, ‘w’)    # выходной файл

for line in input:

output.write(function(line)) # записать измененную строку input.close()

output.close()    # выходной файл имеет расширение ‘.out’

def filter_stream(function):    # отсутствуют явные файлы

while True:    # использовать стандартные потоки

line = sys.stdin.readline() # или: input() if not line: break

print(function(line), end=’’)    # или: sys.stdout.write()

if __name__ == ‘__main__’:

filter_stream(lambda line: line) # копировать stdin в stdout, если

# запущен как самостоятельный сценарий

Обратите внимание, что применение такой новейшей особенности, как менеджеры, контекста, обсуждавшейся выше, позволило бы сэкономить несколько строк программного кода в реализации фильтра из примера 4.3, опирающегося на использование файлов, и гарантировало бы немедленное закрытие файлов в случае появления исключения в функции обработки:

def filter_files(name, function):

with open(name, ‘r’) as input, open(name + ‘.out’, ‘w’) as output: for line in input:

output.write(function(line)) # записать измененную строку

И снова, применение итераторов объектов файлов позволило бы упростить реализацию фильтра на основе потоков ввода-вывода:

def filter_stream(function):

for line in sys.stdin:    # автоматически выполняет построчное чтение

print(function(line), end=’’)

Поскольку стандартные потоки ввода-вывода открываются автоматически, они обычно проще в использовании. Если запустить этот пример, как самостоятельный сценарий, он просто скопирует stdin в stdout:

C:\...\PP4E\System\Filetools> filters.py < hillbillies.txt

*Granny +Jethro *Elly May +”Uncle Jed”

Однако этот модуль более полезен, когда он импортируется как библиотека (клиент предоставляет функцию обработки строк):

>>> from filters import filter_files

>>> filter_filesChillbillies.txt’, str.upper)

>>> print(open('hillbillies.txt.out’).read())

*GRANNY +JETHRO *ELLY MAY +”UNCLE JED”

В оставшейся части книги мы часто будем видеть примеры использования файлов, особенно в наиболее полных и практичных примерах системных программ в главе 6. Однако сначала познакомимся с инструментами обработки жилища наших файлов.


Инструменты для работы с каталогами

Одной из наиболее частых задач для утилит командной оболочки является применение операций к множеству файлов, находящихся в каталоге - «папке» на языке Windows. Сценарии, способные выполнять операции над группой файлов, позволяют автоматизировать (то есть программировать) задачи, которые в противном случае пришлось бы многократно повторять вручную.

Например, допустим, что нужно найти во всех файлах с программным кодом Python из каталога разработки имя глобальной переменной (вы могли забыть, где оно используется). Для каждой платформы существует множество способов решить эту задачу (например, команды find и grep в Unix), но сценарии Python, выполняющие такие задачи, будут работать на любой платформе, где работает Python, - в Windows, Unix, Linux, Macintosh и практически на любой другой распространенной платформе. Достаточно просто скопировать сценарий на любой компьютер, где предполагается его использовать, и он будет работать независимо от имеющихся на нем утилит, - для этого необходимо иметь лишь интерпретатор Python. Кроме того, программирование таких задач на языке Python позволяет по ходу дела выполнять любые действия - замену, удаление и любые другие, какие только можно реализовать на языке Python.


Обход одного каталога

Чаще всего при написании таких инструментов сначала получают список имен файлов, которые нужно обработать, а затем пошагово обходят его в цикле for, поочередно обрабатывая каждый файл. Весь фокус состоит в том, чтобы научиться получать в сценариях такой список содержимого каталога. Существует по меньшей мере три способа сделать это: выполнить команды оболочки для получения списка с помощью os.рорen, отыскать файлы по шаблону имени с помощью glob.glob и получить перечень содержимого каталога с помощью os.listdir. Эти способы различаются по интерфейсу, формату результата и переносимости.

Запуск команд получения списка содержимого каталога с помощью os.popen

Скажите-ка, как вы получали списки файлов в каталоге до того, как услышали о Python? Если у вас нет опыта работы с инструментами командной строки, ответ может быть следующим: «Ну, я запускал в Windows проводник и щелкал, куда нужно». Но здесь у нас речь идет о механизмах, менее ориентированных на графический интерфейс, то есть о механизмах командной строки.

Для получения списков файлов в Unix обычно используется команда ls; в Windows списки можно создавать вводом dir в окне консоли MS-DOS.

Поскольку сценарии Python могут выполнить любую команду оболочки с помощью os.popen, они являются самым универсальным способом получения содержимого каталога из программ на языке Python. Мы уже встречались с функцией os.popen в предыдущей главе - она выполняет команду оболочки и возвращает объект файла, из которого можно прочесть вывод команды. Для иллюстрации допустим сначала, что имеется следующая структура каталогов - на моем ноутбуке с Windows есть обе команды, dir и Unix-подобная ls из Cygwin:

c:\temp> dir /B

parts

PP3E

random.bin

spam.txt

temp.bin

temp.txt

c:\temp> c:\cygwin\bin\ls

PP3E parts random.bin spam.txt temp.bin temp.txt

c:\temp> c:\cygwin\bin\ls parts

part0001 part0002 part0003 part0004

Имена parts и PP3E являются здесь подкаталогами, вложенным в каталог C:\temp (последний из них является копией дерева каталогов с примерами для предыдущего издания книги, часть из которых я использовал в этом издании). Теперь мы знаем, что сценарии могут получать списки имен файлов и каталогов на этом уровне, просто запуская специфическую для платформы команду и читая полученный вывод (текст, обычно выводимый в окно консоли):

C:\temp> python >>> import os

>>> os.popen('dir /B').readlines()

[‘parts\n’, ‘PP3E\n’, ‘random.bin\n’, ‘spam.txt\n’, ‘temp.bin\n’, ‘temp.txt\n’]

Строки, возвращаемые командой оболочки, содержат замыкающий символ конца строки, но его легко можно отсечь. Кроме того, функция os.popen возвращает итератор, точно такой же, как итератор объектов файлов:

>>> for line in os.popen('dir /B'):

... print(line[:-1])

parts

PP3E

random.bin

spam.txt

temp.bin

temp.txt >>> lines = [line[:-1] for line in os.popen('dir /B')]

>>> lines

[‘parts’, ‘PP3E’, ‘random.bin’, ‘spam.txt’, ‘temp.bin’, ‘temp.txt’]

В случае объектов каналов действие итераторов может иметь еще более значимый эффект, чем просто уход от одновременной загрузки всех результатов в память: метод readlines всегда блокирует вызывающий процесс, пока не завершится порожденная программа, тогда как при использовании итераторов этого не происходит.

Обе команды, dir и ls, позволяют задавать требуемые образцы имен файлов и имен каталогов, список содержимого которых должен быть получен, с помощью шаблонов имен. В следующем примере мы снова просто выполняем команды оболочки, поэтому годится все, что можно ввести в командной строке:

>>> os.popen('dir *.bin /B').readlines()

[‘random.bin\n’, ‘temp.bin\n’]

>>> os.popen(r'c:\cygwin\bin\ls *.bin').readlines()

[‘random.bin\n’, ‘temp.bin\n’]

>>> list(os.popen(r'dir parts /B'))

[‘part0001\n’, ‘part0002\n’, ‘part0003\n’, ‘part0004\n’]

>>> [fname for fname in os.popen(r'c:\cygwin\bin\ls parts')]

[‘part0001\n’, ‘part0002\n’, ‘part0003\n’, ‘part0004\n’]

Эти вызовы используют универсальные инструменты и все действуют, как было заявлено. Однако выше отмечалось, что недостатками os.popen являются необходимость использования команд оболочки, специфических для платформы, и потеря производительности при запуске независимых программ. На практике различные инструменты могут возвращать различные результаты:

>>> list(os.popen(r'dir parts\part* /B'))

[‘part0001\n’, ‘part0002\n’, ‘part0003\n’, ‘part0004\n’]

>>>

>>> list(os.popen(r'c:\cygwin\bin\ls parts/part*'))

[‘parts/part0001\n’, ‘parts/part0002\n’, ‘parts/part0003\n’, ‘parts/part0004\n’]

Следующие два альтернативных приема проявляют себя лучше в обоих отношениях.

Модуль glob

Термин globbing (глобальный поиск по шаблону) происходит от группового символа *, используемого в шаблонах имен файлов. На компьютерном сленге символ * трактуется, как «glob» (группа символов). Более приземленно, глобальный поиск по шаблону просто означает получение имен всех элементов в каталоге - файлов и подкаталогов, имена которых соответствуют заданному шаблону. В командных оболочках Unix при глобальном поиске шаблоны имен файлов, указанные в командной строке, расширяются до всех совпадающих имен еще перед выполнением команды. В Python можно делать нечто похожее, вызывая встроенную функцию glob.glob, - инструмент, принимающий шаблон имени файла и возвращающий список (не генератор) имен файлов, соответствующих этому шаблону:

>>> import glob

>>> glob.glob('*')

[‘parts’, ‘PP3E’, ‘random.bin’, ‘spam.txt’, ‘temp.bin’, ‘temp.txt’]

>>> glob.glob('*.bin')

[‘random.bin’, ‘temp.bin’]

>>> glob.glob('parts')

[‘parts’]

>>> glob.glob('parts/*')

[‘parts\\part0001’, ‘parts\\part0002’, ‘parts\\part0003’, ‘parts\\part0004’]

>>> glob.glob('parts\part*')

[‘parts\\part0001’, ‘parts\\part0002’, ‘parts\\part0003’, ‘parts\\part0004’]

Для определения шаблонов в функции glob используется обычный синтаксис шаблонов имен файлов, используемый в командных оболочках: ? означает один любой символ, * означает любое число символов, а [] означает множество символов, доступных для выбора.13 Если поиск нужно осуществлять в каталоге, отличном от текущего рабочего каталога, в шаблон нужно включить путь к каталогу. Кроме того, модуль принимает разделители имен каталогов в стиле Unix или DOS (/ или \). Эта функция реализована так, что не вызывает команды оболочки (она использует функцию os.listdir, описываемую в следующем разделе) и потому должна выполняться быстрее и лучше переноситься на все платформы Python, чем показанные выше приемы с применением функции os.popen.

Вообще функция glob несколько мощнее, чем здесь описано. Получение списка файлов в каталоге является лишь одной из ее возможностей поиска по шаблону. Например, ее можно использовать для получения списка имен из нескольких каталогов, так как каждый уровень в передаваемом пути к каталогу также можно определить в виде шаблона:

>>> for path in glob.glob(r'PP3E\Examples\PP3E\*\s*.py'): print(path)

PP3E\Examples\PP3E\Lang\summer-alt.py

PP3E\Examples\PP3E\Lang\summer.py

PP3E\Examples\PP3E\PyTools\search_all.py

Здесь мы получили список имен файлов, соответствующих шаблону s*py, из двух разных каталогов. Так как в качестве имени предшествующего каталога был использован групповой символ *, Python перебрал все возможные пути к файлам. Запуская команды оболочки с помощью функции os.рорen, такого же результата можно добиться, только если подобная возможность поддерживается самой командной оболочкой или командой вывода списка файлов.

Функция os.listdir

Функция listdir из модуля os является еще одним способом получить список имен файлов. Но она принимает не шаблон имени файла, а простую строку с именем каталога и возвращает список, содержащий имена всех файлов в каталоге - как просто файлов, так и вложенных подкаталогов, - для использования в вызывающем сценарии:

>>> import os >>> os.listdir('.')

[‘parts’, ‘PP3E’, ‘random.bin’, ‘spam.txt’, ‘temp.bin’, ‘temp.txt’]

>>>

>>> os.listdir(os.curdir)

[‘parts’, ‘PP3E’, ‘random.bin’, ‘spam.txt’, ‘temp.bin’, ‘temp.txt’]

>>>

>>> os.listdir('parts')

[‘part0001’, ‘part0002’, ‘part0003’, ‘part0004’]

Эта функция также не привлекает к работе команды оболочки, и поэтому данный способ является не только быстрым, но и переносимым на все основные платформы Python. Результат функции не упорядочен никаким образом (но может быть отсортирован методом списков sort или функцией sorted); возвращает базовые имена файлов без путей к каталогам; не включает имена каталогов «.» или «..» и содержит имена файлов и подкаталогов для данного уровня.

Чтобы сравнить все три способа, запустим их друг за другом для явно заданного каталога. Они отличаются некоторыми деталями, но в целом являются вариациями на одну и ту же тему: функция os.рорen возвращает символы конца строки и способна сортировать имена файлов на некоторых платформах, функция glob.glob принимает шаблоны и возвращает полные имена файлов с путями, а функция os.listdir принимает обычное имя каталога и возвращает имена файлов без путей к каталогам:

>>> os.popen('dir /b parts').readlines()

[‘part0001\n’, ‘part0002\n’, ‘part0003\n’, ‘part0004\n’]

>>> glob.glob(r'parts\*')

[‘parts\\part0001’, ‘parts\\part0002’, ‘parts\\part0003’, ‘parts\\part0004’]

>>> os.listdir('parts')

[‘part0001’, ‘part0002’, ‘part0003’, ‘part0004’]

Из этих трех способов лучшими вариантами являются функции glob и listdir, если важна переносимость сценария и единообразие результатов, при этом функция listdir в последних версиях Python выглядит самой быстрой (тем не менее советую замеры производительности произвести самостоятельно - реализация может со временем измениться).

Разбиение и объединение результатов вывода

В предыдущем примере отмечалось, что функция glob возвращает полные имена файлов с путями, а функция listdir возвращает простые базовые имена файлов. В сценариях для удобства обработки часто требуется разбивать результаты функции glob, чтобы получить базовые имена, либо добавлять полные пути в результаты функции listdir. Такие преобразования легко реализуются, если позволить модулю os.path выполнить всю работу. Например, сценарию, который должен скопировать все файлы в какое-то место, обычно нужно сначала выделить базовые имена файлов из результатов, полученных с помощью функции glob, и затем добавить впереди них другие имена каталогов:

>>> dirname = r'C:\temp\parts'

>>>

>>> import glob

>>> for file in glob.glob(dirname + '/*'):

... head, tail = os.path.split(file)

... print(head, tail, '=>', ('C:\\Other\\' + tail))

C:\temp\parts part0001 => C:\Other\part0001 C:\temp\parts part0002 => C:\Other\part0002 C:\temp\parts part0003 => C:\Other\part0003 C:\temp\parts part0004 => C:\Other\part0004

Здесь после => показаны полные имена файлов, которые получатся после перемещения. Напротив, сценарию, который должен обработать все файлы в каталоге, отличном от того, в котором он выполняется, вероятно, потребуется добавить к результатам функции listdir имя целевого каталога, прежде чем предавать имена файлов другим инструментам:

>>> import os

>>> for file in os.listdir(dirname):

... print(dirname, file, '=>', os.path.join(dirname, file))

C:\temp\parts part0001 => C:\temp\parts\part0001 C:\temp\parts part0002 => C:\temp\parts\part0002 C:\temp\parts part0003 => C:\temp\parts\part0003 C:\temp\parts part0004 => C:\temp\parts\part0004

Когда вы начнете писать действующие инструменты для работы с каталогами, похожие на те, что мы будем разрабатывать в главе 6, пользование этими функциями войдет у вас в привычку.


Обход деревьев каталогов

Возможно, вы обратили внимание, что все предшествующие приемы в этом разделе возвращают имена файлов только из одного каталога (единственным исключением является глобальный поиск по шаблону). Такой подход годится в большинстве случаев, но что если потребуется применить операцию к каждому файлу в каждом каталоге и во всех подкаталогах дерева каталогов?

Например, допустим, что требуется найти в сценариях на языке Python все вхождения некоторого глобального имени. Однако на этот раз наши сценарии организованы в виде пакета модулей - каталога с вложенными подкаталогами, которые могут содержать собственные подкаталоги. Можно вручную запускать наш гипотетический поисковый механизм для одного каталога в каждом из подкаталогов в дереве, но это утомительно, чревато ошибками и точно не доставит удовольствия.

К счастью, реализовать обработку дерева каталогов на языке Python почти так же просто, как и просканировать единственный каталог. Можно написать рекурсивную процедуру обхода дерева или использовать утилиту перемещения по дереву, встроенную в модуль os. Такие инструменты можно использовать для поиска, копирования, сравнения и выполнения любых других операций над произвольными деревьями каталогов на любой платформе, где выполняется Python (то есть почти всюду).

Функция обхода дерева os.walk

Чтобы облегчить применение операции ко всем файлам в дереве каталогов, в составе Python поставляется утилита, выполняющая обход дерева и запускающая в каждом каталоге указанную функцию. Функции os.walk передается имя корневого каталога, и она автоматически обходит все дерево от корня и ниже.

По своей сути функция os.walk является функцией-генератором - для каждого каталога в дереве она возвращает кортеж из трех элементов, содержащий имя текущего каталога, а также списки всех файлов и всех подкаталогов в текущем каталоге. Так как это функция-генератор, обход дерева обычно реализуется с помощью цикла for (или другого инструмента итераций). В каждой итерации функция перемещается к следующему подкаталогу, а инструкция цикла выполняет свое тело для следующего уровня в дереве (например, открывает все файлы в этом подкаталоге и производит поиск по их содержимому).

На первый взгляд, такое описание может показаться ужасно сложным, но когда вы привыкнете к функции os.walk, все окажется довольно простым. В следующем фрагменте, например, тело цикла выполняется для каждого каталога в дереве с корнем в текущем рабочем каталоге. Цикл просто выводит имя каталога и имена всех файлов в нем, добавляя к ним имя каталога. Описать это на языке Python проще, чем на обычном языке (перед тем как запускать этот пример, я удалил каталог PP3E, чтобы сократить вывод):

>>> import os

>>> for (dirname, subshere, fileshere) in os.walk('.'):

...    print('[' + dirname + ']')

...    for fname in fileshere:

...    print(os.path.join(dirname, fname)) # обработка одного файла

[.]

.\random.bin

.\spam.txt

.\temp.bin

.\temp.txt

[.\parts]

.\parts\part0001

.\parts\part0002

.\parts\part0003

.\parts\part0004

Иными словами, мы реализовали наш собственный, легко изменяемый инструмент рекурсивного вывода содержимого каталога на языке Python, Поскольку нам может потребоваться подправить его и использовать где-нибудь еще, давайте сделаем его постоянно доступным в виде файла модуля, как показано в примере 4.4, - теперь, когда мы проработали детали в интерактивном режиме.

Пример 4.4. PP4E\System\Filetools\lister_walk.py

"выводит список файлов в дереве каталогов с помощью os.walk”

import sys, os

def lister(root):    # для корневого каталога

for (thisdir, subshere, fileshere) in os.walk(root): # перечисляет

print(‘[‘ + thisdir + ‘]’)    # каталоги в дереве

for fname in fileshere:    # вывод файлов в каталоге

path = os.path.join(thisdir, fname) # добавить имя каталога print(path)

if __name__ == ‘__main__’:

lister(sys.argv[1])    # имя каталога в

# командной строке

При таком оформлении данный программный код можно также выполнять из командной строки. Ниже приводится пример запуска его для получения списка содержимого другого корневого каталога, который передается в аргументе командной строки:

C:\...\PP4E\System\Filetools> python lister_walk.py C:\temp\test

[C:\temp\test]

C:\temp\test\random.bin

C:\temp\test\spam.txt

C:\temp\test\temp.bin

C:\temp\test\temp.txt

[C:\temp\test\parts]

C:\temp\test\parts\part0001

C:\temp\test\parts\part0002

C:\temp\test\parts\part0003

C:\temp\test\parts\part0004

Ниже приводится более сложный пример использования функции os.walk. Предположим, что имеется дерево каталогов с файлами, и вам необходимо отыскать в нем все файлы с программным кодом на языке Python, которые ссылаются на модуль mimetypes (с этим модулем мы познакомимся в главе 6). Ниже демонстрируется один из способов (хотя и слишком специфичный и не универсальный) решения поставленной задачи:

>>> import os >>> matches = []

>>> for (dirname, dirshere, fileshere) in os.walk(r'C:\temp\PP3E\Examples'):

... for filename in fileshere:

...    if filename.endswith('.py'):

...    pathname = os.path.join(dirname, filename)

...    if 'mimetypes' in open(pathname).read():

...    matches.append(pathname)

>>> for name in matches: print(name)

C:\temp\PP3E\Examples\PP3E\Internet\Email\mailtools\mailParser.py

C:\temp\PP3E\Examples\PP3E\Internet\Email\mailtools\mailSender.py

C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\downloadflat.py

C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\downloadflat_modular.py

C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\ftptools.py

C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\uploadflat.py

C:\temp\PP3E\Examples\PP3E\System\Media\playfile.py

Данная реализация в цикле обходит все файлы в каждом из подкаталогов, отыскивает файлы с расширением .py, содержащие искомую строку. Если совпадение найдено, полное имя файла добавляется в объект списка с результатами. Как вариант, мы могли бы просто создать список всех файлов с расширением .py и организовать поиск требуемой строки в цикле for уже после обхода дерева. Так как в главе 6 мы представим более универсальное решение для этого типа задач, то оставим пока все, как есть.

Если вам будет интересно узнать, что в действительности происходит внутри генератора os.walk, попробуйте несколько раз вызвать его метод __next__(или передать его встроенной функции next), как это автоматически делается циклом for, - каждый раз вы будете перемещаться к очередному подкаталогу в дереве:

>>> gen = os.walk(r'C:\temp\test')

>>> gen.__next__()

(‘C:\\temp\\test’, [‘parts’], [‘random.bin’, ‘spam.txt’, ‘temp.bin’, ‘temp. txt’])

>>> gen.__next__()

(‘C:\\temp\\test\\parts’, [], [‘part0001’, ‘part0002’, ‘part0003’, ‘ part0004’]) >>> gen.__next__()

Traceback (most recent call last):

File “<stdin>”, line 1, in <module>

StopIteration

Описание функции os.walk в руководстве по библиотеке содержит более подробную информацию. Например, эта функция поддерживает порядок обхода не только в направлении сверху вниз, но и снизу вверх - достаточно передать функции необязательный аргумент topdown=False, и вызывающий программный код получит возможность сократить количество посещаемых ветвей дерева, удаляя имена из списка подкаталогов в возвращаемых кортежах.

Для создания списков имен на каждом уровне в дереве каталогов функция os.walk использует функцию os.listdir, с которой мы встречались выше, возвращающую имена файлов и каталогов без определенного порядка и без путей к каталогам. Прежде чем вернуть очередной результат, функция os.walk делит этот список на списки каталогов и файлов (точнее, некаталогов). Обратите также внимание, что функция os.walk использует тот же список подкаталогов, который она возвращает вызывающему программному коду, чтобы затем спуститься в подкаталоги. Списки являются изменяемыми объектами, которые можно изменять непосредственно, поэтому, изменяя содержимое полученного списка подкаталогов, вызывающий программный код может оказывать влияние на дальнейшую работу os.walk. Например, удаляя имена каталогов, можно сократить число посещаемых ветвей, а отсортировав список, можно определить очередность обхода подкаталогов.

Рекурсивный обход с помощью os.listdir

Функция os.walk сама осуществляет обход дерева - нам остается лишь реализовать тело цикла, выполняющее необходимые действия. Но иногда большей гибкости можно достичь, реализовав обход дерева самостоятельно, при этом почти не приложив лишних усилий. В следующем сценарии представлена другая реализация вывода содержимого каталога с использованием рекурсивной функции обхода (функция, которая вызывает саму себя, чтобы повторить операции). Функция mylister в примере 4.5 очень похожа на функцию lister из примера 4.4, но создает списки имен файлов с помощью os.listdir и вызывает саму себя рекурсивно, чтобы спуститься в подкаталоги.

Пример 4.5. PP4E\System\Filetools\lister_recur.py

# выводит список файлов в дереве каталогов с применением рекурсии

import sys, os

def mylister(currdir):

print(‘[‘ + currdir + ‘]’)

for file in os.listdir(currdir):    # генерирует список файлов

path = os.path.join(currdir, file) # добавить путь к каталогу if not os.path.isdir(path): print(path) else:

mylister(path)    # рекурсивный спуск в подкаталоги

if __name__ == ‘__main__’:

mylister(sys.argv[1])    # имя каталога в командной строке

Как обычно, этот файл можно импортировать или запускать как самостоятельный сценарий. Тот факт, что результатом его работы является печать текста, можно отнести к его недостаткам при его использовании в качестве импортируемого инструмента, если только его стандартный поток вывода не перехватывается в другой программе.

Когда этот файл запускается как самостоятельный сценарий, он воспроизводит почти те же результаты, что и пример 4.4; почти, но не полностью - в отличие от версии на основе функции os.walk, рекурсивная версия не обязует пройти все файлы на текущем уровне, прежде чем спуститься в подкаталоги. Можно было бы обойти список имен файлов дважды (чтобы сначала отобрать файлы), но в данной реализации порядок обхода определяется результатами, возвращаемыми функцией os.listdir. Для многих случаев такой порядок обхода может оказаться неприемлемым:

C:\...\PP4E\System\Filetools> python lister_recur.py C:\temp\test

[C:\temp\test]

[C:\temp\test\parts]

C:\temp\test\parts\part0001

C:\temp\test\parts\part0002

C:\temp\test\parts\part0003

C:\temp\test\parts\part0004

C:\temp\test\random.bin

C:\temp\test\spam.txt

C:\temp\test\temp.bin

C:\temp\test\temp.txt

Мы еще воспользуемся большей частью приемов, приведенных в этом разделе, в главе 6 и далее в книге. Например, приведенные выше приемы обхода деревьев будут использованы в сценариях копирования и сравнения деревьев каталогов. По ходу изложения вы увидите эти инструменты в действии. Кроме того, в главе 6 мы реализуем утилиту find, объединяющую в себе обход дерева каталогов с помощью os.walk и поиск имен файлов по шаблону с помощью glob.glob.


Обработка имен файлов в Юникоде в версии 3.X: listdir, walk, glob

Поскольку в Python 3.X все обычные строки состоят из символов Юникода, имена каталогов и файлов, возвращаемые функциями os.listdir, os.walk и glob.glob, в действительности являются строками Юникода. Это может иметь некоторые последствия, если каталоги содержат необычные имена, не поддающиеся декодированию.

Формально имена файлов могут содержать любые символы, поэтому в версии 3.X функция os.listdir может работать в двух режимах: если ей передать аргумент типа bytes, она будет возвращать кодированные имена файлов в виде строк байтов; если ей передать аргумент типа str, она будет возвращать имена файлов в виде строк Юникода, декодированных в соответствии с кодировкой, используемой файловой системой:

C:\...\PP4E\System\Filetools> python

>>> import os

>>> os.listdir('.')[:4]

[‘bigext-tree.py’, ‘bigpy-dir.py’, ‘bigpy-path.py’, ‘bigpy-tree.py’]

>>> os.listdir(b'.')[:4]

[b’bigext-tree.py’, b’bigpy-dir.py’, b’bigpy-path.py’, b’bigpy-tree.py’]

Версия, основанная на использовании строк байтов, может применяться для файлов с недекодируемыми именами. Функции os.walk и glob. glob за кулисами обращаются к функции os.listdir, от которой наследуют то же самое поведение. Функция os.walk обхода деревьев, например, вызывает os.listdir для каждого подкаталога - передача строки байтов в аргументе подавляет декодирование, вследствие чего в результате возвращается строка байтов:

>>> for (dir, subs, files) in os.walk('..'): print(dir)

..\Environment ..\Filetools ..\Processes

>>> for (dir, subs, files) in os.walk(b'..'): print(dir)

b’..’

b’..\\Environment’

b’..\\Filetools’ b’..\\Processes’

Функция glob.glob также вызывает функцию os.listdir перед применением шаблонов имен, и поэтому тоже возвращает имена в виде недекодированных строк байтов, когда получает строку байтов в аргументе:

>>> glob.glob('.\*')[:3]

[‘.\\bigext-out.txt’, ‘.\\bigext-tree.py’, ‘.\\bigpy-dir.py’]

>>>

>>> glob.glob(b'.\*')[:3]

[b’.\\bigext-out.txt’, b’.\\bigext-tree.py’, b’.\\bigpy-dir.py’]

Передавая имена в виде обычных строк (например, посредством аргумента командной строки), вы можете столкнуться с необходимостью преобразовывать обычные строки в строки байтов, с целью подавить декодирование:

>>> name = '.'

>>> os.listdir(name.encode())[:4]

[b’bigext-out.txt’, b’bigext-tree.py’, b’bigpy-dir.py’, b’bigpy-path.py’]

Таким образом, если каталоги могут содержать имена, не поддающиеся декодированию с использованием кодировки, используемой по умолчанию, вам может потребоваться передавать этим инструментам строки байтов, чтобы избежать ошибок, связанных с кодированием Юникода. В ответ вы будете получать строки байтов, которые могут оказаться менее читаемыми при выводе, но это убережет вас от ошибок при обходе каталогов и файлов.

Такой подход может оказаться особенно полезным в системах, где используются простейшие кодировки, такие как ASCII или Latin-1, но могут иметься файлы с именами в произвольных кодировках, скопированными с других компьютеров, из Интернета и так далее. В зависимости от ситуации для подавления некоторых ошибок кодирования можно использовать также обработчики исключений.

Пример того, какое это может иметь значение, мы увидим в первом разделе главы 6, где недекодируемое имя каталога вызывает появление ошибки при выводе во время полного сканирования диска (хотя данная ошибка относится скорее к функции вывода, чем к операции декодирования).

Обратите внимание, что встроенная функция open также может принимать имена открываемых файлов как в виде строк str Юникода, так и в виде строк байтов bytes, однако она использует этот аргумент, только чтобы дать начальное имя файлу, - порядок же обработки содержимого файла определяется дополнительным аргументом режима. Возможность передавать строку байтов в качестве имени файла позволяет использовать произвольные кодированные имена.

Правила использования Юникода: содержимое файлов и имена файлов

Важно помнить, что Юникод может выполнять применительно к файлам две различные задачи: кодирование содержимого файлов и кодирование имен файлов. Интерпретатор Python определяет настройки по умолчанию для этих двух операций в двух различных атрибутах; для Windows 7:

>>> import sys

>>> sys.getdefaultencoding()    # кодировка для содержимого файлов

‘utf-8’

>>> sys.getfilesystemencoding() # кодировка для имен файлов

‘mbcs’

Эти настройки позволяют явно указывать используемые кодировки - кодировка для содержимого используется операциями чтения из файлов и записи в файлы, а кодировка для имен файлов используется при работе с именами файлов, до передачи данных. Кроме того, использование строк байтов bytes для передачи имен файлов различным инструментам позволяет обойти проблему несовместимости со схемой кодирования, используемой файловой системой, а открытие файлов в двоичном режиме позволяет подавить ошибки декодирования их содержимого.

Однако, как мы уже видели выше, открывая текстовые файлы в двоичном режиме, мы можем столкнуться с проблемой несовпадения кодированного текста с искомой строкой в операциях поиска: строки поиска в этом случае также должны быть строками байтов, закодированными с применением определенной кодировки, возможно несовместимой с кодировкой содержимого файла. Фактически данный подход в значительной степени воспроизводит поведение текстовых файлов в Python 2.X и подчеркивает важность использования Юникода в версии 3.X - при работе с такими файлами иногда может сложиться ложное впечатление, что все работает прекрасно. С другой стороны, возможность открывать текстовые файлы в двоичном режиме, чтобы подавить декодирование содержимого файлов и избежать появления связанных с этим ошибок, все еще может быть полезной, если вы не желаете пропустить недекодируемые файлы, содержимое которых не имеет большого значения.

Как правило, необходимо всегда указывать имя кодировки для содержимого текстовых файлов, если она может не совпадать с кодировкой по умолчанию, и в большинстве случаев вам следует опираться на интерфейсы, принимающие имена файлов в виде строк Юникода. Опять же, полную информацию по использованию Юникода в именах файлов вы найдете в руководствах по языку Python, так как здесь недостаточно места, чтобы дать полный охват этой темы, а за информацией о Юникоде вообще обращайтесь к четвертому изданию книги «Изучаем Python»1.

В главе 6 мы собираемся задействовать инструменты, с которыми встретились в этой главе, в реальной задаче. Например, мы применим инструменты для работы с файлами и каталогами при реализации делителей файлов, систем тестирования, инструментов копирования и сравнения каталогов, а также других утилит, опирающихся на использование процедуры обхода деревьев. Мы увидим, что инструменты для работы с каталогами, с которыми мы встретились здесь, обладают качествами, позволяющими автоматизировать огромный круг задач. Однако перед этим прочитаем главу 5, завершающую обзор основных инструментов исследованием еще одной темы системного программирования, которая тесно переплетается с различными прикладными областями, - реализацией параллельной обработки данных на языке Python.

Марк Лутц «Изучаем Python», 4 издание, СПб.: Символ-Плюс, 2010.

Системные инструменты параллельного выполнения


«Расскажите обезьянам, что им делать»

Большинство компьютеров тратит массу времени, ничего не делая. Если запустить системный монитор и посмотреть на уровень загрузки процессора, вы поймете, что я имею в виду: он очень редко достигает 100%, даже если выполняется несколько программ одновременно.14 Просто в программном обеспечении существует очень много задержек - доступ к диску, сетевой трафик, запросы к базам данных, ожидание нажатия клавиши пользователем и тому подобное. Фактически большая часть мощности современных процессоров большую часть времени не используется: более быстрые процессоры дают ускорение во время пиков потребности в производительности, но значительная часть их мощности в целом может оказаться невостребованной.

Еще на заре эпохи компьютеров программисты поняли, что могут воспользоваться такой неиспользуемой вычислительной мощностью, выполняя одновременно несколько программ. Если распределить процессорное время среди множества задач, его мощность не будет тратиться

впустую, пока некоторая конкретная задача ждет осуществления внешнего события. Такая технология обычно называется параллельной обработкой (или, иногда, «мультиобработкой» или даже «многозадачностью»), потому что возникает впечатление одновременного выполнения нескольких заданий параллельно во времени. Это одна из центральных идей современных операционных систем, на основе которой возникло представление о компьютерных интерфейсах с несколькими активными окнами, воспринимаемое нами теперь, как нечто само собой разумеющееся. Даже внутри одной программы разделение обработки на ряд параллельно выполняющихся заданий может увеличить быстродействие системы в целом, во всяком случае по меркам внешних часов.

Столь же важно для современных систем обладание быстрой реакцией на действия пользователя, независимо от объема работы, выполняемой за кулисами. Обычно недопустимо, чтобы программа зависала при выполнении запроса. Взгляните, например, на пользовательский интерфейс клиента электронной почты: обрабатывая запрос на получение почты с сервера, программа должна загрузить электронные письма с сервера через сеть. Если почты достаточно много, а соединение с Интернетом достаточно медленное, для завершения этого этапа может потребоваться несколько минут. Но по ходу выполнения задачи загрузки программа в целом не должна останавливаться - она по-прежнему должна реагировать на запросы обновления экрана, щелчки мышью и так далее.

И здесь на помощь приходит параллельная обработка. Выполняя такие долговыполняющиеся задачи параллельно с остальной частью программы, система в целом может сохранить способность реагировать на действия пользователя независимо от того, насколько занятыми оказываются отдельные ее части. Более того, модель параллельной обработки является вполне естественной для структурирования таких и некоторых иных программ - некоторые задачи легче проще проектировать и реализовывать как набор программных компонентов, действующих независимо и параллельно.

Существует два основных способа реализации одновременного выполнения задач в Python - ветвление процессов (forks) и порожденные потоки (threads) выполнения. Функционально для организации параллельного выполнения программного кода на языке Python оба способа используют службы операционной системы. Процедурно они существенно отличаются в смысле интерфейсов, переносимости и организации взаимодействий между заданиями. Например, на момент написания данной книги возможность прямого ветвления процессов не поддерживалась стандартной реализацией Python для Windows (однако такая поддержка присутствует в версии Python для Cygwin).

Напротив, поддержка потоков выполнения в Python реализована на всех основных платформах. Кроме того, семейство функций os.spawn обеспечивает дополнительные способы запуска способом, не зависящим от типа платформы, - напоминающим ветвление процессов. Для запуска программ переносимым способом, с помощью команд оболочки, также можно использовать функции os.popen, os.system и модуль subprocess, с которыми мы познакомились в главах 2 и 3. Новейший пакет multiprocessing предоставляет дополнительные переносимые способы запуска процессов.

В данной главе мы продолжим рассмотрение системных интерфейсов, доступных программистам на языке Python, исследуем встроенные инструменты для параллельного запуска заданий и обмена информацией с этими заданиями. В некотором смысле мы приступили к этому раньше - функции os.system, os. popen и модуль subprocess, которые мы изучали и использовали в предыдущих трех главах, обеспечивают переносимый способ порождения программ командной строки и обмена информацией с ними. Однако здесь мы не собираемся повторять полное описание этих инструментов.

Вместо этого мы сделаем упор на знакомстве с более прямо относящимися к теме приемами, такими как ветвление процессов, потоки, каналы, сигналы, сокеты и другими, и на использовании встроенных инструментов языка Python, поддерживающими их, такими как функция os.fork и модули threading, queue и multiprocessing. В следующей главе (и в оставшейся части книги) мы будем использовать эти приемы в примерах действующих программ, поэтому, прежде чем двигаться вперед, необходимо усвоить основы.

Одно предварительное замечание: процессы, потоки и механизмы взаимодействий между процессами, которые мы будем исследовать в этой главе, являются основными инструментами организации параллельной обработки в сценариях на языке Python, однако существует множество сторонних инструментов, предлагающих дополнительные возможности, способные обслуживать расширенные или углубленные потребности. В качестве примера приведу систему MPI для Python, позволяющую в сценариях на языке Python использовать стандартный интерфейс передачи сообщений (Message Passing Interface, MPI), дающий возможность организовать взаимодействие между процессами различными способами (подробности ищите в Интернете). Изучение подобных расширений выходит далеко за рамки этой книги, тем не менее большинство расширенных техник, с которыми вы можете встретиться в будущем, также опираются на основы параллельной обработки, которые мы будем исследовать здесь.


Ветвление процессов

Ветвление процессов является традиционным способом организации параллельных вычислений и представляет собой фундаментальную часть инструментального набора Unix. Ветвление процессов - это самый простой способ запуска независимых программ, как отличных, так и не отличных от вызывающей программы. Прием ветвления основан на понятии копирования программ: когда программа вызывает процедуру ветвления, операционная система создает в памяти новую копию этой программы и запускает ее параллельно оригиналу. В некоторых системах исходная программа в действительности не копируется (это слишком дорогостоящая операция), но новая копия работает так, как если бы она действительно была подлинной копией.

После операции ветвления исходный экземпляр программы называется родительским процессом, а копия, созданная с помощью функции os.fork, называется дочерним процессом. Вообще говоря, родитель может воспроизвести любое число потомков, а потомки могут создать собственные дочерние процессы - все ответвленные процессы выполняются независимо и параллельно под управлением операционной системы, и дочерние процессы могут продолжать выполняться даже после завершения родительского процесса.

Возможно, это проще понять на примере, чем в теории. Сценарий Python в примере 5.1 продолжает ответвлять новые дочерние процессы, пока в консоли не будет нажата клавиша q.

Пример 5.1. PP4E\System\Processes\fork1.py

"ответвляет дочерние процессы, пока не будет нажата клавиша ‘q’”

import os

def child():

print(‘Hello from child’, os.getpid())

os._exit(0) # иначе произойдет возврат в родительский цикл

def parent(): while True:

newpid = os.fork() if newpid == 0: child() else:

print(‘Hello from parent’, os.getpid(), newpid) if input() == ‘q’: break

parent()

Инструменты ветвления процессов в Python, находящиеся в модуле os, - это просто тонкие обертки вокруг стандартных средств ветвления из системной библиотеки, используемой также программами на языке C. Запуск нового параллельного процесса осуществляется вызовом функции os.fork. Поскольку эта функция создает копию вызывающей программы, она возвращает различные значения в каждой копии: ноль - в дочернем процессе и числовой идентификатор ID процесса нового потомка - в родительском процессе.

Обычно программы проверяют этот результат, чтобы приступить к выполнению каких-то операций только в дочернем процессе. В этом сценарии, например, функция child вызывается только в дочерних процессах.15

Поскольку ветвление процессов исходно является частью модели программирования в Unix, этот сценарий замечательно будет функционировать в Unix, Linux и в современных версиях Mac OS. К сожалению, этот сценарий не будет работать под управлением стандартной версии Python в Windows, потому что функция fork не стыкуется с моделью Windows. Тем не менее в Windows сценарии на языке Python всегда могут порождать потоки выполнения, а также использовать пакет multiprocessing, описываемый ниже в этой главе. Этот модуль обеспечивает альтернативный и переносимый способ запуска процессов, который позволяет отказаться от приема ветвления процессов в Windows в контекстах, согласующихся с его ограничениями (хотя и за счет необходимости выполнения некоторых низкоуровневых операций).

Однако сценарий из примера 5.1 будет работать в Windows, если использовать версию Python, распространяемую вместе с системой Cygwin (или собранную вами из исходных текстов вместе с библиотеками Cygwin). Cygwin - это бесплатная и открытая система, обеспечивающая полную Unix-подобную функциональность для Windows (описывается ниже, во врезке «Подробнее о Cygwin Python для Windows»). Используя Python для Cygwin в операционной системе Windows, можно использовать прием ветвления процессов, хотя он не полностью соответствует приему ветвления процессов в Unix. Однако, поскольку эта версия Python достаточно близка к рассматриваемым в данной книге, давайте воспользуемся ею, чтобы запустить сценарий:

[C:\...\PP4E\System\Processes]$ python fork1.py Hello from parent 7296 7920 Hello from child 7920

Hello from parent 7296 3988 Hello from child 3988

Hello from parent 7296 6796 Hello from child 6796

q

Эти сообщения представляют три ответвленных дочерних процесса -уникальные идентификаторы всех участвующих процессов получены и выведены с помощью функции os.getpid. Важно отметить, что вызов функции child в дочернем процессе явно завершает его выполнение вызовом функции os._exit. Эту функцию мы более подробно обсудим далее в этой главе, но если ее не вызвать, дочерний процесс продолжит существование после возврата из функции child (не забывайте, что это лишь копия исходного процесса). В этом случае дочерний процесс возвратится в цикл, находящийся в функции parent, и начнет плодить собственных потомков (то есть у родителя появятся внуки). Если удалить вызов выхода и перезапустить сценарий, то для его остановки может понадобиться несколько раз нажать клавишу q, поскольку несколько процессов будут выполнять функцию parent.

В примере 5.1 каждый процесс завершается вскоре после запуска, поэтому перекрытие по времени незначительно. Попробуем сделать нечто более сложное, чтобы лучше продемонстрировать параллельное выполнение нескольких ответвленных процессов. Пример 5.2 запускает 5 копий себя самого, при этом каждая копия считает до 5 с односекундной задержкой между итерациями. Функция time.sleep из стандартной библиотеки просто приостанавливает работу вызывающего процесса на указанное количество секунд (допускается указывать значение с плавающей точкой, чтобы приостановить процесс на дробную часть секунды).

Пример 5.2. PP4E\System\Processes\fork-count.py

Основы ветвления: запустить 5 копий этой программы параллельно оригиналу; каждая копия считает до 5 и выводит счетчик в тот же поток stdout -- при ветвлении копируется память процесса, в том числе дескрипторы файлов; в настоящее время ветвление не действует в Windows без Cygwin: запускайте программы в Windows с помощью функции os.spawnv или пакета multiprocessing; функция spawnv примерно соответствует комбинации функций fork+exec;

import os, time

def counter(count):    #    вызывается в новом процессе

for i in range(count):

time.sleep(1)    #    имитировать работу

print(‘[%s] => %s’ % (os.getpid(), i))

for i in range(5): pid = os.fork()

if pid != 0:    # в родительском процессе:

print(‘Process %d spawned’ % pid) # продолжить цикл else:

counter(5)    #    в дочернем процессе

os._exit(0)    #    вызвать функцию и завершиться

print(‘Main process exiting.’)    # родитель не должен ждать

После запуска этот сценарий сразу запустит 5 процессов и завершит работу. Все 5 ответвленных процессов отображают секундой позже первое показание счетчика и далее - каждую последующую секунду. Обратите внимание, что дочерние процессы продолжают выполняться даже после того, как создавший их родительский процесс завершит свою работу:

[C:\...\PP4E\System\Processes]$ python fork-count.py

Process 4556 spawned

Process 3724 spawned

Process 6360 spawned

Process 6476 spawned

Process 6684 spawned

Main process exiting.

[4556] => 0 [3724] => 0 [6360] => 0 [6476] => 0 [6684] => 0 [4556] => 1 [3724] => 1 [6360] => 1 [6476] => 1 [6684] => 1 [4556] => 2 [3724] => 2 [6360] => 2 [6476] => 2 [6684] => 2

...остальная часть вывода опущена...

Вывод всех этих процессов отображается на одном и том же экране, потому что все они используют стандартный поток вывода (в процессе работы периодически может появляться системное приглашение к вводу). Технически ответвленный процесс получает копию глобальной памяти оригинального процесса, в том числе дескрипторы открытых файлов. Из-за этого глобальные объекты, такие как файлы, начинают работу в дочернем процессе с одними и теми же значениями, поэтому все процессы в этом примере оказываются подключенными к одному и тому же потоку вывода. Но важно помнить, что глобальная память копируется, а не используется совместно, - если дочерний процесс изменит глобальный объект, то изменит только свою копию этого объекта. (Как мы увидим, в потоках выполнения все происходит совсем иначе. Это тема следующего раздела.)


Комбинация fork/exec

В примерах 5.1 и 5.2 дочерние процессы просто вызывали функцию в программе и завершали свою работу. В Unix-подобных платформах ветвление часто служит основой для запуска программ, выполняющихся независимо и совершенно отличных от программы, вызвавшей функцию fork. Так, в примере 5.3 ответвление новых процессов также выполняется, пока не будет нажата клавиша q, но в дочерних процессах вместо вызова функции в том же файле запускается совершенно новая программа.

Пример 5.3. PP4E\System\Processes\fork-exec.py

"запускает программы, пока не будет нажата клавиша ‘q’”

import os

parm = 0 while True: parm += 1 pid = os.fork()

if pid == 0:    # копия процесса

os.execlp(‘python’, ‘python’, ‘child.py’, str(parm)) # подменить прогр. assert False, ‘error starting program’    # возврата быть

# не должно

else:

print(‘Child is’, pid) if input() == ‘q’: break

Если вы достаточно много занимались разработкой программ для Unix, комбинация функций fork/exec наверняка будет вам знакома. Главное, на что следует обратить внимание, - это функция os.execlp. В двух словах, эта функция замещает программу, выполняющуюся в текущем процессе, новой программой. Поэтому комбинация функций os.fork и os.execlp означает запуск нового процесса и запуск новой программы в этом процессе. Другими словами - запуск новой программы параллельно оригинальной.

Формы вызова функции os.exec

Аргументы функции os.execlp определяют программу, которая должна быть выполнена, и аргументы командной строки, которые следует передать ей (доступные в сценариях Python в виде списка sys.argv). В случае успеха начинается выполнение новой программы, и возврата из вызова функции os.execlp не происходит (так как оригинальная программа замещается новой, то возвращаться действительно некуда). Если возврат все-таки происходит, это означает, что произошла ошибка, поэтому в сценарии после вызова функции стоит инструкция assert, при достижении которой всегда возбуждается исключение.

В стандартной библиотеке Python есть несколько разновидностей функции os.exec. Часть из них позволяет настраивать переменные окружения для новой программы, передавать аргументы командной строки в различных форматах и так далее. Все они имеются как в Unix, так и в Windows, и заменяют вызвавшую их программу (то есть интерпретатор Руthon). Всего существует восемь разновидностей функции exec, что может вызывать затруднения в выборе, если не сделать обобщение:

os.execv(program, commandlinesequence)

Базовая «vit-форма функции exec, которой передается имя выполняемой программы вместе со списком или кортежем строк аргументов командной строки, используемых при запуске программы (то есть слов, которые обычно можно ввести в командной строке для запуска программы).

os.execl(program, cmdargl, cmdarg2, ... cmdargN)

Базовая «1»-форма функции exec, которой передается имя выполняемой программы, за которым следуют один или более аргументов командной строки, передаваемых как отдельные аргументы функции. Соответствует вызову функции os.execv(program,    (cmdargl, cmdarg2,

...)).

os.execlp

os.execvp

Символ «р», добавленный к именам execv и execl, означает, что Python станет искать каталог, где находится программа, используя системный путь поиска (то есть переменную PATH).

os.execle

os.execve

Символ «e», добавленный к именам execv и execl, означает, что дополнительный последний аргумент является словарем, содержащим переменные окружения, которые нужно передать программе.

os.execvpe

os.execlpe

Символы «р» и «e», добавленные к базовым именам exec, означают одновременное использование пути поиска и словаря с переменными окружения.

Поэтому, когда сценарий в примере 5.3 вызывает os.execlp, отдельно передаваемые параметры определяют аргументы командной строки для программы, которую нужно выполнить, а слово python отображается в выполняемый файл, находящийся в пути поиска системы (РАТН). Это соответствует выполнению в оболочке команды вида python child.py 1, но каждый раз с разными аргументами командной строки в конце.

Порожденная дочерняя программа

Так же, как при вводе в командной оболочке, строка аргументов, передаваемая функции os.execlp сценарием fork-exec из примера 5.3, запускает еще один файл программы Python, который приводится в примере 5.4.

Пример 5.4. PP4E\System\Processes\child.py

import os, sys

print(‘Hello from child’, os.getpid(), sys.argv[1])

Ниже показано, как этот программный код действует в Linux. Он не сильно отличается от оригинала fork1.py, но в действительности запускает новую программу в каждом ответвленном процессе. Наиболее наблюдательные читатели заметят, что идентификаторы ID дочернего процесса, отображаемые родительской программой и запущенной программой child.py, одинаковые - функция os.execlp просто замещает программу в том же самом процессе:

[C:\...\PP4E\System\Processes]$ python fork-exec.py

Child is 4556

Hello from child 4556 1

Child is 5920

Hello from child 5920 2

Child is 316

Hello from child 316 3

q

В языке Python существуют и другие способы запуска программ, помимо комбинации fork/exec. Например, функции os.system и os.popen и модуль subprocess, с которыми мы познакомились в главах 2 и 3, позволяют выполнять команды оболочки. Функция os.spawnv и пакет multiprocessing, с которым мы познакомимся далее в этой главе, позволяют запускать независимые программы и процессы более переносимым способом. Далее мы увидим, что в некоторых ситуациях модель порождения процессов с помощью пакета multiprocessing может использоваться как переносимая замена функции os.fork (хотя и менее эффективная) и применяться в соединении с функциями os.exec*, показанными здесь, для достижения того же эффекта в стандартной реализации Python для Windows.

Далее в этой главе будут представлены другие примеры ветвления процессов, особенно много - в разделах, посвященных приемам завершения процессов и организации взаимодействий между ними, поэтому мы здесь ограничимся уже приведенными примерами. В следующих главах этой книги мы также рассмотрим другие темы, относящиеся к процессам. Например, в главе 12 мы снова вернемся к приему ветвления процессов, чтобы разобраться с зомби - «мертвыми» процессами, затаившимися в системных таблицах после своего конца. А теперь перейдем к потокам выполнения - к теме, которую по крайней мере некоторые программисты находят значительно менее пугающей...

Подробнее о Cygwin Python для Windows

Как уже упоминалось, функция os.fork присутствует в версии Cygwin Python для Windows. Эта функция отсутствует в стандартной версии Python для Windows, тем не менее вы можете использовать прием ветвления процессов в Windows, если установите и будете использовать Cygwin. Однако реализация функции fork в Cygwin не так эффективна и действует немного не так, как функция fork в настоящих системах Unix.

Cygwin - это бесплатный и открытый пакет, включающий библиотеку, реализующую Unix-подобный прикладной интерфейс для использования в Windows, а также набор инструментов командной строки, реализующих Unix-подобное окружение. Это упрощает применение навыков программирования, полученных в Unix, в операционной системе Windows.

Однако, согласно сборнику часто задаваемых вопросов к этому пакету: «Функция fork() в Cygwin по сути действует, как некопирующая при записи версия fork() (как это было принято в старых версиях Unix). Вследствие этого она может оказаться немного медленнее. В большинстве случаев лучше использовать семейство функций spawn, когда это возможно». Поскольку производительность не является основной целью примеров в этой книге, будем считать представленную версию функции fork в Cygwin удовлетворительной.

В дополнение к функции fork Cygwin предоставляет и другие инструменты Unix, недоступные ни в одной из версий Windows, включая функцию os.mkfifo (обсуждается далее в этой главе). Кроме того, в состав пакета входит компилятор gcc, хорошо знакомый разработчикам программ для Unix и позволяющий выполнять сборку расширений на языке C для Python в Windows. Если вы будете использовать библиотеки Cygwin для сборки своих приложений и вашей версии Python, вы окажетесь очень близки к Unix в Windows.

Однако, как и все сторонние библиотеки, Cygwin привносит дополнительную зависимость. Что самое, пожалуй, важное, - Cygwin в настоящее время выходит под лицензией GNU GPL, которая добавляет дополнительные требования к распространению программ, которые гораздо шире требований лицензии для стандартной версии Python. При использовании библиотеки Cygwin в дополнение к самому интерпретатору Python может потребоваться распространять свои программы с открытыми исходными текстами (впрочем, компания RedHat предлагает возможность «выкупа», освобождающую вас от этого требования). Учтите, что это


достаточно сложный юридический вопрос, и вам необходимо внимательно изучить лицензию на Cygwin, которая может распространять свое действие и на ваши программы. Эта лицензия действительно налагает больше ограничений, чем лицензия на Python (Python распространяется под BSD-подобной лицензией, а не GPL).

Но несмотря на проблемы, связанные с лицензией, Cygwin все-таки может служить отличным способом обрести Unix-подобную функциональность в Windows без установки другой полноценной операционной системы, такой как Linux, - более полного, но и более сложного варианта. За дополнительной информацией обращайтесь по адресу http://cygwin.com или поищите в Интернете по фразе «Cygwin».

Обратите также внимание на пакет multiprocessing из стандартной библиотеки и на семейство функций os.spawn, которые будут рассматриваться далее в этой главе. Эти инструменты предоставляют альтернативный способ запуска параллельно выполняющихся заданий и программ в Unix и Windows, которые не требуют наличия в системе функций fork и exec. Чтобы в Windows запустить простую функцию параллельно основной программе (не в виде внешней программы), можно воспользоваться поддержкой потоков выполнения в стандартной библиотеке, о которой рассказывается далее в этой главе. Потоки выполнения, пакет multiprocessing и функции os.spawn можно использовать в стандартной версии Python для Windows.

Дополнение к четвертому изданию: когда я вносил дополнения в эту главу в феврале 2010 года, в Cygwin официальной версией Python по-прежнему оставалась версия Python 2.5.2. Чтобы получить версию Python 3.1 для Cygwin, ее необходимо собрать из исходных текстов. Если к моменту, когда вы читаете эти строки, данное требование все еще в силе, убедитесь, что в вашем окружении Cygwin установлены компилятор gcc и утилита make, затем загрузите исходные тексты Python с сайта python.org, распакуйте их и соберите Python с помощью следующих команд:

./configure

make

make test

sudo make install

Эти команды установят Python как python3. Ту же процедуру установки можно использовать во всех Unix-подобных системах. В OS X и Cygwin выполняемый файл интерпретатора называется python. exe, в остальных окружениях - python. Вообще говоря, последние


две команды можно не выполнять, если вы пожелаете запускать Python 3.1 из каталога сборки. Обязательно проверьте, не вошла ли версия Python 3.X в стандартный пакет для Cygwin к тому времени, когда вы будете читать эти строки, - при сборке из исходных текстов вам может потребоваться изменить несколько файлов (мне пришлось закомментировать инструкцию #define в файле Modules/main.c), однако эти изменения слишком специфические и необходимость в них может отпасть со временем, поэтому я не буду описывать их здесь.



Потоки выполнения

Потоки выполнения представляют еще один способ запуска операций, выполняемых одновременно. В двух словах, механизм потоков выполнения позволяет запустить функцию (или вызываемый объект другого типа) параллельно основной программе. Иногда их называют «облегченными процессами», потому что они работают параллельно, подобно дочерним процессам, но выполняются в рамках одного и того же процесса. Процессы обычно используются для запуска независимых программ, а потоки выполнения - для решения таких задач, как неблокирующий ввод, и для выполнения продолжительных заданий в программах с графическим интерфейсом. Они также представляют естественную модель реализации алгоритмов, которые можно выразить в терминах независимых заданий. В приложениях, которые выигрывают от параллельной обработки, потоки дают программистам большие выгоды:

Производительность

Поскольку все потоки выполняются в пределах одного процесса, их запуск не сопряжен с высокими накладными расходами, как при копировании процесса в целом. Издержки, связанные с копированием порождаемых дочерних процессов и запуском потоков, могут быть различными в зависимости от платформы, но обычно считается, что потоки обходятся дешевле в смысле производительности.

Простота

Потоки выполнения заметно проще в обращении, особенно если на сцену выходят более сложные аспекты процессов (например, завершение процессов, обмен информацией между процессами и процессы-«зомби», о которых рассказывается в главе 12).

Совместно используемая глобальная память

Кроме того, поскольку потоки выполняются в одном процессе, они используют общую глобальную память процесса. Благодаря этому потоки могут просто и естественно взаимодействовать друг с другом путем чтения и записи данных в глобальной памяти, доступной всем потокам выполнения. Для программиста на языке Python это означает, что глобальные переменные, объекты и их атрибуты и такие компоненты, как импортированные модули, совместно используются всеми потоками выполнения в программе - если, например, в одном потоке выполнения присваивается значение глобальной переменной, ее новое значение увидят все другие потоки выполнения. При обращении к совместно используемым глобальным объектам необходимо проявлять некоторую осторожность, но все равно это обычно проще, чем те средства организации взаимодействий, которые применяются для обмена данными с дочерними процессами и с которыми мы познакомимся ниже в этой главе (например, каналы, потоки ввода-вывода, сигналы, сокеты и так далее). Как и многое в программировании, все вышеизложенное не является универсальной и общепринятой истиной, поэтому вам самим придется взвесить и оценить различия с позиции своих программ и платформ.

Переносимость

Возможно, важнее всего, что приемы работы с потоками выполнения лучше переносятся на другие платформы, чем приемы работы с процессами. На момент написания данной книги функция os.fork вообще не поддерживается стандартной версией Python для Windows, тогда как потоки выполнения поддерживаются. Если вам необходимо обеспечить параллельное выполнение заданий в сценариях на языке Python переносимым способом, и вы не желаете или не можете установить в Windows Unix-подобную библиотеку, такую как Cygwin, потоки выполнения окажутся, скорее всего, лучшим решением. Инструменты для работы с потоками выполнения в Python автоматически учитывают специфические для каждой платформы различия в потоках выполнения и предоставляют единообразный интерфейс для всех операционных систем. Следует отметить, что относительно новый пакет multiprocessing, описываемый далее в этой главе, предлагает еще одно решение проблемы переносимости, которое может использоваться в некоторых случаях.

Так в чем же подвох? Существует три основных потенциальных недостатка, о которых следует знать, прежде чем нырять в свои потоки выполнения:

Вызовы функций и запуск программ

Прежде всего, потоки выполнения не являются способом, по крайней мере, не самым простым способом, запуска других программ. Потоки выполнения предназначены для запуска функций (точнее, любого вызываемого объекта, включая связанные и несвязанные методы), выполняющихся параллельно с основной программой. Как мы видели в предыдущем разделе, после выполнения операции ветвления дочерние процессы могут вызывать функции или запускать новые программы. Естественно, функция, запущенная в отдельном потоке выполнения, также способна запускать другие сценарии с помощью встроенной функции exec и новые программы с помощью таких инструментов, как функции os.system, os.popen и модуль subprocess, особенно если они производят продолжительные вычисления. Но вообще, потоки выполнения предназначены для запуска функций внутри программы.

С практической точки зрения это обычно не рассматривается, как недостаток. Для многих приложений возможность параллельного выполнения функций сама по себе является достаточно мощным приобретением. Например, если вам необходимо реализовать неблокирующий ввод и вывод или избежать «подвисания» графического интерфейса из-за выполнения продолжительной операции, с этим прекрасно справятся потоки выполнения - просто создайте поток выполнения. который запустит функцию, производящую продолжительные вычисления, а основная программа продолжит выполняться независимо.

Синхронизация потоков выполнения и очереди

Во-вторых, тот факт, что потоки выполнения совместно используют объекты и переменные в глобальной памяти процесса, имеет свои положительные и отрицательные стороны - это упрощает организацию взаимодействий, но при этом нам необходимо синхронизировать выполнение различных операций. Как мы увидим далее, даже такие операции, как вывод, могут стать источником конфликтов, потому что они пользуются одним потоком вывода sys.stdout процесса.

К счастью, модуль queue из стандартной библиотеки, описываемый в этом разделе, упрощает решение этой проблемы: на практике многопоточные программы обычно создают один или несколько потоков производителей (рабочих потоков), которые добавляют данные в очередь, и один или более потоков потребителей, которые извлекают данные из очереди и обрабатывают их. Например, в типичной реализации графического интерфейса производители могут загружать или вычислять данные и помещать их в очередь, а потребитель -главный поток выполнения в программе - периодически проверять наличие данных в очереди по событиям от таймера и отображать их в графическом интерфейсе. Поскольку стандартная реализация очередей уже предусматривает возможность работы с несколькими потоками выполнения, программы, структурированные таким способом, автоматически обеспечивают синхронизацию доступа к данным из нескольких потоков выполнения.

Глобальная блокировка интерпретатора (Global Interpreter Lock, GIL) Наконец, как мы узнаем далее в этом разделе, реализация механизма потоков выполнения в Python допускает выполнение виртуальной машиной только одного потока в каждый конкретный момент времени. Потоки выполнения в Python являются настоящими потоками выполнения операционной системы, но каждый поток должен приобрести единственную общедоступную блокировку, когда будет готов к запуску, и каждый поток выполнения может быть вытеснен через короткий промежуток времени (в настоящее время - после выполнения виртуальной машиной некоторого количества инструкций, хотя такой порядок может измениться в Python 3.2).

Вследствие этого потоки выполнения в языке Python не могут выполняться одновременно на нескольких процессорах в многопроцессорных системах. Чтобы воспользоваться преимуществами многопроцессорных систем, можно вместо потоков выполнения воспользоваться механизмом ветвления процессов (объем и сложность программного кода в обоих случаях остаются примерно одинаковыми). Кроме того, части потоков выполнения, реализованные как расширения на языке C, могут выполняться по-настоящему независимо, если они освобождают GIL, чтобы обеспечить возможность выполнения программного кода Python в других потоках. Однако программный код на языке Python не может выполняться одновременно в нескольких потоках.

Преимущество реализации механизма потоков выполнения в Python -высокая производительность. Первые попытки внедрить механизм поддержки потоков выполнения в виртуальную машину привели к двукратному снижению скорости выполнения программ в Windows, и еще большее снижение наблюдалось в Linux. Даже однопоточные программы работали в два раза медленнее.

Даже при том, что наличие GIL снижает практическую пользу потоков выполнения в языке Python, не позволяя использовать преимущества многопроцессорных систем, - потоки выполнения остаются полезным инструментом реализации неблокирующих операций, особенно в приложениях с графическим интерфейсом. Кроме того, новый пакет multiprocessing, с которым мы познакомимся далее, предлагает другое решение этой проблемы - он предоставляет переносимый прикладной интерфейс, похожий на интерфейс механизма потоков выполнения, но основанный на процессах, благодаря чему программы получают простоту обращения с потоками выполнения и преимущества выполнения независимых процессов в многопроцессорных системах.

Несмотря на то, что после прочтения этого обзора у вас могло сложиться иное мнение, я утверждаю, что потоки выполнения в языке Python удивительно просты в использовании. Фактически когда запускается программа, она уже выполняется в потоке, который обычно называется «главным потоком» процесса. Для запуска новых, независимых потоков выполнения в рамках одного и того же процесса в программах на языке Python обычно используется либо низкоуровневый модуль _thread, позволяющий запускать функции в порожденных потоках выполнения, либо высокоуровневый модуль threading, предоставляющий возможность управления потоками выполнения с помощью объектов высокого уровня, созданных на основе классов. Оба модуля также предусматривают инструменты синхронизации доступа к совместно используемым объектам с помощью блокировок.

В данной книге будут исследоваться оба модуля, _thread и threading, и в примерах они будут использоваться взаимозаменяемо. Некоторые программисты на языке Python могли бы порекомендовать всегда использовать модуль threading и оставить модуль _thread в покое. Последний из них ранее назывался thread и в версии 3.X получил название _thread, которое предполагает менее высокий статус модуля. Лично я считаю, что это крайность (это одна из причин, почему в некоторых примерах в данной книге используется конструкция as thread в инструкциях импортирования, позволяющая использовать оригинальное имя модуля в программном коде).

Если только вам не требуются мощные инструменты из модуля threading, выбор между этими двумя модулями является вопросом личных предпочтений, при этом дополнительные требования модуля threading могут считаться ничем не оправданными.

В базовом модуле _thread не используются приемы объектноориентированного программирования, и он очень прост в использовании, как будет показано в примерах этого раздела. Модуль threading лучше подходит для решения более сложных задач, которые требуют сохранения информации в контексте потоков или наблюдения за потоками, но не все многопоточные программы требуют применения дополнительных инструментов, и во многих из них используется достаточно ограниченный набор возможностей многопоточной модели. Фактически сравнение этих модулей напоминает сравнение функции os.walk с классами, реализующими обход дерева, с которыми мы встретимся в главе 6, - оба приема имеют своих сторонников и область применения. Как всегда, не забывайте основное правило Python: не добавляйте сложностей, когда сложности не нужны.


Модуль _thread

Поскольку базовый модуль _thread немного проще, чем более мощный модуль threading, о котором рассказывается далее в этом разделе, начнем с рассмотрения его интерфейсов. Этот модуль предоставляет переносимый интерфейс к любой системе потоков выполнения, имеющейся на вашей платформе: его интерфейсы одинаково работают в Windows, Solaris, SGI и любой другой системе, где установлена реализация pthreads потоков POSIX (включая Linux). Сценарии на языке Python, использующие модуль _thread, будут работать на всех этих платформах без внесения каких-либо изменений в исходный программный код.

Основы использования

Для начала поэкспериментируем со сценарием, демонстрирующим применение основных интерфейсов механизма потоков выполнения. Сценарий в примере 5.5 порождает потоки выполнения, пока в консоли не будет нажата клавиша q, и напоминает по духу (будучи немного проще) сценарий в примере 5.1; но он запускает параллельно потоки, а не дочерние процессы.

Пример 5.5. PP4E\System\Threads\thread1.py

"порождает потоки выполнения, пока не будет нажата клавиша ‘q’”

import _thread def child(tid):

print(‘Hello from thread’, tid) def parent():

i = 0

while True:

i += 1

_thread.start_new_thread(child, (i,)) if input() == ‘q’: break

parent()

В действительности в этом сценарии только две строки имеют отношение к потокам выполнения: инструкция импортирования модуля _thread и вызов функции, создающей поток. Чтобы запустить новый поток выполнения, достаточно просто вызвать функцию _thread.start_new_thread, независимо от того, на какой платформе выполняется программа.16 Эта функция принимает функцию (или другой вызываемый объект) и кортеж аргументов, и запускает новый поток выполнения, в котором будет вызвана указанная функция с переданными аргументами. Это очень похоже на синтаксис вызова function(*args) - и тут, и там принимается необязательный словарь именованных аргументов, - но в данном случае функция начинает выполняться параллельно основной программе.

Сама функция _thread.start_new_thread сразу же возвращает управление вызывающей, не возвращая какого-либо полезного значения, а порожденный ею поток тихо завершается, когда происходит возврат из выполняемой функции (значение, возвращаемое функцией, выполняемой в потоке, просто игнорируется). Кроме того, если выполняемая в потоке функция возбудит исключение, интерпретатор выведет трассировочную информацию и завершит работу потока, но остальная программа продолжит работу. На большинстве платформ при использовании модуля _thread вся программа завершит работу без вывода каких-либо сообщений, когда завершится главный поток (однако, как будет показано далее, при использовании модуля threading может потребоваться предпринять дополнительные действия, если дочерние потоки к этому моменту еще продолжают выполняться).

На практике, однако, использование потоков выполнения в сценариях на языке Python почти тривиально. Запустим эту программу и позволим ей породить несколько новых потоков. На этот раз ее можно выполнять как в Unix-подобных системах, так и в Windows, потому что потоки переносятся лучше, чем ветвление процессов. Ниже приводится пример порождения потоков в Windows:

C:\...\PP4E\System\Threads> python thread1.py

Hello from thread 1

Hello from thread 2

Hello from thread 3

Hello from thread 4

q

Здесь каждое сообщение выводится новым потоком выполнения, который завершается почти сразу после запуска.

Другие способы реализации потоков с помощью модуля _thread

В предыдущем примере сценарий запускает простую функцию, тем не менее в отдельном потоке выполнения можно запустить любой вызываемый объект, благодаря тому что все потоки выполняются в рамках одного и того же процесса. Например, в отдельном потоке можно запустить lambda-функцию или связанный метод объекта (ниже приводится фрагмент сценария thread-alts.py, входящего в состав пакета с примерами к книге):

import _thread    # во всех 3 случаях

# выводится 4294967296

def action(i):    # простая функция

print(i ** 32)

class Power:

def __init__(self, i):

self.i = i

def action(self):    # связанный метод

print(self.i ** 32)

_thread.start_new_thread(action, (2,))    # запуск простой функции

_thread.start_new_thread((lambda: action(2)), ())    # запуск lambda-функции

obj = Power(2)

_thread.start_new_thread(obj.action, ())    # запуск связанного метода

Как будет показано далее в книге, в более крупных примерах, в этой роли особенно полезными оказываются связанные методы - так как они хранят в себе и ссылку на функцию, и ссылку на экземпляр объекта, то они обладают доступом к информации о состоянии и методам класса, которые могут использовать в процессе выполнения внутри потока.

Если смотреть глубже - так как все потоки выполняются в рамках одного и того же процесса, то связанные методы, выполняемые в отдельных потоках, имеют доступ к оригинальному экземпляру объекта, а не к его копии. Следовательно, любые изменения, выполненные в потоке, автоматически будут видимы для всех остальных потоков. Кроме того, связанные методы экземпляров классов, как вызываемые объекты, могут использоваться вместо простых функций, поэтому использование их в потоках выполнения не влечет никаких сложностей. И, как будет показано далее, тот факт, что они являются обычными объектами, позволяет сохранять их в общедоступных очередях.

Запуск нескольких потоков

По-настоящему ощутить всю мощь параллельно выполняющихся потоков можно, только если реализовать в них выполнение продолжительных операций, как мы делали это выше для процессов. Изменим программу fork-count из предыдущего раздела так, чтобы в ней использовались потоки выполнения. В сценарии из примера 5.6 запускается 5 экземпляров функции counter, которые выполняются параллельно в отдельных потоках.

Пример 5.6. PP4E\System\Threads\thread-count.py

основы потоков: запускает 5 копий функции в параллельных потоках; функция time. sleep используется, чтобы главный поток не завершился слишком рано, так как на некоторых платформах это приведет к завершению остальных потоков выполнения; поток вывода stdout - общий: результаты, выводимые потоками выполнения, в этой версии могут перемешиваться произвольным образом.

import _thread as thread, time

def counter(myId, count):    # эта функция выполняется в потоках

for i in range(count):

time.sleep(1)    # имитировать работу

print(‘[%s] => %s’ % (myId, i))

for i in range(5):    # породить 5 потоков выполнения

thread.start_new_thread(counter, (i, 5)) # каждый поток выполняет 5 циклов

time.sleep(6)

print(‘Main thread exiting.’)    # задержать выход из программы

Каждая параллельно выполняющаяся копия функции counter просто считает здесь от нуля до четырех и при каждом увеличении счетчика выводит сообщение в поток стандартного вывода.

Обратите внимание, что в самом конце этот сценарий приостанавливается на 6 секунд. В Windows и в Linux, как было проверено, главный поток не должен завершаться, пока все порожденные потоки не закончили работу, если важно, чтобы они доработали. Если главный поток завершится раньше, все порожденные потоки будут немедленно завершены. Этим потоки выполнения отличаются от процессов, где дочерние процессы продолжают работать после завершения родительского процесса. Если убрать вызов функции sleep в конце сценария, порожденные потоки выполнения будут немедленно завершены, практически сразу же после их запуска.

Может показаться, что так сделано специально, но это необходимо не на всех платформах, и программы обычно реализованы так, чтобы главный поток выполнения продолжал работать столько же, сколько потоки, им запущенные. Например, интерфейс пользователя может начать загрузку файла по протоколу FTP в потоке, но продолжительность операции загрузки значительно короче, чем время жизни самого интерфейса пользователя. Далее в этом разделе мы увидим, как различными способами можно избежать этой паузы с помощью глобальных блокировок и флагов, позволяющих потокам выполнения сигнализировать о своем завершении.

Кроме того, далее мы узнаем, что модуль threading предоставляет метод join, который позволяет дождаться завершения порожденных потоков и не дает программе завершиться до того, пока хотя бы один обычный поток выполнения продолжает работу (что было бы полезно в данном случае, но в других случаях может потребовать выполнения дополнительных операций по принудительному завершению потоков). Пакет multiprocessing, с которым мы встретимся далее в этой главе, также позволяет потомкам продолжать работу после завершения родителя, но это в значительной степени объясняется использованием модели процессов.

Если теперь запустить сценарий из примера 5.6 в Windows 7 под управлением Python 3.1, он выведет:


C:\.

..

PP4E\

\System\Threads> python thread-count.py

[1]

=>

0


[1]

=>

0


[0]

=>

0


[1]

=>

0


[0]

=>

0


[2]

=>

0


[3]

=>

0


[3]

=>

0


[1]

=>

1


[3]

=>

1


[3]

=>

1


[0]

=>

1[2]

=>

1

[3]

=>

1


[0]

=>

1[2]

=>

1

[4]

=>

1


[1]

=>

2


[3]

=>

2[4]

=>

2

[3]

=>

2[4]

=>

2

[0]

=>

2


[3]

=>

2[4]

=>

2

[0]

=>

2


[2]

=>

2


[3]

=>

2[4]

=>

2

[0]

=>

2


[2]

=>

2


...часть вывода опущена...

Main thread exiting.

Полученные результаты, возможно, покажутся вам странными, но так они и должны выглядеть. Данный пример демонстрирует один из наиболее необычных аспектов потоков выполнения. В этом примере результаты 5 потоков, действующих параллельно, перемешались между собой. Поскольку все потоки выполняются в рамках одного и того же процесса, все они совместно используют один и тот же поток стандартного вывода (в терминах языка Python они совместно используют файл sys.stdout, куда выводит текст функция print). В результате вывод потоков выполнения может перемешиваться произвольно. На практике при каждом запуске этого сценария могут быть получены разные результаты. В Python 3 перемешивание вывода стало еще более явным, что, вероятно, обусловлено новой реализацией вывода в файлы.

Из этого следует важный вывод: когда несколько потоков выполнения могут совместно использовать некоторый ресурс, как в данном примере, операции доступа в них должны синхронизироваться, чтобы избежать перекрытия во времени, а как - будет описано в следующем разделе.

Синхронизация доступа к глобальным объектам и переменным

Приятной особенностью потоков выполнения является наличие готового механизма обмена данными между заданиями: объектов и переменных процесса, совместно используемых потоками. Например, поскольку все потоки выполняются в одном и том же процессе, то при изменении глобальной переменной одним потоком это изменение видно всем другим потокам в процессе: и главному потоку, и дочерним. Точно так же потоки могут совместно использовать изменяемые объекты в памяти процесса, при условии, что они хранят ссылки на них (например, полученные в виде аргументов). Это упрощает возможность передачи информации между потоками программы - флаги завершения, объекты с результатами, индикаторы событий и так далее.

Недостатком такой схемы является то, что потоки должны следить за тем, чтобы не изменять глобальные объекты одновременно. Если два потока одновременно изменяют объект, может случиться так, что одно из двух изменений будет утрачено (или, что еще хуже, совместно используемый объект придет в полностью негодное состояние): один поток может приступить к выполнению операций, когда другой поток еще не завершил работу с объектом. Приводит ли это к ошибке, зависит от приложения; иногда проблем вообще не возникает.

Проблемы могут возникнуть и там, где этого совсем не ждешь. Например, файлы и потоки ввода-вывода совместно используются всеми потоками выполнения программы - если несколько потоков выполнения одновременно производят запись в один и тот же поток ввода-вывода, в последнем могут появиться перемежающиеся искаженные данные. Пример 5.6 из предыдущего раздела является простой, но показательной демонстрацией подобного рода конфликтов, которые могут происходить при параллельном выполнении нескольких потоков. Даже простейшие изменения могут пустить все вкривь и вкось, когда есть вероятность одновременного их выполнения. Чтобы исключить подобные ошибки, программы должны управлять доступом к глобальным объектам, чтобы в каждый конкретный момент времени только один поток выполнения мог использовать их.

К счастью, в модуле _thread имеются собственные простые в использовании инструменты синхронизации потоков, выполняющих операции с совместно используемыми объектами. Эти инструменты основаны на понятии блокировки - чтобы изменить совместно используемый объект, потоки приобретают блокировку, производят требуемые изменения и освобождают блокировку для использования в других потоках выполнения. Интерпретатор гарантирует, что в каждый конкретный момент времени только один поток выполнения будет владеть блокировкой, - если запрос на приобретение блокировки поступит в тот момент, когда она удерживается некоторым потоком, запросивший поток будет приостановлен до того момента, пока блокировка не будет освобождена.

Объекты блокировки размещаются в памяти, обрабатываются с помощью простых и переносимых функций из модуля _thread и автоматически отображаются на механизмы блокировки потоков, существующие на соответствующей платформе.

Так, в примере 5.7 с помощью функции _thread.allocate_lock создается объект блокировки, который приобретается и освобождается каждым потоком выполнения перед вызовом функции print, с помощью которой осуществляется вывод в совместно используемый стандартный поток вывода.

Пример 5.7. PP4E\System\Threads\thread-count-mutex.py

синхронизирует доступ к stdout: так как это общий глобальный объект, данные, которые выводятся из потоков выполнения, могут перемешиваться, если не синхронизировать операции

import _thread as thread, time

def counter(myId, count):    # эта функция выполняется в потоках

for i in range(count):

time.sleep(1)    # имитировать работу

mutex.acquire()

print(‘[%s] => %s’ % (myId, i))    # теперь работа функции print

# не будет прерываться

mutex.release()

mutex = thread.allocate_lock()    # создать объект блокировки

for i in range(5):    # породить 5 потоков выполнения

thread.start_new_thread(counter, (i, 5)) # каждый поток выполняет 5 циклов

time.sleep(6)

print(‘Main thread exiting.’)    # задержать выход из программы

В действительности этот сценарий является всего лишь расширенной версией примера 5.6, в которую была добавлена синхронизация обращений к функции print с применением блокировки. Благодаря этому никакие два потока выполнения в этом сценарии не смогут одновременно вызвать функцию print - блокировка гарантирует исключительный доступ к стандартному потоку вывода stdout. Таким образом, мы получаем вывод, сходный с выводом оригинальной версии, за исключением того, что текст на выходе никогда не будет перемешиваться из-за перекрывающихся операций вывода:

C:\...\PP4E\System\Threads> thread-count-mutex.py

[0] => 0

[1] => 0

[3]    => 0

[2] => 0

[4]    => 0

[0] => 1

[1] => 1

[3]    => 1

[2] => 1

[4]    => 1

[0] => 2

[1] => 2

[3]    => 2

[4]    => 2

[2] => 2

[0]    => 3

[1]    => 3

[3]    => 3

[4]    => 3

[2]    => 3

[0]    => 4

[1]    => 4

[3]    => 4

[4]    => 4

[2]    => 4

Main thread exiting.

Порядок, в каком потоки выполнения выводят свои данные, зависит от платформы и по-прежнему может изменяться от запуска к запуску, потому что они выполняются параллельно (в конце концов, потоки выполнения как раз и предназначены для параллельной обработки данных). Но они больше не конфликтуют при выводе текста. Далее в этой главе мы увидим другие случаи использования блокировок - блокировки являются важной составляющей многопоточной модели выполнения.

Ожидание завершения порожденных потоков выполнения

Помимо устранения конфликтов при выводе данных, блокировки модуля потоков имеют и другие очень полезные применения. Они могут использоваться в качестве основы парадигм синхронизации более высокого уровня (например, семафоров) и использоваться как универсальные инструменты взаимодействий между потоками.17 18 19 20 21 22 В частности, в примере 5.8 глобальный список блокировок позволяет установить окончание работы всех дочерних потоков.

Пример 5.8. PP4E\System\Threads\thread-count-wait1.py

использование мьютексов в родительском/главном потоке выполнения для определения момента завершения дочерних потоков, взамен time.sleep; блокирует stdout, чтобы избежать конфликтов при выводе;

import _thread as thread

stdoutmutex = thread.allocate_lock()

exitmutexes = [thread.allocate_lock() for i in range(10)]

def counter(myId, count): for i in range(count): stdoutmutex.acquire() print('[%s] => %s’ % (myId, i)) stdoutmutex.release()

exitmutexes[myId].acquire()    # сигнал главному потоку

for i in range(10):

thread.start_new_thread(counter, (i, 100))

for mutex in exitmutexes:

while not mutex.locked(): pass print(‘Main thread exiting.’)

Для проверки состояния блокировки можно использовать ее метод locked. Главный поток создает по одной блокировке для каждого дочернего потока, помещая их в глобальный список exitmutexes (не забывайте, что функция потока использует глобальную область совместно с главным потоком). По завершении каждый поток приобретает свою блокировку в списке, а главный поток просто ждет, когда будут приобретены все блокировки. Это значительно более точный подход, чем просто приостанавливать работу на определенное время, пока выполняются дочерние потоки, в надежде обнаружить после возобновления, что все они будут завершены.

В зависимости от операций, выполняемых в потоках, все это можно организовать еще проще: поскольку потоки в любом случае совместно используют глобальную память, того же результата можно добиться с помощью простого глобального списка целых чисел, а не блокировок. В примере 5.9 пространство имен модуля (область видимости), как и прежде, совместно используется программным кодом верхнего уровня и функцией, выполняемой в потоке. Имя exitmutexes ссылается на один и тот же объект списка в главном потоке и во всех порождаемых потоках. По этой причине изменения, производимые в потоке, видны в главном потоке без использования лишних блокировок.

Пример 5.9. PP4E\System\Threads\thread-count-wait2.py

использование простых глобальных данных (не мьютексов) для определения момента завершения всех потоков в родительском/главном потоке; потоки совместно используют список, но не его элементы, при этом предполагается, что после создания список не будет перемещаться в памяти import _thread as thread stdoutmutex = thread.allocate_lock() exitmutexes = [False] * 10

def counter(myId, count): for i in range(count): stdoutmutex.acquire() print(‘[%s] => %s’ % (myId, i)) stdoutmutex.release()

exitmutexes[myId] = True    # сигнал главному потоку

for i in range(10):

thread.start_new_thread(counter, (i, 100))

while False in exitmutexes: pass print(‘Main thread exiting.’)

Вывод этого сценария похож на вывод предыдущего - 10 потоков параллельно ведут счет до 100 и в процессе работы синхронизируют свои обращения к функции print. Фактически оба последних сценария с потоками-счетчиками производят вывод, в общем аналогичный первоначальному сценарию thread_count.py, но данные при выводе в stdout не повреждаются, значения счетчиков больше и отличается случайный порядок вывода строк. Основное отличие состоит в том, что главный поток завершает работу сразу после (и не раньше!) порожденных дочерних потоков:

C:\...\PP4E\System\Threads> python thread-count-wait2.py ...часть вывода удалена...

[4]

=>

98

[6]

=>

98

[8]

=>

98

[5]

=>

98

[0]

=>

99

[7]

=>

98

[9]

=>

98

[1]

=>

99

[3]

=>

99

[2]

=>

99

[4]

=>

99

[6]

=>

99

[8]

=>

99

[5]

=>

99


[7] => 99 [9] => 99

Main thread exiting.

Альтернативные приемы: циклы занятости, аргументы и менеджеры контекста

Обратите внимание, что главные потоки выполнения в двух последних сценариях в конце выполняют цикл ожидания, который может заметно снизить производительность в критически важных приложениях. В таких ситуациях достаточно просто добавить в цикл ожидания вызов функции time.sleep, чтобы оформить паузу между проверками и освободить процессор для других заданий: эта функция будет приостанавливать только вызывающий поток выполнения (в данном случае - главный поток). Можно также попробовать добавить вызов функции sleep в функцию, которая выполняется в потоках, чтобы сымитировать выполнение продолжительных операций.

Для единообразия вместо использования глобальной области видимости можно было бы также организовать передачу блокировки в виде аргумента функции, которая выполняется в потоках. В этом случае все потоки выполнения будут ссылаться на один и тот же объект блокировки, потому что все они являются частью одного и того же процесса. Память процесса, занятая объектом, является памятью, совместно используемой потоками независимо от того, как будет получена ссылка на этот объект (через глобальные переменные, через аргументы функции, через атрибуты объектов или каким-либо другим способом).

И еще - чтобы гарантировать освобождение блокировки при выходе потока выполнения из критического блока, можно использовать инструкцию with, как мы делали это в предыдущей главе, чтобы обеспечить закрытие файлов. Менеджер контекста блокировки приобретает блокировку при входе в инструкцию with и освобождает ее при выходе из тела инструкции, независимо от того, возникло исключение или нет. Этот прием позволяет сэкономить одну строку программного кода и дополнительно гарантирует освобождение блокировки в ситуациях, когда возможно появление исключения. Все эти приемы реализованы в примере 5.10, представляющем улучшенную версию нашего сценария с потоками-счетчиками.

Пример 5.10. PP4E\System\Threads\thread-count-wait3.py

объект мьютекса, совместно используемый всеми потоками выполнения, передается функции в виде аргумента; для автоматического приобретения/освобождения блокировки используется менеджер контекста; чтобы избежать излишней нагрузки в цикле ожидания, и для имитации выполнения продолжительных операций добавлен вызов функции sleep

import _thread as thread, time

stdoutmutex = thread.allocate_lock() numthreads = 5

exitmutexes = [thread.allocate_lock() for i in range(numthreads)]

def counter(myId, count, mutex): # мьютекс передается в аргументе for i in range(count):

time.sleep(1 / (myId+1))    # различные доли секунды

with mutex:    # приобретает/освобождает блокировку: with

print(‘[%s] => %s’ % (myId, i))

exitmutexes[myId].acquire() # глобальный список: сигнал главному потоку

for i in range(numthreads):

thread.start_new_thread(counter, (i, 5, stdoutmutex))

while not all(mutex.locked() for mutex in exitmutexes): time.sleep(0.25) print(‘Main thread exiting.’)

Различные времена ожидания для разных потоков выполнения делают их более независимыми:

C:\...\PP4E\System\Threads> thread-count-wait3.py

[4] => 0

[3]    => 0

[2] => 0

[4]    => 1

[1] => 0

[3]    => 1

[4]    => 2

[2] => 1

[3]    => 2

[4]    => 3 [4] => 4

[0] => 0

[1] => 1

[2] => 2

[3] => 3 [3] => 4 [2] => 3 [1] => 2 [2] => 4 [0] => 1 [1] => 3 [1] => 4 [0] => 2 [0] => 3 [0] => 4

Main thread exiting.

Конечно, потоки выполнения могут решать гораздо более сложные задачи, чем простой подсчет. Более практичный пример использования глобальных данных мы рассмотрим в разделе «Добавляем пользовательский интерфейс» в главе 13, где они будут играть роль сигналов главному потоку, управляющему графическим интерфейсом на основе библиотеки tkinter, о завершении дочерним потоком передачи данных по сети, а также в главе 10, в примере реализации модуля threadtools, и в главе 14, в примере приложения PyMailGUI, для отображения результатов отправки электронной почты в графическом интерфейсе (дополнительные указания по этой теме вы найдете в разделе «Графические интерфейсы и потоки выполнения: предварительное знакомство» ниже, в этой главе). Возможность совместного доступа к глобальным данным из потоков выполнения также является основой организации очередей, которые обсуждаются далее в главе, - каждый поток выполнения может извлекать или добавлять данные, используя один и тот же общий объект очереди.


Модуль threading

В стандартную библиотеку Python входят два модуля для работы с потоками: _thread - основной низкоуровневый интерфейс, который демонстрировался до сих пор, и threading - интерфейс более высокого уровня, основанный на объектах и классах. Внутри модуль threading использует модуль _thread для реализации объектов, представляющих потоки и инструменты синхронизации. Он в какой-то мере основан на подмножестве модели потоков выполнения языка Java, но есть различия, которые заметят только программисты Java.23 В примере 5.11 приводится еще одна, последняя версия нашего сценария с потоками-счетчиками, демонстрирующая интерфейсы этого нового модуля.

Пример 5.11. PP4E\System\Threads\thread-classes.py

экземпляры класса Thread, сохраняющие информацию о состоянии и обладающие методом run() для запуска потоков выполнения; в реализации используется высокоуровневый и Java-подобный метод join класса Thread модуля threading (вместо мьютексов и глобальных переменных), чтобы известить главный родительский поток о завершении дочерних потоков; подробности о модуле threading ищите в руководстве по стандартной библиотеке;

import threading

class Mythread(threading.Thread):    # подкласс класса Thread

def __init__(self, myId, count, mutex):

self.myId = myId

self.count = count    # информация для каждого потока

self.mutex = mutex    # совместно используемые объекты,

threading.Thread.__init__(self) # вместо глобальных переменных

def run(self):    # run реализует логику потока

for i in range(self.count):    # синхронизировать доступ к stdout

with self.mutex:

print(‘[%s] => %s’ % (self.myId, i))

stdoutmutex = threading.Lock()    # то же, что и thread.allocate_lock()

threads = []

for i in range(10):

thread = Mythread(i, 100, stdoutmutex) # создать/запустить 10 потоков thread.start()    # вызвать метод run потока

threads.append(thread)

for thread in threads:

thread.join()    # ждать завершения потока

print(‘Main thread exiting.’)

Этот сценарий производит точно такой же вывод, как и его предшественники (и снова строки случайно распределены по времени, в зависимости от используемой платформы):

C:\...\PP4E\System\Threads> python thread-classes.py ...часть вывода удалена...

[4]    => 98

[8]    => 97

[9]    => 97

[5]    => 98

[3]    => 99

[6]    => 98

[7]    => 98

[4]    => 99

[8]    => 98

[9]    => 98

[5]    => 99

[6]    => 99

[7]    => 99

[8]    => 99

[9]    => 99

Main thread exiting.

Использование модуля threading заключается в основном в определении новых классов. Потоки в этом модуле реализуются с помощью объекта Thread - класса Python, который наследуется и специализируется в каждом приложении путем реализации метода run, определяющего действия, выполняемые потоком. Например, в данном сценарии создается подкласс Mythread класса Thread, метод run которого будет вызываться родительским классом Thread в новом потоке после создания экземпляра класса Mythread и вызова его метода start.

Иными словами, этот сценарий просто обеспечивает методы, предполагаемые структурой класса Thread. Преимущество этого приема, требующего создания большего объема программного кода, заключается в том, что он обеспечивает «бесплатный» доступ к информации о состоянии каждого потока в отдельности (в виде атрибутов экземпляра) и к ряду дополнительных инструментов для работы с потоками, предоставляемых данной структурой. К примеру, используемый в конце сценария метод Thread.join ожидает завершения (по умолчанию) потока выполнения - этот метод можно использовать, чтобы предотвратить завершение главного потока до того, как завершится дочерний поток, и отказаться от вызова функции time.sleep, глобальных блокировок и переменных, использовавшихся в предыдущих примерах с потоками.

Кроме того, для синхронизации доступа к стандартному потоку вывода в примере 5.11 используется конструктор threading.Lock (хотя в текущей реализации это просто синоним конструктора _thread.allocate_lock). Модуль threading предоставляет и другие структуры классов, но они не влияют на общую картину многопоточной модели параллельной обработки данных.

Другие способы реализации потоков выполнения с помощью модуля threading

Класс Thread можно также использовать для запуска простых функций и вызываемых объектов других типов, вообще не создавая подклассы. Метод run класса Thread по умолчанию просто вызывает объект, переданный конструктору в аргументе target, со всеми дополнительными аргументами, переданными в аргументе args (который по умолчанию является пустым списком ()). Это позволяет использовать класс Thread для запуска простых функций, хотя такая форма вызова ненамного проще использования модуля _thread. Например, в следующих фрагментах демонстрируются четыре различных способа запуска одного и того же потока (смотрите сценарии four-threads*.py в дереве примеров; вы можете запустить все четыре потока в одном сценарии, но при этом вам понадобится синхронизировать обращения к функции print, чтобы избежать смешивания выводимых данных):

import threading, _thread def action(i): print(i ** 32)

#    подкласс, хранящий собственную информацию о состоянии class Mythread(threading.Thread):

def __init__(self, i): self.i = i

threading.Thread.__init__(self)

def run(self):    # переопределить метод run

print(self.i ** 32)

Mythread(2).start()    # метод start вызовет метод run()

#    передача простой функции

thread = threading.Thread(target=(lambda: action(2))) # run вызовет target thread.start()

#    то же самое, но без lambda-функции,

#    сохраняющей информацию о состоянии в образуемом ею замыкании threading.Thread(target=action, args=(2,)).start() # вызываемый объект

# и его аргументы

#    с помощью модуля thread

_thread.start_new_thread(action, (2,))    # полностью процедурный интерфейс

Как правило, выбирать реализацию потоков на основе классов имеет смысл, когда потоки должны сохранять информацию о своем состоянии или когда желательно использовать какие-либо из многочисленных преимуществ ООП. Однако классы потоков выполнения необязательно должны наследовать класс Thread. Фактически, как и при использовании модуля _thread, реализация потоков в модуле threading может принимать в аргументе target вызываемые объекты любого типа. При объединении с такими приемами, как связанные методы и вложенные области видимости, различия между приемами программирования становятся еще менее выраженными:

#    обычный класс с атрибутами, ООП class Power:

def __init__(self, i): self.i = i def action(self):

print(self.i ** 32)

obj = Power(2)

threading.Thread(target=obj.action).start() # запуск связанного метода

#    вложенная область видимости, для сохранения информации о состоянии def action(i):

def power():

print(i ** 32) return power

threading.Thread(target=action(2)).start()    # запуск возвращаемой функции

#    запуск обоих вариантов с помощью модуля _thread

_thread.start_new_thread(obj.action, ())    # запуск вызываемого объекта

_thread.start_new_thread(action(2), ())

Как видите, интерфейс модуля threading такой же гибкий, как и сам язык Python.

Еще раз о синхронизации доступа к совместно используемым объектам и переменным

Выше мы видели, что вызовы функции print в потоках выполнения необходимо синхронизировать с помощью блокировок, чтобы избежать смешивания выводимых данных, потому что стандартный поток вывода совместно используется всеми потоками выполнения. Строго говоря, потоки выполнения должны синхронизировать любые операции изменения совместно используемых объектов и переменных. В зависимости от целей программы в число этих объектов могут входить:

•    Изменяемые объекты в памяти (объекты, ссылки на которые передаются потокам или приобретаются каким-то иным способом, продолжительность существования которых превышает время работы потоков)

•    Переменные в глобальной области видимости (изменяемые переменные, объявленные за пределами функций и классов, выполняемых в потоках)

•    Содержимое модулей (для каждого модуля существует всего одна копия записи в системной таблице модулей)

Даже при работе с простыми глобальными переменными может потребоваться координация действий, если есть вероятность одновременных попыток их изменения, как показано в примере 5.12.

Пример 5.12. PP4E\System\Threads\thread-add-random.py

"выводит различные результаты при каждом запуске под Windows 7”

import threading, time count = 0

def adder(): global count

count = count + 1    # изменяет глобальную переменную

time.sleep(0.5) # потоки выполнения совместно используют count = count + 1    # глобальные объекты и переменные

threads = []

for i in range(100):

thread = threading.Thread(target=adder, args=())

thread.start()

threads.append(thread)

for thread in threads: thread.join() print(count)

Этот пример порождает 100 потоков выполнения, каждый из которых дважды изменяет одну и ту же глобальную переменную (с задержкой между ними, чтобы обеспечить чередование операций в различных потоках). При каждом запуске в Windows 7 этот сценарий будет воспроизводить различные результаты:

C:\...\PP4E\System\Threads> thread-add-random.py

189

C:\...\PP4E\System\Threads> thread-add-random.py

200

C:\...\PP4E\System\Threads> thread-add-random.py

194

C:\...\PP4E\System\Threads> thread-add-random.py

191

Это объясняется тем, что потоки выполнения произвольно перекрываются друг с другом по времени: интерпретатор не гарантирует, что инструкции - даже такие простые инструкции присваивания, как в данном примере, - будут выполнены полностью до того, как управление перейдет другому потоку выполнения (то есть они не являются атомарными). Когда один поток изменяет значение глобальной переменной, он может получить промежуточный результат, произведенный другим потоком. Как следствие этого мы наблюдаем непредсказуемое поведение. Чтобы заставить этот сценарий работать корректно, необходимо снова воспользоваться блокировками для синхронизации изменений - в какой бы момент мы ни запускали сценарий из примера 5.13, он всегда будет выводить число 200.

Пример 5.13. PP4E\System\Threads\thread-add-synch.py

"всегда выводит 200 - благодаря синхронизации доступа к глобальному ресурсу”

import threading, time count = 0

def adder(addlock):    # совместно используемый объект блокировки

global count

with addlock:    # блокировка приобретается/освобождается

count = count + 1 # автоматически time.sleep(0.5)

with addlock:    # в каждый конкретный момент времени

count = count + 1 # только 1 поток может изменить значение переменной

a