Однако, благодаря поддержке взаимодействий независимых программ, файлы fifo могут найти более широкое применение в моделях взаимодействий клиент/сервер. Например, с помощью именованных каналов можно сделать связь отладчика командной строки с графическим интерфейсом, о реализации которой на основе анонимных каналов я рассказывал выше, более гибкой - при использовании файлов fifo для соединения потоков ввода-вывода графического интерфейса с потоками ввода-вывода отладчика командной строки, графический интерфейс можно было бы запускать независимо.
Подобную функциональность предоставляют сокеты, которые к тому же подкупают свойственной им возможностью передачи данных по
сети и переносимостью на платформу Windows, о чем рассказывается в следующем разделе.
Сокеты: первый взгляд
Сокеты, реализация которых на языке Python находится в модуле socket, представляют собой более универсальный механизм IPC, чем каналы, которые мы рассматривали перед этим. Сокеты позволяют передавать данные не только между программами, выполняющимися на одном и том же компьютере, но и между программами, выполняющимися на разных компьютерах, соединенных сетью. При использовании сокетов для реализации механизма взаимодействий между процессами, выполняющимися на одном и том же компьютере, программы подключаются к сокетам, используя глобальный для этого компьютера номер порта, и передают данные. При использовании сокетов для выполнения сетевых соединений программы указывают сетевое имя компьютера и номер порта и обмениваются данными с удаленными программами.
Основы сокетов
Сокеты - это один из наиболее часто используемых инструментов IPC, тем не менее невозможно до конца понять их API, не понимая его роль в сетевых взаимодействиях. Вследствие этого я отложу подробное освещение особенностей сокетов, пока мы не исследуем порядок их использования в сетевых приложениях в главе 12. В этом разделе дается краткое введение и предварительный обзор сокетов, благодаря которому вы сможете сравнить их с именованными каналами (fifo), представленными в предыдущем разделе. В двух словах:
• Подобно именованным каналам, сокеты являются глобальным для компьютера механизмом - они не требуют наличия памяти, совместно используемой потоками выполнения или процессами, и поэтому могут использоваться независимыми программами.
• В отличие от именованных каналов, сокеты идентифицируются по номеру порта, а не по имени файла в файловой системе, - при работе с ними используется совершенно иной API, не похожий на файлы, тем не менее имеется возможность обертывать их объектами файлов. Сокеты обладают более высокой степенью переносимости: они поддерживаются практически на всех платформах, включая стандартную версию Python для Windows.
Кроме того, сокеты могут играть роли, выходящие далеко за рамки взаимодействий между процессами и за рамки этой главы. Тем не менее, чтобы проиллюстрировать основные особенности использования сокетов, в примере 5.25 приводится сценарий, который запускает сервер и 5 клиентов в виде потоков выполнения, работающих параллельно на одном компьютере и обменивающихся данными через сокеты. Так как все клиенты подключаются к одному и тому же порту, сервер получает данные, отправляемые всеми клиентами.
Пример 5.25. PP4E\System\Processes\socket_preview.py
использует сокеты для обмена данными между заданиями: запускает потоки выполнения, взаимодействующие с помощью сокетов; независимые программы также могут использовать сокеты для взаимодействий, потому что они принадлежат системе в целом, как и именованные каналы; смотрите части книги, посвященные разработке графических интерфейсов и сценариев для Интернета, где приводятся более практичные примеры использования сокетов; некоторым серверам может потребоваться взаимодействовать через сокеты с клиентами в виде потоков выполнения и процессов; данные через сокеты передаются в виде строк байтов, но точно так же через них можно передавать сериализованные объекты или кодированный текст Юникода;
ВНИМАНИЕ: при обращении к функции print в потоках выполнения может потребоваться синхронизировать их, если есть вероятность перекрытия по времени;
from socket import socket, AF_INET, SOCK_STREAM # переносимый API сокетов
port = 50008 # номер порта, идентифицирующий сокет
host = ‘localhost’ # сервер и клиент выполняются на локальном компьютере
def server():
sock = socket(AF_INET, SOCK_STREAM) # IP-адрес TCP-соединения sock.bind((‘’, port)) # подключить к порту на этой машине
sock.listen(5) # до 5 ожидающих клиентов
while True:
conn, addr = sock.accept() # ждать соединения с клиентом
data = conn.recv(1024) # прочитать байты данных от клиента
reply = ‘server got: [%s]’ % data # conn - новый подключенный сокет conn.send(reply.encode()) # отправить байты данных клиенту
def client(name):
sock = socket(AF_INET, SOCK_STREAM)
sock.connect((host, port)) # подключить сокет к порту
sock.send(name.encode()) # отправить байты данных серверу
reply = sock.recv(1024) # принять байты данных от сервера
sock.close() # до 1024 байтов в сообщении
print(‘client got: [%s]’ % reply)
if __name__ == ‘__main__’:
from threading import Thread sthread = Thread(target=server)
sthread.daemon = True # не ждать завершения потока сервера
sthread.start() # ждать завершения дочерних потоков
for i in range(5):
Thread(target=client, args=(‘client%s’ % i,)).start()
Внимательно рассмотрите программный код этого примера и комментарии, чтобы получить представление о том, как используются методы объекта сокета для передачи данных. В двух словах, метод accept сокетов данного типа, который вызывается сервером, принимает соединения от клиентов, по умолчанию блокируя выполнение сервера, пока клиент не пришлет запрос на обслуживание, и возвращает новый сокет, соединенный с клиентом. После установления соединения клиент и сервер приступают к обмену строками байтов с помощью методов приема и передачи, вместо записи и чтения. Однако, как будет показано далее в этой книге, сокеты могут обертываться объектами файлов, как это делалось выше с дескрипторами каналов. Кроме того, подобно дескрипторам каналов, необернутые сокеты работают со строками bytes, а не с текстовыми строками str. Это объясняет, почему результат форматирования строк в примере кодируется вручную.
Ниже приводится вывод этого сценария после запуска в Windows:
C:\...\PP4E\System\Processes> socket_preview.py client got: [b”server got: [b’client1’]”] client got: [b”server got: [b’client3’]”] client got: [b”server got: [b’client4’]”] client got: [b”server got: [b’client2’]”] client got: [b”server got: [b’client0’]”]
В этих результатах нет ничего особенного; каждая строка отражает данные, отправленные клиентом серверу и затем отправленные обратно: сервер принимает строку байтов от клиента и отправляет их обратно, добавив некоторый текст. Так как все потоки выполняются параллельно, клиенты на этом компьютере обслуживаются в случайном порядке.
Сокеты и независимые программы
Потоки выполнения могут использовать сокеты для взаимодействий между собой, однако модель потоков с общей памятью часто позволяет использовать более простые механизмы, такие как глобальные переменные и объекты и очереди. Наиболее ярко сокеты проявляют себя, когда они используются для организации взаимодействий отдельных процессов и программ, запускаемых независимо друг от друга. В примере 5.26 повторно используются функции server и client из предыдущего примера, но теперь они вызываются процессами и потоками из программ, запускаемых независимо друг от друга.
Пример 5.26. PP4E\System\Processes\socket-preview-progs.py
тоже сокет, но теперь для общения независимых программ, а не только потоков выполнения; сервер в этом примере обслуживает клиентов, выполняющихся в виде отдельных процессов и потоков; сокеты, как и именованные каналы, являются глобальными для компьютера: для их использования не требуется совместно используемая память
from socket_preview import server, client # оба используют тот же номер порта import sys, os
from threading import Thread mode = int(sys.argv[1])
if mode == 1: # запустить сервер в этом процессе
server()
elif mode == 2: # запустить клиента в этом процессе
client(‘client:process=%s’ % os.getpid()) else: # запустить 5 потоков-клиентов
for i in range(5):
Thread(target=client, args=(‘client:thread=%s’ % i,)).start()
Запустим этот сценарий в Windows (переносимость - важное преимущество сокетов). Сначала запустим в отдельном окне сервер, как независимую программу, - этот процесс будет выполняться без остановки, ожидая от клиентов запросов на соединение (как и в предыдущем примере с каналами, вам может потребоваться воспользоваться Диспетчером задач (Task Manager) или закрыть окно, чтобы прервать работу сервера):
C:\...\PP4E\System\Processes> socket-preview-progs.py 1
Теперь, в другом окне, запустим несколько клиентов, выполняющихся в виде процессов и потоков, как независимые программы; если передать сценарию аргумент 2 в командной строке, он запустит один клиентский процесс, а если передать 3, он породит пять потоков выполнения, обменивающихся данными с сервером параллельно:
C:\...\PP4E\System\Processes> socket-preview-progs.py 2
client got: [b”server got: [b’client:process=7384’]”]
C:\...\PP4E\System\Processes> socket-preview-progs.py 2
client got: [b”server got: [b’client:process=7604’]”]
C:\...\PP4E\System\Processes> socket-preview-progs.py 3
client got: [b”server got: [b’client:thread=1’]”]
client got: [b”server got: [b’client:thread=2’]”]
client got: [b”server got: [b’client:thread=0’]”]
client got: [b”server got: [b’client:thread=3’]”]
client got: [b”server got: [b’client:thread=4’]”]
C:\..\PP4E\System\Processes> socket-preview-progs.py 3
client got: [b”server got: [b’client:thread=3’]”]
client got: [b”server got: [b’client:thread=1’]”]
client got: [b”server got: [b’client:thread=2’]”]
client got: [b”server got: [b’client:thread=4’]”]
client got: [b”server got: [b’client:thread=0’]”]
C:\...\PP4E\System\Processes> socket-preview-progs.py 2
client got: [b”server got: [b’client:process=6428’]”]
Области применения сокетов
Примеры в этом разделе иллюстрируют основную роль сокетов как механизма IPC, но они не дают полного представления о практической ценности сокетов. Несмотря на то, что они могут работать лишь со строками байтов, совсем не трудно вообразить области возможного их применения. Приложив небольшие усилия, например:
• Через сокеты можно передавать произвольные объекты. Python, такие как списки и словари (или, по крайней мере, их копии), сериализуя их в строки байтов с помощью модуля pickle, который был представлен в главе 1 и подробно рассматривается в главе 17.
• Как мы увидим в главе 10, стандартный поток вывода сценария можно перенаправить в окно графического интерфейса, подключив поток вывода сценария к его сокету, который задействован в графическом интерфейсе в неблокирующем режиме.
• Программы, загружающие произвольный текст из Интернета, могут читать его в виде строки байтов через сокеты и декодировать вручную, используя имена кодировок, встроенные в заголовки content-type или непосредственно в теги самих данных.
• Интернет в целом можно рассматривать, как область применения сокетов - как мы увидим в главе 12, в самом конце, электронная почта, FTP и веб-страницы - это просто форматированные строки байтов, доставляемые через сокеты.
Плюс любые другие ситуации, когда необходимо организовать обмен данными между программами, - сокеты являются универсальным, переносимым и гибким инструментом. Например, они могут играть ту же роль, что и каналы fifo в примере с графическим интерфейсом для отладчика, приведенным выше, но при этом сокеты могут использоваться в Windows и позволили бы даже подключать графический интерфейс к отладчику, выполняющемуся на другом компьютере. В силу этого они оцениваются многими как более мощный инструмент IPC.
Повторю еще раз, что вы должны рассматривать этот раздел лишь как краткий обзор. Более полный охват сокетов подразумевает знакомство с сетевыми концепциями, поэтому мы отложим более детальное изучение API сокетов до главы 12. Кроме того, мы встретимся с сокетами в главе 10, где будем исследовать возможность перенаправления потоков ввода-вывода графического интерфейса, о которой говорилось выше, и познакомимся с различными дополнительными областями их применения в части книги, посвященной программированию для Интернета. В четвертой части, например, мы будем использовать сокеты для передачи целых файлов и создадим более надежные серверы, порождающие потоки выполнения или процессы для обмена данными с клиентами, чтобы избежать ситуации отказа в обслуживании. А теперь, продолжая тему этой главы, перейдем к еще одному, последнему инструменту IPC, - к сигналам.
Сигналы
Сигналы, в отсутствие лучшей аналогии, можно сравнить с палкой, которой тыкают в процесс. Программы генерируют сигналы, чтобы запустить обработчик данного сигнала в другом процессе. Операционная система тоже этим занимается - некоторые сигналы, генерируемые при необычных системных событиях, могут завершить программу, если их не обработать. Если это несколько напоминает возбуждение исключений в Python, то это неслучайно: сигналы являются программно генерируемыми событиями и аналогом исключений, действующими между процессами. Однако, в отличие от исключений, сигналы идентифицируются по номеру, не помещаются в очередь и в действительности являются механизмом асинхронных событий за пределами интерпретатора Python, управляемым операционной системой.
Чтобы сигналы можно было использовать в сценариях, в состав стандартной библиотеки Python входит модуль signal, который позволяет программам на языке Python регистрировать функции в качестве обработчиков сигналов. Этот модуль доступен как в Unix-подобных системах, так и в Windows (хотя в версии для Windows число сигналов может оказаться меньше). Для иллюстрации базового интерфейса сигналов в примере 5.27 приводится сценарий, устанавливающий функцию обработчика сигнала, номер которого передается как аргумент командной строки.
Пример 5.27. PP4E\System\Processes\signal1.py
обработка сигналов в Python; номер сигнала N передается как аргумент командной строки; чтобы передать сигнал этому процессу, используйте команду оболочки “kill -N pid”; большинство обработчиков сигналов восстанавливаются интерпретатором после обработки сигнала (смотрите главу, посвященную сетевым сценариям, где приводится описание сигнала SIGCHLD); в Windows модуль signal также доступен, но он определяет небольшое количество типов сигналов, а кроме того, в Windows отсутствует функция os.kill;
import sys, signal, time
def now(): return time.ctime(time.time()) # строка с текущим временем
def onSignal(signum, stackframe): # обработчик сигнала
print(‘Got signal’, signum, ‘at’, now()) # большинство обработчиков
# остаются действующими
signum = int(sys.argv[1])
signal.signal(signum, onSignal) # установить обработчик сигнала
while True: signal.pause() # ждать сигнала (или: pass)
Здесь используются только две функции из модуля signal: signal.signal
Принимает номер сигнала, объект функции и устанавливает эту функцию в качестве обработчика сигнала с данным номером. Интерпретатор Python автоматически восстанавливает большинство обработчиков сигналов, когда они возникают, поэтому нет необходимости повторно вызывать эту функцию внутри самого обработчика сигнала, чтобы заново его зарегистрировать. То есть обработчики всех сигналов, за исключением сигнала SIGCHLD, остаются установленными, пока не будут сброшены явно (например, путем установки его в значение SIG_DFL, чтобы восстановить режим по умолчанию, или в значение SIG_IGN, чтобы игнорировать сигнал). Поведение сигнала SIGCHLD зависит от платформы.
signal.pause
Приостанавливает процесс, пока не будет перехвачен следующий сигнал. Функция time.sleep действует аналогично, но не работает с сигналами в моей системе Linux, - она генерирует ошибку прерванного системного вызова. Цикл while True: pass тоже остановит сценарий, но будет напрасно тратить ресурсы процессора.
Ниже приводится вывод сценария, выполняющегося под управлением Cygwin в Windows (точно так же он будет действовать в любой Unix-подобной системе, такой как Linux): номер ожидаемого сигнала (12) передается в командной строке, а программа запускается в фоновом режиме с помощью оператора оболочки & (доступного в большинстве Unix-подобных оболочек):
$ ps
[C:\...\PP4E\System\Processes]$ python signal1.py 12 &
[1] 8224
PID
PPID
PGID
WINPID
TTY
UID
STIME
COMMAND
8944
1
8944
8944
con
1004
18
09
54
/usr/bin/bash
8224
7336
8224
10020
con
1004
18
26
47
/usr/local/bin/python
8380
7336
8380
428
con
1004
18
26
50
/usr/bin/ps
$ kill -12 8224
Got signal 12 at Sun Mar 7 18:27:28 2010 $ kill -12 8224
Got signal 12 at Sun Mar 7 18:27:30 2010 $ kill -9 8224
[1]+ Killed python signal1.py 12
Ввод и вывод здесь несколько перемешаны, потому что процесс осуществляет вывод на тот же экран, в котором вводятся новые команды оболочки. Послать программе сигнал можно с помощью команды оболочки kill, которая принимает номер сигнала и ID процесса (8224). Всякий раз, когда очередная команда kill посылает сигнал, процесс отвечает сообщением, сгенерированным функцией обработчика сигнала. Сигнал с номером 9 всегда завершает процесс.
Модуль signal экспортирует также функцию signal.alarm, с помощью которой определяется интервал времени в секундах, по истечении которого должен быть отправлен сигнал SIGALRM. Чтобы определить максимальное время ожидания и обработать ситуацию его превышения, достаточно установить обработчик сигнала SIGALRM и вызвать функцию signal.alarm, как показано в примере 5.28.
Пример 5.28. PP4E\System\Processes\signal2.py
установка сигналов по истечении времени ожидания и их обработка на языке Python; функция time.sleep некорректно ведет себя при появлении сигнала SIGALARM (как и любого другого сигнала на моем компьютере, работающем под управлением Linux), поэтому здесь вызывается функция signal.pause, которая приостанавливает выполнение сценария до получения сигнала;
import sys, signal, time
def now(): return time.asctime()
def onSignal(signum, stackframe): # обработчик сигнала
print(‘Got alarm’, signum, ‘at’, now()) # большинство обработчиков
# остаются действующими
while True:
print(‘Setting at’, now())
signal.signal(signal.SIGALRM, onSignal) # установить обработчик сигнала signal.alarm(5) # послать сигнал через 5 секунд
signal.pause() # ждать сигнала
После запуска этого сценария под управлением Cygwin в Windows функция обработчика onSignal будет вызываться каждые пять секунд:
[C:\...\PP4E\System\Processes]$ python signal2.py
Setting at Sun Mar 7 18:37:10 2010
Got alarm 14 at Sun Mar 7 18:37:15 2010
Setting at Sun Mar 7 18:37:15 2010
Got alarm 14 at Sun Mar 7 18:37:20 2010
Setting at Sun Mar 7 18:37:20 2010
Got alarm 14 at Sun Mar 7 18:37:25 2010
Setting at Sun Mar 7 18:37:25 2010
Got alarm 14 at Sun Mar 7 18:37:30 2010
Setting at Sun Mar 7 18:37:30 2010
...Ctrl-C для выхода...
Вообще говоря, сигналы следует использовать осторожно, что не явствует из приведенных примеров. В частности, некоторые системные вызовы плохо реагируют на прерывание сигналами, а в многопоточной программе только главный поток может устанавливать обработчики сигналов и реагировать на них.
Однако при правильном использовании сигналы предоставляют механизм связи, основанный на событиях. Он не такой мощный, как потоки ввода-вывода данных типа каналов, но в некоторых ситуациях его достаточно, например, когда нужно только сообщить программе, что произошло нечто важное, не передавая подробностей о самом событии. Иногда сигналы используют в комбинации с другими инструментами IPC. Например, начальный сигнал может сообщить программе, что клиент хочет установить связь через именованный канал, - примерно как если похлопать кого-то по плечу, чтобы привлечь его внимание, прежде чем начать говорить. На большинстве платформ резервируется один или несколько номеров сигналов SIGUSR для определяемых пользователем событий такого рода. Такой способ интеграции иногда может служить альтернативой обращения к блокируемой функции ввода в дочернем потоке выполнения.
Обратите также внимание на функцию os.kill(pid, sig), которая передает сигналы известным процессам из сценариев на языке Python в Unix-подобных системах, - она очень похожа на команду kill оболочки, использованную нами выше. Необходимый идентификатор ID можно получить из значения, возвращаемого функцией os.fork, порождающей дочерний процесс, или с помощью других функций. Как и os.fork, эта функция доступна в версии Cygwin Python, но она отсутствует в стандартной версии Python для Windows. Кроме того, смотрите обсуждение приема использования сигналов для удаления процессов «зомби» в главе 12.
Пакет multiprocessing
Теперь, когда мы познакомились с альтернативными механизмами IPC и получили возможность исследовать процессы, потоки выполнения и обсудили вопросы переносимости и ограничения GIL, накладываемые на потоки выполнения, пришло время познакомиться с еще одной альтернативой, которая стремится предоставить все лучшее из обоих миров. Как упоминалось выше, пакет multiprocessing из стандартной библиотеки Python позволяет сценариям порождать процессы, используя API, близко напоминающий модуль threading.
Этот относительно новый пакет может использоваться и в Unix, и в Windows, в отличие от низкоуровневого приема ветвления процессов. Он поддерживает платформонезависимую модель запуска процессов и предоставляет сопутствующие инструменты, такие как средства IPC, включая блокировки, каналы и очереди. Кроме того, для выполнения параллельных операций он использует не потоки выполнения, а процессы, что позволяет эффективно обойти ограничения GIL. Из этого следует, что пакет multiprocessing позволяет программисту использовать преимущества многопроцессорных систем для выполнения параллельных задач, сохраняя при этом простоту и переносимость модели потоков выполнения.
Зачем нужен пакет multiprocessing?
Так зачем же нам изучать еще одну парадигму параллельной обработки и еще один инструмент, когда у нас уже имеются потоки выполнения, процессы, инструменты IPC, такие как сокеты, каналы и очереди, изучавшиеся выше? Прежде чем погружаться в детали, мне хотелось бы сказать несколько слов о том, зачем вам может (или не может) потребоваться этот пакет. Несмотря на то, что по своей производительности этот пакет не может конкурировать с низкоуровневыми механизмами потоков выполнения и порождения процессов, во многих случаях он может оказаться весьма привлекательным решением:
• В отличие от приема ветвления процессов, этот пакет обеспечивает высокую степень переносимости и предоставляет мощные инструменты IPC.
• В отличие от механизма потоков выполнения, этот пакет обеспечивает по-настоящему параллельное выполнение задач в многопроцессорных системах, хотя и за счет некоторой потери времени, необходимого на выполнение процедуры запуска.
С другой стороны, этот пакет накладывает некоторые ограничения, отсутствующие в механизме потоков выполнения:
• Так как объекты приходится копировать через границы процессов, они не могут располагаться в совместно используемой памяти, как в случае потоков выполнения, - изменения в одном процессе не могут быть замечены в другом процессе. На практике возможность совместного использования памяти может оказаться самой веской причиной использования потоков выполнения, а отсутствие такой возможности в этом пакете может ограничить круг его применения в определенных контекстах.
• Так как этот пакет требует, чтобы процессы в Windows, а также некоторые инструменты IPC поддерживали возможность сериализации, это может усложнить или сделать непереносимой реализацию некоторых парадигм программирования, особенно когда в них предусматривается использование связанных методов или передача несериализуемых объектов, таких как сокеты, в порожденные процессы.
Например, типичный прием использования lambda-функций, который прекрасно работает при использовании модуля threading, не может использоваться для передачи вызываемых объектов процессам в Windows, потому что они не могут быть сериализованы. Аналогично из-за невозможности сериализовать связанные методы объектов в многопоточных программах может потребоваться применять обходные решения, если потоки выполняют связанные методы или завершение потоков реализовано как передача им вызываемых объектов (возможно, даже связанных методов) через общие очереди. Модель потоков, выполняющихся в пределах одного и того же процесса, поддерживает возможность непосредственного использования lambda-функций и связанных методов, но модель отдельных процессов, реализованная в пакете multiprocessing, такой возможности не поддерживает.
В главе 10 мы напишем механизм управления потоками для графических интерфейсов, который опирается на возможность передачи вызываемых объектов через очередь в реализации операций завершения потоков, - вызываемые объекты помещаются в очередь рабочими потоками выполнения, извлекаются и переправляются дальше главным потоком. Многопоточная программа PyMailGUI, которая будет реализована в главе 14, использует этот механизм управления потоками для передачи через очередь связанных методов, реализующих операции завершения потоков, и выполнения этих методов в главном потоке. Эту схему невозможно непосредственно перенести на модель отдельных процессов, реализованную в пакете multiprocessing.
Не углубляясь в детали, отмечу: чтобы задействовать пакет multiprocessing в приложении PyMailGUI, операции в нем пришлось бы реализовать в виде простых функций или в виде подклассов процессов, чтобы обеспечить возможность сериализации. Хуже того, эти операции может потребоваться реализовать в виде простых идентификаторов, передаваемых через главный процесс, если они обновляют графический интерфейс или изменяют состояние объекта в целом, - сериализация приводит к созданию копии объекта в принимающем процессе и не является ссылкой на оригинал, а операция ветвления в Unix вообще копирует процесс целиком. Модификация изменяемого объекта, полученного из сериализованной копии в новом процессе, например, не окажет никакого влияния на оригинал.
Требование поддержки сериализации для аргументов процессов в Windows может ограничить область применения пакета multiprocessing и в других контекстах. Например, в главе 12 мы увидим, что этот пакет не может напрямую использоваться для решения проблемы непереносимости функции os.fork при традиционном подходе к разработке сетевых серверов в Windows, потому что подключенные сокеты некорректно сериализуются при передаче новому процессу, созданному этим пакетом, и не могут использоваться для общения с клиентом. В этом контексте потоки выполнения обеспечивают более переносимое и, пожалуй, более эффективное решение.
Ситуация с приложениями, которые обмениваются простыми сообщениями, обстоит намного лучше. Ограничения, касающиеся сообщений, проще преодолеть, особенно если эти сообщения составляют часть архитектуры приложения, изначально основанной на применении процессов. Кроме того, в этом пакете имеются другие инструменты, такие как менеджеры и API разделяемой памяти, которые, хотя и являются узкоспециализированными и не такими универсальными, как память, совместно используемая потоками выполнения, тем не менее способны в некоторых случаях предоставить дополнительные возможности обмена информацией между процессами.
Однако в целом, благодаря тому что пакет multiprocessing опирается на использование отдельных процессов, он лучше подходит для реализации относительно независимых задач, которым не требуется общая память и для нормальной работы вполне достаточно возможности обмена сообщениями и инструментов доступа к разделяемой памяти, имеющихся в этом пакете. К их числу можно отнести множество приложений, но это не означает, что с помощью данного пакета можно напрямую заменить любые многопоточные программы, и он не во всех случаях может быть альтернативой приему, основанному на ветвлении процессов.
Чтобы по-настоящему оценить преимущества и слабые стороны этого пакета, обратимся к первому примеру и попутно исследуем реализацию пакета.
Основы: процессы и блокировки
В этой книге не так много места, чтобы можно было дать полную оценку этому сложному пакету, поэтому за более подробным описанием обращайтесь к руководству по библиотеке Python. Но если говорить кратко: большинство интерфейсов в этом пакете отражают интерфейсы модулей threading и queue, с которыми мы уже встречались, поэтому они должны показаться вам знакомыми. Например, класс Process в пакете multiprocessing имитирует класс Thread, встречавшийся нам выше, из модуля threading - он позволяет запускать функции параллельно вызывающему сценарию, только в данном случае функция запускается в отдельном процессе, а не в потоке выполнения. Эти основы иллюстрируются в примере 5.29:
Пример 5.29. PP4E\System\Processes\multi1.py
основы применения пакета multiprocessing: класс Process по своему действию напоминает класс threading.Thread, но выполняет функцию в отдельном процессе, а не в потоке; для синхронизации можно использовать блокировки, например, для вывода текста; запускает новый процесс интерпретатора в Windows, порождает дочерний процесс в Unix;
import os
from multiprocessing import Process, Lock def whoami(label, lock):
msg = ‘%s: name:%s, pid:%s’ with lock:
print(msg % (label, __name__, os.getpid()))
if __name__ == ‘__main__’: lock = Lock()
whoami(‘function call’, lock)
p = Process(target=whoami, args=(‘spawned child’, lock))
p.start()
p.join()
for i in range(5):
Process(target=whoami, args=((‘run process %s’ % i), lock)).start() with lock:
print(‘Main process exit.’)
Если запустить этот сценарий, он сначала вызовет функцию непосредственно в процессе; затем запустит эту функцию в новом процессе и дождется его завершения; и наконец, в цикле породит пять параллельно выполняющихся процессов вызовов функции - во всех случаях используется API, идентичный классу threading.Thread, который мы изучали выше в этой главе. Ниже приводится результат запуска сценария в Windows. Обратите внимание, что пять дочерних процессов, порождаемых в конце сценария, завершаются уже после своего родителя, что вполне обычное явление для процессов:
C:\...\PP4E\System\Processes> multi1.py
function call
name
__main__,
pid
8752
spawned child: name Main process exit.
__main__,
pid
9268
run process 3
name
__main__,
pid
9296
run process 1
name
__main__,
pid
8792
run process 4
name
__main__,
pid
2224
run process 2
name
__main__,
pid
8716
run process 0
name
__main__,
pid
6936
Так же как класс threading.Thread, встречавшийся нам выше, объект multiprocessing.Process может принимать функцию в аргументе target с параметрами (как сделано в этом примере) или использоваться в качестве родительского класса для переопределения его метода run. Метод start вызывает метод run в новом процессе, а метод run по умолчанию просто вызывает функцию, переданную в аргументе target. Кроме того, как и в модуле threading, метод join ожидает завершения дочернего процесса, а объект Lock является одним из инструментов синхронизации процессов - здесь он используется, чтобы избежать смешивания текста, выводимого процессами на платформах, на которых это может происходить (в Windows такого не происходит).
Реализация и правила использования
Технически, с целью обеспечить переносимость этот модуль на разных платформах использует разные инструменты:
• В Unix он использует прием ветвления процессов и вызывает метод run объекта Process в новом дочернем процессе.
• В Windows он запускает новый процесс интерпретатора, используя инструменты Windows создания процессов, передает сериализованный объект Process новому процессу через канал и выполняет команду «python -с» в новом процессе, которая запускает специальную функцию на языке Python в этом пакете. Эта функция читает сериализованную версию объекта Process, распаковывает ее и вызывает метод run.
Мы уже встречались с сериализацией в главе 1 и будем еще изучать ее далее в книге. На самом деле реализация немного сложнее, чем описано выше, и, конечно же, со временем может изменяться, но это действительно очень интересный трюк. Несмотря на то, что переносимый API в целом скрывает подробности реализации от вашего программного кода, тем не менее существуют некоторые тонкие особенности его использования. Например:
• В Windows логику главного процесса вообще следует вкладывать
в условную инструкцию проверки условия__name__==__main__, как
это сделано здесь, чтобы модуль можно было импортировать без побочных эффектов. Как мы узнаем в главе 17, при десериализации классов и функций необходимо импортировать вмещающие их модули, что является основным требованием.
• Кроме того, значения глобальных переменных в дочерних процессах на платформе Windows могут отличаться от значений этих же переменных в родительском процессе, имевших место на момент вызова метода start, потому что вмещающие их модули будут импортироваться в новом процессе.
• Дополнительно, в Windows все аргументы конструктора Process должны быть сериализуемыми объектами. Поскольку к числу этих аргументов относится и аргумент target, в нем допускается передавать только простые функции, которые могут быть сериализованы, - в этом аргументе нельзя передавать связанные или несвязанные методы объектов и функции, созданные с помощью инструкции lambda. Подробнее о правилах сериализации рассказывается в описании модуля pickle в руководстве по библиотеке Python - сериализовать можно практически любой объект, но чтобы сериализовать вызываемые объекты, такие как функции и классы, они должны быть доступными для импортирования - эти объекты сериализуются по имени и позднее импортируются для воссоздания байт-кода. В Windows объекты, хранящие системную информацию, такие как подключенные сокеты, вообще не могут использоваться в виде аргументов конструктора Process, потому что они не могут быть сериализованы.
• Точно так же сериализуемыми объектами должны быть экземпляры подклассов класса Process в Windows. Это относится и к значениям их атрибутов. Объекты, доступные в этом пакете (например, Lock в примере 5.29), поддерживают возможность сериализации и поэтому могут использоваться в виде аргументов конструктора Process и в виде атрибутов подклассов.
• Объекты IPC в этом пакете, с которыми мы встретимся в последующих примерах, такие как Pipe и Queue, принимают только сериализуемые объекты, из-за особенностей их реализации (подробнее об этом рассказывается в следующем разделе).
• В Unix дочерний процесс может использовать глобальные элементы, созданные родительским процессом, однако лучше передавать такие объекты дочернему процессу в виде аргументов конструктора Process, что обеспечит совместимость с Windows и позволит избежать возможных проблем на тот случай, если эти объекты будут утилизированы сборщиком мусора в родительском процессе.
В руководстве по библиотеке приводятся и другие правила. Однако в целом, если вы будете придерживаться правил передачи процессам совместно используемых объектов и использовать инструменты взаимодействий, предоставляемые этим пакетом, ваш программный код будет переносим. Теперь рассмотрим практическое применение некоторых из этих инструментов.
Инструменты IPC: каналы, разделяемая память и очереди
Процессы, создаваемые этим пакетом, всегда могут взаимодействовать с помощью общесистемных инструментов, таких как сокеты и файлы fifo, с которыми мы встречались выше, тем не менее пакет multiprocessing также предоставляет свои переносимые инструменты обмена сообщениями, специально предусмотренные для организации взаимодействий между процессами, порождаемыми этим пакетом:
• Объект Pipe реализует анонимный канал, соединяющий два процесса. При вызове конструктора Pipe он возвращает два объекта Connection, представляющие концы канала. По умолчанию каналы являются двунаправленными и позволяют передавать и принимать любые объекты Python, поддерживающие возможность сериализации. В настоящее время в Unix они используют либо пару соединенных друг с другом сокетов, либо порождаются функцией os.pipe, с которой мы встречались выше, а в Windows они реализованы на основе именованных каналов, специфических для этой платформы. Однако, как и объект Process, описанный выше, переносимый API объекта Pipe скрывает все эти тонкости от вызывающей программы.
• Объекты Value и Array реализуют общую память процессов/потоков, используемую для обмена данными между процессами. Конструкторы этих объектов возвращают одиночный объект и массив объектов, созданные с помощью модуля ctypes в разделяемой памяти, доступ к которой синхронизируется по умолчанию.
• Объект Queue играет роль списка FIFO объектов Python, допускающий возможность работы с множеством производителей и потребителей. По сути, эта очередь является каналом, снабженным механизмом блокировки для координации попыток доступа к ней, и наследует ограничения объекта Pipe, связанные с требованием к поддержке сериализации объектами, помещаемыми в очередь.
Все эти механизмы обеспечивают возможность безопасной работы с нескольким процессами, поэтому они часто играют роль точек синхронизации взаимодействий и позволяют отказаться от использования низкоуровневых инструментов, таких как блокировки, работая сходным с очередями в потоках, с которыми мы встречались выше, образом. Как обычно, канал (или их пара) может использоваться для реализации модели запрос/ответ. Очереди поддерживают более гибкие модели взаимодействий - фактически для преодоления ограничений GIL, вместо потоков выполнения в графическом интерфейсе можно было бы использовать объекты Process и Queue из пакета multiprocessing, и порождать с их помощью долгоживущие процессы, обменивающиеся результатами. Как упоминалось выше, хотя при таком подходе на некоторых платформах на запуск процессов тратится дополнительное время - в сравнении с потоками выполнения, зато он обеспечивает по-настоящему параллельное выполнение задач, если это позволяет сама платформа.
Важное замечание: каналы (и, соответственно, очереди), реализованные в этом пакете, выполняют сериализацию объектов, передаваемых через них, благодаря чему они могут быть реконструированы в принимающем процессе (как мы уже видели, в Windows принимающий процесс может выполняться под управлением полностью независимой копии интерпретатора Python). По этой причине они не поддерживают объекты, которые нельзя сериализовать. Как отмечалось выше, к ним относятся некоторые вызываемые объекты, такие как связанные методы и lambda-функции (программный код, нарушающий эти ограничения, смотрите в файле multi-badq.py в пакете с примерами для этой книги). Передача объектов с системной информацией также может терпеть неудачу. Большинство остальных типов объектов Python, включая классы и простые функции, прекрасно передаются через каналы и очереди.
Кроме того, имейте в виду, что из-за необходимости сериализации объекты, передаваемые с помощью этих инструментов, в принимающем процессе фактически являются копиями - прямые изменения в таких объектах не видимы передавшему их процессу. Это вполне объяснимо, если вспомнить, что данный пакет запускает независимые процессы с их собственными областями памяти - информация не может так же свободно передаваться между ними, как в потоках выполнения, независимо от используемого инструмента IPC.
Каналы в пакете multiprocessing
Чтобы продемонстрировать приемы работы с инструментами IPC, перечисленными выше, далее приводятся три примера, в которых взаимодействия между родительским и дочерним процессами реализованы тремя разными способами. В примере 5.30 используется простой объект канала, по которому передаются данные между родительским и дочерним процессами.
Пример 5.30. PP4E\System\Processes\multi2.py
Реализует взаимодействие с помощью анонимных каналов из пакета multiprocessing. Возвращаемые 2 объекта Connection представляют концы канала: объекты передаются в один конец и принимаются из другого конца, хотя каналы по умолчанию являются двунаправленными
import os
from multiprocessing import Process, Pipe def sender(pipe):
передает объект родителю через анонимный канал
pipe.send([‘spam’] + [42, ‘eggs’]) pipe.close()
def talker(pipe):
передает и принимает объекты из канала
pipe.send(dict(name=’Bob’, spam=42)) reply = pipe.recv() print(‘talker got:’, reply)
if__name__== ‘__main__’:
(parentEnd, childEnd) = Pipe()
Process(target=sender, args=(childEnd,)).start() # породить потомка
# с каналом
print(‘parent got:’, parentEnd.recv()) # принять от потомка
parentEnd.close() # или может быть закрыт
# автоматически сборщиком
# мусора
(parentEnd, childEnd) = Pipe()
child = Process(target=talker, args=(childEnd,))
child.start()
print(‘parent got:’, parentEnd.recv()) # принять от потомка
parentEnd.send({x * 2 for x in ‘spam’}) # передать потомку
child.join() # ждать завершения потомка
print(‘parent exit’)
Ниже приводится вывод этого сценария, запущенного в Windows. Первый потомок просто передает объект родителю, а второй - передает и принимает объекты по одному и тому же каналу:
C:\...\PP4E\System\Processes> multi2.py parent got: [‘spam’, 42, ‘eggs’] parent got: {‘name’: ‘Bob’, ‘spam’: 42} talker got: {‘ss’, ‘aa’, ‘pp’, ‘mm’} parent exit
Объекты каналов в этом модуле делают реализацию взаимодействий между двумя процессами переносимой (и практически тривиальной).
Разделяемая память и глобальные объекты
В примере 5.31 используется разделяемая память, которая служит вводом и выводом порожденных процессов. Чтобы обеспечить переносимость этого приема, необходимо создать объекты с помощью пакета и передать их конструктору Process. Последний тест в этом примере («loop 4») представляет, пожалуй, наиболее типичный случай использования разделяемой памяти - распределение заданий по нескольким параллельно выполняющимся процессам.
Пример 5.31. PP4E\System\Processes\multi3.py
Реализует взаимодействие с помощью объектов разделяемой памяти из пакета.
В Windows передаваемые объекты используются совместно, а глобальные объекты - нет. Последняя проверка здесь отражает типичный случай использования: распределение заданий между процессами.
import os
from multiprocessing import Process, Value, Array
procs = 3 # глобальные переменные, отдельные для каждого процесса,
count = 0 # не являются совместно используемыми
def showdata(label, val, arr):
выводит значения данных в этом процессе
msg = ‘%-12s: pid:%4s, global:%s, value:%s, array:%s’ print(msg % (label, os.getpid(), count, val.value, list(arr)))
def updater(val, arr):
обменивается данными через разделяемую память global count
count += 1 # глобальный счетчик недоступен другим процессам
val.value += 1 # а передаваемый в объекте - доступен
for i in range(3): arr[i] += 1
if__name__== ‘__main__’:
scalar = Value(‘i’, 0) # разделяемая память: предусматривает
# синхронизацию процессов/потоков vector = Array(‘d’, procs) # коды типов из ctypes: int, double
# вывести начальные значения в родительском процессе showdata(‘parent start’, scalar, vector)
# породить дочерний процесс, передать данные в разделяемой памяти p = Process(target=showdata, args=(‘child ‘, scalar, vector)) p.start(); p.join()
# изменить значения в родителе и передать через разделяемую память,
# ждать завершения каждого потомка
# все потомки видят изменения, выполненные в родительском процессе и
# переданные в виде аргументов (но не в глобальной памяти)
print(‘\nloop1 (updates in parent, serial children)...’) for i in range(procs): count += 1 scalar.value += 1 vector[i] += 1
p = Process(target=showdata, args=((‘process %s’ % i),
scalar, vector))
p.start(); p.join()
# то же самое, но потомки запускаются параллельно
# все они видят результат последней итерации, потому что они хранятся
# в совместно используемых объектах
print(‘\nloop2 (updates in parent, parallel children)...’) ps = []
for i in range(procs): count += 1 scalar.value += 1 vector[i] += 1
p = Process(target=showdata, args=((‘process %s’ % i),
scalar, vector))
p.start() ps.append(p) for p in ps: p.join()
# объекты в разделяемой памяти изменяются потомками,
# ждать завершения каждого из них
print(‘\nloop3 (updates in serial children)...’) for i in range(procs):
p = Process(target=updater, args=(scalar, vector))
p.start()
p.join()
showdata(‘parent temp’, scalar, vector)
# то же самое, но потомки запускаются параллельно ps = []
print(‘\nloop4 (updates in parallel children)...’) for i in range(procs):
p = Process(target=updater, args=(scalar, vector)) p.start() ps.append(p) for p in ps: p.join()
# глобальная переменная count=6 доступна только родителю
# выведет последние результаты # scalar=12: +6 в родителе, +6 в 6 потомках showdata(‘parent end’, scalar, vector) # array[i]=8:
# +2 в родителе, +6 в 6 потомках
Ниже приводится вывод этого сценария, запущенного в Windows. Проследите, как выполняется этот программный код. Обратите внимание, что глобальная переменная недоступна дочерним процессам для совместного использования в Windows, а переданные им объекты Value и Array - доступны. Последняя строка в этом выводе отражает изменения, выполненные в разделяемой памяти родительским и дочерними процессами, - все элементы в массиве имеют значение 8.0, потому что все они дважды увеличивались в родительском процессе и по одному разу - в каждом из шести дочерних процессов. Скалярное значение также отражает изменения, выполненные в родительском и в дочерних процессах, но, в отличие от потоков выполнения, в Windows глобальные переменные доступны только вмещающему их процессу:
C:\...\PP4E\System\Processes> multi3.py
parent
start
pid
6204,
global:0,
value
0,
array
[0.0, 0.0,
0.0]
child
pid
9660,
global:0,
value
0,
array
[0.0, 0.0,
0.0]
loop1 (updates in
parent, serial
children).
process
0
pid
3900,
global
0,
value
1,
array
[1.0, 0.0,
0.0]
process
1
pid
5072,
global
0,
value
2,
array
[1.0, 1.0,
0.0]
process
2
pid
9472,
global
0,
value
3,
array
[1.0, 1.0,
1.0]
loop2 (updates in
parent, parallel children)...
process
1
pid
9468,
global
0,
value
6,
array
[2.0, 2.0,
2.0]
process
2
pid
9036,
global
0,
value
6,
array
[2.0, 2.0,
2.0]
process
0
pid
9548,
global
0,
value
6,
array
[2.0, 2.0,
2.0]
loop3 (updates in
serial children
)...
parent
temp
pid
6204,
global
6,
value
9,
array
[5.0, 5.0,
5.0]
loop4 (updates in parallel children)...
parent end : pid:6204, global:6, value:12, array:[8.0, 8.0, 8.0]
А теперь представьте, что в последнем тесте используется намного больший массив и запускается большее количество дочерних процессов, -тогда вы начнете понимать, какие возможности предоставляет этот пакет для реализации параллельной обработки данных.
Очереди и подклассы
Наконец, помимо простых инструментов запуска и взаимодействия с дочерними процессами, пакет multiprocessing дополнительно:
• Позволяет создавать подклассы класса Process, обеспечивающего базовую структуру процесса и сохранение информации (так же, как threading.Thread, но для процессов).
• Реализует объект Queue, который может совместно использоваться любым количеством процессов для удовлетворения более широких потребностей при обмене данными (так же, как queue.Queue, но для процессов).
Очереди поддерживают более гибкую модель клиент/сервер. Так, сценарий в примере 5.32 порождает три процесса производителя, отправляющие данные в совместно используемую очередь, и периодически проверяет ее на предмет появления результатов - очень похоже на то, как графический интерфейс собирает результаты параллельных вычислений и выводит их, однако здесь параллельные операции выполняются не потоками, а процессами.
Пример 5.32. PP4E\System\Processes\multi4.py
От класса Process можно породить подкласс, так же, как от класса threading. Thread;
объект Queue действует подобно queue.Queue, но обеспечивает обмен данными между процессами, а не между потоками выполнения
import os, time, queue
from multiprocessing import Process, Queue # общая очередь для процессов
# очередь - это канал +
# блокировки/семафоры
class Counter(Process): label = ‘ @’
def __init__(self, start, queue): # сохраняет данные для
self.state = start # использования в методе run
self.post = queue Process.__init__(self)
def run(self): # вызывается в новом процессе
for i in range(3): # методом start()
time.sleep(1) self.state += 1
print(self.label ,self.pid, self.state) # self.pid - pid потомка self.post.put([self.pid, self.state]) # stdout совместно
# используется всеми
print(self.label, self.pid, ‘-’)
if__name__== ‘__main__’:
print(‘start’, os.getpid()) expected = 9
post = Queue()
p = Counter(0, post) # запустить 3 процесса, использующих общую очередь q = Counter(100, post) # потомки являются производителями r = Counter(1000, post) p.start(); q.start(); r.start()
while expected: # родитель потребляет данные из очереди
time.sleep(0.5) # очень напоминает графический интерфейс, try: # хотя в ГИ часто используются потоки
data = post.get(block=False) except queue.Empty: print(‘no data...’) else:
print(‘posted:’, data) expected -= 1
p.join(); q.join(); r.join() # дождаться завершения дочерних процессов print(‘finish’, os.getpid(), r.exitcode) # exitcode - код завершения
# потомка
Обратите внимание, что в этом сценарии:
• Функция time.sleep имитирует выполнение длительных операций в процессах-производителях.
• Все четыре процесса совместно используют один и тот же поток вывода - функции print выводят текст в одно и то же место, но в Windows их вывод не перемешивается (как мы видели выше, пакет multiprocessing предоставляет также совместно используемый объект Lock, который при необходимости может использоваться для синхронизации процессов).
• Код завершения дочернего процесса доступен после его завершения в атрибуте exitcode.
Если запустить этот сценарий, главный процесс-потребитель будет сообщать о своих попытках извлечения данных из очереди, а дочерние процессы-производители (строки с отступами) будут выводить свои идентификаторы ID процессов и данные.
C:\...\PP4E\System\Processes> multi4.py start 6296
no data.
no data.
@ 8008
101
posted:
[8008,
101]
@ 6068
1
@ 3760
1001
posted:
[6068,
1]
@ 8008
102
posted:
[3760,
1001]
@ 6068
2
@ 3760
1002
posted:
[8008,
102]
@ 8008
103
@ 8008
-
posted:
[6068,
2]
@ 6068
3
@ 6068
-
@ 3760
1003
@ 3760
-
posted:
[3760,
1002]
posted:
[8008,
103]
posted:
[6068,
3]
posted:
[3760,
1003]
finish 6296 0
А теперь представьте, что строки, начинающиеся с символа «@», являются результатами длительных операций, а остальные строки представляют отражение работы главного потока выполнения графического интерфейса; широта возможностей этого пакета наверняка станет для вас более очевидной.
Запуск независимых программ
Как мы узнали выше, независимые программы обычно взаимодействуют между собой с помощью общесистемных инструментов, таких как сокеты и файлы fifo, изученные нами ранее. Процессы, порождаемые с помощью пакета multiprocessing, также могут пользоваться этими инструментами, однако благодаря их близкому родству мы можем использовать дополнительные механизмы IPC, предоставляемые этим пакетом.
Как и потоки выполнения, пакет multiprocessing предназначен для параллельного выполнения функций, а не для запуска отдельных программ. Порожденные функции могут использовать такие инструменты, как os.system, os.popen и модуль subprocess для запуска других программ, если выполняемая операция может заблокировать вызывающий процесс, но часто нет никакого смысла порождать процесс таким способом, чтобы запустить другую программу (другую программу можно запустить, пропустив этот шаг). Фактически в Windows пакет multiprocessing в настоящее время использует ту же функцию запуска процессов, что и модуль subprocess, поэтому нет смысла использовать первый из них для запуска двух процессов.
Существует также возможность запускать новые программы из дочерних процессов с помощью инструментов, таких как функции os.exec*, с которыми мы встречались выше. Порождая процесс переносимым способом с помощью пакета multiprocessing и запуская в нем новую программу, мы тем самым запускаем независимую программу и эффективно решаем проблему отсутствия функции os.fork в стандартной версии Python для Windows.
Как правило, запуск новой программы не предполагает передачу каких-либо ресурсов конструктору Process (при запуске новая программа затрет программу, выполнявшуюся до нее в этом процессе), но этот конструктор представляет собой переносимый эквивалент комбинации функций fork/exec в Unix. Кроме того, программы, запущенные таким способом, точно так же могут использовать более традиционные инструменты IPC, такие как сокеты и именованные каналы, с которыми мы встречались выше в этой главе. Этот прием иллюстрируется в примере 5.33.
Пример 5.33. PP4E\System\Processes\multi5.py
Использует пакет multiprocessing для запуска независимых программ, с помощью os.fork или других функций
import os
from multiprocessing import Process def runprogram(arg):
os.execlp(‘python’, ‘python’, ‘child.py’, str(arg))
if__name__== ‘__main__’:
for i in range(5):
Process(target=runprogram, args=(i,)).start() print(‘parent exit’)
Этот сценарий запускает 5 экземпляров сценария child.py из примера 5.4 в виде независимых процессов и не ждет их завершения. Ниже приводится вывод этого сценария, запущенного в Windows, после удаления лишних строк с системным приглашением к вводу, которые произвольно появляются в середине вывода (в Cygwin сценарий действует точно так же, но в этом случае вывод не перемешивается):
C:\...\PP4E\System\Processes> type child.py import os, sys
print(‘Hello from child’, os.getpid(), sys.argv[1])
C:\...\PP4E\System\Processes> multi5.py parent exit
Hello from child 9844 2 Hello from child 8696 4 Hello from child 1840 0 Hello from child 6724 1 Hello from child 9368 3
Этот прием невозможно применить к потокам выполнения, потому что все потоки выполняются в пределах одного процесса, - запуск новой программы уничтожит все эти потоки выполнения. Хотя данный прием едва ли будет выполняться с той же скоростью, как комбинация a fork/exec в Unix, он, по крайней мере, предоставляет похожий и переносимый способ для Windows.
И многое другое
Наконец, пакет multiprocessing предоставляет множество других инструментов, не демонстрировавшихся в примерах выше, включая инструменты синхронизации по условиям, событиям и с помощью семафоров, а также локальные и удаленные менеджеры, реализующие серверы для управления совместно используемыми объектами. Так, в примере 5.34 демонстрируется поддержка пулов (pools) - групп дочерних процессов, совместно работающих над решением определенной задачи.
Пример 5.34. PP4E\System\Processes\multi6.py
Плюс многое другое: пулы процессов, менеджеры, блокировки, условные переменные,...
import os
from multiprocessing import Pool def powers(x):
#print(os.getpid()) # раскомментируйте, чтобы увидеть работу потомков return 2 ** x
if__name__== ‘__main__’:
workers = Pool(processes=5)
results = workers.map(powers, [2]*100)
print(results[:16])
print(results[-2:])
results = workers.map(powers, range(100))
print(results[:16])
print(results[-2:])
После запуска Python равномерно распределит задания между рабочими процессами, выполняющимися параллельно:
C:\...\PP4E\System\Processes> multi6.py
[4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
[4, 4]
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768] [316912650057057350374175801344, 633825300114114700748351602688]
И немножко меньше...
Справедливости ради следует отметить, что помимо дополнительных инструментов и возможностей пакет multiprocessing несет с собой также дополнительные ограничения, кроме тех, что мы уже обсудили (поддержка возможности сериализации, доступ к совместно используемым данным и так далее). Например, взгляните на следующий фрагмент программного кода:
def action(arg1, arg2): print(arg1, arg2)
if__name__== ‘__main__’:
Process(target=action, args=(‘spam’, ‘eggs')).start() # оболочка ждет
# завершения потомка
Этот сценарий действует, как ожидается, но если изменить последнюю строку, как показано ниже, он не будет работать в Windows, потому что lambda-функции не могут быть сериализованы (в действительности, они не могут быть импортированы):
Process(target=(lambda: action(‘spam’, ‘eggs’))).start() # не работает! -
# не сериализуется
Это не позволяет использовать распространенный прием программирования с применением lambda-функций для передачи данных в вызовы, который мы часто будем использовать для передачи функций обратного вызова в части книги, посвященной графическим интерфейсам. Кроме того, данная особенность отличает пакет multiprocessing от модуля threading, который послужил прототипом для этого пакета, - вызовы функций, которые могут использоваться при работе с потоками выполнения, такие как приведены ниже, необходимо преобразовать в вызываемые объекты и аргументы:
threading.Thread(target=(lambda: action(2, 4))).start() # но с потоками
# lambda-функции
# работают
И напротив, некоторые особенности поведения модуля threading имитируются в пакете multiprocessing, хотите вы этого или нет. Из-за того, что программы, использующие этот пакет, по умолчанию ожидают завершения дочерних процессов, мы вынуждены помечать процессы, устанавливая атрибут daemon, если нежелательно, чтобы программный код, как показано ниже, блокировал командную оболочку (технически, родительский процесс пытается завершить дочерние процессы-демоны при выходе, то есть программа может завершиться, только когда все потомки являются демонами, что очень похоже на модуль threading):
def action(arg1, arg2): print(arg1, arg2)
time.sleep(5) # обычно препятствует завершению родителя
if__name__== ‘__main__’:
p = Process(target=action, args=(‘spam’, ‘eggs’)) p.daemon = True # не ждать завершения этого потомка p.start()
Дополнительные подробности о некоторых из этих проблем вы найдете в руководстве по библиотеке Python. Они являются не непреодолимыми препятствиями, но специальными случаями и потенциальными ловушками. Мы еще вернемся к проблемам lambda-выражений и процессов-демонов в более прагматичном контексте в главе 8, где будем использовать модуль multiprocessing для запуска графических интерфейсов, выполняющихся независимо.
Зачем нужен пакет multiprocessing? Заключение
Как следует из примеров в этом разделе, пакет multiprocessing представляет собой мощную альтернативу, объединяющую в себе переносимость и удобство потоков выполнения со способностью к параллельному выполнению, свойственной процессам, и предлагает дополнительные инструменты IPC для получения кодов возврата и решения других задач, сопутствующих параллельной обработке данных.
Хотелось бы надеяться, что данный раздел позволил вам глубже понять достоинства и недостатки пакета, обсуждавшиеся в начале раздела. В частности, его модель, основанная на выполнении отдельных процессов, препятствует свободному использованию общей памяти, как это делается в потоках выполнения, не позволяет передавать связанные методы и lambda-функции из-за ограничений, связанных с сериализацией, а также из-за особенности реализации механизма запуска процессов в Windows. Кроме того, в Windows он требует, чтобы аргументы процесса были сериализуемыми объектами, что препятствует их использованию в серверах для организации взаимодействий с клиентами.
Хотя он и не может служить универсальной заменой модуля threading, тем не менее во многих ситуациях пакет multiprocessing предлагает достаточно привлекательное решение. В частности, для задач параллельного программирования, когда ограничения пакета легко преодолимы, он способен предложить такие качества, как переносимость и производительность, которые отсутствуют в других, более низкоуровневых многозадачных инструментах Python.
К сожалению, в данной книге не так много места, чтобы дать более всесторонний охват этого пакета помимо этого краткого введения. За дополнительными подробностями обращайтесь к руководству по библиотеке Python. А теперь мы обратим наше внимание к следующей группе дополнительных инструментов запуска программ, знакомством с которыми закончим эту главу.
Другие способы запуска программ
До сих пор в этой книге мы видели разные способы запуска программ -от комбинации функций os. fork/exec в Unix до переносимых способов запуска команд, таких как функции os.system, os.popen, модуль subprocess и переносимые механизмы из пакета multiprocessing, представленные в последнем разделе. Тем не менее в стандартной библиотеке Python существуют и другие способы, часть из которых менее зависимы от типа платформы, а часть - менее понятны по сравнению с другими. Данный раздел завершает главу кратким обзором этого набора инструментов.
Семейство функций os.spawn
Функции os.spawnv и os.spawnve впервые были представлены как инструменты запуска программ в Windows, напоминающие по своему действию комбинацию функций fork/exec в Unix-подобных системах. На сегодняшний день эти функции доступны на обеих платформах, в Windows и в Unix-подобных системах, а кроме того, были добавлены варианты, повторяющие функциональность других членов семейства os.exec.
В последние версии Python был включен переносимый модуль subprocess с целью заменить эти функции. Фактически руководство по библиотеке Python включает примечание, отмечающее, что в составе этого модуля имеются более мощные и эквивалентные инструменты, которым следует отдавать предпочтение перед функциями os.spawn. Кроме того, новейший пакет multiprocessing, в совокупности с функциями os.exec, позволяет добиться тех же результатов переносимым способом, как мы уже видели выше. Тем не менее функции семейства os.spawn по-прежнему доступны и могут встретиться вам в действующих сценариях на языке Python.
Функции из семейства os.spawn запускают программу, указанную в командной строке, в виде нового процесса в Windows и в Unix-подобных системах. По своему действию они напоминают комбинацию функций fork/exec в Unix и могут использоваться как альтернатива функциям system и popen, с которыми мы уже познакомились. В следующем примере выполняется запуск программы на языке Python двумя традиционными способами (во втором случае дополнительно выполняется чтение стандартного потока вывода программы):
C:\...\PP4E\System\Processes> python
>>> print(open('makewords.py').read())
print(‘spam’)
print(‘eggs’)
print(‘ham’)
>>> import os
>>> os.system('python makewords.py')
spam
eggs
ham
0
>>> result = os.popen('python makewords.py').read()
>>> print(result)
spam
eggs
ham
Функции, эквивалентные функции os.spawn, имеющие чуть более сложную сигнатуру, что обеспечивает более полный контроль над способом запуска программы, - позволяют получить тот же эффект:
>>> os.spawnv(os.P_WAIT, r'C:\Python31\python', (‘python’, 'makewords.py'))
spam
eggs
ham
0
>>> os.spawnl(os.P_NOWAIT, r'C:\Python31\python', ‘python’, ‘makewords.py’)
1820
>>> spam
eggs
ham
Из всех этих способов функция spawn больше всего напоминает прием ветвления программ в Unix. В действительности она не копирует вызывающий процесс (поэтому операции, использующие общие дескрипторы, не работают), но может использоваться для запуска программы Windows, выполняемой совершенно независимо от вызвавшей программы. Сценарий в примере 5.35 делает сходство с шаблонами программирования в Unix еще более очевидным. Он запускает программу с помощью комбинации fork/exec в Unix-подобных системах (включая оболочку Cygwin) или вызывает os.spawnv в Windows.
Пример 5.35. PP4E\System\Processes\spawnv.py
запускает параллельно 10 копий child.py; для запуска программ в Windows использует spawnv (как fork+exec); флаг P_OVERLAY обозначает замену, флаг P_DETACH перенаправляет stdout потомка в никуда; можно также использовать переносимые инструменты из модуля subprocess или из пакета multiprocessing!
import os, sys
for i in range(10):
if sys.platform[:3] == ‘win’: pypath = sys.executable
os.spawnv(os.P_NOWAIT, pypath, (‘python’, ‘child.py’, str(i))) else:
pid = os.fork() if pid != 0:
print(‘Process %d spawned’ % pid) else:
os.execlp(‘python’, ‘python’, ‘child.py’, str(i)) print(‘Main process exiting.’)
Чтобы понять, как действуют эти примеры, вам необходимо познакомиться с аргументами, которые передаются функциям spawn. В этом сценарии мы передаем функции os.spawnv флаг режима запуска процесса, полный путь к выполняемому файлу интерпретатора Python и кортеж строк, представляющих команду оболочки, запускающую новую программу. Путь к выполняемому файлу интерпретатора Python доступен сценариям как sys.executable. В общем случае флаг режима запуска процесса может состоять из следующих предопределенных значений:
os.P_NOWAIT и os.P_NOWAITO
Функции spawn возвращают управление сразу после запуска нового процесса и возвращают его числовой идентификатор ID. Доступны в Unix и Windows.
os.P_WAIT
Функции spawn не возвращают управление, пока новый процесс не завершится, и в случае успеха возвращают код его завершения, в противном случае - отрицательный номер сигнала («-signal»), если работа процесса была прервана сигналом. Доступен в Unix и Windows.
os.P_DETACH и os.P_OVERLAY
Флаг P_DETACH похож на флаг P_NOWAIT, но при этом новый процесс отсоединяется от консоли вызывающего процесса. Если был использован флаг P_OVERLAY, текущая программа будет замещена (как при использовании функции os.exec). Доступен в Windows.
Фактически в семействе spawn насчитывается восемь различных функций. Все они выполняют запуск программ, но несколько отличаются сигнатурами. Символ «l» в их именах означает, что аргументы программы передаются в виде списка, символ «р» означает, что поиск выполняемого файла программы будет производиться с учетом системного пути, а символ «е» означает, что функции может быть передан словарь, определяющий окружение порождаемой программы: функция os.spawnve, например, действует так же, как функция os.spawnv, но принимает в дополнительном четвертом аргументе словарь, определяющий иное окружение для порождаемой программы (по умолчанию порождаемые процессы наследуют окружение родительского процесса):
os.spawnl(mode, path, ...) os.spawnle(mode, path, ..., env)
os.spawnlp(mode, file, ...) # только в Unix
os.spawnlpe(mode, file, ..., env) # только в Unix os.spawnv(mode, path, args) os.spawnve(mode, path, args, env) os.spawnvp(mode, file, args) # только в Unix
os.spawnvpe(mode, file, args, env) # только в Unix
Имена этих функций повторяют имена и сигнатуры функций из семейства os.exec, поэтому дополнительные подробности, касающиеся отличий между их вариантами, вы найдете в описании функций os.exec, выше в этой главе. В отличие от функций os.exec только половина функций os.spawn, не использующих системный путь (то есть без символа «p» в их именах), в настоящее время реализованы в версии Python для Windows. В Windows поддерживаются все флаги режимов запуска процессов, но флаги os.P_DETACH и os.P_OVERLAY недоступны в Unix. Со временем перечисленные особенности могут измениться, поэтому обязательно проверьте их описание в руководстве по библиотеке Python или запустите встроенную функцию dir, передав ей имя модуля os после его импортирования.
Ниже приводится вывод сценария из примера 5.35, запущенного в Windows. Он порождает 10 копий программы child.py, с которой мы встречались выше в этой главе:
C:\...\PP4E\System\Processes> type child.py import os, sys
print(‘Hello from child’, os.getpid(), sys.argv[1])
C:\...\PP4E\System\Processes> python spawnv.py
Hello from child -583587 0
Hello from child -558199 2
Hello from child -586755 1
Hello from child -562171 3
Main process exiting.
Hello from child -581867 6 Hello from child -588651 5 Hello from child -568247 4 Hello from child -563527 7
Hello from child -543163 9 Hello from child -587083 8
Обратите внимание, что эти копии программы выводят свою информацию в случайном порядке, а родительская программа завершается раньше, чем завершатся все дочерние; все эти программы действительно выполняются в Windows параллельно. Обратите также внимание, что вывод дочерней программы появляется в окне консоли, где был запущен сценарий spawnv.py, - при использовании флага P_NOWAIT стандартный вывод попадает на родительскую консоль, но отправляется в никуда, если использовать флаг P_DETACH (что не является ошибкой при порождении программ с графическим интерфейсом).
После того как я продемонстрировал эту функцию, следует отметить, что оба модуля, subprocess и multiprocessing, на сегодняшний день предлагают более переносимые альтернативные способы запуска программ с использованием командной строки. Фактически если функции os.spawn не обеспечивают вам какого-то уникального поведения, без которого вы не можете обойтись (например, управление всплывающим окном консоли в Windows), платформозависимые отрезки кода, присутствующие в примере 5.35, можно было бы полностью заменить переносимыми инструментами из пакета multiprocessing, использованными в примере 5.33.
Функция os.startfile в Windows
Если на сегодняшний день функции os.spawn могут рассматриваться, как излишество, то в пользу других инструментов можно привести достаточно веские аргументы. Например, функция os.system может использоваться в Windows для запуска команды start DOS, которая открывает (то есть запускает) файлы в соответствии с ассоциациями для расширений имен файлов в Windows, как это делается при выполнении двойного щелчка на файле. Функция os.startfile, появившаяся в последних версиях Python, делает эту операцию еще более простой, и, в отличие от некоторых других инструментов, позволяет избежать блокирования вызывающего процесса.
Использование команды DOS start
Чтобы понять, почему это происходит, нужно сначала разобраться, как действует команда DOS start в целом. Грубо говоря, командная строка DOS вида start command действует, как если бы команда command вводилась в диалоговом окне Windows Выполнить (Run), которое можно открыть с помощью меню кнопки Пуск (Start). Если command является именем файла, он открывается точно так же, как если щелкнуть на его имени в графическом интерфейсе Проводника Windows (Windows Explorer).
Например, следующие три команды DOS автоматически запускают Internet Explorer, программу просмотра файлов графических изображений, и программу проигрывания звуковых файлов для соответствующих файлов в командах. Windows просто открывает файл в той программе, которая определена для обработки файлов с указанным расширением. Более того, все три эти программы выполняются независимо от окна консоли DOS, в котором введена команда:
C:\...\PP4E\System\Media> start lp4e-preface-preview.html
C:\...\PP4E\System\Media> start ora-lp4e.jpg
C:\...\PP4E\System\Media> start sousa.au
Поскольку команда start может запустить любой файл и командную строку, нет причин, по которым ее нельзя было бы использовать для запуска независимо выполняемой программы Python:
C:\...\PP4E\System\Processes> start child.py 1
Это возможно благодаря тому, что при установке Python регистрируется для открытия файлов с расширениями .py. Сценарий child.py будет запущен независимо от окна консоли DOS, несмотря на то, что не было задано ни имя выполняемого файла интерпретатора Python, ни путь к нему. Однако, поскольку child.py просто выводит сообщение и завершается, результат не вполне удовлетворителен: новое окно DOS появляется, чтобы обслужить стандартный вывод сценария, и тут же исчезает, когда он завершается. Лучше будет, если добавить в конец программы вызов функции input, чтобы перед завершением происходило ожидание нажатия какой-либо клавиши:
C:\...\PP4E\System\Processes> type child-wait.py import os, sys
print(‘Hello from child’, os.getpid(), sys.argv[1])
input("Press
C:\...\PP4E\System\Processes> start child-wait.py 2
Теперь окно DOS дочерней программы появляется на экране и сохраняется после возврата из команды start. Нажатие клавиши Enter во всплывающем окне DOS заставляет его закрыться.
Использование команды start в сценариях Python
Мы знаем, что функции os.system и os.popen можно вызывать в сценариях для запуска любых команд, которые можно ввести в командной строке DOS, поэтому из сценариев на языке Python можно запускать независимо выполняемые программы простым выполнением команды DOS start. Например:
C:\...\PP4E\System\Media> python >>> import os
>>> cmd = ‘start lp4e-preface-preview.html’ # запустит броузер IE
>>> os.system(cmd) # как независимую программу
0
Вызов функции os.system в этом примере запустит броузер веб-страниц, зарегистрированный в вашей системе как средство просмотра файлов с расширением .html (если только эти программы уже не выполняются). Запущенные программы выполняются совершенно независимо от сеанса Python - при выполнении команды DOS start функция os.system не ждет завершения запущенной программы.
Функция os.startfile
Команда start оказалась настолько удобной, что в последние версии Python была добавлена функция os.startfile, которая по сути выполняет те же действия, что и команда DOS start, выполняемая с помощью функции os.system, и действует, как если бы на указанном файле был выполнен двойной щелчок. Например, следующие вызовы имеют похожий эффект:
>>> os.startfileClp-code-readme.txt’)
>>> os.system('start lp-code-readme.txt’)
На моем компьютере, работающем под управлением Windows, оба вызова открывают текстовый файл в программе Блокнот (Notepad). Однако, в отличие от второго способа, функция os.startfile, не предоставляет возможности задержать закрытие запущенного приложения (что достигается передачей ключа /WAIT команде DOS start) и не позволяет получить код завершения приложения (возвращаемый функцией os.system).
В последних версиях Windows следующий вызов также имеет похожий эффект, потому что в них для выполнения команд задействуется реестр (однако такая форма вызова блокируется до закрытия программы просмотра файлов, как при использовании команды start /WAIT):
>>> os.systemClp-code-readme.txt’) # команду ‘start’ можно не указывать
Это довольно удобный способ открытия произвольных документов и медиафайлов, но имейте в виду, что функция os.startfile работает только в Windows, потому что она использует реестр Windows, чтобы определить, как открывать файл. Существуют и другие, еще более запутанные и непереносимые способы запуска программ, включая инструменты в пакете PyWin32, который мы не будем рассматривать здесь. Если вам требуется обеспечить переносимость своих сценариев, используйте инструменты запуска программ, из числа представленных выше, такие как функции os.popen или os.spawnv. Но еще лучше напишите модуль, скрывающий тонкости за переносимым интерфейсом, как показано в следующем, заключительном разделе.
Переносимый модуль запуска программ
Из-за всех этих различий в запуске программ на разных платформах может оказаться трудным запомнить, какие средства должны использоваться в конкретной ситуации. Более того, некоторые из этих инструментов вызываются способами, которые настолько сложны, что быстро забываются. В настоящее время существуют модули, такие как subprocess и multiprocessing, предлагающие полностью переносимые механизмы, тем не менее для конкретной платформы порой лучше подходят другие инструменты, обладающие более тонкими особенностями поведения. В Windows, например, часто бывает желательно подавить вывод окна командной оболочки.
Мне настолько часто приходится писать сценарии, которым требуется запускать программы на языке Python, что в итоге я создал модуль, постаравшись скрыть в нем большую часть закулисных деталей. Скрыв детали реализации за переносимым интерфейсом, я получил возможность изменять их и использовать новые инструменты, которые появятся в будущем, не влияя на работоспособность программного кода, использующего этот модуль. Работая над модулем, я сделал его достаточно сообразительным, чтобы он мог автоматически выбирать схему запуска, соответствующую платформе, на которой он применяется. Лень породила не один полезный модуль.
В примере 5.36 приводится исходный текст модуля, в котором собрана немалая часть тех приемов, которые встретились нам в этой главе. В нем реализован абстрактный суперкласс LaunchMode, определяющий, что значит запустить программу Python, но не определяющий, как это сделать. Вместо этого его подклассы предоставляют метод run, который действительно запускает программу Python согласно выбранной схеме, и (при необходимости) определяют метод announce для вывода имени программы при запуске.
Пример 5.36. PP4E\launehmodes.py
##############################################################################
запускает программы Python с помощью механизмов командной строки и классов схем запуска; автоматически вставляет "python” и/или путь к выполняемому файлу интерпретатора в начало командной строки; некоторые из инструментов в этом модуле предполагают, что выполняемый файл ‘python’ находится в системном пути поиска (смотрите Launcher.py);
можно было бы использовать модуль subprocess, но он сам использует функцию os.popen(), и к тому же цель этого модуля состоит в том, чтобы запустить независимую программу, а не подключиться к ее потокам ввода-вывода; можно было бы также использовать пакет multiprocessing, но данный модуль предназначен для выполнения программ, а не функций: не имеет смысла запускать процесс, когда можно использовать одну из уже имеющихся возможностей;
новое в этом издании: при запуске сценария передает путь к файлу сценария через функцию normpath(), которая в Windows замещает все / на \; исправьте соответствующие участки программного кода в PyEdit и в других сценариях; вообще в Windows допускается использовать / в командах открытия файлов, но этот символ может использоваться не во всех инструментах запуска программ; ############################################################################## import sys, os
pyfile = (sys.platform[:3] == ‘win’ and ‘python.exe’) or ‘python’
pypath = sys.executable # использовать sys в последних версиях Python
def fixWindowsPath(cmdline):
замещает все / на \ в путях к сценариям в начале команд; используется только классами, которые запускают инструменты, требующие этого в Windows; в других системах в этом нет необходимости (например, os.system в Unix);
splitline = cmdline.lstrip().split(‘ ‘) # разбить по пробелам
fixedpath = os.path.normpath(splitline[0]) # заменить прямые слешы return ‘ ‘.join([fixedpath] + splitline[1:]) # снова объединить в строку
class LaunchMode:
при вызове экземпляра класса выводится метка и запускается команда; подклассы форматируют строки команд для метода run(), если необходимо; команда должна начинаться с имени запускаемого файла сценария Python и не должна начинаться со слова "python” или с полного пути к нему;
def __init__(self, label, command): self.what = label self.where = command
def __call__(self): # вызывается при вызове экземпляра,
self.announce(self.what) # например как обработчик щелчка на кнопке self.run(self.where) # подклассы должны определять метод run() def announce(self, text): # подклассы могут переопределять метод
print(text) # announce() вместо логики if/elif
def run(self, cmdline):
assert False, ‘run must be defined’
class System(LaunchMode):
запускает сценарий Python, указанный в команде оболочки внимание: может блокировать вызывающую программу, если в Unix не добавить &
def run(self, cmdline):
cmdline = fixWindowsPath(cmdline) os.system(‘%s %s’ % (pypath, cmdline))
class Popen(LaunchMode):
запускает команду оболочки в новом процессе
внимание: может блокировать вызывающую программу, потому что
канал закрывается немедленно
def run(self, cmdline):
cmdline = fixWindowsPath(cmdline) os.popen(pypath + ‘ ‘ + cmdline) # предполагается, что нет данных
# для чтения
class Fork(LaunchMode):
запускает команду в явно созданном новом процессе только для Unix-подобных систем, включая cygwin
def run(self, cmdline):
assert hasattr(os, ‘fork’)
cmdline = cmdline.split() # превратить строку в список
if os.fork() == 0: # запустить новый процесс
os.execvp(pypath, [pyfile] + cmdline) # запустить новую программу
class Start(LaunchMode):
запускает команду, независимую от вызывающего процесса
только для Windows: использует ассоциации с расширениями имен файлов
def run(self, cmdline):
assert sys.platform[:3] == ‘win’ cmdline = fixWindowsPath(cmdline) os.startfile(cmdline)
class StartArgs(LaunchMode):
только для Windows: в аргументах могут присутствовать символы прямого слеша
def run(self, cmdline):
assert sys.platform[:3] == ‘win’
os.system(‘start ‘ + cmdline) # может создать окно консоли class Spawn(LaunchMode):
запускает python в новом процессе, независимом от вызывающего, для Windows и Unix; используйте P_NOWAIT для окна dos; символы прямого слеша допустимы
def run(self, cmdline):
os.spawnv(os.P_DETACH, pypath, (pyfile, cmdline)) class Top_level(LaunchMode):
запускает тот же процесс в новом окне
на будущее: требуется информация о классе графического интерфейса def run(self, cmdline):
assert False, ‘Sorry - mode not yet implemented’
#
# выбор "лучшего” средства запуска для данной платформы
# возможно, выбор придется уточнить в других местах
#
if sys.platform[:3] == ‘win’:
PortableLauncher = Spawn
else:
PortableLauncher = Fork
class QuietPortableLauncher(PortableLauncher): def announce(self, text): pass
def selftest():
file = ‘echo.py’
input(‘default mode...’)
launcher = PortableLauncher(file, file)
launcher() # не блокирует
input(‘system mode...’)
System(file, file)() # блокирует
if sys.platform[:3] == ‘win’:
input(‘DOS start mode...’) # не блокирует
StartArgs(file, file)()
if __name__ == ‘__main__’: selftest()
Ближе к концу файла модуль выбирает класс по умолчанию, исходя из значения атрибута sys.platform: в Windows в атрибут PortableLauncher записывается класс, использующий spawnv, и класс, использующий комбинацию fork/exec, на других платформах. В последних версиях Python можно было бы использовать функцию spawnv на всех платформах, но альтернативные инструменты в этом модуле могут использоваться в других контекстах. Если импортировать этот модуль и всегда использовать его атрибут PortableLauncher, то можно позабыть о многочисленных специфических для платформы деталях, перечисленных в данной главе.
Чтобы запустить программу Python, просто импортируйте класс PortableLauncher, создайте экземпляр, передав метку и командную строку (без слова «python» впереди), а затем вызовите объект экземпляра, как если бы это была функция. Программа запускается операцией eall - методом __call__ перегрузки операторов, вместо самого метода; поэтому классы этого модуля можно также использовать для создания обработчиков обратного вызова в графических интерфейсах на базе tkinter. Как будет показано в следующих главах, нажатие кнопок в tkinter запускает вызываемый объект без аргументов. Зарегистрировав экземпляр PortableLauncher для обработки нажатия кнопки, можно автоматически запускать новую программу из графического интерфейса другой программы. Связать инструмент запуска с нажатием кнопки в графическом интерфейсе можно следующим способом:
Button(root, text=name, command=PortableLauncher(name, commandLine))
При автономном выполнении, как обычно, вызывается функция selftest этого модуля. При использовании класса System вызывающий процесс блокируется до завершения запускаемой программы, а при использовании PortableLauncher (в действительности, Spawn или Fork) и Start - нет.
C:\...\PP4E> type echo.py
print(‘Spam’) input(‘press Enter’)
C:\...\PP4E> python launchmodes.py
default mode... echo.py system mode... echo.py Spam
press Enter
DOS start mode...
echo.py
Практическое применение этого файла мы увидим в главе 8, где он будет использоваться для запуска диалога с графическим интерфейсом, и в нескольких примерах в главе 10, включая PyDemos и PyGadgets, -сценарии, предназначенные для обеспечения переносимого способа запуска основных примеров в этой книге, которые находятся в вершине дерева примеров. Эти сценарии просто импортируют Portable-Launcher и регистрируют экземпляры, которые будут откликаться на события в графическом интерфейсе, поэтому они прекрасно работают и в Windows, и в Unix без изменений (конечно, в этом помогает и переносимость tkinter). Сценарий PyGadgets даже настраивает PortableLauncher для изменения метки в графическом интерфейсе во время запуска.
class Launcher(launchmodes.PortableLauncher): # обертка класса запуска def announce(self, text): # изменяет метку в ГИ
Info.config(text=text)
Мы исследуем эти два и другие клиентские сценарии, такие как PyEdit во второй части книги, после того как приступим к созданию графических интерфейсов в третьей части. Отчасти из-за своей роли в сценарии PyEdit в данном издании книги этот модуль был дополнен функцией, автоматически замещающей символы прямого слеша в пути к файлу символами обратного слеша. В PyEdit в некоторых именах файлов используются символы прямого слеша, потому что они допустимы в операциях открытия файлов в системе Windows, но некоторые инструменты запуска программ в Windows требуют использования символов обратного слеша. В частности, функции system, popen и startfile из модуля os требуют использования символов обратного слеша, а функция spawnv - нет. Сценарий PyEdit и другие автоматически наследуют исправление путей к файлам в виде функции fixWindowsPath за счет импортирования и использования классов из этого модуля. Сценарий PyEdit был изменен так, чтобы устранить влияние этой функции, как неподходящее для данного конкретного случая (смотрите главу 11), но другие клиенты получают это исправление автоматически.
Обратите также внимание, что некоторые из классов в этом примере используют строку пути sys.executable, чтобы получить полный путь к выполняемому файлу Python. Отчасти это обусловлено их использованием в сценариях запуска демонстрационных примеров. В предыдущих версиях, до появления атрибута sys.executable, эти классы использовали две функции, экспортируемые модулем Launcher.py, которые отыскивали выполняемый файл интерпретатора независимо от того, поместил ли пользователь этот путь в переменную окружения PATH.
Теперь необходимость в таком поиске отпала. Поскольку я еще буду возвращаться к этому модулю в следующих главах, а также потому, что необходимость такого поиска отпала, - благодаря бесконечному потаканию Python профессиональным желаниям программистов - я отложу бессмысленные педагогические наставления в сторону. (Точка.)
Другие системные инструменты
На этом мы завершаем тур по инструментам системного программирования, имеющимся в языке Python. В этой и в трех предыдущих главах мы познакомились с большинством часто используемых системных инструментов из библиотеки Python. Попутно мы научились использовать их для таких полезных вещей, как запуск программ, обработка каталогов и так далее. Следующая глава завершает исследование этой области представлением примеров использования инструментов, с которыми мы только что познакомились, для реализации сценариев, выполняющих более полезную и практическую работу на системном уровне.
Тем не менее в Python есть и другие системные инструменты, которые появятся в этой книге дальше. Например:
• Сокеты, используемые для обмена данными с другими программами, которые коротко были представлены здесь, встретятся нам снова в главе 10, где будут использоваться в графическом интерфейсе, а полный охват сокетов вы найдете в главе 12.
• Функции выбора, используемые для организации многозадачности, также будут представлены в главе 12, как средство реализации серверов.
• Прием блокировки файлов с помощью функции os.open, представленной в главе 4, будет обсуждаться в последующих примерах.
• Регулярные выражения, поиск строк по шаблону, используемый во многих инструментах обработки текста, применяемых в системном администрировании, появятся только в главе 19.
Кроме того, такие приемы, как ветвление и потоки, интенсивно используются в главах, посвященных разработке сценариев для Интернета: смотрите обсуждение многопоточных графических интерфейсов в главах 9 и 10; реализации серверов в главе 12; графические интерфейсы клиентов FTP в главе 13 и пример программы PyMailGUI в главе 14. По пути нам также встретятся высокоуровневые модули Python, такие как socketserver, использующие приемы ветвления и потоки выполнения для реализации серверов. Многие инструменты, описанные в этой главе, будут постоянно появляться в дальнейших примерах этой книги - а для чего же еще создаются переносимые библиотеки общего назначения?
Последнее, но не маловажное, что я хотел бы еще раз подчеркнуть: в библиотеке Python есть много других инструментов, которые вообще не фигурируют в данной книге. При наличии сотен модулей в библиотеке и еще большего количества сторонних модулей авторам, пишущим книги по Python, приходится ограничивать себя в отборе тем! Как всегда, напомню о необходимости изучения руководства по этой библиотеке, как в начале, так и на всем протяжении вашей карьеры программиста Python.
6
Законченные системные программы
«Ярость поиска»
Эта глава завершает обзор системных интерфейсов Python и представляет коллекцию более крупных сценариев на языке Python, которые решают практические системные задачи - сравнение и копирование деревьев каталогов, разрезание файлов, поиск файлов и каталогов, тестирование других программ, настройка окружения запускаемых программ и так далее. Примеры в этой главе являются системными утилитами на языке Python, иллюстрирующими типичные решения и приемы программирования, применяемые в этой области, и основное внимание здесь уделяется использованию встроенных инструментов, таких как инструменты обработки файлов и деревьев каталогов.
Главная цель главы состоит в том, чтобы дать вам почувствовать практические сценарии в действии. Размеры этих примеров также дают возможность увидеть действие таких парадигм программирования на языке Python, как объектно-ориентированное программирование (ООП) и повторное использование программного кода. Только в контексте нетривиальных программ, таких как примеры в этой главе, применение подобных инструментов начинает приносить ощутимые плоды. В данной главе вы найдете ответы не только на вопрос «как», но и «почему» - попутно я буду показывать, для удовлетворения каких насущных потребностей создавались сценарии, которые мы будем рассматривать, чтобы помочь вам совместить подробности реализации с контекстом.
Одно предварительное замечание: в этой главе мы будем двигаться вперед очень быстро, а некоторые из представленных здесь примеров предназначены в основном для самостоятельного изучения. Все сценарии хорошо документированы и используют системные инструменты Python, описанные в предыдущих главах, поэтому я не буду подробно описывать весь программный код. Вам следует самостоятельно ознакомиться с исходными текстами сценариев и поэкспериментировать с ними на своем компьютере, чтобы лучше почувствовать, как комбинировать системные интерфейсы для решения практических задач. Все сценарии включены в состав пакета с примерами для этой книги, и большая их часть работает на всех основных платформах.
Следует также упомянуть, что большую часть этих программ я использую на практике, то есть это не просто примеры, написанные для книги. Они создавались на протяжении нескольких лет и решают самые разнообразные задачи, поэтому их ничто не объединяет, кроме реальности. С другой стороны, эти примеры позволяют показать полезность системных инструментов, продемонстрировать крупные концепции разработки, что невозможно сделать на простых примерах, и наглядно доказать простоту автоматизации системных задач на языке Python переносимым способом. Овладев основами, вы будете сожалеть, что не сделали этого раньше.
Игра: «Найди самый большой файл Python»
Попробуйте быстро ответить на вопрос: «Как называется самый большой файл с программным кодом на языке Python на вашем компьютере?» Этот невинный вопрос был однажды задан мне студентом на курсах, которые я веду. Поскольку я не знал ответ на него, это послужило поводом включить реализацию такого сценария в качестве примера в мой курс обучения, который стал отличной иллюстрацией способов применения системных инструментов Python для решения практических задач. В действительности этот вопрос звучит несколько неопределенно, потому что не совсем понятна область, к которой он относится. Подразумевается ли наибольший файл в каком-то определенном каталоге, в дереве каталогов, в стандартной библиотеке, в пути поиска модулей или вообще на всем жестком диске? Различные области подразумевают различные решения.
Сканирование каталога стандартной библиотеки
Так, в примере 6.1 приводится первое решение, которое отыскивает наибольший файл Python в ограниченной области, - в одном каталоге, но этого вполне достаточно для начала.
Пример 6.1. PP4E\System\Filetools\bigpy-dir.py
Отыскивает наибольший файл с исходным программным кодом на языке Python в единственном каталоге.
Поиск выполняется в каталоге стандартной библиотеки Python для Windows, если в аргументе командной строки не был указан какой-то другой каталог.
import os, glob, sys
dirname = r’C:\Python31\Lib’ if len(sys.argv) == 1 else sys.argv[1]
allsizes = []
allpy = glob.glob(dirname + os.sep + ‘*.py’) for filename in allpy:
filesize = os.path.getsize(filename) allsizes.append((filesize, filename))
allsizes.sort()
print(allsizes[:2])
print(allsizes[-2:])
Для обхода файлов в каталоге этот сценарий использует модуль glob. Он определяет размеры и имена файлов и сохраняет информацию в списке, который затем сортируется. Поскольку размер является первым элементом кортежей, помещаемых в список, список будет отсортирован по размерам файлов, вследствие чего информация о наибольшем файле окажется в конце списка. Вместо того чтобы сохранять весь список, можно было бы сохранять только текущий наибольший файл, но решение со списком выглядит более гибким. Сценарий сканирует каталог стандартной библиотеки Python в Windows, если в аргументе командной строки ему не был передан другой каталог, и выводит информацию о наибольшем и наименьшем файлах, обнаруженных им:
C:\...\PP4E\System\Filetools> bigpy-dir.py
[(0, ‘C:\\Python31\\Lib\\build_class.py’), (56, ‘C:\\Python31\\Lib\\struct.py’)] [(147086, ‘C:\\Python31\\Lib\\turtle.py’), (211238, ‘C:\\Python31\\Lib\\decimal.py’)]
C:\...\PP4E\System\Filetools> bigpy-dir.py .
[(21, ‘ .\\__init__.py’), (461, ‘.\\bigpy-dir.py’)]
[(1940, ‘.\\bigext-tree.py’), (2547, ‘.\\split.py’)]
C:\...\PP4E\System\Filetools> bigpy-dir.py ..
[(21, ‘..\\__init__.py’), (29, ‘..\\testargv.py’)]
[(541, ‘..\\testargv2.py’), (549, ‘..\\more.py’)]
Сканирование дерева каталогов стандартной библиотеки
Решение, предложенное в предыдущем разделе, работает, но совершенно очевидно, представляет частичный ответ на поставленный вопрос -файлы с исходными текстами на языке Python обычно располагаются более чем в одном каталоге. Даже стандартная библиотека содержит множество подкаталогов с модулями, которые могут произвольно вкладываться друг в друга. В действительности нам необходимо реализовать обход всего дерева каталогов. Кроме того, в выводе сценария, приведенном выше, не так-то просто разобраться. Исправить эту проблему нам поможет модуль pprint (от «pretty print» - форматированный вывод). Все эти улучшения добавлены в сценарий, представленный в примере 6.2.
Пример 6.2. PP4E\System\Filetools\bigpy-tree.py
Отыскивает наибольший файл с исходным программным кодом на языке Python в дереве каталогов.
Поиск выполняется в каталоге стандартной библиотеки, отображение результатов выполняется с помощью модуля pprint.
import sys, os, pprint trace = False
if sys.platform.startswith(‘win’):
dirname = r’C:\Python31\Lib’ # Windows
else:
dirname = ‘/usr/lib/python’ # Unix, Linux, Cygwin
allsizes = []
for (thisDir, subsHere, filesHere) in os.walk(dirname): if trace: print(thisDir) for filename in filesHere:
if filename.endswith(‘.py’):
if trace: print(‘...’, filename) fullname = os.path.join(thisDir, filename) fullsize = os.path.getsize(fullname) allsizes.append((fullsize, fullname))
allsizes.sort()
pprint.pprint(allsizes[:2])
pprint.pprint(allsizes[-2:])
Для поиска наибольшего файла с программным кодом на языке Python в дереве каталогов эта версия использует os.walk. Если вы хотите увидеть, как выполняется обход каталогов, измените значение переменной trace. В данной реализации сценарий способен выполнять обход дерева каталогов стандартной библиотеки Python и в Windows, и в Unix-подобных системах:
C:\...\PP4E\System\Filetools> bigpy-tree.py [(0, ‘C:\\Python31\\Lib\\build_class.py’),
(0, ‘C:\\Python31\\Lib\\email\\mime\\___init__.py’)]
[(211238, ‘C:\\Python31\\Lib\\decimal.py’),
(380582, ‘C:\\Python31\\Lib\\pydoc_data\\topics.py’)]
Сканирование пути поиска модулей
Как и следовало ожидать, сценарий из предыдущего раздела отыскивает наименьший и наибольший файлы в дереве каталогов. Хотя поиск в полном дереве каталогов стандартной библиотеки Python дает более исчерпывающий ответ, тем не менее его никак нельзя признать полным: на компьютере могут находиться дополнительные модули, установленные в другие каталоги, включенные в путь поиска модулей, но за пределами дерева файлов с исходными текстами на языке Python. Чтобы дать более полный ответ, необходимо выполнить все тот же поиск в дереве каталогов, но при этом следует просмотреть все каталоги, включенные в путь поиска модулей. Это улучшение было добавлено в сценарий, представленный в примере 6.3, - он просматривает все модули Python, доступные для импортирования и располагающиеся непосредственно в пути поиска, а также расположенные во вложенных каталогах пакетов.
Пример 6.3. PP4E\System\Filetools\bigpy-path.py
Отыскивает наибольший файл с исходным программным кодом на языке Python, присутствующий в пути поиска модулей.
Пропускает каталоги, которые уже были просканированы; нормализует пути и регистр символов, обеспечивая корректность сравнения; включает в выводимые результаты счетчики строк. Здесь недостаточно использовать os.environ[‘PYTHONPATH’]: этот список является лишь подмножеством списка sys.path.
import sys, os, pprint
trace = 0 # 1=каталоги, 2=+файлы
visited = {}
allsizes = []
for srcdir in sys.path:
for (thisDir, subsHere, filesHere) in os.walk(srcdir): if trace > 0: print(thisDir) thisDir = os.path.normpath(thisDir) fixcase = os.path.normcase(thisDir) if fixcase in visited: continue else:
visited[fixcase] = True for filename in filesHere:
if filename.endswith(‘.py’):
if trace > 1: print(’...’, filename) pypath = os.path.join(thisDir, filename) try:
pysize = os.path.getsize(pypath) except os.error:
print(‘skipping’, pypath, sys.exc_info()[0]) else:
pylines = len(open(pypath, ‘rb’).readlines()) allsizes.append((pysize, pylines, pypath))
print(‘By size...’) allsizes.sort() pprint.pprint(allsizes[:3]) pprint.pprint(allsizes[-3:])
print(‘By lines...’) allsizes.sort(key=lambda x: x[1]) pprint.pprint(allsizes[:3]) pprint.pprint(allsizes[-3:])
Этот сценарий выполняет обход всех каталогов, включенных в путь поиска, и для каждого из них пытается выполнить поиск в полном дереве подкаталогов. В результате получается тройной вложенный цикл -цикл по элементам пути, цикл по каталогам в дереве очередного элемента и цикл по файлам в каталоге. Так как путь поиска модулей может содержать каталоги, имена которых могут записываться произвольно, кроме всего прочего этот сценарий должен побеспокоиться, чтобы:
• Нормализовать пути к каталогам - исправить символы слеша и точки, чтобы привести имена каталогов к общепринятому виду.
• Нормализовать регистр символов в именах каталогов - привести к нижнему регистру все символы в именах файлов и каталогов в нечувствительном к регистру символов Windows, чтобы свести определение эквивалентности имен каталогов к простой операции сравнения строк, но не изменять регистр символов в Unix, где он имеет значение.
• Выявлять повторы, чтобы избежать повторного сканирования одних и тех же каталогов (один и тот же каталог может оказаться достижимым при сканировании разных элементов, включенных в sys.path).
• Пропускать все элементы, похожие на файлы, для которых функция os.path.getsize возбуждает исключение (по умолчанию os.walk сама молча игнорирует элементы на любом уровне вложенности, которые не являются каталогами).
• Избежать возможных ошибок декодирования символов Юникода в содержимом файлов, открывая их для подсчета строк в двоичном режиме. В текстовом режиме выполняется обязательное декодирование содержимого, а некоторые файлы в дереве каталогов библиотеки Python 3.1 не могут быть корректно декодированы в Windows. Перехват ошибок декодирования с помощью инструкции try позволил бы предотвратить преждевременное завершение программы, но при этом могли бы быть пропущены потенциальные кандидаты на звание большего или меньшего файла.
В эту версию был добавлен подсчет строк, что может существенно увеличить время работы сценария, но это очень интересный отчетный показатель. Фактически данная версия использует это значение как ключ сортировки, чтобы определить три наибольших и наименьших по количеству строк файла, - эти результаты могут не совпадать с результатами, когда наибольший и наименьший размер определяется по размеру файла в байтах. Ниже приводятся результаты выполнения этого сценария в Python 3.1 на моем компьютере, работающем под управлением Windows 7. Так как результаты зависят от платформы, наличия дополнительных расширений и настроек пути поиска, у вас могут получиться другие наибольший и наименьший файлы в пути sys.path:
C:\...\PP4E\System\Filetools> bigpy-path.py By size...
[(0, 0, ‘C:\\Python31\\lib\\build_class.py’),
(0, 0, ‘C:\\Python31\\lib\\email\\mime\\__init__.py’),
(0, 0, ‘C:\\Python31\\lib\\email\\test\\__init__.py’)]
[(161613, 3754, ‘C:\\Python31\\lib\\tkinter\\__init__.py’),
(211238, 5768, ‘C:\\Python31\\lib\\decimal.py'),
(380582, 78, ‘C:\\Python31\\lib\\pydoc_data\\topics.py’)]
By lines...
[(0, 0, ‘C:\\Python31\\lib\\build_class.py’),
(0, 0, ‘C:\\Python31\\lib\\email\\mime\\__init__.py’),
(0, 0, ‘C:\\Python31\\lib\\email\\test\\__init__.py’)]
[(147086, 4132, ‘C:\\Python31\\lib\\turtle.py’),
(150069, 4268, ‘C:\\Python31\\lib\\test\\test_descr.py’),
(211238, 5768, ‘C:\\Python31\\lib\\decimal.py’)]
И снова, если вам интересно увидеть, как выполняется обход каталогов, измените значение переменной trace. Как видите, результаты поиска наибольшего файла по размеру в байтах и по количеству строк отличаются. Это несоответствие, вероятно, мы должны тщательно обсудить на нашей следующей встрече.
Сканирование всего компьютера
Наконец, несмотря на то, что путь поиска модулей обычно включает все исходные файлы Python, доступные для импортирования на вашем компьютере, тем не менее и этот ответ может оказаться неполным. С технической точки зрения, эта версия проверяет только модули -файлы с исходным программным кодом на языке Python, которые запускаются как самостоятельные сценарии, могут не включаться в путь поиска модулей. Кроме того, путь поиска модулей в некоторых сценариях может изменяться вручную прямо во время выполнения (например, прямым изменением списка sys.path в сценариях, выполняющихся на веб-сервере), чтобы включить в него дополнительные каталоги, которые недоступны для сценария в примере 6.3.
В конечном счете, чтобы отыскать наибольший исходный файл на компьютере, необходимо просканировать весь жесткий диск; эта возможность почти полностью поддерживается сценарием в примере 6.2. Нам нужно лишь передать ему в аргументе имя корневого каталога и добавить в него некоторые улучшения из версии, сканирующей путь поиска модулей (позволяющие избежать повторного сканирования одних и тех же каталогов в случае поиска по всему компьютеру, пропускать ошибки и подсчитывать строки, если нам не жалко на это времени). В примере 6.4 приводится реализация такого универсального сценария сканирования дерева каталогов, снабженного усовершенствованиями, необходимыми для сканирования всего диска.
Пример 6.4. PP4E\System\Filetools\bigext-tree.py
Отыскивает наибольший файл заданного типа в произвольном дереве каталогов. Пропускает каталоги, которые уже были просканированы; перехватывает ошибки; добавляет возможность вывода трассировки поиска и подсчета строк.
Кроме того, использует множества, итераторы файлов и генераторы, чтобы избежать загрузки содержимого файлов целиком, и пытается обойти проблемы, возникающие при выводе недекодируемых имен файлов/каталогов.
import os, pprint from sys import argv, exc_info
trace = 1 # 0=выкл., 1=каталоги, 2=+файлы
dirname, extname = os.curdir, ‘.py’ # по умолчанию файлы .py в cwd
if len(argv) > 1: dirname = argv[1] # например: C:\, C:\Python31\Lib
if len(argv) > 2: extname = argv[2] # например: .pyw, .txt
if len(argv) > 3: trace = int(argv[3]) # например: ". .py 2”
def tryprint(arg): try:
print(arg) # непечатаемое имя файла?
except UnicodeEncodeError:
print(arg.encode()) # вывести как строку байтов
visited = set() allsizes = []
for (thisDir, subsHere, filesHere) in os.walk(dirname): if trace: tryprint(thisDir) thisDir = os.path.normpath(thisDir) fixname = os.path.normcase(thisDir) if fixname in visited:
if trace: tryprint(‘skipping ‘ + thisDir) else:
visited.add(fixname) for filename in filesHere:
if filename.endswith(extname):
if trace > 1: tryprint(‘+++’ + filename) fullname = os.path.join(thisDir, filename) try:
bytesize = os.path.getsize(fullname) linesize = sum(+1 for line in open(fullname, ‘rb’)) except Exception:
print(‘error’, exc_info()[0]) else:
allsizes.append((bytesize, linesize, fullname)) for (title, key) in [(‘bytes’, 0), (‘lines’, 1)]:
print(‘\nBy %s...’ % title) allsizes.sort(key=lambda x: x[key]) pprint.pprint(allsizes[:3]) pprint.pprint(allsizes[-3:])
В отличие от предыдущей версии, эта позволяет выполнять поиск файлов с определенными расширениями и в определенных каталогах. По умолчанию производится поиск файлов Python в текущем рабочем каталоге:
C:\...\PP4E\System\Filetools> bigext-tree.py
By bytes...
[(21, 1, ‘ .\\__init_.py’),
(461, 17, ‘.\\bigpy-dir.py’),
(818, 25, ‘.\\bigpy-tree.py’)]
[(1 696, 48, ‘.\\join.py’),
(1940, 49, ‘.\\bigext-tree.py’),
(2547, 57, ‘.\\split.py’)]
By lines...
[(21, 1, ‘ .\\__init_.py’),
(461, 17, ‘.\\bigpy-dir.py’),
(818, 25, ‘.\\bigpy-tree.py’)]
[(1 696, 48, ‘.\\join.py’),
(1940, 49, ‘.\\bigext-tree.py’),
(2547, 57, ‘.\\split.py’)]
Настраивая работу сценария, можно задать имя каталога, расширение искомых файлов и уровень подробности вывода трассировочной информации (уровень 0 запрещает трассировку, при уровне 1 (по умолчанию) выводятся имена сканируемых каталогов):
C:\...\PP4E\System\Filetools> bigext-tree.py .. .py 0
By bytes...
[(21, 1, ‘..\\__init__.py’),
(21, 1, ‘..\\Filetools\\__init_.py’),
(28, 1, ‘..\\Streams\\hello-out.py’)]
[(2278, 67, ‘..\\Processes\\multi2.py’),
(2547, 57, ‘..\\Filetools\\split.py’),
(4361, 105, ‘..\\Tester\\tester.py’)]
By lines...
[(21, 1, ‘..\\__init__.py’),
(21, 1, ‘..\\Filetools\\__init__.py’),
(28, 1, ‘..\\Streams\\hello-out.py’)]
[(2547, 57, ‘..\\Filetools\\split.py’),
(2278, 67, ‘..\\Processes\\multi2.py’),
(4361, 105, ‘..\\Tester\\tester.py’)]
Кроме того, этот сценарий поволяет выполнять поиск файлов разных типов. Ниже приводятся результаты поиска наибольшего и наименьшего текстового файла, начиная с каталога уровнем выше текущего (эти результаты имели место на тот момент времени, когда я запускал сценарий):
C:\...\PP4E\System\Filetools> bigext-tree.py .. .txt 1
..\Environment
..\Filetools
..\Processes
..\Streams
..\Tester
..\Tester\Args
..\Tester\Errors
..\Tester\Inputs
..\Tester\Outputs
..\Tester\Scripts
..\Tester\xxold
..\Threads
By bytes...
[(4, 2, ‘..\\StreamsY\input.txt’),
(13, 1, ‘..\\Streams\\hello-in.txt’),
(20, 4, ‘..\\Streams\\data.txt’)]
[(104, 4, ‘..\\Streams\\output.txt’),
(172, 3, ‘..\\Tester\\xxold\\README.txt.txt’),
(435, 4, ‘..\\Filetools\\temp.txt’)]
By lines...
[(13, 1, ‘..\\Streams\\hello-in.txt’),
(22, 1, ‘..\\spam.txt’),
(4, 2, ‘..\\Streams\\input.txt’)]
[(20, 4, ‘..\\Streams\\data.txt’),
(104, 4, ‘..\\Streams\\output.txt’),
(435, 4, ‘..\\Filetools\\temp.txt’)]
А чтобы выполнить поиск по всей системе, достаточно просто передать сценарию имя корневого каталога (в Unix-подобных системах вместо C:\ используйте /) и расширение файлов (по умолчанию используется расширение .py). Итак, победитель... (только не заключайте никакие пари):
C:\...\PP4E\dev\Examples\PP4E\System\Filetools> bigext-tree.py C:\
C:\
C:\SRecycle.Bin
C:\SRecycle.Bin\S-1-5-21-3951091421-2436271001-910485044-1004
C:\cygwin
C:\cygwin\bin
C:\cygwin\cygdrive
C:\cygwin\dev
C:\cygwin\dev\mqueue
C:\cygwin\dev\shm
C:\cygwin\etc
...МНОГО строк опущено...
By bytes...
[(0, 0, ‘C:\\cygwin\\...\\python31\\Python-3.1.1\\Lib\\build_class.py’),
(0, 0, ‘C:\\cygwin\\...\\python31\\Python-3.1.1\\Lib\\email\\mime\\__init__.
py’),
(0, 0, ‘C:\\cygwin\\...\\python31\\Python-3.1.1\\Lib\\email\\test\\__init__.
py’)]
[(380582, 78, ‘C:\\Python31\\Lib\\pydoc_data\\topics.py’),
(398157, 83, ‘C:\\...\\Install\\Source\\Python-2.6\\Lib\\pydoc_topics.py’), (412434, 83, ‘C:\\Python26\\Lib\\pydoc_topics.py’)]
By lines...
[(0, 0, ‘C:\\cygwin\\...\\python31\\Python-3.1.1\\Lib\\build_class.py’),
(0, 0, ‘C:\\cygwin\\...\\python31\\Python-3.1.1\\Lib\\email\\mime\\__init__.
py’),
(0, 0, ‘C:\\cygwin\\...\\python31\\Python-3.1.1\\Lib\\email\\test\\__init__.
py’)]
[(204107, 5589, ‘C:\\...\Install\\Source\\Python-3.0\\Lib\\decimal.py’),
(205470, 5768, ‘C:\\cygwin\\...\\python31\\Python-3.1.1\\Lib\\decimal.py’), (211238, 5768, ‘C:\\Python31\\Lib\\decimal.py’)]
Логика трассировки построена так, что позволяет следить за тем, как выполняется обход каталогов. Я сократил список каталогов в этом примере, чтобы не перегружать его лишней информацией (и уместить на страницу). Для выполнения этой команды может потребоваться достаточно продолжительное время. На моем нетбуке, работающем, как это ни печально, под управлением Windows 7, потребовалось 11 минут, чтобы просканировать жесткий диск, содержащий примерно 59 Гбайт данных, 200K файлов и 25K каталогов, при невысокой нагрузке на систему (8 минут - при отключенной трассировке, и полчаса, когда было запущено множество других приложений). Однако этот сценарий дает самый исчерпывающий ответ на поставленный вопрос.
Это решение настолько полное, насколько позволяет пространство в книге. Ради интереса подумайте о возможности сканирования нескольких дисков; о том, что исходные файлы Python могут находиться в zip-архивах, как в пути поиска модулей, так и за его пределами (os. walk просто игнорирует zip-файлы в примере 6.3). Кроме того, файлы с исходными текстами могут иметь разные расширения - файлы с расширением .pyw подавляют вывод окна консоли в Windows, а файлы сценариев верхнего уровня могут иметь произвольные расширения. Фактически имена файлов сценариев верхнего уровня вообще могут не иметь расширения и при этом оставаться файлами с исходными текстами на языке Python. Кроме того, некоторые модули, доступные для импортирования, не являясь файлами с исходными текстами на языке Python, могут присутствовать в формате скомпилированных двоичных файлов или быть статически связаны с выполняемым файлом интерпретатора Python. В интересах экономии места мы оставим эти варианты (довольно сложные в реализации!) расширения процедуры поиска, как упражнение для самостоятельного решения.
Вывод имен файлов с символами Юникода
Одна тонкость, прежде чем двинуться дальше: обратите внимание на, казалось бы, излишнюю конструкцию обработки исключений в функции tryprint из примера 6.4. Когда я в первый раз попытался просканировать весь диск с помощью сценария, приведенного в предыдущем разделе, этот сценарий завершился с ошибкой декодирования символов Юникода при попытке вывести имя каталога сохраненной вебстраницы. Добавление обработчика исключения позволило просто пропустить этот каталог.
Этот случай наглядно демонстрирует тонкую, но имеющую большое практическое значение проблему: ориентированность Python 3.X на Юникод распространяется и на имена файлов, даже в случае простого их вывода. Как мы узнали в главе 4, имена файлов могут содержать произвольный текст, поэтому функция os.listdir способна возвращать имена файлов в двух различных представлениях - она возвращает декодированные строки Юникода, если получает аргумент типа str, и кодированную строку байтов, если получает аргумент типа bytes:
>>> import os
>>> os.listdir('.')[:4]
[‘bigext-tree.py’, ‘bigpy-dir.py’, ‘bigpy-path.py’, ‘bigpy-tree.py’]
>>> os.listdir(b'.')[:4]
[b’bigext-tree.py’, b’bigpy-dir.py’, b’bigpy-path.py’, b’bigpy-tree.py’]
Обе функции, os.walk (используется в примере 6.4) и glob.glob, наследуют это поведение при возвращении имен файлов и каталогов, потому что внутри они используют функцию os.listdir. Во всех этих функциях передача строки байтов в аргументе подавляет декодирование символов Юникода в именах файлов и каталогов. Передача обычной строки предполагает, что имена файлов могут быть декодированы при применении кодировки, используемой файловой системой.
Причина, по которой данная особенность может иметь важное значение для сценария из этого раздела, состоит в том, что версия, выполняющая поиск по всему жесткому диску, в конце концов может столкнуться с недекодируемыми именами файлов (например, ранее сохраненная веб-страница с необычным именем), что приводит к возбуждению исключения при попытке вывести их с помощью функции print. Ниже приводится упрощенный пример, выполняемый в окне консоли Windows, который воспроизводит ошибку:
>>> root = r'C:\py3000'
>>> for (dir, subs, files) in os.walk(root): print(dir)
C:\py3000
C:\py3000\FutureProofPython - PythonInfo Wiki_files C:\py3000\0akwinter_com Code » Porting setuptools to py3k_files Traceback (most recent call last):
File “
File “C:\Python31\lib\encodings\cp437.py”, line 19, in encode return codecs.charmap_encode(input,self.errors,encoding_map)[0] UnicodeEncodeError: ‘charmap’ codec can’t encode character ‘\u2019’ in position 45: character maps to
(UnicodeDecodeError: кодек ‘charmap’ не может преобразовать символ \ u2019' в позиции 45: отображается в символ
Один из способов выхода из этого затруднительного положения состоит в том, чтобы передавать имя корневого каталога в виде строки типа bytes, - это подавляет выполнение операции декодирования имен файлов в функции os.listdir, вызываемой функцией os.walk, и эффективно ограничивает область действия последующих операций вывода простыми байтами. Так как в этом случае операциям вывода не приходится иметь дело с кодировками, они выполняются без ошибок. Декодирование строк в байты вручную перед выводом тоже может помочь, но результаты получаются немного другими:
>>> root.encode()
b’C:\\py3000’
>>> for (dir, subs, files) in os.walk(root.encode()): print(dir)
b’C:\\py3000’
b’C:\\py3000\\FutureProofPython - PythonInfo Wiki_files’ b’C:\\py3000\\0akwinter_com Code \xbb Porting setuptools to py3k_files’ b’C:\\py3000\\What\x92s New in Python 3_0 \x97 Python Documentation’
>>> for (dir, subs, files) in os.walk(root): print(dir.encode())
b’C:\\py3000’
b’C:\\py3000\\FutureProofPython - PythonInfo Wiki_files’ b’C:\\py3000\\0akwinter_com Code \xc2\xbb Porting setuptools to py3k_files’ b’C:\\py3000\\What\xe2\x80\x99s New in Python 3_0 \xe2\x80\x94 Python Documentation’
К сожалению, при любом подходе все имена каталогов, которые выводятся в процессе обхода, отображаются как непонятные строки байтов. Чтобы не отказываться от обычных строк, обеспечивающих более высокую удобочитаемость, я выбрал вариант реализации с обработчиком исключений. Это позволяет полностью исключить проблемы:
>>> for (dir, subs, files) in os.walk(root):
... try:
... print(dir)
... except UnicodeEncodeError:
... print(dir.encode()) # или просто пропустить, если encode
... # может потерпеть неудачу
C:\py3000
C:\py3000\FutureProofPython - PythonInfo Wiki_files C:\py3000\0akwinter_com Code » Porting setuptools to py3k_files b’C:\\py3000\\What\xe2\x80\x99s New in Python 3_0 \xe2\x80\x94 Python Documentation’
Странно, но похоже, что ошибка связана скорее с выводом, чем с кодированием символов Юникода в именах файлов, - поскольку при работе с именами файлов никаких ошибок не возникает, пока не предпринимаются попытки вывести их, следовательно, они были успешно декодированы в момент первоначального преобразования их в строки. Именно поэтому достаточно обернуть в инструкции try вызовы функции print, в противном случае ошибка возникала бы раньше.