• Кроме того, подписи, предшествующие примерам с листингами программного кода из файлов, сообщают, где находится файл в пакете с примерами. Так, подпись к примеру 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) # выведет: 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 действует точно так же, как и прежде:

Sue Jones: hardware, 40000> 40000 Jones Bob Smith: None, 0.0>

Sue Jones: hardware, 44000.0>

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

Interactive Page

Enter your name:

Обратите внимание, что в атрибуте 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('Reply Page’) # html-разметка ответа if not ‘user’ in form:

print(‘

Who are you?

’) else:

print(‘

Hello %s!

' % cgi.escape(form[‘user’].value))

И если все пройдет как надо, мы получим в ответ страницу, изображенную на рис. 1.11, которая, по сути, просто выводит данные, введенные на странице с формой ввода. Страница на этом рисунке была воспроизведена разметкой HTML, которую вывел CGI-сценарий на стороне сервера. В данном случае имя пользователя сначала было передано со стороны клиента на сервер, а затем обратно, возможно преодолев по пути многие сети и километры. Безусловно, это очень небольшой веб-сайт, но одни и те же принципы действуют для любого сайта, выводит он просто введенные данные или является полноценным сайтом электронного бизнеса.

Рис. 1.11. Страница ответа, воспроизведенная сценарием egi101.py в ответ на получение формы ввода

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

Обратите также внимание, что CGI-сценарий в примере 1.31 выводит не полную разметку HTML: здесь отсутствуют теги и , которые можно увидеть в примере 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’Reply Page\n

Hello Sue Smith!

\n'

>>> urlopen('http://localhost/cgi-bin/cgi101.py').read()

b’Reply Page\n

Who are you?

\n’

>>> urlopen('http://localhost/cgi-bin/cgi101.py?user=Bob').read()

b’Reply Page\n

Hello Bob!

\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 Smith, - последовательность символов в середине преобразуется этой функцией в последовательность , благодаря чему исключается влияние этой последовательности на фактическую разметку 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

People Input Form

Key
Name
Age
Job
Pay

Обработкой формы и других запросов будет заниматься 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 = """

People Input Form

key

$ROWS$

# вставить разметку html с данными в позицию $ROWS$

rowhtml = ‘ %is\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, ‘?’) # недопустимое значение

Загрузка...