find.find; вызывает visitfile для каждой строки в результатах, полученных вызовом функции find.find с шаблоном “*”; ##############################################################################
import os, sys listonly = False
textexts = [‘.py’, ‘.pyw’, ‘.txt’, ‘.c’, ‘.h’] # игнорировать двоичные файлы
def searcher(startdir, searchkey): global fcount, vcount fcount = vcount = 0
for (thisDir, dirsHere, filesHere) in os.walk(startdir):
for fname in filesHere: # для каждого некаталога
fpath = os.path.join(thisDir, fname) # fname не содержит пути visitfile(fpath, searchkey)
def visitfile(fpath, searchkey): # для каждого некаталога
global fcount, vcount # искать строку
print(vcount+1, ‘=>’, fpath) # пропустить защищенные файлы
try:
if not listonly:
if os.path.splitext(fpath)[1] not in textexts: print(‘Skipping’, fpath) elif searchkey in open(fpath).read():
input(‘%s has %s’ % (fpath, searchkey)) fcount += 1
except:
print(‘Failed:’, fpath, sys.exc_info()[0]) vcount += 1
if __name__ == ‘__main__’:
searcher(sys.argv[1], sys.argv[2])
print(‘Found in %d files, visited %d’ % (fcount, vcount))
Функционально этот сценарий делает примерно то, что мы получили бы, вызвав его функцию visitfile для всех строк, сгенерированных нашей функцией find.find с шаблоном «*». Но поскольку эта версия настроена на поиск по содержимому файлов, она лучше соответствует своей цели. В действительности это сходство обусловлено лишь использованием шаблона «*», который вынуждает find.find выполнить обход всех файлов, а это, собственно, то, чем занята новая функция searcher. Инструмент поиска хорошо подходит для выбора файлов определенного типа, при этом преимущество данного сценария состоит в возможности произвести определенные действия непосредственно в процессе обхода.
При запуске в виде самостоятельного сценария ключ поиска передается в командной строке, а при импортировании клиент вызывает функцию searcher непосредственно. Например, чтобы найти все вхождения строки в дереве примеров для книги, выполните в команду в оболочке DOS или Unix, как показано ниже:
C:\\PP4E> Tools\search_all.py . mimetypes
1 => .\LaunchBrowser.py
2 => .\Launcher.py
3 => .\Launch_PyDemos.pyw
4 => .\Launch_PyGadgets_bar.pyw
5 => .\__init__.py
6 => .\__init__.pyc
Skipping .\__init__.pyc
7 => .\Preview\attachgui.py
8 => .\Preview\bob.pkl Skipping .\Preview\bob.pkl
...множество строк опущено: ожидает нажатия клавиши Enter после обнаружения каждого совпадения...
Found in 2 files, visited 184
Сценарий выводит список всех проверяемых им файлов, сообщает о пропущенных файлах (имена с расширениями, отсутствующими в переменной textexts, которые, как предполагается, являются двоичными файлами) и останавливается, ожидая нажатия клавиши Enter после вывода сообщения о нахождении в файле искомой строки. Точно так же сценарий search_all работает и при импортировании, но не выводит итоговой строки со статистикой (функции fcount и vcount находятся в модуле, и их также можно импортировать, чтобы получить итоговые сведения):
C:\...\PP4E\dev\Examples\PP4E> python >>> import Tools.search_all
>>> search_all.searcher(r’C:\temp\PP3E\Examples’, 'mimetypes’)
... множество строк опущено: останавливается 8раз в ожидании нажатия клавиши Enter...
>>> search_all.fcount, search_all.vcount # совпадений, файлов (8, 1429)
Каким бы образом ни запускался этот сценарий, он находит все вхождения искомой строки в целом дереве каталогов - например, изменившееся имя файла примера, объекта или каталога. Это именно то, что мне было необходимо, - по крайней мере, я так думал, пока не погрузился в дальнейшие размышления, пытаясь отыскать более полные и лучше структурированные решения, о которых рассказывается в следующем разделе.
Обязательно ознакомьтесь с обсуждением регулярных выражений в главе 19. В данном случае сценарий search_all пытается отыскать в каждом файле простую строку, применяя простое выражение проверки на вхождение, однако его легко можно было усовершенствовать, добавив возможность поиска по регулярному выражению (грубо говоря, для этого достаточно заменить оператор in вызовом метода поиска объекта регулярного выражения). Разумеется, это усовершенствование покажется вам намного проще, когда мы узнаем, как это делается.
Обратите также внимание на список textexts в примере 6.17.
В нем перечислены все допустимые расширения текстовых файлов. Решение можно сделать более универсальным и надежным, если использовать логику модуля mimetypes, с которым мы познакомимся ближе к концу этой главы, который позволяет делать предположения о типе содержимого файла по его имени. Однако список допустимых расширений обеспечивает более точное управление, и его вполне достаточно для деревьев, поиск по которым я осуществлял с помощью этого сценария.
Наконец, обратите внимание, что для простоты во всех примерах поиска по дереву каталогов в этой главе предполагается, что текстовые файлы содержат текст Юникода, закодированный с применением кодировки по умолчанию, используемой платформой. Чтобы избежать ошибок при декодировании, в примерах можно было бы открывать текстовые файлы в двоичном режиме, но при этом результаты поиска могут оказаться неточными, если сравниваемые строки байтов будут закодированы с применением различных схем кодирования. Более удачное решение вы найдете в реализации утилиты «grep» в примере приложения PyEdit с графическим интерфейсом, где обеспечивается возможность применения указанной пользователем кодировки и пропускаются те текстовые или двоичные файлы, попытка декодирования которых завершается неудачей.
Visitor: обход каталогов «++»
Лень - двигатель прогресса. Имея в своем распоряжении переносимый сценарий search_all из примера 6.17, я мог точнее находить файлы, которые было необходимо отредактировать при изменении содержимого или структуры дерева примеров в книге. Первоначально я в одном окне запускал search_all, чтобы отобрать подозрительные файлы, и вручную редактировал каждый из них в другом окне.
Однако довольно скоро и это стало утомительным. Вводить вручную имена файлов в командах запуска редактора - занятие невеселое, особенно если нужно отредактировать много файлов. Поскольку у меня всегда найдется более интересное занятие, чем десятки раз запускать редактор вручную, я стал думать, как автоматически запускать редактор для каждого подозрительного файла.
К сожалению, сценарий search_all просто выводит полученные результаты на экран. Хотя этот текст можно перехватить и проанализировать с помощью другой программы, запускаемой функцией os.popen, проще может оказаться подход, когда редактор запускается прямо во время поиска, но для этого могут потребоваться большие изменения в реализации сценария. Здесь мне пришли в голову три мысли.
Избыточность
Написав несколько утилит обхода каталогов, я понял, что каждый раз я снова и снова пишу однотипный программный код. Обход можно упростить еще больше, если скрыть детали под оболочкой и тем самым упростить повторное использование решения. Инструмент os.walk позволяет избежать необходимости писать рекурсивные функции, но при его использовании выполняются лишние действия (например, присоединение имен каталогов, вывод трассировочной информации).
Расширяемость
Исходя из прошлого опыта, очевидно, что в долгосрочной перспективе легче добавлять новые возможности в универсальный механизм поиска в каталогах в виде внешних компонентов, чем менять программный код исходного сценария. Редактирование файлов могло быть одним из возможных расширений (а что вы думаете об автоматизации операции замены текста?), поэтому предпочтительнее выглядит более обобщенное и настраиваемое решение, допускающее возможность многократного использования. Функция os.walk достаточно проста в использовании, но прием, основанный на циклах, не так хорошо поддается настройке, как использование классов.
Инкапсуляция
Опираясь на прошлый опыт, я также знаю, что всегда желательно стараться максимально скрывать детали реализации инструментов от программ. Функция os.walk скрывает свою рекурсивную природу, тем не менее она предлагает весьма специфический интерфейс, который вполне может измениться в будущем. Подобные изменения имели место в прошлом - ближе к концу этого раздела я расскажу, как из версии Python 3.X был исключен один из инструментов обхода деревьев, что сразу же привело к нарушениям в работе программного кода, использующего его. Было бы лучше скрыть подобные зависимости за более нейтральным интерфейсом, чтобы клиентский программный код не приходил в негодность, как только нам потребуется внести изменения в реализацию нашего инструмента.
Конечно, если вы в достаточной мере изучили язык Python, то вы не можете не понимать, что все эти цели указывают на необходимость использования объектно-ориентированного подхода к реализации обхода и поиска. В примере 6.18 приводится одна из возможных реализаций этих целей. Этот модуль экспортирует универсальный класс FileVisitor, который в основном служит лишь оболочкой для os.walk, облегчающей использование и расширение, а также базовый класс SearchVisitor, обобщающий идею поиска в каталоге.
Сам по себе класс SearchVisitor делает то же самое, что делал сценарий search_all, но кроме этого, он открывает новые возможности по на-
стройке процедуры поиска - какие-то черты его поведения могут модифицироваться путем перегрузки методов в подклассах. Более того, его базовая логика поиска может быть использована везде, где требуется поиск: достаточно просто определить подкласс, в котором будут добавлены специфические для поиска расширения. То же относится и к классу FileVisitor - переопределяя его методы и используя его атрибуты, можно внедряться в процесс обхода деревьев, используя приемы ООП. Это обычное дело в программировании - как только вы начинаете достаточно часто решать одни и те же тактические задачи, они наталкивают вас на подобные стратегические размышления.
Пример 6.18. PP4E\Tools\visitor.py
############################################################################## Тест: “python ...\Tools\visitor.py dir testmask [строка]”. Использует классы и подклассы для сокрытия деталей использования функции os.walk при обходе и поиске; testmask - битовая маска, каждый бит в которой определяет тип самопроверки; смотрите также: подклассы visitor_*/.py; вообще подобные
фреймворки должны использовать псевдочастные имена вида __X, однако в данной
реализации все имена экспортируются для использования в подклассах и клиентами; переопределите метод reset для поддержки множественных, независимых объектов-обходчиков, требующих обновлений в подклассах; ##############################################################################
import os, sys class FileVisitor:
Выполняет обход всех файлов, не являющихся каталогами, ниже startDir (по умолчанию ‘.’); при создании собственных обработчиков файлов/каталогов переопределяйте методы visit*; аргумент/атрибут context является необязательным и предназначен для хранения информации, специфической для подкласса; переключатель режима трассировки trace: 0 -нет трассировки, 1 - подкаталоги, 2 - добавляются файлы
def __init__(self, context=None, trace=2):
self.fcount = 0 self.dcount = 0 self.context = context self.trace = trace
def run(self, startDir=os.curdir, reset=True): if reset: self.reset()
for (thisDir, dirsHere, filesHere) in os.walk(startDir): self.visitdir(thisDir)
for fname in filesHere: # для некаталогов
fpath = os.path.join(thisDir, fname) # fname не содержит пути self.visitfile(fpath)
def reset(self): # используется обходчиками,
self.fcount = self.dcount = 0 # выполняющими обход независимо
def visitdir(self, dirpath): # вызывается для каждого каталога
self.dcount += 1 # переопределить или расширить
if self.trace > 0: print(dirpath, ‘...’)
def visitfile(self, filepath): # вызывается для каждого файла
self.fcount += 1 # переопределить или расширить
if self.trace > 1: print(self.fcount, ‘=>’, filepath)
class SearchVisitor(FileVisitor):
Выполняет поиск строки в файлах, находящихся в каталоге startDir и ниже; в подклассах: переопределите метод visitmatch, списки расширений, метод candidate, если необходимо; подклассы могут использовать testexts, чтобы определить типы файлов, в которых может выполняться поиск (но могут также переопределить метод candidate, чтобы использовать модуль mimetypes для определения файлов с текстовым содержимым: смотрите далее)
skipexts = []
testexts = [‘.txt’, ‘.py’, ‘.pyw’, ‘.html’, ‘.c’, ‘.h’] # допустимые расш. #skipexts = [‘.gif’, ‘.jpg’, ‘.pyc’, ‘.o’, ‘.a’, ‘.exe’] # или недопустимые
# расширения
def __init__(self, searchkey, trace=2):
FileVisitor.__init__(self, searchkey, trace) self.scount = 0
def reset(self): # в независимых обходчиках
self.scount = 0
def candidate(self, fname): # переопределить, если желательно
ext = os.path.splitext(fname)[1] # использовать модуль mimetypes if self.testexts:
return ext in self.testexts # если допустимое расширение else: # или, если недопустимое
return ext not in self.skipexts # расширение
def visitfile(self, fname): # поиск строки
FileVisitor.visitfile(self, fname) if not self.candidate(fname):
if self.trace > 0: print(‘Skipping’, fname) else:
text = open(fname).read() # ‘rb’ для недекодируемого текста
if self.context in text: # или text.find() != -1
self.visitmatch(fname, text) self.scount += 1
def visitmatch(self, fname, text): # обработка совпадения
print(‘%s has %s’ % (fname, self.context)) # переопределить
if __name__ == ‘__main__’:
# логика самотестирования dolist = 1
dosearch = 2 # 3 = список и поиск
donext = 4 # при добавлении следующего теста
def selftest(testmask): if testmask & dolist:
visitor = FileVisitor(trace=2) visitor.run(sys.argv[2]) print(‘Visited %d files and %d dirs’ %
(visitor.fcount, visitor.dcount))
if testmask & dosearch:
visitor = SearchVisitor(sys.argv[3], trace=0)
visitor.run(sys.argv[2])
print(‘Found in %d files, visited %d’ %
(visitor.scount, visitor.fcount))
selftest(int(sys.argv[1])) # например, 3 = dolist | dosearch
Этот модуль служит в основном для экспорта классов, используемых другими программами, но и при запуске в виде самостоятельного сценария делает кое-что полезное. Если вызвать его как сценарий с одним аргументом 1, он создаст и запустит объект FileVisitor и выведет полный список всех файлов и каталогов, начиная с того каталога, откуда он вызван, и ниже:
C:\...\PP4E\Tools> visitor.py 1 C:\temp\PP3E\Examples
C:\temp\PP3E\Examples ...
1 => C:\temp\PP3E\Examples\README-root.txt C:\temp\PP3E\Examples\PP3E ...
2 => C:\temp\PP3E\Examples\PP3E\echoEnvironment.pyw
3 => C:\temp\PP3E\Examples\PP3E\LaunchBrowser.pyw
4 => C:\temp\PP3E\Examples\PP3E\Launcher.py
5 => C:\temp\PP3E\Examples\PP3E\Launcher.pyc
...множество строк опущено (передайте по конвейеру команде more или перенаправьте в файл)...
1424 => C:\temp\PP3E\Examples\PP3E\System\Threads\thread-count.py
1425 => C:\temp\PP3E\Examples\PP3E\System\Threads\thread1.py C:\temp\PP3E\Examples\PP3E\TempParts ...
1426 => C:\temp\PP3E\Examples\PP3E\TempParts\109_0237.JPG
1427 => C:\temp\PP3E\Examples\PP3E\TempParts\lawnlake1-jan-03.jpg
1428 => C:\temp\PP3E\Examples\PP3E\TempParts\part-001.txt
1429 => C:\temp\PP3E\Examples\PP3E\TempParts\part-002.html Visited 1429 files and 186 dirs
Если же вызвать этот сценарий с 2 в первом аргументе, он создаст и запустит объект SearchVisitor, используя третий аргумент в качестве ключа поиска. Эта форма напоминает запуск знакомого нам сценария search_all.py, но в данном случае при обнаружении совпадений сценарий не останавливается:
C:\...\PP4E\Tools> visitor.py 2 C:\temp\PP3E\Examples mimetypes
C:\temp\PP3E\Examples\PP3E\extras\LosAlamosAdvancedClass\day1-system\data.txt has mimetypes
C:\temp\PP3E\Examples\PP3E\Internet\Email\mailtools\mailParser.py has mimetypes C:\temp\PP3E\Examples\PP3E\Internet\Email\mailtools\mailSender.py has mimetypes C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\downloadflat.py has mimetypes C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\downloadflat_modular.py has mimetypes
C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\ftptools.py has mimetypes C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\uploadflat.py has mimetypes C:\temp\PP3E\Examples\PP3E\System\Media\playfile.py has mimetypes Found in 8 files, visited 1429
Технически при передаче сценарию числа 3 в первом аргументе он выполнит оба объекта, FileVisitor и SearchVisitor (осуществив два отдельных обхода). Первый аргумент в действительности используется в качестве битовой маски для выбора одной или более поддерживаемых самопроверок - если бит для какого-либо теста установлен в двоичном значении аргумента, этот тест будет выполнен. Поскольку 3 представляется в двоичном виде, как 011, выбираются одновременно поиск (010) и вывод списка (001). В более дружественной системе можно было бы определить символические параметры (например, искать аргументы -search и -list), но для целей данного сценария достаточно битовых масок.
Как обычно, этот модуль можно также использовать в интерактивном сеансе. Ниже приводится один из способов определения количества файлов и каталогов внутри определенного каталога. Последняя команда выполняет обход всего жесткого диска (и выводит результаты после заметной задержки!). Смотрите также пример «Найди самый большой файл Python» в начале этой главы, где описываются такие проблемы, как повторное посещение подкаталогов, не обрабатываемые данной реализацией:
C:\...\PP4E\Tools> python
>>> from visitor import FileVisitor
>>> V = FileVisitor(trace=0)
>>> V.run(r’C:\temp\PP3E\Examples’)
>>> V.dcount, V.fcount (186, 1429)
>>> V.run('..’) # независимый обход (сброс счетчиков)
>>> V.dcount, V.fcount
(19, 181)
>>> V.run('..’, reset=False) # накопительный обход (счетчики сохраняются)
>>> V.dcount, V.fcount
(38, 362) >>> V = FileVisitor(trace=0) # новый независимый обходчик (свои счетчики)
>>> V.run(r’C:\\’) # весь диск: в Unix попробуйте ‘/’
>>> V.dcount, V.fcount
(24992, 198585)
Модуль visitor удобно использовать как самостоятельный сценарий, чтобы получить список файлов и выполнить поиск в дереве каталогов, но в действительности он создавался, чтобы служить основой для расширения. В оставшейся части этого раздела мы коротко познакомимся с некоторыми клиентами этого модуля, которые добавляют свои операции с деревьями каталогов, используя приемы ООП.
Редактирование файлов в деревьях каталогов (Visitor)
Теперь, после обобщения обхода деревьев и поиска, легко сделать следующий шаг и добавить отдельный, совершенно новый компонент автоматического редактирования файлов. В примере 6.19 приводится определение нового класса EditVisitor, который просто переопределяет метод visitmatch класса SearchVisitor, новая версия которого открывает найденный файл в текстовом редакторе. Да, это законченная программа - что-либо особое нужно делать только при обработке найденных файлов, и только это поведение должно обеспечиваться. Все остальное, касающееся логики обхода и поиска, остается неизменным и приобретается по наследству.
Пример 6.19. PP4E\Tools\visitor_edit.py
Порядок использования: “python ...\Tools\visitor_edit.py string rootdir?”. Добавляет подкласс класса SearchVisitor, который автоматически запускает текстовый редактор. В процессе обхода автоматически открывает в текстовом редакторе файлы, содержащие искомую строку; в Windows можно также использовать editor=’edit’ или ‘notepad’; чтобы воспользоваться текстовым редактором, реализация которого будет представлена далее в книге, попробуйте r’python Gui\ TextEditor\textEditor.py’; при работе с некоторыми редакторами можно было бы передать команду перехода к первому совпадению с искомой строкой;
import os, sys
from visitor import SearchVisitor
class EditVisitor(SearchVisitor):
открывает для редактирования файлы, содержащие искомую строку и находящиеся в каталоге startDir и ниже
editor = r’C:\cygwin\bin\vim-nox.exe’ # у вас может быть другой редактор!
def visitmatch(self, fpathname, text):
os.system(‘%s %s’ % (self.editor, fpathname))
if __name__ == ‘__main__’:
visitor = EditVisitor(sys.argv[1])
visitor.run(‘.’ if len(sys.argv) < 3 else sys.argv[2])
print(‘Edited %d files, visited %d’ % (visitor.scount, visitor.fcount))
При использовании объекта EditVisitor текстовый редактор запускается посредством передачи командной строки функции os.system, которая обычно блокирует вызывающий программный код до момента, когда завершится порожденная программа. На моих машинах при каждом обнаружении сценарием совпадения во время обхода запускается текстовый редактор vi в том окне консоли, где был запущен сценарий. При выходе из редактора обход дерева возобновляется.
Найдем и отредактируем несколько файлов. При запуске этого файла как самостоятельного сценария мы передаем ему искомую строку в аргументе командной строки (здесь используется строка «mimetypes»). Корневой каталог всегда передается методу run как «.» (текущий рабочий каталог). Сообщения о состоянии обхода выводятся на консоль, но каждый файл, в котором обнаружено совпадение с искомой строкой, тут же автоматически открывается в текстовом редакторе. В данном случае редактор запускается восемь раз - попробуйте запустить этот сценарий в своем дереве каталогов и со своим редактором, чтобы лучше почувствовать, как он работает:
C:\...\PP4E\Tools> visitor_edit.py mimetypes C:\temp\PP3E\Examples
C:\temp\PP3E\Examples ...
1 => C:\temp\PP3E\Examples\README-root.txt
C:\temp\PP3E\Examples\PP3E ...
2 => C:\temp\PP3E\Examples\PP3E\echoEnvironment.pyw
3 => C:\temp\PP3E\Examples\PP3E\LaunchBrowser.pyw
4 => C:\temp\PP3E\Examples\PP3E\Launcher.py
5 => C:\temp\PP3E\Examples\PP3E\Launcher.pyc
Skipping C:\temp\PP3E\Examples\PP3E\Launcher.pyc
...множество строк опущено...
1427 => C:\temp\PP3E\Examples\PP3E\TempParts\lawnlake1-jan-03.jpg
Skipping C:\temp\PP3E\Examples\PP3E\TempParts\lawnlake1-jan-03.jpg
1428 => C:\temp\PP3E\Examples\PP3E\TempParts\part-001.txt
1429 => C:\temp\PP3E\Examples\PP3E\TempParts\part-002.html
Edited 8 files, visited 1429
В итоге получился именно тот инструмент, который мне был нужен, чтобы упростить сопровождение дерева примеров книги. После значительных изменений, например задания имен совместно используемых модулей или файлов и каталогов, я запускаю этот сценарий в корневом каталоге дерева примеров с соответствующей строкой поиска и нужным образом редактирую все открывающиеся файлы. Мне все еще приходится вручную изменять файлы в редакторе, но это зачастую безопаснее, чем вслепую выполнять глобальную замену.
Глобальная замена в деревьях каталогов (Visitor)
Но раз уж я затронул этот вопрос, то, имея общий класс для обхода дерева, легко написать и подкласс для глобального поиска и замены. В примере 6.20 приводится определение класса ReplaceVisitor, наследующего класс FileVisitor, который переопределяет метод visitfile так, чтобы глобально заменять все вхождения одной строки другой строкой во всех текстовых файлах, находящихся в корневом каталоге и ниже. Он также составляет список всех изменившихся файлов, чтобы их можно было просмотреть и проверить автоматически сделанные изменения (можно, например, автоматически вызывать текстовый редактор для каждого измененного файла).
Пример 6.20. PP4E\Tools\visitor_replace.py
Использование: “python ...\Tools\visitor_replace.py rootdir fromStr toStr”. Выполняет глобальный поиск с заменой во всех файлах в дереве каталогов: заменяет fromStr на toStr во всех текстовых файлах; это мощный, но опасный инструмент!! visitor_edit.py запускает редактор, чтобы дать возможность проверить и внести коррективы, и поэтому он более безопасный; чтобы просто получить список соответствующих файлов, используйте visitor_collect.py; режим простого вывода списка здесь напоминает SearchVisitor и CollectVisitor;
import sys
from visitor import SearchVisitor
class ReplaceVisitor(SearchVisitor):
Заменяет fromStr на toStr в файлах в каталоге startDir и ниже; имена изменившихся файлов сохраняются в списке obj.changed
def__init__(self, fromStr, toStr, listOnly=False, trace=0):
self.changed = []
self.toStr = toStr
self.listOnly = listOnly
SearchVisitor.__init__(self, fromStr, trace)
def visitmatch(self, fname, text): self.changed.append(fname) if not self.listOnly:
fromStr, toStr = self.context, self.toStr text = text.replace(fromStr, toStr) open(fname, ‘w’).write(text)
if __name__ == ‘__main__’:
listonly = input(‘List only?’) == ‘y’
visitor = ReplaceVisitor(sys.argv[2], sys.argv[3], listonly) if listonly or input(‘Proceed with changes?’) == ‘y’: visitor.run(startDir=sys.argv[1])
action = ‘Changed’ if not listonly else ‘Found’ print(‘Visited %d files’ % visitor.fcount) print(action, ‘%d files:’ % len(visitor.changed)) for fname in visitor.changed: print(fname)
Чтобы применить этот сценарий к определенному дереву каталогов, выполните команду, как показано ниже, указав соответствующую искомую строку и строку замены. На моем, жутко нерасторопном нетбуке, обработка дерева с 1429 файлами, из которых 101 потребовалось изменить, заняла примерно три секунды реального времени, когда система была не слишком занята другими задачами:
C:\...\PP4E\Tools> visitor_replace.py C:\temp\PP3E\Examples PP3E PP4E
List only?y Visited 1429 files Found 101 files:
C:\temp\PP3E\Examples\README-root.txt
C:\temp\PP3E\Examples\PP3E\echoEnvironment.pyw
C:\temp\PP3E\Examples\PP3E\Launcher.py
...большое количество имен файлов, соответствующих критерию поиска, опущено...
C:\...\PP4E\Tools> visitor_replace.py C:\temp\PP3E\Examples PP3E PP4E
List only?n
Proceed with changes?y Visited 1429 files Changed 101 files:
C:\temp\PP3E\Examples\README-root.txt
C:\temp\PP3E\Examples\PP3E\echoEnvironment.pyw
C:\temp\PP3E\Examples\PP3E\Launcher.py
...большое количество имен изменившихся файлов опущено...
C:\...\PP4E\Tools> visitor_replace.py C:\temp\PP3E\Examples PP3E PP4E
List only?n
Proceed with changes?y Visited 1429 files Changed 0 files:
Естественно, проверить работу этого сценария можно с помощью сценария visitor (и суперкласса SearchVisitor):
C:\...\PP4E\Tools> visitor.py 2 C:\temp\PP3E\Examples PP3E
Found in 0 files, visited 1429
C:\...\PP4E\Tools> visitor.py 2 C:\temp\PP3E\Examples PP4E
C:\temp\PP3E\Examples\README-root.txt has PP4E C:\temp\PP3E\Examples\PP3E\echoEnvironment.pyw has PP4E C:\temp\PP3E\Examples\PP3E\Launcher.py has PP4E
...большое количество имен файлов, соответствующих критерию поиска, опущено...
Found in 101 files, visited 1429
Это одновременно очень мощный и опасный сценарий. Если заменяемая строка может обнаружиться в неожиданных местах, запуск определенного здесь объекта ReplaceVisitor может разрушить все дерево файлов. С другой стороны, если строка является чем-то очень специфическим, этот объект поможет избежать необходимости вручную редактировать подозрительные файлы. Например, адреса веб-сайтов в файлах HTML достаточно специфичны, чтобы случайно появиться в других местах.
Подсчет строк исходного программного кода (Visitor)
Два приведенных выше примера использования модуля visitor были ориентированы на выполнение поиска, однако базовый класс, реализующий обход дерева каталогов, легко можно было бы расширить для реализации более специфических задач. Так, в примере 6.21 приводится сценарий, расширяющий класс FileVisitor возможностью подсчета количества строк в файлах с исходными текстами программ во всем дереве каталогов. Принцип его действия основан на вызове метода vis-itfile этого класса для каждого файла, найденного инструментом поиска, написанным нами выше в этой главе, но применение ООП обеспечивает более высокую гибкость и расширяемость.
Пример 6.21. PP4E\Tools\visitor_sloc.py
Подсчитывает строки во всех файлах с исходными текстами программ в дереве каталогов, указанном в командной строке, и выводит сводную информацию, сгруппированную по типам файлов (по расширениям). Реализует простейший алгоритм SLOC (source lines of code - строки исходного текста): если необходимо, добавьте пропуск пустых строк и комментариев.
import sys, pprint, os
from visitor import FileVisitor
class LinesByType(FileVisitor):
srcExts = [] # define in subclass
def __init__(self, trace=1):
FileVisitor.__init__(self, trace=trace)
self.srcLines = self.srcFiles = 0
self.extSums = {ext: dict(files=0, lines=0) for ext in self.srcExts}
def visitsource(self, fpath, ext):
if self.trace > 0: print(os.path.basename(fpath)) lines = len(open(fpath, ‘rb’).readlines()) self.srcFiles += 1 self.srcLines += lines self.extSums[ext][‘files’] += 1 self.extSums[ext][‘lines’] += lines
def visitfile(self, filepath):
FileVisitor.visitfile(self, filepath) for ext in self.srcExts:
if filepath.endswith(ext):
self.visitsource(filepath, ext) break
class PyLines(LinesByType):
srcExts = [‘.py’, ‘.pyw’] # just python files
class SourceLines(LinesByType):
srcExts = [‘.py’, ‘.pyw’, ‘.cgi’, ‘.html’, ‘.c’, ‘.cxx’, ‘.h’, ‘.i’]
if __name__ == ‘__main__’: walker = SourceLines() walker.run(sys.argv[1])
print(‘Visited %d files and %d dirs’ % (walker.fcount, walker.dcount)) print(‘-’*80)
print(‘Source files=>%d, lines=>%d’ % (walker.srcFiles, walker.srcLines)) print(‘By Types:’) pprint.pprint(walker.extSums) print(‘\nCheck sums:’, end=’ ‘)
print(sum(x[‘lines’] for x in walker.extSums.values()), end=’ ‘)
print(sum(x[‘files’] for x in walker.extSums.values()))
print(‘\nPython only walk:’)
walker = PyLines(trace=0)
walker.run(sys.argv[1])
pprint.pprint(walker.extSums)
Если запустить его как самостоятельный сценарий, в процессе обхода будут выводиться трассировочные сообщения (опущены здесь для экономии места) и в конце будет представлен отчет о количестве строк, сгруппированный по типам файлов. Выполните этот сценарий в своем дереве каталогов, чтобы увидеть, как он действует. В моем дереве каталогов содержится 907 файлов с исходными текстами, насчитывающих 48 000 строк, включая 783 файла (.py) и 34 000 строк с исходными текстами на языке Python:
C:\...\PP4E\Tools> visitor_sloc.py C:\temp\PP3E\Examples
Visited 1429 files and 186 dirs
Source files=>907, lines=>48047 By Types:
{‘.c’: {‘files’: 45, ‘lines’: 7370},
‘.cgi’: {‘files’: 5, ‘lines’: 122},
‘.cxx’: {‘files’: 4, ‘lines’: 2278},
‘.h’: {‘files’: 7, ‘lines’: 297},
‘.html’: {‘files’: 48, ‘lines’: 2830},
‘.i’: {‘files’: 4, ‘lines’: 49}, ‘.py’: {‘files’: 783, ‘lines’: 34601 },
‘.pyw’: {‘files’: 11, ‘lines’: 500}}
Check sums: 48047 907
Python only walk:
{‘.py’: {‘files’: 783, ‘lines’: 34601 }, ‘.pyw’: {‘files’: 11, ‘lines’: 500}}
Копирование деревьев каталогов с помощью классов (Visitor)
Рассмотрим еще один случай использования классов-обходчиков. Когда я впервые написал сценарий cpall.py ранее в этой главе, я не понимал, как можно применить иерархию классов-обходчиков, с которой мы встретились выше, для копирования деревьев каталогов. При копировании необходимо выполнять обход сразу двух деревьев каталогов (оригинального и его копии), а visitor выполняет обход только одного дерева, используя функцию os.walk. Тогда мне казалось, что будет совсем не просто следить за тем, где находится сценарий в копии дерева.
Хитрость, до которой я в конце концов додумался, заключается в том, что следить за позицией в дереве копии вообще не требуется. Вместо этого сценарий в примере 6.22 просто замещает строку пути к каталогу «из» строкой пути к каталогу «в» и добавляет ее перед всеми именами каталогов, полученными от функции os.walk. В результате такой подмены получаются пути, куда должны быть скопированы оригинальные файлы и каталоги.
Пример 6.22. PP4E\Tools\visitor_cpall.py
Использование: “python ...\Tools\visitor_cpall.py fromDir toDir trace?”
Действует подобно сценарию System\Filetools\cpall.py, но использует классы-обходчики и функцию os.walk; заменяет строку fromDir на toDir перед всеми именами, возвращаемыми обходчиком; предполагается, что изначально каталог toDir не существует;
import os
from visitor import FileVisitor # обходчик в каталоге ‘.’
from PP4E.System.Filetools.cpall import copyfile # PP4E - в пути поиска
class CpallVisitor(FileVisitor):
def__init__(self, fromDir, toDir, trace=True):
self.fromDirLen = len(fromDir) + 1 self.toDir = toDir
FileVisitor.__init__(self, trace=trace)
def visitdir(self, dirpath):
toPath = os.path.join(self.toDir, dirpath[self.fromDirLen:])
if self.trace: print(‘d’, dirpath, ‘=>’, toPath)
os.mkdir(toPath)
self.dcount += 1
def visitfile(self, filepath):
toPath = os.path.join(self.toDir, filepath[self.fromDirLen:]) if self.trace: print(‘f’, filepath, ‘=>’, toPath) copyfile(filepath, toPath) self.fcount += 1
if __name__ == ‘__main__’: import sys, time fromDir, toDir = sys.argv[1:3] trace = len(sys.argv) > 3 print(‘Copying...’) start = time.clock()
walker = CpallVisitor(fromDir, toDir, trace) walker.run(startDir=fromDir)
print(‘Copied’, walker.fcount, ‘files,’, walker.dcount, ‘directories’, end=’ ‘)
print(‘in’, time.clock() - start, ‘seconds’)
Эта версия достигает почти той же цели, что и оригинал, но в ней сделано несколько допущений, чтобы упростить реализацию. Предполагается, что каталог «в» изначально не существует, а исключения, возникающие в процессе копирования, не игнорируются. Следующий пример демонстрирует копирование дерева примеров для предыдущего издания книги в Windows:
C:\...\PP4E\Tools> set PYTHONPATH
PYTHONPATH=C:\Users\Mark\Stuff\Books\4E\PP4E\dev\Examples
C:\...\PP4E\Tools> rmdir /S copytemp
copytemp, Are you sure (Y/N)? y
C:\...\PP4E\Tools> visitor_cpall.py C:\temp\PP3E\Examples copytemp
Copying...
Copied 1429 files, 186 directories in 11.1722033777 seconds
C:\...\PP4E\Tools> fc /B copytemp\PP3E\Launcher.py
C:\temp\PP3E\Examples\PP3E\Launcher.py
Comparing files COPYTEMP\PP3E\Launcher.py and C:\TEMP\PP3E\EXAMPLES\PP3E\ LAUNCHER.PY
FC: no differences encountered
Несмотря на то, что в этой версии выполняется дополнительная операция извлечения среза из строки, она действует так же быстро, как и оригинал (фактическое отличие можно списать на разницу в нагрузке на систему). Кроме того, эта версия позволяет проследить за всеми путями «из» и «в» в процессе обхода, если передать ей третий аргумент командной строки:
C:\...\PP4E\Tools> rmdir /S copytemp
copytemp, Are you sure (Y/N)? y
C:\...\PP4E\Tools> visitor_cpall.py C:\temp\PP3E\Examples copytemp 1
Copying...
d C:\temp\PP3E\Examples => copytemp\
f C:\temp\PP3E\Examples\README-root.txt => copytemp\README-root.txt d C:\temp\PP3E\Examples\PP3E => copytemp\PP3E
...множество строк опущено: запустите этот сценарий у себя, чтобы увидеть вывод целиком...
Другие примеры обходчиков (внешние)
Инструменту обхода деревьев на основе классов можно найти самые разные применения, однако у нас недостаточно места в книге, чтобы исследовать дополнительные подклассы. Дополнительные примеры реализации клиентов и их использования вы найдете в следующих примерах, входящих в состав пакета, описанного в Предисловии:
• Tools\visitor_collect.py отбирает и/или выводит имена файлов, содержащие искомую строку
• Tools\visitor_poundbang.py замещает пути к каталогам в строках «#!», находящихся в начале файлов сценариев в Unix
• Tools\visitor_cleanpyc.py - переработанная версия сценария удаления файлов с байт-кодом, использующая классы-обходчики
• Tools\visitor_bigpy.py - версия примера «Найди самый большой файл Python», приводившегося в начале главы, использующая классы-обходчики
Реализация большинства из них выглядит почти тривиально, как и реализация visitor_edit.py в примере 6.19, благодаря тому что все детали обхода деревьев каталогов автоматически скрываются за интерфейсом классов-обходчиков. Реализация отбора файлов, например, просто добавляет в список имена файлов, соответствующих критериям поиска, и позволяет переопределить список допустимых расширений имен файлов в каждом экземпляре - она напоминает комбинацию команд find и grep в Unix:
>>> from visitor_collect import CollectVisitor
>>> V = CollectVisitor('mimetypes', testexts=['.py', ‘.pyw’], trace=0)
>>> V.run(r'C:\temp\PP3E\Examples')
>>> for name in V.matches: print(name) # файлы .py и .pyw со строкой
... # ‘mimetypes’
C:\temp\PP3E\Examples\PP3E\Internet\Email\mailtools\mailParser.py
C:\temp\PP3E\Examples\PP3E\Internet\Email\mailtools\mailSender.py
C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\downloadflat.py
C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\downloadflat_modular.py
C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\ftptools.py
C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\uploadflat.py
C:\temp\PP3E\Examples\PP3E\System\Media\playfile.py
C:\...\PP4E\Tools> visitor_collect.py mimetypes C:\temp\PP3E\Examples # как
# сценарий
Основная логика сценария поиска наибольшего файла также выглядит достаточно просто и напоминает логику сценария в начале главы:
class BigPy(FileVisitor):
def __init__(self, trace=0):
FileVisitor.__init__(self, context=[], trace=trace)
def visitfile(self, filepath):
FileVisitor.visitfile(self, filepath) if filepath.endswith(‘.py’):
self.context.append((os.path.getsize(filepath), filepath))
Пример реализации удаления файлов байт-кода на основе классов-обходчиков также возвращает нас назад, демонстрируя альтернативное решение задачи, с которой мы уже сталкивались выше в этой главе. Это, по сути, тот же самый программный код, но он вызывает функцию os.remove при обнаружении файлов с расширением «.pyc».
Наконец, хотя классы-обходчики действительно являются всего лишь обертками вокруг функции os.walk, тем не менее они автоматизируют рутинные операции, выполняемые при обходе, и обеспечивают универсальный фундамент и альтернативную структуру на основе классов, которая может оказаться более естественной для некоторых решений, чем неструктурированные циклы. Кроме того, они наглядно демонстрируют, насколько хорошо поддержка ООП в языке Python позволяет отображать реальные структуры, такие как файловые системы. Приемы на основе функции os.walk хорошо подходят для одноразовых сценариев, тогда как лучшая расширяемость, уменьшенная избыточность и сокрытие, свойственные приемам ООП, могут быть основным вкладом в разработку действующих программ, которые могут развиваться в течение долгого времени с учетом наших потребностей.
Эти потребности действительно изменились с течением времени. Между третьим и четвертым изданиями этой книги из Python 3.X была исключена оригинальная функция os.path. walk, и функция os.walk стала единственным инструментом автоматизированного обхода деревьев в стандартной библиотеке. Это привело к тому, что примеры из предыдущего издания, использовавшие os.path.walk, оказались неработоспособными. Тогда как клиенты, реализованные на основе классов-обходчиков, использующих ту же функцию, не пострадали. Поскольку перевод классов-обходчиков на использование функции os.walk не отразился на их интерфейсе, инструменты, использующие их, сохранили работоспособность без переделки.
Это, пожалуй, один из самых ярких примеров преимуществ инкапсуляции в ООП. Будущее нельзя предсказать с достаточной степенью надежности, тем не менее на практике инструменты, определяемые пользователем, такие как классы-обходчики, обычно обеспечивают более полный контроль над изменениями в инструментах стандартной библиотеки, таких как os.walk. Можете поверить мне на слово - как человек, который обновлял три книги о языке Python на протяжении последних 15 лет, я могу с определенной долей уверенности сказать, что Python будет изменяться постоянно!
Проигрывание медиафайлов
В этой главе у нас еще осталось немного места, поэтому закроем ее небольшим развлекательным примером. Обратили ли вы внимание, что в сценариях поиска в двух предыдущих разделах мы жестко определяли расширения имен текстовых и двоичных файлов? Такой подход вполне оправдан для деревьев каталогов, к которым мы применяли их, но в общем случае его нельзя назвать законченным или переносимым. Было бы лучше, если бы мы могли автоматически определять тип файла по его имени. Такую возможность предоставляет модуль mimetypes. В этом разделе мы будем использовать его при создании сценария, который будет пытаться запускать файлы, опираясь на их типы, и по ходу дела разработаем универсальные инструменты для открытия медиафайлов переносимым способом, с использованием специализированных или универсальных программ-проигрывателей.
Как мы уже видели, в Windows эта задача решается тривиально просто - функция os.startfile открывает файл, используя реестр Windows, где хранятся соответствия между расширениями имен файлов и программами для работы с ними. На других платформах можно либо запускать определенные программы-проигрыватели для каждого типа файлов, либо возвращаться к использованию веб-броузера по умолчанию, открывая файлы с помощью модуля webbrowser. Воплощение этих идей в программный код приводится в примере 6.23.
Пример 6.23. PP4E\System\Media\playfile.py
#!/usr/local/bin/python
############################################################################## Пытается проигрывать медиафайлы различных типов. Позволяет определять специализированные программы-проигрыватели вместо использования универсального приема открытия файла в веб-броузере. В текущем своем виде может не работать в вашей системе; для открытия аудиофайлов в Unix используются фильтры и команды, в Windows используется команда start, учитывающая ассоциации с расширениями имен файлов (то есть для открытия файлов .au, например, она может запустить проигрыватель аудиофайлов или веб-броузер). Настраивайте и расширяйте сценарий под свои потребности. Функция playknownfile предполагает, что вы знаете, какой тип медиафайла пытаетесь открыть, а функция playfile пробует определить тип файла автоматически, используя модуль mimetypes; обе они пробуют запустить вебброузер с помощью модуля webbrowser, если тип файла не удается определить. ##############################################################################
import os, sys, mimetypes, webbrowser helpmsg = “””
Sorry: can’t find a media player for ‘%s’ on your system!
Add an entry for your system to the media player dictionary for this type of file in playfile.py, or play the file manually.
def trace(*args): print(*args) # с разделяющими пробелами
##############################################################################
# приемы проигрывания: универсальный и другие: дополните своими приемами ##############################################################################
class MediaTool:
def __init__(self, runtext=’’):
self.runtext = runtext
def run(self, mediafile, **options): # options обычно игнорируется
fullpath = os.path.abspath(mediafile) # cwd может быть любым
self.open(fullpath, **options)
class Filter(MediaTool):
def open(self, mediafile, **ignored): media = open(mediafile, ‘rb’)
player = os.popen(self.runtext, ‘w’) # запустить команду оболочки
player.write(media.read()) # отправить файл в stdin
class Cmdline(MediaTool):
def open(self, mediafile, **ignored):
cmdline = self.runtext % mediafile # запустить команду os.system(cmdline) # использовать %s для имени файла
class Winstart(MediaTool): # использует реестр Windows
def open(self, mediafile, wait=False, **other): # позволяет дождаться
if not wait: # окончания проигрывания файла
os.startfile(mediafile) # или os.system(‘start file’)
else:
os.system(‘start /WAIT ‘ + mediafile)
class Webbrowser(MediaTool):
# file:// требует указывать абсолютный путь def open(self, mediafile, **options):
webbrowser.open_new(‘file://%s’ % mediafile, **options)
##############################################################################
# медиа- и платформозависимые методы: измените или укажите один из имеющихся ##############################################################################
# соответствия платформ и проигрывателей: измените! audiotools = {
‘sunos5’: Filter(‘/usr/bin/audioplay’), # os.popen().write()
‘linux2’: Cmdline(‘cat %s > /dev/audio’), # по крайней мере в PDA Zaurus ‘sunos4’: Filter(‘/usr/demo/SOUND/play’), # да, даже такая древность! ‘win32’: Winstart() # startfile или system
#’win32’: Cmdline(‘start %s’)
}
videotools = {
‘linux2’: Cmdline(‘tkcVideo_c700 %s’), # PDA Zaurus
‘win32’: Winstart(), # предотвратить вывод окна DOS
}
imagetools = {
‘linux2’: Cmdline(‘zimager %s’), # PDA Zaurus
‘win32’: Winstart(),
}
texttools = {
‘linux2’: Cmdline(‘vi %s’), # PDA Zaurus
‘win32’: Cmdline(‘notepad %s’) # или попробовать PyEdit?
}
apptools = {
‘win32’: Winstart() # doc, xls, и др.: используйте
# на свой страх и риск!
}
# таблица соответствия между типами файлов и программами-проигрывателями
mimetable = {‘audio’: audiotools,
‘video’: videotools,
‘image’: imagetools,
‘text’: texttools, # не-html текст: броузер
‘application’: apptools}
##############################################################################
# интерфейсы высокого уровня
############################################################################## def trywebbrowser(filename, helpmsg=helpmsg, **options): пытается открыть файл в веб-броузере
как последнее средство, если тип файла или платформы неизвестен, а также для файлов типа text/html
trace(‘trying browser’, filename) try:
player = Webbrowser() # открыть в броузере
player.run(filename, **options) except:
print(helpmsg % filename) # никакой из способов не работает
def playknownfile(filename, playertable={}, **options):
проигрывает медиафайл известного типа: использует программы-проигрыватели для данной платформы или запускает веб-броузер, если для этой платформы не определено ничего другого; принимает таблицу соответствий расширений и программ-проигрывателей
if sys.platform in playertable: # известный
playertable[sys.platform].run(filename, **options) # инструмент
else: # универсальный
trywebbrowser(filename, **options) # прием
def playfile(filename, mimetable=mimetable, **options):
проигрывает медиафайл любого типа: использует модуль mimetypes для определения типа медиафайла и таблицу соответствий между расширениями и программами-проигрывателями; запускает веб-броузер для файлов с типом text/html, с неизвестным типом и при отсутствии таблицы соответствий
contenttype, encoding = mimetypes.guess_type(filename) # проверить имя if contenttype == None or encoding is not None: # не определяется
contenttype = ‘?/?’ # возм. .txt.gz
maintype, subtype = contenttype.split(‘/’, 1) # ‘image/jpeg’
if maintype == ‘text’ and subtype == ‘html’:
trywebbrowser(filename, **options) # спец. случай
elif maintype in mimetable:
playknownfile(filename, mimetable[maintype], **options) # по таблице else:
trywebbrowser(filename, **options) # другие типы
##############################################################################
# программный код самопроверки
##############################################################################
if __name__ == ‘__main__’:
# тип медиафайла известен
playknownfile(‘sousa.au’, audiotools, wait=True) playknownfile(‘ora-pp3e.gif’, imagetools, wait=True) playknownfile(‘ora-lp4e.jpg’, imagetools)
# тип медиафайла определяется
input(‘Stop players and press Enter’) playfile\ora-lp4e.jpg’) # image/jpeg
playfile\ora-pp3e.gif’) # image/gif
playfile(‘priorcalendar.html’) # text/html
playfile(‘lp4e-preface-preview.html’) # text/html
playfile\lp-code-readme.txt’) # text/plain
playfile(‘spam.doc’) # app
playfile(‘spreadsheet.xls’) # app
playfile(‘sousa.au’, wait=True) # audio/basic
input(‘Done’) # приостановиться, если
# сценарий запущен щелчком мыши
В наши дни медиафайлы большинства типов можно открыть с помощью веб-броузера, тем не менее этот модуль представляет собой простую основу для программ, открывающих медиафайлы с помощью более специализированных инструментов, предназначенных для обработки медиафайлов данного типа на данной платформе. Веб-броузер используется только в крайнем случае, при отсутствии других возможностей. В результате получился расширяемый сценарий проигрывания медиафайлов, специализированность и переносимость которого зависит от настроек в его таблицах соответствий.
Инструменты запуска программ, используемые этим сценарием, мы видели в предыдущих главах. Основным новшеством в этом сценарии является использование новых модулей: модуль webbrowser используется для открытия некоторых файлов в локальном веб-броузере, а модуль mimetypes применяется для определения типов медиафайлов по их именам. Поскольку они являются основой этого сценария, коротко познакомимся с ними, прежде чем запускать сценарий.
Модуль webbrowser
Модуль webbrowser, входящий в состав стандартной библиотеки и используемый в этом примере, предоставляет переносимый интерфейс для запуска веб-броузера из сценариев на языке Python. Он пытается отыскать подходящий веб-броузер на локальном компьютере, чтобы открыть указанный адрес URL (полное имя файла или веб-адрес). Интерфейс модуля достаточно прост:
>>> import webbrowser
>>> webbrowser.open_new(‘file://’ + fullfilename) # используйте
# os.path.abspath()
Этот программный код откроет указанный файл в новом окне броузера, который удастся обнаружить на локальном компьютере, или возбудит исключение. Имеется возможность явно перечислить броузеры, имеющиеся в системе, и определить порядок, в каком они будут использоваться, с помощью переменной окружения BROWSER и функции register. По умолчанию модуль webbrowser автоматически пытается обеспечить переносимость между платформами.
Чтобы открыть файл, находящийся на локальном компьютере или на веб-сервере, строка аргумента должна иметь вид «file://...» или «http://...» соответственно. Фактически можно передать строку URL любого вида, которую воспринимает броузер. Например, следующая инструкция откроет домашнюю страницу проекта Python в новом окне броузера:
>>> webbrowser.open_new(‘http://www.python.org’)
Кроме всего прочего, этот модуль обеспечивает простой способ отображения документов HTML, а также медиафайлов, как демонстрируется в примере из этого раздела. К тому же этот модуль может использоваться и как самостоятельный сценарий командной строки (здесь вам поможет ключ -m командной строки интерпретатора, включающий путь поиска модулей), и как импортируемый инструмент:
C:\Users\mark\Stuff\Websites\public_html> python -m webbrowser about-pp.html C:\Users\mark\Stuff\Websites\public_html> python -m webbrowser -n about-pp.html C:\Users\mark\Stuff\Websites\public_html> python -m webbrowser -t about-pp.html
C:\Users\mark\Stuff\Websites\public_html> python >>> import webbrowser
>>> webbrowser.open('about-pp.html') # повторное использование,
True # новое окно, новая вкладка
>>> webbrowser.open_newCabout-pp.html') # file:// не обязательно в Windows
True
>>> webbrowser.open_new_tabCabout-pp.html')
True
В обоих режимах применения различия между тремя формами заключаются в том, что в первом случае предпринимается попытка повторно использовать уже открытое окно броузера, если это возможно, во втором - выполняется попытка открыть новое окно, и в третьем - выполняется попытка открыть новую вкладку. Однако на практике поведение всех трех форм зависит от типа веб-броузера и в целом от типа платформы. Все три формы могут вести себя совершенно одинаково.
В Windows, например, все три просто вызывают функцию os.startfile по умолчанию, в результате чего создается новая вкладка в уже открытом окне Internet Explorer 8. Это объясняет, почему я не указывал полный префикс «file://» URL в предыдущем фрагменте. Формально, броузер Internet Explorer запускается, только если он зарегистрирован как инструмент открытия файлов указанного типа, в противном случае запускается специализированный инструмент для работы с такими файлами. Некоторые изображения, например, могут открываться в специализированных программах просмотра фотографий. На других платформах, таких как Unix и Mac OS X, поведение броузера отличается, и файлы, имена которых не являются полноценным адресом URL, могут не открываться, поэтому для переносимости используйте префикс «file://».
Мы еще вернемся к этому модулю далее в книге. Например, программа PyMailGUI в главе 14 будет использовать его как инструмент отображения сообщений электронной почты и вложений в формате HTML, а также отображения справки к программе. За дополнительной информацией обращайтесь к руководству по библиотеке Python. В главах 13 и 15 мы также встретим родственную функцию urllib.request.urlopen, которая извлекает текст веб-страницы, находящейся по указанному адресу URL, но не открывает его в броузере - этот текст можно проанализировать, сохранить и использовать как-то иначе.
Модуль mimetypes
Чтобы сделать этот модуль проигрывателя медиафайлов еще более удобным, мы использовали в нем модуль mimetypes, входящий в состав стандартной библиотеки Python, который автоматически определяет тип медиафайла по его имени. Если тип может быть определен, модуль возвращает строку типа содержимого вида type/subtype, или None - если тип не определяется:
>>> import mimetypes
>>> mimetypes.guess_type('spam.jpg')
(‘image/jpeg’, None)
>>> mimetypes.guess_type(‘TheBrightSideOfLife.mp3’)
(‘audio/mpeg’, None)
>>> mimetypes.guess_type('lifeofbrian.mpg')
(‘video/mpeg’, None)
>>> mimetypes.guess_type('lifeofbrian.xyz') # неизвестный тип (None, None)
Выделив первую часть строки типа содержимого, можно получить обобщенный тип медиафайла, который можно использовать для выбора универсальной программы-проигрывателя. Вторая часть (подтип) сообщает, например, является текст разметкой HTML или простым текстом:
>>> contype, encoding = mimetypes.guess_type('spam.jpg')
>>> contype.split('/')[0]
‘image’
>>> mimetypes.guess_type('spam.txt') # подтип ‘plain’
(‘text/plain’, None)
>>> mimetypes.guess_type('spam.html')
(‘text/html’, None)
>>> mimetypes.guess_type('spam.html')[0].split('/')[1]
‘html’
Тут есть одна тонкость: второй элемент кортежа, возвращаемого функцией определения типа из модуля mimetypes, является названием кодировки, которое мы не используем здесь. Тем не менее мы не должны забывать про него - если данный элемент имеет значение, отличное от
None, это означает, что файл был сжат (с помощью gzip или compress), даже если в первом элементе кортежа возвращается тип медиафайла. Например, если файл имеет такое имя, как spam.gif.gz, будет считаться, что это сжатое изображение, которое не следует пытаться открывать непосредственно:
>>> mimetypes.guess_type('spam.gz') # тип содержимого неизвестен
(None, ‘gzip’)
>>> mimetypes.guess_type('spam.gif.gz') # не открывать напрямую!
(‘image/gif’, ‘gzip’)
>>> mimetypes.guess_type('spam.zip') # архивы
(‘application/zip’, None)
>>> mimetypes.guess_type('spam.doc') # файлы офисных приложений
(‘application/msword’, None)
Если имя файла, передаваемое функции, содержит путь к каталогу, то эта часть имени будет игнорироваться (при определении типа используется только расширение). Этот модуль позволяет даже определять расширение имени файла по заданному типу, что можно использовать при создании имен файлов по известному типу содержимого:
>>> mimetypes.guess_type(r'C:\songs\sousa.au')
(‘audio/basic’, None)
>>> mimetypes.guess_extension('audio/basic')
‘.au’
Поэкспериментируйте с другими функциями из этого модуля, чтобы получить более полное представление о нем. Мы еще раз вернемся к модулю mimetypes в примерах FTP в главе 13, где будем определять тип передаваемых данных (текст или двоичные), а также в примерах работы с электронной почтой в главах 13, 14 и 16, где реализуем отправку, сохранение и открытие вложений.
В примере 6.23 модуль mimetypes используется для организации выбора платформозависимых команд запуска программ-проигрывателей для данного типа медиафайлов. То есть модуль выбирает проигрыватель из таблицы по указанному типу медиафайла, а затем выбирает из таблицы проигрывателей команду для данной платформы. На любом из двух этапах мы отступаем назад и запускаем веб-броузер, если не было найдено чего-то более определенного.
Использование модуля mimetypes в классе SearchVisitor
Чтобы задействовать этот модуль для управления выбором текстовых файлов в сценариях поиска, написанных нами выше в этой главе, можно просто извлекать и анализировать первый элемент кортежа, возвращаемого для заданного имени файла. Например, все расширения в следующем списке считаются расширениями имен текстовых файлов (кроме расширения «.pyw», которое можно считать особым случаем):
>>> for ext in ['.txt', '.py', '.pyw', '.html', '.c', '.h', '.xml']:
... print(ext, mimetypes.guess_type('spam' + ext))
.txt (‘text/plain’, None)
.py (‘text/x-python’, None)
.pyw (None, None)
.html (‘text/html’, None)
.c (‘text/plain’, None)
.h (‘text/plain’, None)
.xml (‘text/xml’, None)
Мы можем добавить этот прием в предыдущую реализацию класса SearchVisitor, переопределив метод candidate и заменив список расширений имен файлов, используемый по умолчанию, анализом возвращаемого типа модулем mimetypes - еще одно яркое свидетельство гибкости ООП:
C:\...\PP4E\Tools> python >>> import mimetypes
>>> from visitor import SearchVisitor # или PP4E.Tools.visitor, если не .
>>>
>>> class SearchMimeVisitor(SearchVisitor):
... def candidate(self, fname):
... contype, encoding = mimetypes.guess_type(fname)
... return (contype and
... contype.split('/')[0] == 'text' and
... encoding == None)
>>> V = SearchMimeVisitor('mimetypes', trace=0) # ключ поиска >>> V.run(r'C:\temp\PP3E\Examples') # корневой каталог
C:\temp\PP3E\Examples\PP3E\extras\LosAlamosAdvancedClass\day1-system\data.txt has mimetypes
C:\temp\PP3E\Examples\PP3E\Internet\Email\mailtools\mailParser.py has mimetypes C:\temp\PP3E\Examples\PP3E\Internet\Email\mailtools\mailSender.py has mimetypes C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\downloadflat.py has mimetypes C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\downloadflat_modular.py has mimetypes
C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\ftptools.py has mimetypes C:\temp\PP3E\Examples\PP3E\Internet\Ftp\mirror\uploadflat.py has mimetypes C:\temp\PP3E\Examples\PP3E\System\Media\playfile.py has mimetypes >>> V.scount, V.fcount, V.dcount
(8, 1429, 186)
Однако этот прием не обеспечивает абсолютную точность (в случае необходимости придется добавить логику обслуживания таких расширений, как «.pyw», которые не определяются модулем mimetypes) и подходит далеко не для всех клиентов (в некоторых случаях может потребоваться искать только определенные типы текстовых файлов), поэтому данная схема не использовалась в оригинальном классе. Использование и настройку его для ваших собственных нужд мы оставим в качестве самостоятельного упражнения.
Запускаем сценарий
Если теперь запустить сценарий из примера 6.23 и все пойдет как надо, программный код самопроверки в конце сценария откроет множество аудиофайлов, изображений, текста и других файлов, находящихся в каталоге сценария, используя либо специализированные программы-проигрыватели для данной платформы, либо веб-броузер. На моем ноутбуке с Windows 7 файлы GIF и HTML открываются в новых вкладках броузера IE; файлы JPEG - в программе Windows Photo Viewer; простые текстовые файлы - в программе Notepad; файлы DOC и XLS -в Microsoft Word и Excel; а аудиофайлы - в Windows Media Player.
Однако, поскольку набор используемых программ и их поведение могут значительно отличаться для разных компьютеров, вам будет лучше самостоятельно изучить реализацию сценария и опробовать его на собственном компьютере со своими тестовыми файлами, чтобы своими глазами увидеть, что произойдет. Как обычно, тестирование можно выполнить и в интерактивной оболочке (используйте путь к пакету, как в следующем фрагменте, чтобы импортировать из другого каталога, при этом предполагается, что корневой каталог PP4E находится в пути поиска модулей):
>>> from PP4E.System.Media.playfile import playfile
>>> playfile(r'C:\movies\mov10428.mpg') # video/mpeg
Мы еще будем использовать модуль playfile в главе 13 для открытия медиафайлов, загруженных по FTP. И снова вам может потребоваться настроить таблицы в сценарии, определив в них ассоциации со своими программами-проигрывателями. Кроме того, этот сценарий предполагает, что медиафайл находится на локальном компьютере (даже при том, что модуль webbrowser поддерживает удаленные файлы с именами, начинающимися с «http://») и в настоящее время не позволяет использовать различные программы-проигрыватели для большинства различных подтипов MIME (текстовые файлы являются особым случаем, где подтипы «plain» и «html» обрабатываются по-разному, но это не относится к другим типам). Этот сценарий является своего рода основой, предназначенной для дальнейшего расширения. Как обычно, изучайте и осваивайте - в конце концов, это Python.
Автоматизированный запуск программ (внешние примеры)
Наконец, кое-что для дополнительного чтения - в пакете с примерами для этой книги (доступном на сайтах, перечисленных в Предисловии) вы найдете дополнительные системные сценарии, описать которые здесь мы не можем из-за нехватки места:
• PP4E\Launcher.py - содержит инструменты, используемые некоторыми программами с графическим интерфейсом, рассматриваемыми далее в этой книге, для запуска программ Python без необходимости выполнять настройку окружения. Грубо говоря, этот модуль выполняет настройку системного пути поиска файлов и пути поиска импортируемых модулей, необходимых для запуска примеров, которые наследуются порождаемыми программами. Используя этот модуль для поиска файлов и автоматической настройки окружения, пользователи могут избежать или, по крайней мере, отсрочить необходимость изучать особенности настройки окружения вручную перед запуском программ. В этом примере вы найдете не так много нового с точки зрения системных интерфейсов, однако мы еще будем ссылаться на него позднее, когда будем исследовать программы с графическим интерфейсом, использующие эти инструменты, а также родственные им инструменты запуска, о которых рассказывалось в главе 5.
• PP4E\Launch_PyDemos.pyw и PP4E\Launch_PyGadgets_bar.pyw -используют Launcher.py для запуска основных примеров книги без необходимости выполнять настройку окружения. Поскольку все порождаемые процессы наследуют настройки, выполненные модулем запуска, все они выполняются с соответствующими настройками путей поиска. При непосредственном запуске сценарии PyDemos2. pyw и PyGadgets_bar.pyw (которые мы будем исследовать в конце главы 10) будут использовать общесистемные настройки. Другими словами, сценарий Launcher эффективно скрывает особенности настройки от графических интерфейсов, заключая их в настроенную программную оболочку.
• PP4E\LaunchBrowser.pyw - переносимым образом отыскивает и запускает веб-броузер на локальном компьютере для просмотра содержимого локального файла или удаленной веб-страницы. Поиск броузера в предыдущей версии выполнялся с помощью инструментов из Launcher.py. Изначальная реализация этого модуля может быть в значительной степени заменена модулем webbrowser из стандартной библиотеки, который появился уже после того, как этот пример был создан (питоны думают одинаково!). В этом издании модуль Launch-Browser просто анализирует аргументы командной строки для обратной совместимости и вызывает функцию open из модуля webbrowser. Примеры его использования вы найдете в справочном тексте внутри модуля или в примерах PyGadgets и PyDemos в главе 10.
На этом мы заканчиваем исследование системных инструментов. В следующей части книги мы оставим царство командной строки и перейдем к исследованию способов создания графических интерфейсов для наших программ. Позднее мы проделаем то же самое, но уже с использованием веб-интерфейсов. Тем не менее не будем забывать, что системные инструменты, с которыми мы познакомились в этой части книги, находят применение в самых разных программах. Например, мы будем использовать потоки выполнения для выполнения длительных операций в части книги, посвященной графическим интерфейсам; потоки выполнения и процессы будут использоваться для реализации серверов в части книги, посвященной программированию для Интернета; и на протяжении всей книги будем применять файлы и системные вызовы, связанные с файлами.
Будь то сценарии командной строки, многооконные графические интерфейсы или распределенные веб-сайты, действующие по схеме кли-ент/сервер, системные инструменты Python всегда будут играть важную роль в вашей карьере программиста на Python.
Программирование графических интерфейсов
Эта часть книги демонстрирует, как применять Python для создания переносимых графических интерфейсов пользователя, в первую очередь, с помощью стандартной библиотеки Python tkinter. Следующие главы всесторонне освещают эту тему:
Глава 7
Эта глава очерчивает возможности создания графических интерфейсов, доступные в языке Python, а затем представляет учебный материал, иллюстрирующий базовые понятия программирования с использованием tkinter.
Глава 8
Эта глава начинает обзор двух частей библиотеки tkinter - набора графических элементов и сопутствующих инструментов. В первой части обзора рассказывается о наиболее простых инструментах и графических элементах библиотеки: всплывающих окнах, различных видах кнопок, изображениях и так далее.
Глава9
Здесь продолжается обзор библиотеки, начатый в предыдущей главе. В этой главе представляется остальная часть библиотеки графических элементов tkinter, включая меню, текст, холст, полосы прокрутки, сетки, события таймера и анимацию.
Глава 10
В этой главе рассматриваются приемы программирования графических интерфейсов: здесь мы узнаем, как автоматически конструировать меню из шаблонов объектов, как запускать графический интерфейс в виде отдельной программы, как реализовать выполнение
продолжительных операций параллельно с основной программой с помощью потоков выполнения и очередей и многое другое.
Глава 11
Эта глава объединяет идеи из предыдущих глав для реализации набора интерфейсов пользователя. В ней будут представлены более крупные примеры графических интерфейсов - часов, текстовых редакторов, программ для рисования и просмотра графических изображений и других - которые также демонстрируют приемы программирования на Python в целом.
Как и в первой части книги, представленный здесь материал применим в целом ряде областей и будет снова использован в дальнейшем для создания настроенных на конкретные области применения интерфейсов в последующих главах книги. Так, примеры PyMailGUI и PyCalc в последующих главах предполагают, что вы знакомы с основами, охватываемыми здесь.
7
Графические интерфейсы пользователя
«Я здесь, я смотрю на тебя, детка»
Для большинства программных систем графический интерфейс пользователя (Graphical User Interface, GUI) стал непременной частью пакета. Даже если акроним «GUI» вам незнаком, вы, вероятно, знакомы с такими элементами, как окна, кнопки и меню, используемые при работе с программами. В действительности большая часть работы с компьютерами сегодня осуществляется с помощью того или иного графического интерфейса вида «укажи-и-щелкни». Программы, от веб-броузера до системных инструментов, стандартно оснащаются компонентами GUI, повышающими гибкость и простоту их использования.
В этой части мы узнаем, как заставлять сценарии Python порождать такие графические интерфейсы, изучая примеры программирования с помощью модуля tkinter - переносимой библиотеки GUI, являющейся стандартной частью системы Python и широко используемой многими программистами. Как будет показано, легкость программирования интерфейсов пользователя в сценариях Python обеспечивается как простотой языка, так и мощью его библиотек GUI. Дополнительным преимуществом графических интерфейсов, запрограммированных на Python с использованием tkinter, является то, что они автоматически переносимы на большинство компьютерных систем.
Темы программирования GUI
Поскольку графические интерфейсы представляют собой большую область, я хочу сказать еще несколько слов об этой части книги. Чтобы сделать усвоение проще, темы программирования GUI разделены между следующими пятью главами этой книги:
• Данная глава начинается с краткого учебника по библиотеке tkinter, знакомящего с основами ее использования. Здесь намеренно сохраняется простота интерфейсов, чтобы вы могли овладеть базовыми знаниями, прежде чем перейти к интерфейсам из следующей главы. С другой стороны, в этой главе полностью освещаются все основы: обработка событий, менеджер компоновки pack, использование наследования и композиции в GUI и многое другое. Как будет показано, объектно-ориентированное программирование (ООП) не является обязательным для tkinter, но оно делает графические интерфейсы структурированными и многократно используемыми.
• Главы 8 и 9 представляют обзор набора графических элементов (виджетов) в библиотеке tkinter.30 В общих чертах, в главе 8 представлены простые графические элементы, а в главе 9 - более сложные графические элементы и связанные с ними инструменты. Здесь рассматривается большинство привычных элементов интерфейсов: ползунки, меню, диалоги, изображения и тому подобное. Эти две главы не могут служить полным справочником по библиотеке tkinter (который вполне мог бы превратиться в весьма объемную книгу), но их должно хватить, чтобы начать создавать серьезные графические интерфейсы на языке Python. Примеры в этих главах сосредоточены на графических элементах и инструментах tkinter, но попутно исследуется поддержка повторного использования программного кода в Python.
• В главе 10 представлены более сложные приемы программирования GUI. Здесь будут исследованы приемы автоматизации типичных задач, решаемых в графических интерфейсах на языке Python. И хотя библиотека tkinter является полнофункциональной, тем не менее небольшое количество многократно используемого программного кода на языке Python сможет сделать интерфейсы еще более мощными и простыми в использовании.
• Глава 11 завершает эту часть книги представлением нескольких программ с графическим интерфейсом, в которых используются приемы программирования и графические элементы, показанные в четырех предшествующих главах. Здесь мы узнаем, как реализовать текстовые редакторы, средства просмотра графических изображений, часы и многое другое.
Поскольку графические интерфейсы являются инструментами, используемыми в самых разных областях, другие примеры их реализации будут появляться на протяжении всей оставшейся части книги. Например, позднее мы увидим законченные графические интерфейсы для работы с электронной почтой и калькуляторы, а также простой клиент FTP с графическим интерфейсом. Дополнительные примеры, такие как средства представления деревьев и броузеры таблиц, доступны в виде внешних примеров, входящих в состав пакета с примерами. В главе 11 приводится список ссылок на другие примеры использования tkinter в данной книге.
Закончив исследование графических интерфейсов, мы в четвертой части узнаем, как конструировать пользовательские интерфейсы внутри веб-броузера, используя HTML и сценарии на языке Python, выполняющиеся на веб-сервере, - совершенно иная модель со своими достоинствами и недостатками, понимать которые просто необходимо. Далее в этой главе описываются такие новейшие технологии, как RIA (Rich Internet Application - полнофункциональные интернет-приложения), основанные на модели веб-броузера и способные предложить еще более широкие возможности в конструировании интерфейсов.
А пока сосредоточимся на более традиционных графических интерфейсах, известных как «настольные» приложения или как «автономные» графические интерфейсы. Как мы увидим далее, в части книги, посвященной созданию сценариев для Интернета, когда встретимся с графическими интерфейсами клиентов FTP и электронной почты, такие программы часто устанавливают сетевые соединения, необходимые для их работы.
Запуск примеров
Сразу же хочу отметить одно обстоятельство: в большинстве своем графические интерфейсы являются динамическими и интерактивными, а лучшее, что я могу здесь сделать, - это показать статические снимки экрана, представляющие избранные состояния взаимодействий, реализуемых такими программами. Для большинства примеров это несправедливо. Если вы еще не работаете с примерами, настоятельно рекомендую запускать примеры графических интерфейсов в этой и последующей главах самостоятельно.
В Windows стандартная установка Python имеет встроенную поддержку tkinter, поэтому все эти примеры должны работать сразу. Mac OS X также поставляется со встроенной поддержкой tkinter в Python. В других системах Python с поддержкой tkinter либо уже входит в состав системы, либо доступен для установки (подробности смотрите в файле верхнего уровня README-PP4E.txt в дереве примеров для этой книги). Несмотря на то, что вам, возможно, потребуется установить дополнительные пакеты, чтобы обеспечить работоспособность tkinter, это стоит сделать, потому что возможность поэкспериментировать с этим про-
граммами является отличным способом изучения не только программирования графических интерфейсов, но и самого языка Python.
Смотрите также замечания о переносимости примеров к этой книге в Предисловии. Несмотря на то, что и сам язык Python, и библиотека tkinter в значительной степени являются нейтральными к платформе, на которой они используются, вам, возможно, придется столкнуться с некоторыми небольшими проблемами, если вы будете пытаться запускать примеры на платформах, которые не использовались в процессе работы над этой книгой. В Mac OS X, например, могут обнаружиться тонкие отличия в работе некоторых примеров. Обязательно загляните на веб-сайт книги, где можно найти дополнительные указания и исправления, которые помогут использовать примеры на других платформах.
Кто-нибудь заметил, что «GUI» совпадает с тремя первыми буквами в имени «GUIDO»?
Создатель Python, Гвидо ван Россум (Guido van Rossum), изначально не ставил своей целью создание инструмента для разработки GUI, но из-за простоты использования Python и краткости цикла разработки он выдвинулся на одну из первых ролей. С точки зрения реализации, графические интерфейсы в Python опираются на использование расширений на языке C, а расширяемость была одной из главных идей при создании Python. Когда сценарий создает кнопки и меню, он в конечном счете обращается к библиотеке C, а когда сценарий реагирует на пользовательское событие, библиотека C в конечном счете обращается обратно к Python. Это может рассматриваться как пример взаимодействия Python с внешними библиотеками.
С практической точки зрения графические интерфейсы являются жизненно важной частью современных систем и идеальной областью применения таких инструментов, как Python. Как будет показано далее, простота синтаксиса Python и его объектноориентированные возможности хорошо сочетаются с моделью GUI - каждый изображаемый на экране графический элемент естественным образом представляется в виде класса Python. Кроме того, краткость цикла разработки в Python позволяет программистам быстро экспериментировать с вариантами расположения элементов и их поведением, что невозможно при использовании обычных технологий разработки. На практике обычно можно за считаные секунды сделать изменения в GUI, созданном на Python, и посмотреть на их результат. Не пытайтесь добиться такой же скорости на C или C++ !
Различные возможности создания GUI в Python
Прежде чем погрузиться в tkinter, рассмотрим перспективные варианты разработки GUI в Python в целом. Поскольку Python показал себя вполне подходящим инструментом для работы с графическим интерфейсом, в последние годы в этой области наблюдалась высокая активность. На практике, хотя в качестве инструмента для создания GUI в Python чаще всего используется библиотека tkinter, на сегодняшний день существуют и другие способы программирования пользовательских интерфейсов в Python. Некоторые являются специфическими для Windows или X Window,31 другие представляют собой решения для нескольких платформ, и у всех них есть свои приверженцы и свои сильные стороны. Чтобы быть справедливыми ко всем вариантам, приведем краткий перечень инструментов создания графических интерфейсов, доступных программистам Python на момент написания этих строк:
tkinter
Библиотека для разработки графических интерфейсов, распространяемая с открытыми исходными текстами, ставшая де-факто стандартом для разработки переносимых графических интерфейсов на языке Python. Сценарии на языке Python, использующие tkinter для построения GUI, выполняются переносимым образом в Windows, X Window (Unix и Linux) и Macintosh OS X, создавая на каждой из этих платформ графический интерфейс, со свойственным им внешним видом. Кроме того, она может легко расширяться программным кодом на языке Python, а также имеет множество дополнительных пакетов, таких как Pmw (сторонняя библиотека виджетов); Tix (еще одна библиотек виджетов, ныне ставшая стандартной частью Python); PIL (расширение для обработки изображений) и ttk (библиотека виджетов Tk, поддерживающих темы оформления, также ставшая стандартной частью Python, начиная с версии 3.1). Подробнее о подобных расширениях рассказывается ниже в этом введении.
Библиотека Tk, на которой основана библиотека tkinter, является стандартом в мире открытого программного обеспечения в целом и используется также языками сценариев Perl, Ruby, PHP, Common Lisp и Tcl, благодаря чему количество пользователей этой библиотеки может исчисляться миллионами. Промежуточная библиотека, связывающая Python и Tk, дополняет последнюю простой объектной моделью Python - благодаря ей виджеты Tk из строковых команд превратились в легко настраиваемые объекты. Библиотека tkinter в Python 3.X приняла форму пакета с вложенными модулями, обеспечивающими группировку некоторых из ее инструментов по функциональности (ранее, в Python 2.X, эта библиотека имела форму модуля tkinter, но была переименована в соответствии с общепринятыми соглашениями об именовании и реструктурирована для обеспечения иерархической организации).
tkinter - зрелая, надежная, широко используемая и хорошо документированная библиотека. Она включает около 25 основных типов виджетов, а также различные диалоги и другие инструменты. Издана книга, посвященная описанию библиотеки, а кроме того, имеется огромное количество опубликованной документации с описанием tkinter и Tk. Также важно, что так как tkinter основана на библиотеке, разработанной для использования в языках сценариев, она является относительно легковесным набором инструментов и поэтому прекрасно сочетается с языком Python.
Благодаря таким характеристикам библиотека tkinter распространяется в составе Python как модуль стандартной библиотеки и стала основой стандартной интегрированной среды разработки IDLE с графическим интерфейсом. Фактически библиотека tkinter является единственным набором инструментов для создания графических интерфейсов, ставшим частью Python, - все остальные в этом списке являются сторонними расширениями. На некоторых платформах (включая Windows, Mac OS X и большинство Linux и Unix-подобных систем) библиотека Tk также распространяется в составе Python. Вы можете с достаточной степенью уверенности рссчитывать, что к моменту запуска сценария библиотека tkinter будет присутствовать на компьютере, а при необходимости вы сможете обеспечить это, скомпилировав свой графический интерфейс в автономный выполняемый файл с помощью таких инструментов, как PyInstaller и py2exe (подробности ищите в Интернете).
Несмотря на простоту библиотеки tkinter, ее текстовые и графические виджеты обеспечивают достаточно широкие возможности для реализации веб-страниц, трехмерных изображений и анимации. Кроме того, на сегодняшний день многие системы предоставляют построители графических интерфейсов для связки Python/tkinter, включая GUI Builder (ранее входивший в состав среды разработки Komodo IDE и родственной ей SpecTCL), Rapyd-Tk, xRope и другие (этот перечень имеет свойство значительно изменяться с течением времени; более полный перечень вы найдете на странице http://wiki. python.org/moin/GuiProgramming или поискав самостоятельно в Интернете). Однако, как будет показано далее, библиотека tkinter во многих случаях оказывается настолько удобна в использовании, что построители графических интерфейсов не получили широкого распространения. В справедливости этого утверждения мы убедимся, как только покинем область статических интерфейсов, которые обычно поддерживаются построителями.
wxPython
Интерфейс Python к библиотеке wxWidgets с открытыми исходными текстами (ранее называвшейся wxWindows), которая представляет собой переносимую структуру классов GUI, первоначально созданную для использования из программ на языке C++. Система wxPython является модулем расширения, служащим оболочкой для классов wxWidgets. По общему мнению эта библиотека превосходно подходит для создания сложных интерфейсов и сегодня является, вероятно, вторым по популярности инструментом создания графических интерфейсов в Python после tkinter. Графические интерфейсы, созданные с применением wxPython, переносимы на Windows и Unix-подобные платформы и Mac OS X.
Поскольку интерфейс wxPython опирается на библиотеку классов C++, многие полагают, что он более сложен в использовании, чем библиотека tkinter: он предоставляет доступ к сотням классов, для чего требуется прибегать к объектно-ориентированному стилю программирования, и имеет архитектуру, которая некоторым напоминает библиотеку классов MFC в Windows. Применение wxPython часто требует от программистов писать больше программного кода, отчасти потому, что этот интерфейс обладает более широкими функциональными возможностями, а отчасти потому, что именно такой образ мышления он унаследовал от библиотеки C++, лежащей в его основе.
Кроме того, часть документации wxPython ориентирована на программистов, использующих язык C++. Впрочем, недавно эта ситуация немного улучшилась - после выхода книги, посвященной wxPython. Библиотеке tkinter, напротив, посвящена книга, огромные разделы в других книгах о языке Python и еще масса литературы по библиотеке Tk, лежащей в ее основе. Однако, поскольку мир книг о языке Python в последние годы расширялся достаточно динамично, вам следует тщательно подходить к выбору литературы - некоторые книги со временем устаревают, но регулярно появляются новые.
В обмен на повышенную сложность библиотека wxPython обеспечивает набор мощных инструментов. В состав wxPython входит более богатый набор виджетов, чем в библиотеку tkinter, включая деревья и компоненты просмотра HTML - чтобы получить такие же компоненты при использовании tkinter, может потребоваться задействовать такие расширения, как Pmw, Tix или ttk. Кроме того, некоторым нравится, как выглядят графические интерфейсы, созданные с помощью wxPython. BoaConstructor и wxDesigner среди других возможностей предоставляют построители графических интерфейсов, которые генерируют программный код для wxPython. Некоторые инструменты в библиотеке wxWidgets также поддерживают операции, не имеющие отношения к графическим интерфейсам. Чтобы быстро посмотреть, как выглядят виджеты wxPython и соответствующий программный код, запустите демонстрационный пример, который поставляется вместе с wxPython (смотрите страницу http://wxpython.org/ или поищите в Интернете самостоятельно).
PyQt
Интерфейс Python к библиотеке Qt (ныне принадлежит Nokia, ранее принадлежала компании Trolltech), занимающей, пожалуй, третье место среди наиболее часто используемых инструментов GUI для Python. PyQt - это полноценная библиотека создания графических интерфейсов, которая на сегодняшний день переносима в Windows, Mac OS X, Unix и Linux. Подобно wxPython, библиотека PyQt в целом более сложна в использовании, чем tkinter, и при этом обладает более богатыми возможностями - она содержит сотни классов и тысячи функций и методов. Библиотека Qt зародилась и выросла в Linux, но со временем была перенесена и на другие системы. Вследствие своего происхождения расширения PyQt и PyKDE предоставляют доступ к библиотекам KDE (PyKDE требует библиотеку PyQt). Системы BlackAdder и Qt Designer предоставляют построители GUI для PyQt.
Самым важным, пожалуй, недостатком Qt в прошлом считалась неполная открытость библиотеки для коммерческого использования. Сегодня библиотека Qt распространяется не только под коммерческой лицензией, но и под открытыми лицензиями GPL и LGPL. Версии, распространяемые под LGPL и GPL, являются открытыми и следуют требованиям лицензии GPL (GPL накладывает требования, отсутствующие в лицензии BSD, под которой распространяется сам Python, - согласно GPL, например, вы должны сделать исходные тексты программ доступными для конечных пользователей).
PyGTK
Интерфейс Python к GTK, переносимой библиотеке GUI, первоначально использовавшейся как ядро оконной системы Gnome в Linux. Пакеты расширений gnome-python и PyGTK экспортируют функции в инструментальных наборах Gnome и GTK для использования в сценариях Python. К моменту написания этих строк интерфейс PyGTK поддерживал возможность работы в Windows и в POSIX-совместимых системах, таких как Linux и Mac OS X (согласно документации, в настоящее время требуется, чтобы в Mac OS был установлен X-сервер, при этом разрабатывается версия для Mac).
Jython
Jython (система, известная ранее, как JPython) является реализацией Python для Java, которая компилирует исходный программный код Python в байт-код Java и обеспечивает сценариям Python беспрепятственный доступ к библиотекам классов Java на локальном компьютере. Благодаря этому библиотеки для построения графических интерфейсов на языке Java, такие как swing и awt, дают еще один способ построения GUI на языке Python, выполняемом в системе JPython. Очевидно, такие решения являются специфическими для Java, и их переносимость ограничена переносимостью языка Java и его библиотек. Кроме того, следует отметить, что swing является самым крупным и самым сложным способом создания GUI в Python. Кроме того, существует новый пакет под названием jt-kinter, который является версией tkinter для Jython, использующей Java JNI, - если он установлен, сценарии на языке Python смогут также использовать tkinter для построения GUI в Jython. Еще раз с Jython мы встретимся в главе 12, когда будем знакомиться с его ролью в Интернете.
IronPython
Очень напоминающая Jython, система IronPython является реализацией языка Python для окружения .NET, которая, кроме всего прочего, компилирует программы на языке Python в байт-код .NET, что также позволяет сценариям Python использовать возможности конструирования графических интерфейсов, имеющиеся в .NET Framework. Вы пишете программный код на языке Python, но для конструирования интерфейсов и приложений в целом используете компоненты C#/.NET. Программный код на IronPython может выполняться в Windows, под управлением .NET, и в Linux, под управлением Mono, реализации .NET, и Silverlight, клиентской платформы полнофункциональных интернет-приложений (RIA) для вебброузеров (обсуждается далее).
PythonCard
Построитель и библиотека GUI с открытыми исходными текстами, реализованные поверх wxPython. Считается одним из самых близких Python-эквивалентов того вида построителей GUI, которые хорошо знакомы разработчикам на Visual Basic. PythonCard позиционируется разработчиками, как конструктор GUI для создания на языке Python кроссплатформенных приложений, способных выполняться в Windows, Mac OS X и Linux.
Dabo
Построитель графических интерфейсов с открытыми исходными текстами, также реализованный на основе wxPython, но не только. Dabo -это переносимый, трехуровневый, кроссплатформенный фреймворк для разработки настольных приложений на языке Python, созданный по образу и подобию Visual FoxPro. Трехуровневая организация обеспечивает возможность доступа к базе данных, реализации бизнес-логики и пользовательского интерфейса. Открытая архитектура способна поддерживать различные типы баз данных и механизмов создания графических интерфейсов (wxPython, tkinter и даже HTML через HTTP).
Полнофункциональные интернет-приложения (Rich Internet Applications, RIA)
Хотя веб-страницы, отображаемые с помощью HTML, также могут считаться своего рода пользовательским интерфейсом, они исторически обладают массой ограничений, что затрудняет их отнесение к категории универсальных графических интерфейсов. Тем не менее некоторые специалисты склонны расширять рамки этой категории и включать в нее системы, обеспечивающие интерфейсам на основе броузеров более высокую динамичность по сравнению с традиционными веб-страницами. Поскольку такие системы предоставляют комплекты виджетов, отображаемых веб-броузерами, они способны предложить те же преимущества переносимости, которые свойственны веб-страницам в целом.
Это новое поколение инструментов называется модным термином полнофункциональные интернет-приложения (Rich Internet Applications, RIA). В их число входят AJAX и фреймворки, ориентированные на широкое применение JavaScript на стороне клиента, такие как:
Flex
Фреймворк с открытыми исходными текстами от компании Adobe и часть платформы Flash*
Silverlight
Фреймворк от корпорации Microsoft, который также реализован в Linux в виде фреймворка Moonlight для Mono, - доступный из программного кода на языке Python при использовании описанной выше системы IronPython*
JavaFX
Платформа Java для построения RIA, которые способны выполняться на различных связанных между собой устройствах*
pyjamas
Версия фреймворка Google Web Toolkit, опирающегося на использование AJAX, реализованная на языке Python, в состав которой входит набор виджетов пользовательского интерфейса и компилятор с языка Python на язык JavaScript, что позволяет выполнять сценарии в броузере, на стороне клиента*
Свои решения в этой области предлагает и стандарт HTML5, находящийся в разработке.
Вообще говоря, веб-броузеры сами являются «настольными» приложениями с графическим интерфейсом, но они получили гораздо более широкое распространение, чем библиотеки GUI, и благодаря инструментам RIA способны отображать другие графические интерфейсы. С помощью таких фреймворков можно строить графические интерфейсы, основанные на использовании виджетов, но за ними стоят накладные расходы, связанные с необходимостью сетевых взаимодействий, и они часто подразумевают использование более тяжеловесного программного комплекса, чем традиционные инструменты создания графических интерфейсов. В действительности, чтобы превратить броузеры в универсальные платформы отображения графических интерфейсов, полнофункциональные интернет-приложения предполагают использование дополнительных программных уровней и зависимостей и даже нескольких языков программирования. Поэтому, а также потому, что далеко не все занимаются разработкой веб-приложений (независимо от того, что вы могли об этом слышать), мы не будем включать их в рассмотрение традиционных автономных/настольных графических интерфейсов в этой части книги.
Дополнительные сведения о полнофункциональных интернет-приложениях и о пользовательских интерфейсах на основе веб-броузеров вы найдете в части книги, посвященной разработке сценариев для Интернета. К тому же обязательно следите за новостями и веяньями по этой теме. Интерактивность, которую обеспечивают эти инструменты, является ключевой составляющей того, что иногда называют «Web 2.0», делая больший акцент на Web, а не на традиционные графические интерфейсы. Однако, так как здесь для нас больший интерес представляют последние (и поскольку взаимодействие с пользователем - это взаимодействие с пользователем, независимо от используемого профессионального жаргона), мы отложим дальнейшее обсуждение этой темы до следующей части книги.
Платформозависимые инструменты.
Помимо переносимых инструментов, таких как tkinter, wxPython и PyQt, и платформонезависимых решений, таких как полнофункциональные интернет-приложения, большинство основных платформ обладают также непереносимыми средствами создания графических интерфейсов на языке Python. Например, в Macintosh OS X имеется интерфейс PyObjC для Python, обеспечивающий доступ к фреймворку Objective-C/Cocoa компании Apple, который составляет основу разработки многих приложений для Mac. В Windows имеется расширение PyWin32 для Python, включающее обертки для доступа к фреймворку Microsoft Foundation Classes (MFC) (библиотека, включающая интерфейсные компоненты), а также Pythonwin - пример типичной программы на основе MFC, реализующей среду разработки на языке Python с графическим интерфейсом. Несмотря на то, что технически в Linux имеется поддержка .NET, тем не менее система IronPython, упоминавшаяся выше, предлагает дополнительные возможности, которые могут использоваться только в Windows.
За дополнительными подробностями обо всех перечисленных инструментах обращайтесь на веб-сайты этих проектов. Существуют и другие, менее известные инструментарии GUI для Python, а к тому времени, когда вы будете читать эту книгу, наверняка появятся новые (например, IronPython стал новинкой для третьего издания, а для четвертого издания новинкой стали инструменты реализации полнофункциональных интернет-приложений). Кроме того, перечень доступных пакетов постоянно изменяется. Свежий список имеющихся инструментов можно найти с помощью поисковых систем, а также на сайте http:// www.python.org и в каталоге PyPI пакетов сторонних разработчиков, поддерживаемом там же.
Обзор tkinter
Однако все эти способы создания графических интерфейсов сегодня значительно опережает библиотека tkinter как стандарт де-факто реализации переносимых пользовательских интерфейсов на Python, которой и посвящена эта часть книги. Доводы в пользу использования этого подхода уже приводились в главе 1 - я предпочел подробно описать один инструментарий вместо поверхностного обзора нескольких. Кроме того, большинство концепций программирования с использованием библиотеки tkinter, с которыми вы познакомитесь здесь, можно с легкостью перенести на любые другие инструменты GUI.
Практические преимущества tkinter
Но как бы то ни было, существуют более прагматичные причины, объясняющие, почему библиотека tkinter де-факто стала в мире Python стандартом разработки переносимых графических интерфейсов. Такие преимущества, как доступность, переносимость, простота получения, документированность и наличие расширений, делают ее наиболее широко используемым решением для Python в области GUI в течение многих лет:
Доступность
Библиотека tkinter в целом рассматривается как облегченный набор инструментов и одно из наиболее простых решений в области разработки графических интерфейсов для Python из имеющихся на сегодняшний день. В отличие от более крупных фреймворков, библиотека tkinter позволяет сразу же приступить к работе с ней, без необходимости предварительно осваивать значительно более крупные модели взаимодействия классов. Как будет показано далее, программист может создать с помощью tkinter простой интерфейс, написав всего несколько строк программного кода, и постепенно наращивать его возможности до достижения промышленного уровня. Несмотря на всю простоту прикладного интерфейса библиотеки tkinter, она позволяет добавлять новые виджеты, написанные на языке Python, или подключать дополнительные расширения, такие как Pmw, Tix и ttk.
Переносимость
Сценарий на языке Python, в котором графический интерфейс строится с помощью библиотеки tkinter, будет работать без изменений на всех основных современных оконных платформах: Microsoft Windows, X Window (в Unix и Linux) и Macintosh OS X (а также в классической версии Mac). Более того, этот сценарий создаст интерфейс, внешний вид которого будет привычен пользователям каждой из этих платформ. Эта особенность развивалась по мере того как библиотека Tk становилась все более зрелой. Графический интерфейс, реализованный сценарием Python/tkinter, в Windows выглядит, как должен выглядеть интерфейс программы для Windows; в Unix и Linux обеспечивает такое же взаимодействие с пользователем, но демонстрирует внешний вид, знакомый пользователям X Window; и на Mac он выглядит так, как должна выглядеть программа Mac.
Простота получения
tkinter является модулем стандартной библиотеки Python, поставляемой вместе с интерпретатором. Если Python установлен на вашем компьютере, то у вас есть доступ и к библиотеке tkinter. Более того, в большинство пакетов установки Python (включая стандартный пакет установки Python для Windows, пакет установки для Mac и пакеты установки для большинства дистрибутивов Linux) уже включена поддержка tkinter. Благодаря этому сценарии, написанные с использованием модуля tkinter, сразу могут работать с большинством интерпретаторов Python, не требуя дополнительных действий по установке. Библиотека tkinter также в целом лучше поддерживается, чем существующие сегодня альтернативные пакеты. Поскольку задействованная в ней библиотека Tk используется также языками программирования Tcl и Perl (и многими другими), ей уделяется больше внимания и усилий разработчиков, чем другим имеющимся инструментариям.
Естественно, при использовании инструментов разработки графических интерфейсов важную роль играют и другие факторы, такие как
наличие документации и расширений. Рассмотрим вкратце, что можно
в этом отношении сказать о библиотеке tkinter.
Документация tkinter
В данной книге исследуются основы использования tkinter и большинство виджетов, чего должно быть достаточно, чтобы приступить к созданию графических интерфейсов на языке Python. С другой стороны, книга не является исчерпывающим справочником по библиотеке tkinter или по расширениям к ней. К счастью, когда я пишу этот абзац, в продаже есть по крайней мере одна книга, посвященная использованию tkinter в Python, и готовятся к выходу другие (подробности ищите в Интернете). Кроме книг можно найти электронную документацию по tkinter - полный комплект руководств по tkinter в настоящее время присутствует на сайте http://www.pythonware.com/library.
Кроме того, поскольку инструментарий Tk, используемый в tkinter, также де-факто является стандартом в сообществе открытого программного обеспечения в целом, можно использовать и другие источники документации. Например, библиотека Tk принята также к использованию в языках программирования Tcl и Perl, поэтому книги и документация по Tk, написанные для этих двух языков, могут также непосредственно использоваться при использовании связки Python/tkinter (правда, при этом необходимо будет учитывать синтаксические различия).
Честно говоря, я изучал библиотеку tkinter по книгам и справочникам Tcl/Tk - просто замените строки Tcl объектами Python, и вы получите в свое распоряжение дополнительные библиотеки справочников (чтение документации по Tk облегчит руководство по преобразованию Tk в tkinter, представленное в виде табл. 7.2 в конце данной главы). Например, книга «Tcl/Tk Pocket Reference» («Карманный справочник по Tcl/ Tk»), выпущенная издательством O’Reilly, может служить прекрасным дополнением к учебному материалу по tkinter в данной части книги. Кроме того, поскольку понятия Tk знакомы большому числу программистов, поддержку по Tk можно легко получить в Сети.
После того как вы изучите основы, вы также сможете немало почерпнуть из примеров. В Интернете можно найти множество примеров демонстрационных программ, использующих tkinter, помимо тех, что будут представлены в этой книге. Даже в составе дистрибутива с исходными текстами Python имеется несколько демонстрационных программ, в подкаталоге Demos\tkinter. Среда разработки IDLE с графическим интерфейсом, упоминающаяся в следующем разделе, также представляет интерес для изучения.
Расширения для tkinter
Благодаря широкому использованию библиотеки tkinter, программистам доступны готовые расширения, предназначенные для работы с ней или дополняющие ее. На момент написания этих строк некоторые из них еще не были доступны для версии Python 3.X. Например:
Pmw
Python Mega Widgets является расширением, предназначенным для создания составных виджетов высокого уровня в Python с помощью модуля tkinter. Он расширяет прикладной интерфейс tkinter набором более сложных графических элементов для разработки улучшенных графических интерфейсов, и предоставляет основу для создания собственных виджетов. В число готовых и расширяемых графических элементов, имеющихся в пакете, входят блокноты, комбинированные списки, элементы выбора, панели, окна с прокруткой, диалоговые окна, виджеты группировки кнопок, всплывающие подсказки и интерфейс к пакету Blt для построения графиков.
Прикладной интерфейс графических элементов («мегавиджетов») Pmw напоминает интерфейс базовых графических элементов tkinter, поэтому в сценариях на языке Python могут совместно использоваться элементы Pmw и стандартные элементы tkinter. Кроме того, расширение Pmw написано исключительно на языке Python и потому не требует наличия компилятора с языка C или установки дополнительных инструментов. Чтобы ознакомиться с виджетами из этого расширения, а также посмотреть на программный код их реализации, запустите сценарий demos\All.py, входящий в дистрибутив Pmw. Получить Pmw можно по адресу: http://pmw.sourceforge.net.
Tix
Tix - это коллекция из более чем 40 улучшенных виджетов, изначально написанных для Tcl/Tk, но теперь доступных для использования в программах Python/tkinter. В настоящее время этот пакет входит в состав стандартной библиотеки Python под названием tkinter.tix. Подобно Tk, библиотека Tix, используемая модулем, распространяется в составе дистрибутива Python для Windows. Другими словами, во время установки Python в Windows одновременно устанавливается и библиотека Tix с дополнительными виджетами.
Пакет Tix содержит множество тех же графических элементов, которые входят в состав Pmw, включая счетчики, деревья, блокноты с вкладками, всплывающие подсказки, панели и многие другие. За дополнительной информацией обращайтесь к разделу о пакете Tix в руководстве по библиотеке Python. Чтобы быстро посмотреть, как выглядят виджеты из этого пакета, а также взглянуть, как они используются в программном коде, воспользуйтесь демонстрационной программой tixwidgets.py в каталоге Demo\tix, в дистрибутиве с исходными текстами Python (этот каталог не устанавливается по умолчанию в Windows и часто изменяется, но его можно отыскать после распаковки дистрибутива с исходными текстами, полученного с сайта проекта Python.org).
ttk
Библиотека ttk виджетов Tk, поддерживающих темы оформления, -это относительно новый набор виджетов, в которых предпринята попытка отделить реализацию поведения от реализации их внешнего вида. Классы виджетов сохраняют информацию о своем состоянии и поддерживают систему обратных вызовов, при этом внешний вид элементов реализуется отдельно, с применением тем оформления. Подобно Tix, это расширение начинало свое существование как отдельная библиотека, но совсем недавно, в версии Python 3.1, было интегрировано в стандартную библиотеку Python как модуль tkinter.ttk.
Кроме того, подобно Tix, это расширение включает улучшенные виджеты, часть из которых отсутствует в стандартной библиотеке tkinter. Точнее, ttk содержит 17 виджетов, из которых 11 уже присутствуют в библиотеке и предназначены для замены некоторых стандартных виджетов, а 6 - являются новыми: Combobox, Notebook, Progressbar, Separator, Sizegrip и Treeview. В двух словах: чтобы задействовать новые виджеты ttk, сценарии должны импортировать их из модуля ttk после импортирования модуля tkinter и вместо настройки самих виджетов настроить объекты стилей, которые могут использоваться совместно несколькими виджетами.
Как будет показано далее в этой главе, имеется возможность обеспечить единство оформления с виджетами из стандартной библиотеки tkinter - за счет создания подклассов виджетов с применением обычных приемов ООП (смотрите раздел «Настройка виджетов с помощью классов» ниже). При этом ttk предлагает дополнительные возможности оформления и улучшенные типы виджетов. За дополнительной информацией о виджетах ttk обращайтесь к соответствующему разделу в руководстве по библиотеке Python или поищите в Интернете - эта книга описывает основы использования tkinter, а расширения tix и ttk слишком большие, чтобы их можно было охватить с достаточной степенью подробности.
PIL
Python Imaging Library (PIL) является расширением с открытыми исходными текстами, добавляющим в Python дополнительные средства для работы с графикой. Помимо всего прочего, он добавляет инструменты для создания миниатюр изображений, их трансформации и преобразования, расширяет базовый набор графических объектов tkinter и обеспечивает поддержку отображения многих типов графических файлов. Расширение PIL, например, позволяет графическим интерфейсам на базе tkinter отображать изображения в форматах JPEG, TIFF и PNG, не поддерживаемых базовыми инструментами tkinter (без дополнительных расширений, библиотека tkinter поддерживает только формат GIF и некоторые растровые форматы). Более подробное описание и примеры использования этого расширения вы найдете в конце главы 8 - мы будем использовать это расширение в некоторых примерах, связанных с обработкой изображений. Расширение PIL можно найти на странице проекта http:// www.pythonware.com или выполнив поиск в Интернете.
IDLE
Интегрированная среда разработки на языке Python IDLE сама написана на Python и tkinter. Она поставляется и устанавливается вместе с пакетом Python (если у вас свежий интерпретатор Python, то должна быть и среда IDLE, - в Windows щелкните на кнопке Пуск (Start), выберите пункт меню Все программы (ALL Programs), щелкните на элементе меню Python и вы увидите ее). Как среда разработки IDLE предоставляет текстовые редакторы с подсветкой синтаксиса, интерфейс отладки «указал-и-щелкнул» и многое другое. Эта среда может служить примером использования tkinter.
Другие
Многие расширения, предоставляющие инструменты визуализации для Python, основаны на библиотеке tkinter и ее виджете холста. Дополнительные примеры расширений библиотеки tkinter можно найти на веб-сайте PyPI или воспользовавшись поисковыми системами.
Если вы собираетесь заниматься коммерческой разработкой графических интерфейсов на основе tkinter, вам, вероятно, следует ознакомиться с такими расширениями, как Pmw, PIL, Tix и ttk, после изучения основ tkinter в данной книге. Они помогут сэкономить время разработки и добавить блеска в ваши интерфейсы. Самые последние новости и ссылки смотрите на сайтах, посвященных Python, указанных выше.
Структура tkinter
С технической точки зрения, библиотека tkinter является интегрирующей системой, требующей некоторой особой организации программ. Что это означает, мы увидим чуть ниже, а пока кратко познакомимся с некоторыми терминами и понятиями, лежащими в основе программирования графических интерфейсов на языке Python.
Структура реализации
Строго говоря, tkinter является просто названием интерфейса к Tk - библиотеке GUI, первоначально написанной для использования с языком программирования Tcl и разработанной создателем Tcl Джоном Оустер-хаутом (John Ousterhout). Модуль tkinter обращается к библиотеке Tk, которая в свою очередь обращается к оконной системе: Microsoft Windows, X Window в Unix или к графической подсистеме Macintosh. Переносимость библиотеки tkinter фактически зависит от переносимости библиотеки Tk.
tkinter - программный слой поверх Tk, позволяющий сценариям на языке Python обращаться к библиотеке Tk, конструирующей и настраивающей интерфейсы и возвращающей управление обратно в сценарии Python, которые обрабатывают события, генерируемые пользователем (например, щелчки мышью). Таким образом, обращения к графическому интерфейсу из сценария Python направляются в tkinter, а затем в Tk; события, возникающие в графическом интерфейсе, направляются из Tk в tkinter, а затем обратно в сценарий Python. В главе 20 мы встретимся с этими переходами под именами, которые они имеют в интеграции с языком C: расширение и встраивание.
Технически в настоящее время библиотека tkinter организована как комбинация файлов пакета tkinter на языке Python и модуля расширения с именем _tkinte r, который написан на языке C. Модуль _tkinte r обращается к библиотеке Tk, используя инструменты расширений, и производит обратные вызовы объектов Python, используя инструменты встраивания, - модуль tkinter просто добавляет объектно-ориентированный интерфейс поверх _tkinter. Однако вам в своих сценариях практически всегда придется импортировать модуль tkinter, а не _tkinter - последний, являясь модулем реализации, предназначен исключительно для внутреннего использования (и получил такое необычное название именно по этой причине).
Программная структура
К счастью, программистам на языке Python обычно не нужно беспокоиться обо всей этой интеграции и маршрутизации вызовов, реализуемыми внутренними механизмами: они просто создают виджеты и регистрируют функции на языке Python для обработки событий этих элементов. Однако из-за имеющейся в итоге структуры обработчики событий обычно называют обработчиками обратного вызова, потому что библиотека GUI «делает обратный вызов» программного кода на языке Python, когда происходят события.
Ниже мы увидим, что программы Python/tkinter полностью управляются событиями: они создают графические интерфейсы и регистрируют обработчики событий, а затем ничего не делают и только ждут, когда произойдут события. Во время этого ожидания библиотека Tk выполняет цикл событий, который следит за щелчками мыши, нажатием клавиш и так далее. Вся обработка, выполняемая прикладной программой, происходит в зарегистрированных обработчиках, вызываемых в ответ на происходящие события. Кроме того, вся информация, необходимая одновременно разным событиям, должна храниться по ссылкам с длительным сроком жизни, например в глобальных переменных и атрибутах экземпляров классов. Представление об обычной линейной логике выполнения программы неприменимо к графическим интерфейсам - здесь нужно мыслить на языке небольших фрагментов программного кода.
В языке Python библиотека Tk становится объектно-ориентированной просто потому, что сам Python является объектно-ориентированным языком: слой tkinter экспортирует прикладной интерфейс библиотеки Tk как классы Python. При работе с библиотекой tkinter можно использовать простой подход, основанный на вызовах функций, создающих виджеты и интерфейсы, или объектно-ориентированные приемы, такие как наследование и композиция, для настройки и расширения классов из базового набора tkinter. Большие графические интерфейсы на базе tkinter обычно строятся как деревья связанных между собой графических элементов и часто реализуются в виде классов Python, чтобы обеспечить структурированность и сохранность информации о состоянии в промежутках между событиями. В этой части книги мы увидим, что графические интерфейсы на базе tkinter, реализация которых представляется классами, почти по умолчанию становятся многократно используемыми программными компонентами.
Взбираясь по кривой обучения программированию графических интерфейсов
Теперь перейдем к программированию. Начнем с нескольких небольших примеров, иллюстрирующих основные понятия, и покажем, как создаются окна на экране компьютера. По ходу изложения примеры будут становиться более сложными, но для начала - об основных принципах.
«Hello World» в четыре строки (или меньше)
Обычно первым делом при изучении особенностей создания графических интерфейсов демонстрируется пример, выводящий в окне строку «Hello World». Пример 7.1 делает это в четырех строках.
Пример 7.1. PP4E\Gui\Intro\gui1.py
from tkinter import Label # импортировать виджет
widget = Label(None, text='Hello GUI world!’) # создать его
widget.pack() # разместить
widget.mainloop() # запустить цикл событий
Это законченная программа на языке Python, реализующая графический интерфейс с помощью библиотеки tkinter. При запуске этого сценария получается простое окно с меткой посередине. Его вид, как оно выглядит в Windows 7 на моем ноутбуке, показан на рис. 7.1 (я специально растягивал некоторые окна, изображения которых приводятся в этой книге, по горизонтали, чтобы заголовки окон были видны полностью; внешний вид окон у вас может немного отличаться, в зависимости от используемой платформы).
Пока здесь нечем особенно похвастать, но обратите внимание, что это независимое полнофункциональное окно на экране компьютера. Его можно распахнуть на весь экран, свернуть и спрятать на системной панели, изменить его размер. Щелкните на кнопке «X» в правом верхнем углу окна, чтобы закрыть его и завершить программу.
Рис. 7.1. «Hello World» (gui1) в Windows
Кроме того, сценарий, создающий это окно, полностью переносим. Запустите его на своем компьютере, чтобы увидеть окно, создаваемое им. При запуске этого файла в Linux создается аналогичное окно, но ведет оно себя в соответствии с работающим в Linux менеджером окон. Даже в одной и той же системе один и тот же программный код на языке Python в разных оконных системах (например, в KDE и Gnome) может воспроизводить окна, выглядящие по-разному. Тот же сценарий будет воспроизводить окна, отличающиеся внешним видом, в Macintosh и других Unix-подобных системах. Однако на всех платформах его поведение будет одним и тем же.
Основы использования tkinter
Сценарий gui1 является тривиальным примером, но он иллюстрирует шаги, которые выполняются большинством программ, использующих библиотеку tkinter. Этот сценарий делает следующее:
1. Загружает класс виджета из модуля tkinter*
2. Создает экземпляр импортированного класса Label*
3. Упаковывает (размещает) новый объект Label в его родительском элементе*
4. Вызывает функцию mainloop, чтобы показать окно и начать цикл событий tkinter*
Метод mainloop, вызываемый последним, помещает метку на экран и входит в состояние ожидания, в котором отслеживаются события графического интерфейса, генерируемые пользователем. Внутри функции mainloop библиотека tkinter следит за такими вещами, как клавиатура или мышь, чтобы обнаружить порожденные пользователем события. Функционально функция mainloop в библиотеке tkinter напоминает следующий псевдокод:
def mainloop():
пока главное окно не закрыто: если возникло событие:
вызвать соответствующий обработчик
В этой модели вызов mainloop в примере 7.1 никогда не вернет управление сценарию, пока графический интерфейс отображается на экране.32 Как мы увидим, добравшись до больших сценариев, единственным способом выполнять какие-либо операции после вызова mainloop является регистрация обработчиков обратного вызова, реагирующих на события.
Это называется разработкой программ, управляемых событиями, и, возможно, это один из самых необычных аспектов графических интерфейсов. Программы с графическим интерфейсом принимают форму набора обработчиков событий, которые совместно используют хранящуюся информацию, а не линейного потока выполнения. Как это выглядит в действующем программном коде, мы увидим в последующих примерах.
Обратите внимание, что для создания графического интерфейса в этом сценарии действительно необходимо выполнить шаги 3 и 4. А чтобы отобразить окно, нужно вызвать mainloop - для вывода виджетов внутри окна они должны быть скомпонованы (то есть размещены), чтобы менеджер компоновки tkinter знал о них. На самом деле, если вызвать только mainloop или только pack, не вызывая второго из них, окно будет показывать не то, что нужно: mainloop без pack выведет пустое окно, а pack без mainloop не выведет ничего, потому что сценарий не войдет в состояние ожидания событий (можете попробовать). Иногда вызывать функцию mainloop необязательно, например, при программировании в интерактивной оболочке, но в общем случае вам не следует полагаться на это.
Поскольку понятия, иллюстрируемые этим простым примером, лежат в центре большинства программ tkinter, рассмотрим их несколько глубже, прежде чем двинуться дальше.
Создание виджетов
При создании графических элементов в tkinter можно указать, как они должны быть настроены. Сценарий gui1 передает два аргумента конструктору класса Label:
• Первый аргумент определяет объект родительского виджета, к которому нужно прикрепить новую метку. В данном случае None означает «прикрепить новый виджет Label к установленному по умолчанию окну верхнего уровня данной программы». Позднее в этом аргументе мы будем передавать фактические виджеты, чтобы прикреплять метки к другим объектам-контейнерам.
• Второй аргумент служит параметром настройки для Label. Он имеет форму именованного аргумента: параметр text определяет строку текста, которая должна появиться в виде метки. Большинство конструкторов виджетов принимают несколько именованных аргументов, определяющих различные параметры (цвет, размер, обработчики событий и так далее). Однако большинство параметров настройки виджетов имеют вполне разумные значения по умолчанию для каждой платформы, чем в значительной мере объясняется простота tkinter. Большинство параметров требуется определять, только если необходимо выполнить нестандартные настройки.
Как будет показано далее, аргумент, определяющий родительский виджет, является основой для построения сложных графических интерфейсов в виде деревьев виджетов. Библиотека tkinter действует по принципу «что построишь, то и получишь» - деревья объектов виджетов создаются как модели того, что мы хотим видеть на экране, а затем мы просим дерево изобразить себя с помощью вызова mainloop.
Менеджеры компоновки
Метод pack виджетов, к которому обращается сценарий gui1, вызывает менеджер компоновки - один из трех способов управления организацией графических элементов в окне. Менеджеры компоновки в библиотеке tkinter просто организуют один или несколько виджетов внутри контейнера (который иногда называется родителем или владельцем). Контейнерами могут служить окна верхнего уровня и фреймы (особая разновидность виджетов, с которой мы познакомимся далее), при этом сами контейнеры могут вкладываться внутрь других контейнеров, образуя иерархические отображения.