from sys import argv # пример клиентского программного кода

myargs = getopts(argv) if ‘-i’ in myargs:

print(myargs[‘-i’])

print(myargs)

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

C:\...\PP4E\System> python testargv2.py

{}

C:\...\PP4E\System> python testargv2.py -i data.txt -o results.txt

data.txt

{‘-o’: ‘results.txt’, ‘-i’: ‘data.txt’}

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

• Модуль getopt моделирует поведение одноименной утилиты Unix/C

• Модуль optparse является более современной альтернативой и по общему признанию - более мощной

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

Выполняемые сценарии в Unix

Пользователям Unix и Linux: текстовые файлы с исходным программным кодом на языке Python можно сделать непосредственно исполняемыми, добавив в их начало особую строку, содержащую путь к интерпретатору Python, и присвоив файлу права на выполнение. Например, сохраните следующий фрагмент в текстовом файле с именем myscript:

#!/usr/bin/python print(‘And nice red uniforms’)

Первая строка будет восприниматься интерпретатором как комментарий (она начинается с #), но при запуске этого файла операционная система будет посылать строки этого файла интерпретатору, указанному после #! в первой строке. Если этот файл сделать непосредственно исполняемым с помощью команды chmod +x myscript, его можно будет запускать непосредственно, не вводя слово python в команде, как если бы это был двоичный файл исполняемой программы:

% myscript a b с And nice red uniforms

При запуске таким способом список sys.argv по-прежнему будет содержать имя сценария в первом элементе: [“myscript”, “a”, “b”, “с”] - в точности, как если бы сценарий был запущен с помощью более явного и переносимого формата команды python myscript a b с. Превращение сценариев в непосредственно исполняемые файлы на самом деле является трюком ОС Unix, а не особенностью Python, но стоит отметить, что можно сделать его несколько менее машинно-зависимым, указав в начале команду Unix env вместо пути к исполняемому файлу Python:

#!/usr/bin/env python print(‘Wait for it...’)

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

% python myscript a b с


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

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



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

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

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


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

В Python окружение оболочки является простым предустановленным объектом, для обращения к которому не требуется использовать специальный синтаксис. Операция индексирования объекта os.environ строками с именами переменных оболочки (например, os.environ[‘USER’ ]) является эквивалентом знака доллара перед именем переменной в большинстве оболочек Unix (например, $USER), использования с двух сторон знака процента в DOS (%USER%) и вызова getenv(“USER”) в программе на языке С. Запустим интерактивный сеанс и поэкспериментируем (следующий сеанс выполнялся в Python 3.1 в Windows 7):

>>> import os

>>> os.environ.keys()

KeysView()

>>> list(os.environ.keys())

[‘TMP’, ‘COMPUTERNAME’, ‘USERDOMAIN’, ‘PSMODULEPATH’, ‘ COMMONPROGRAMFILES’, ...множество строк было удалено...

‘NUMBER_OF_PROCESSORS’, ‘PROCESSOR_LEVEL’, ‘USERPROFILE’, ‘OS’, ‘PUBLIC’, ‘QTJAVA’]

>>> os.environ['TEMP']

‘C:\\Users\\mark\\AppData\\Local\\Temp’

Здесь метод keys возвращает итерируемый объект со списком установленных переменных, а операция индексирования возвращает значение переменной TEMP в Windows. В Linux эти инструкции действуют точно так же, но обычно при запуске Python устанавливаются другие переменные. Поскольку нам знакома переменная PYTHONPATH, посмотрим в Python на ее значение и убедимся в его правильности (когда я писал эти строки, в эту переменную временно был добавлен путь к корневому каталогу с примерами к четвертому изданию книги):

>>> os.environ['PYTHONPATH']

‘C:\\PP4thEd\\Examples;C:\\Users\\Mark\\temp’

>>> for srcdir in os.environ['PYTHONPATH'].split(os.pathsep):

... print(srcdir)

C:\PP4thEd\Examples

C:\Users\Mark\temp

>>> import sys >>> sys.path[:3]

[‘’, ‘C:\\PP4thEd\\Examples’, ‘C:\\Users\\Mark\\temp’]

Переменная PYTHONPATH содержит строку, содержащую список каталогов, разделенных символом, используемым для разделения таких элементов пути на вашей платформе (например, ; в DOS/Windows, : в Unix и Linux). Чтобы разделить эту строку на составляющие, передадим строковому методу split разделитель os.pathsep (переносимая константа, дающая правильный разделитель для соответствующей системы). Как обычно, фактический путь поиска, используемый во время выполнения, хранится в списке sys.path и является объединением пути к текущему рабочему каталогу и содержимого переменной окружения PYTHONPATH.


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

Как и обычные словари, объект os.environ поддерживает обращение по ключу и присваивание. Операция присваивания, применяемая к словарям, изменяет значение ключа:

>>> os.environ['TEMP']

‘C:\\Users\\mark\\AppData\\Local\\Temp >>> os.environ['TEMP'] = r'c:\temp'

>>> os.environ['TEMP']

‘c:\\temp’

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

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

Пример 3.3. PP4E\System\Environment\setenv.py

import os

print(‘setenv...’, end=’ ‘)

print(os.environ[‘USER’]) # выведет текущее значение переменной оболочки

os.environ[‘USER’] = ‘Brian’ # неявно вызовет функцию os.putenv os.system(‘python echoenv.py’)

os.environ[‘USER’] = ‘Arthur’ # изменение передается порождаемым программам os.system(‘python echoenv.py’) # и связанным с процессом библ. модулям на C

os.environ[‘USER’] = input(‘?’) print(os.popen(‘python echoenv.py’).read())

Данный сценарий setenv.py просто изменяет переменную оболочки USER и запускает другой сценарий, выводящий значение этой переменной, который приводится в примере 2.5.

Пример 3.4. PP4E\System\Environment\echoenv.py

import os

print(‘echoenv...’, end=’ ‘) print(‘Hello,’, os.environ[‘USER’])

Независимо от способа запуска сценарий echoenv.py выводит значение переменной окружения USER. При запуске из командной строки этот сценарий выведет значение, установленное нами в самой оболочке:

C:\...\PP4E\System\Environment> set USER=Bob

C:\...\PP4E\System\Environment> python echoenv.py echoenv... Hello, Bob

Однако при запуске из другого сценария, например, из setenv.py, с помощью функции os.system или os.popen, с которыми мы познакомились ранее, сценарий echoenv.py получит то значение переменной USER, которое было установлено родительской программой:

C:\...\PP4E\System\Environment> python setenv.py

setenv... Bob

echoenv... Hello, Brian

echoenv... Hello, Arthur

?Gumby

echoenv... Hello, Gumby

C:\...\PP4E\System\Environment> echo %USER%

Bob

Точно так же этот механизм действует и в Linux. Вообще говоря, порождаемая программа всегда наследует значения переменных окружения от своих родителей. Порожденными программами являются такие, которые запускаются средствами Python, например os.spawnv, комбинацией os.fork/exec в Unix-подобных системах, и os.popen, os.system или с помощью модуля subprocess на ряде других платформ. Все программы, запущенные таким способом, получают значения переменных окружения, существующие в момент запуска в родительском процессе.9

Подобный способ установки переменных окружения перед запуском новой программы является одним из способов передачи информации в новую программу. Например, можно написать сценарий, изменяющий переменную PYTHONPATH включением в нее пользовательских каталогов, перед запуском других сценариев. Благодаря этому запущенный сценарий получит свой путь поиска модулей в списке sys.path, потому что переменные оболочки передаются потомкам (такой запускающий сценарий будет представлен в конце главы 6).


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

Обратите внимание на последнюю команду в предыдущем примере -после завершения программы верхнего уровня переменная USER получает свое первоначальное значение. Присвоения значений ключам os.environ передаются за пределы интерпретатора вниз по цепочке порожденных программ и никогда не передаются вверх процессам родительских программ (включая системную оболочку). Это относится и к программам на языке C, использующим библиотечный вызов putenv, то есть данная особенность не является ограничением, характерным именно для Python.

Это едва ли вызовет проблемы в сценарии Python, являющемся вершиной приложения. Но помните, что настройки оболочки, сделанные внутри программы, действуют, лишь пока выполняется эта программа и порожденные ею дочерние программы. Если вам потребуется экспортировать настройки окружения, чтобы они действовали после завершения программы на языке Python, вам необходимо будет найти платформозависимые расширения, реализующие такую возможность. Попробуйте поискать их на сайте http://www.python.org и в Интернете.

Другая тонкость: в нынешней реализации изменение значений в os.en-viron автоматически приводит к вызову функции os.putenv, которая вызывает функцию putenv в библиотеке языка C, если она доступна на вашей платформе, чтобы экспортировать измененное значение за пределы интерпретатора Python во все связанные с ним расширения на языке C. Однако, хотя изменения в os.environ приводят к вызову os.putenv, тем не менее прямой вызов функции os.putenv не оказывает влияния на содержимое os.environ. По этой причине для изменения окружения предпочтительнее использовать интерфейс os.environ.

Обратите также внимание, что настройки окружения загружаются в os.environ на этапе запуска программы, а не при каждом обращении к этому объекту. По этой причине изменения, выполненные в расширениях на языке C уже после запуска программы, могут не отражаться в os.environ. В языке Python на самом деле имеется более конкретная функция os.getenv, но она не вызывает функцию getenv из библиотеки языка C, а просто выбирает значения ключей из os.environ в большинстве платформ (во всех в версии 3.X). Для большинства приложений в этом нет ничего плохого, особенно если они содержат программный код только на языке Python. На платформах, где отсутствует функция putenv, для настройки окружения порождаемой программы можно передавать словарь os.environ инструментам запуска программ в виде параметра.


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

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

>>> import sys

>>> for f in (sys.stdin, sys.stdout, sys.stderr): print(f)

<_io.TextIOWrapper name=’’ encoding=’cp437’>

<_io.TextIOWrapper name=’’ encoding=’cp437’>

<_io.TextIOWrapper name=’’ encoding=’cp437’>

Стандартные потоки - это всего лишь предварительно открытые объекты файлов Python, которые автоматически подключаются к стандартным потокам ввода-вывода программы при запуске. По умолчанию все они связаны с окном консоли, в котором был запущен интерпретатор Python (или программа на языке Python). Поскольку встроенные функции print и input являются не чем иным, как дружественными интерфейсами к стандартным потокам вывода-ввода, по своему действию они аналогичны прямому использованию stdout и stdin в sys:

>>> print('hello stdout world')

hello stdout world

>>> sys.stdout.write('hello stdout world' + '\n')

hello stdout world 19

>>> input('hello stdin world>')

hello stdin world>spam ‘spam’

>>> print('hello stdin world>'); sys.stdin.readline()[:-1]

hello stdin world> eggs

‘eggs’

Стандартные потоки в Windows

Пользователям Windows: при запуске программ на языке Python из проводника Windows щелчком на имени файла с расширением .ру (или с помощью os.system) автоматически появляется окно консоли DOS, служащее стандартным потоком программы. Если программа создает собственные окна, можно избежать открытия окна консоли, дав файлу с исходным текстом программы расширение .pyw, а не .ру. Расширение .pyw означает просто исходный файл .ру программы, для запуска которой не требуется открывать окно DOS в Windows (это обеспечивается настройками в реестре Windows, где файлам с расширением .pyw поставлена в соответствие специализированная версия Python). Файлы с расширением .pyw могут импортироваться, как обычные файлы .py.

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



Перенаправление потоков ввода-вывода в файлы и программы

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

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

Несмотря на всю мощь этой парадигмы, сам механизм перенаправления весьма прост в использовании. В качестве примера рассмотрим простой цикл «прочесть-вычислить-вывести», представленный в примере 3.5.

Пример 3.5. PP4E\System\Streams\teststreams.py

“читает числа до символа конца файла и выводит их квадраты”

def interact():

print(‘Hello stream world’) # print выводит в sys.stdout while True: try:

reply = input(‘Enter a number>’) # input читает из sys.stdin except EOFError:

break # исключение при встрече символа eof

else: # входные данные в виде строки

num = int(reply)

print(“%d squared is %d” % (num, num ** 2)) print(‘Bye’)

if__name__== ‘__main__’:

interact() # если выполняется, а не импортируется

Как обычно, функция interact вызывается автоматически, если файл не импортируется, а выполняется как самостоятельный сценарий. По умолчанию запуск этого файла из командной строки вызывает появление стандартного потока в месте, где вводилась команда. Сценарий просто читает числа, пока не достигнет конца файла в стандартном потоке ввода (в Windows конец файла обычно можно ввести комбинацией двух клавиш CtrL+Z; в Unix нужно нажать комбинацию CtrL+D):10

C:\...\PP4E\System\Streams> python teststreams.py

Hello stream world Enter a number>12 12 squared is 144 Enter a number>10 10 squared is 100 Enter a number>^Z Bye

И в Windows, и в Unix-подобных системах стандартный поток ввода можно перенаправить в файл - с помощью синтаксической конструкции < filename оболочки. Ниже приводится сеанс работы в окне консоли DOS под Windows, где сценарий читает входные данные из текстового файла input.txt. То же самое можно проделать и в Linux, только команду DOS type нужно заменить командой Unix cat:

C:\...\PP4E\System\Streams> type input.txt 8 6

C:\...\PP4E\System\Streams> python teststreams.py < input.txt

Hello stream world Enter a number>8 squared is 64 Enter a number>6 squared is 36 Enter a number>Bye

Здесь ввод данных, которые обычно поступают с клавиатуры в интерактивном режиме, автоматизирован с помощью файла input.txt: сценарий читает данные из этого файла, а не с клавиатуры. Точно так же можно перенаправить в файл и стандартный поток вывода - с помощью синтаксической конструкции > filename оболочки. При этом перенаправление ввода и вывода можно объединить в одной команде:

C:\...\PP4E\System\Streams> python teststreams.py < input.txt > output.txt

C:\...\PP4E\System\Streams> type output.txt

Hello stream world

Enter a number>8 squared is 64

Enter a number>6 squared is 36

Enter a number>Bye

На этот раз стандартные потоки ввода и вывода сценария отображаются в текстовые файлы, а не в сеанс интерактивной консоли.

Соединение программ с помощью каналов

В Windows и в Unix-подобных системах имеется возможность направлять стандартный вывод одной программы в стандартный ввод другой, помещая между командами символ |. Обычно это называется операцией создания «канала» или «конвейера»: оболочка создает канал, соединяющий вывод и ввод двух команд. Попробуем отправить вывод сценария на вход программы more, чтобы увидеть, как действует этот прием:

C:\...\PP4E\System\Streams> python teststreams.py < input.txt | more

Hello stream world Enter a number>8 squared is 64 Enter a number>6 squared is 36 Enter a number>Bye

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

C:\...\PP4E\System\Streams> type writer.py print(“Help! Help! I’m being repressed!”) print(42)

C:\...\PP4E\System\Streams> type reader.py print(‘Got this: “%s”’ % input()) import sys

data = sys.stdin.readline()[:-1]

print(‘The meaning of life is’, data, int(data) * 2)

C:\...\PP4E\System\Streams> python writer.py Help! Help! I’m being repressed!

42

C:\...\PP4E\System\Streams> python writer.py | python reader.py

Got this: “Help! Help! I’m being repressed!”

The meaning of life is 42 84

На этот раз связь устанавливается между двумя программами на языке Python. Сценарий reader получает входные данные от сценария writer -оба сценария просто используют стандартные функции чтения и записи, не задумываясь о работе механизма потоков. На практике такое соединение программ в цепочку является простой формой организации взаимодействий между программами. Оно облегчает повторное использование утилит, предусматривающих возможность взаимодействий через stdin и stdout, самыми неожиданными способами. Например, программу на языке Python, которая сортирует текст, поступающий из stdin, можно использовать для работы с любым источником данных, в том числе с выводом других сценариев. Рассмотрим сценарии командной строки из примеров 3.6 и 3.7, которые сортируют строки с числами, поступающие в стандартный поток ввода, и складывают их.

Пример 3.6. PP4E\System\Streams\sorter.py

import sys # или sorted(sys.stdin)

lines = sys.stdin.readlines() # читает входные строки из stdin,

lines.sort() # сортирует их

for line in lines: print(line, end=’’) # отправляет результаты в stdout

# для дальнейшей обработки

Пример 3.7. PP4E\System\Streams\adder.py

import sys sum = 0 while True: try:

line = input() # или sys.stdin.readlines() except EOFError: # или for line in sys.stdin:

break # input отсекает символы \n в конце строк

else:

sum += int(line) # во 2-м издании использовалась функция sting.atoi() print(sum)

Мы можем использовать эти универсальные инструменты командной строки, чтобы с их помощью сортировать и складывать содержимое произвольных файлов и вывода других программ (примечание для пользователей Windows: на моей предыдущей машине с Windows XP и Python 2.X я должен был вводить команду «python file.py», а не просто «file.py», в противном случае перенаправление не давало ожидаемых результатов; ныне, в Windows 7 и Python 3.X, обе формы команд действуют корректно):

C:\...\PP4E\System\Streams> type data.txt 123 000 999 042

C:\...\PP4E\System\Streams> python sorter.py < data.txt сортировка файла

000

042

123

999

C:\...\PP4E\System\Streams> python adder.py < data.txt вычисление суммы

1164

C:\...\PP4E\System\Streams> type data.txt | python adder.py вычисление суммы 1164 для вывода

команды type

C:\...\PP4E\System\Streams> type writer2.py for data in (123, 0, 999, 42): print(‘%03d’ % data)

C:\...\PP4E\System\Streams> python writer2.py | python sorter.py сортировка 000 вывода сценария

042

123

999

C:\...\PP4E\System\Streams> writer2.py | sorter.py краткая форма записи выводит те же результаты, что и предыдущая команда Windows...

C:\...\PP4E\System\Streams> python writer2.py | python sorter.py | python adder. py

1164

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

Альтернативные реализации сценариев adder и sorter

Если присмотреться, можно заметить, что сценарий sorter.py читает сразу все данные, имеющиеся в stdin, используя метод readlines, а сценарий adder.py читает данные по одной строке. Если источником входных данных является другая программа, то в некоторых системах соединенные каналом программы выполняются параллельно. В таких системах, особенно если пересылается большой объем данных, лучше производить построчное чтение: читающей программе не придется ждать, пока пишущая программа полностью завершит работу, чтобы заняться обработкой данных. Так как функция input просто читает данные из потока stdin, схему построчного ввода, используемую в adder.py, можно также реализовать прямым обращением к sys.stdin:

C:\...\PP4E\System\Streams> type adder2.py import sys sum = 0 while True:

line = sys.stdin.readline() if not line: break sum += int(line) print(sum)

Данная версия использует тот факт, что функция int допускает наличие пробельных символов вокруг числа (функция readline возвращает строку вместе с символом \n, но мы не должны использовать [:-1] или rstrip() для его удаления). Фактически для достижения того же эффекта можно использовать более современные итераторы файлов - цикл for, например, автоматически извлекает из объекта файла по одной строке в каждой итерации (подробнее об итераторах файлов рассказывается в следующей главе):

C:\...\PP4E\System\Streams> type adder3.py import sys

sum = 0

for line in sys.stdin: sum += int(line) print(sum)

Однако перевод сценария sorte r на построчное чтение едва ли даст большой выигрыш в производительности, потому что метод sort списков требует, чтобы весь список был заполнен. Как будет показано в главе 18, запрограммированные вручную алгоритмы сортировки, скорее всего, будут работать значительно медленнее, чем метод сортировки списка Python.

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

C:\...\PP4E\System\Streams> type sorterSmall.py import sys

for line in sorted(sys.stdin): print(line, end=’’)

C:\...\PP4E\System\Streams> type adderSmall.py import sys

print(sum(int(line) for line in sys.stdin))

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


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

Выше в этом разделе мы направили вывод сценария teststreams.py на вход стандартной программы командной строки more с помощью следующей команды:

C:\...\PP4E\System\Streams> python teststreams.py < input.txt | more

Но поскольку в предыдущей главе мы уже написали на языке Python собственную утилиту «more» постраничного вывода, почему не сделать так, чтобы она тоже принимала ввод из stdin? Например, если изменить последние три строки в файле more.py, представленном в примере 2.1, на следующие:

if __name__ == ‘__main__’: # если выполняется, а не импортируется

import sys

if len(sys.argv) == 1: # вывести данные из stdin, если нет аргументов

more(sys.stdin.read())

else:

more(open(sys.argv[1]).read())

Тогда, похоже, мы сможем перенаправить стандартный вывод сценария teststreams.py на стандартный ввод more.py:

C:\...\PP4E\System\Streams> python teststreams.py < input.txt | python ..\more. py

Hello stream world Enter a number>8 squared is 64 Enter a number>6 squared is 36 Enter a number>Bye

В целом такой прием можно использовать в сценариях на языке Python. Здесь сценарий teststreams.py снова принимает данные из файла. И, как и в предыдущем разделе, вывод одной программы отправляется по каналу на ввод другой - сценарий more.py в родительском (. ) каталоге.

Однако в предыдущей команде more^Y кроется малозаметная проблема. В действительности эта цепочка сработала по чистой случайности: если первый сценарий будет выводить слишком много данных и сценарию more придется спрашивать пользователя о разрешении продолжить вывод, произойдет полный отказ сценария (точнее, когда функция input возбудит исключение EOFError).

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

Если потребуется принимать входные данные из stdin и использовать консоль для взаимодействия с пользователем, в сценарий нужно будет внести дополнительные изменения: нам придется отказаться от использования функции input и задействовать специальные интерфейсы для чтения ответов пользователя непосредственно с клавиатуры. В Windows такую возможность обеспечивает модуль msvcrt, входящий в состав стандартной библиотеки Python; в большинстве Unix-подобных систем достаточно будет использовать файл устройства /dev/tty.

Поскольку это достаточно редкая ситуация, мы рассмотрим полную реализацию предложенного решения. В примере 3.8 показана модифицированная версия сценария more.py для Windows, которая постранично выводит данные из стандартного потока ввода при вызове без аргументов, а кроме того, в ней используются низкоуровневые и зависящие от платформы средства взаимодействия с пользователем через клавиатуру.

Пример 3.8. PP4E\System\Streams\moreplus.py

обеспечивает постраничный вывод в stdout содержимого строки, файла или потока; если запускается как самостоятельный сценарий, обеспечивает постраничный вывод содержимого потока stdin или файла, имя которого указывается в виде аргумента командной строки; когда входные данные поступают через поток stdin, исключается возможность использовать его для получения ответов пользователя --вместо этого можно использовать платформозависимые инструменты или графический интерфейс;

import sys def getreply():

читает клавишу, нажатую пользователем,

даже если stdin перенаправлен в файл или канал

if sys.stdin.isatty(): # если stdin связан с консолью,

return input(‘?’) # читать ответ из stdin

else:

if sys.platform[:3] == ‘win’: # если stdin был перенаправлен,

import msvcrt # его нельзя использовать для чтения

msvcrt.putch(b’?’) # ответа пользователя

key = msvcrt.getche() # использовать инструмент консоли

msvcrt.putch(b’\n’) # getch(), которая не выводит символ

return key # для нажатой клавиши

else:

assert False, ‘platform not supported’

#для Linux: open(‘/dev/tty’).readline()[:-1]

def more(text, numlines=10):

реализует постраничный вывод содержимого строки в stdout

lines = text.splitlines() while lines:

chunk = lines[:numlines]

lines = lines[numlines:]

for line in chunk: print(line)

if lines and getreply() not in [b’y’, b’Y’]: break

if name == ‘ main ’: # если выполняется, а не импортируется

if len(sys.argv) == 1: # если нет аргументов командной строки

more(sys.stdin.read()) # вывести содержимое stdin

else:

more(open(sys.argv[1]).read()) # иначе вывести содержимое файла

Большая часть нововведений этой версии находится в функции getreply Метод файла isatty сообщает, соединен ли stdin с консолью, - если да функция просто считывает ответ из stdin, как и раньше. Конечно, по добная дополнительная логика необходима только в сценариях, предусматривающих возможность взаимодействия с пользователем и получения входных данных из stdin. В приложениях с графическим интерфейсом можно было бы, например, выводить диалог, реализовать обработку событий от клавиатуры в виде функций обратного вызова и так далее (знакомиться с графическими интерфейсами мы будем в главе 7).

Имея на вооружении функцию getreply, можно спокойно запускать утилиту moreplus различными способами. Как и прежде, можно импортировать и непосредственно вызывать функцию этого модуля, передавая ей ту строку, которую требуется вывести постранично:

>>> from moreplus import more

>>> more(open('adderSmall.py').readO)

import sys

print(sum(int(line) for line in sys.stdin))

И так же, как и прежде, при запуске с аргументом командной строки этот сценарий интерактивно будет пролистывать текст указанного файла:

C:\...\PP4E\System\Streams> python moreplus.py adderSmall.py

import sys

print(sum(int(line) for line in sys.stdin))

C:\...\PP4E\System\Streams> python moreplus.py moreplus.py

обеспечивает постраничный вывод в stdout содержимого строки, файла или потока; если запускается как самостоятельный сценарий, обеспечивает постраничный вывод содержимого потока stdin или файла, имя которого указывается в виде аргумента командной строки; когда входные данные поступают через поток stdin, исключается возможность использовать его для получения ответов пользователя - вместо этого можно использовать платформозависимые инструменты или графический интерфейс;

import sys

def getreply():

?n

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

C:\...\PP4E\System\Streams> python moreplus.py < moreplus.py

обеспечивает постраничный вывод в stdout содержимого строки, файла или потока; если запускается как самостоятельный сценарий, обеспечивает постраничный вывод содержимого потока stdin или файла, имя которого указывается в виде аргумента командной строки; когда входные данные поступают через поток stdin, исключается возможность использовать его для получения ответов пользователя - вместо этого можно использовать платформозависимые инструменты или графический интерфейс;

import sys

def getreply():

?n

C:\...\PP4E\System\Streams> type moreplus.py | python moreplus.py

обеспечивает постраничный вывод в stdout содержимого строки, файла или потока; если запускается как самостоятельный сценарий, обеспечивает постраничный вывод содержимого потока stdin или файла, имя которого указывается в виде аргумента командной строки; когда входные данные поступают через поток stdin, исключается возможность использовать его для получения ответов пользователя - вместо этого можно использовать платформозависимые инструменты или графический интерфейс;

import sys

def getreply():

?n

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

......\System\Streams> python teststreams.py < input.txt | python moreplus.py

Hello stream world Enter a number>8 squared is 64 Enter a number>6 squared is 36 Enter a number>Bye

Здесь стандартный вывод одного сценария подается на стандартный ввод другого сценария, находящегося в том же каталоге: moreplus.py читает вывод teststreams.py.

Все перенаправления в таких командах действуют только потому, что сценариям безразлично, чем в действительности являются стандартный ввод и вывод - консолью, файлами или каналами между программами. Например, при запуске moreplus.py как самостоятельного сценария он просто читает поток sys.stdin; командная оболочка (например, DOS в Windows, csh в Linux) прикрепляет такие потоки к источникам, определяемым командой, перед запуском сценария. Для доступа к этим источникам сценарии используют заранее открытые объекты файлов stdin и stdout, независимо от их истинной природы.

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


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

Все приведенные выше способы перенаправления стандартных потоков действуют для программ, написанных на любом языке программирования, который обеспечивает возможность перехватывать стандартные потоки, и зависят скорее от процессора командной строки оболочки, чем от самого интерпретатора. Операции перенаправления в командной строке, такие как < filename и | program, обрабатываются оболочкой, а не интерпретатором Python. Более «питонистый» способ перенаправления можно реализовать в самих сценариях, присваивая переменным sys.stdin и sys.stdout объекты, похожие на файлы.

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

• Любой объект, обладающий методами чтения, может быть присвоен переменной sys.stdin, в результате чего ввод будет осуществляться через методы чтения этого объекта.

• Любой объект, обладающий методами записи, может быть присвоен переменной sys.stdout; в результате весь стандартный вывод будет отправляться методам этого объекта.

Так как функции print и input просто вызывают методы write и readline объектов, на которые ссылаются sys.stdout и sys.stdin, мы можем генерировать и перехватывать стандартные текстовые потоки с помощью объектов, реализованных с помощью классов.

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

Пример 3.9. PP4E\System\Streams\redirect.py

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

import sys # импортировать встроенный модуль

class Output: # имитирует выходной файл

def __init__(self):

self.text = ‘’ # при создании строка пустая

def write(self, string): # добавляет строку байтов

self.text += string

def writelines(self, lines): # добавляет все строки в список

for line in lines: self.write(line)

class Input: # имитирует входной файл

def __init__(self, input=’’): # аргумент по умолчанию

self.text = input # сохранить строку при создании

def read(self, size=None): # необязательный аргумент

if size == None: # прочитать N байт или все

res, self.text = self.text, ‘’ else:

res, self.text = self.text[:size], self.text[size:] return res def readline(self):

eoln = self.text.find(‘\n’) # найти смещение следующего eoln

if eoln == *1: # извлечь строку до eoln

res, self.text = self.text, ‘’ else:

res, self.text = self.text[:eoln+1], self.text[eoln+1:] return res

def redirect(function, pargs, kargs, input): # перенаправляет stdin/out savestreams = sys.stdin, sys.stdout # вызывает объект функции sys.stdin = Input(input) # возвращает текст в stdout

sys.stdout = Output() try:

result = function(*pargs, **kargs) # вызвать функцию с аргументами output = sys.stdout.text

finally: # восстановить, независимо от

sys.stdin, sys.stdout = savestreams # того, было ли исключение return (result, output) # вернуть результат,

# если исключения не было

В этом модуле определены два класса, маскирующиеся под настоящие файлы:

Output

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

Input

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

Функция redirect в конце этого файла объединяет эти два объекта, чтобы выполнить единственную функцию, для которой стандартные потоки ввода и вывода будут перенаправлены в объекты Python. Функции, которая вызывается функцией redirect, не требуется ни знать, ни заботиться о том, что вызываемые ею функции print и input или методы stdin и stdout в действительности будут иметь дело с нашими объектами, а не с настоящим файлом, каналом или пользователем.

Чтобы продемонстрировать, как действует эта функция, импортируем и вызовем функцию interact, лежащую в основе сценария teststreams, представленного в примере 3.5, который прежде мы запускали из командной строки (для использования вспомогательной функции перенаправления нужно действовать на языке функций, а не файлов). При непосредственном вызове функция читает данные с клавиатуры и выводит результаты на экран, как если бы она выполнялась как программа без перенаправления:

C:\...\PP4E\System\Streams> python >>> from teststreams import interact >>> interact()

Hello stream world Enter a number>2

2 squared is 4 Enter a number>3

3 squared is 9 Enter a number^Z Bye

>>>

Теперь вызовем эту функцию под управлением функции перенаправления в redirect.py и передадим ей некоторый готовый входной текст. В этом случае на вход функции interact поступит переданная строка ('4\n5\n6\n ’ - три строки с явными символами конца строки), а результатом выполнения функции будет кортеж, содержащий возвращаемое значение и строку с текстом, который был записан в стандартный поток вывода:

>>> from redirect import redirect

>>> (result, output) = redirect(interact, (), {}, '4\n5\n6\n')

>>> print(result)

None

>>> output

‘Hello stream world\nEnter a number>4 squared is 16\nEnter a number>5 squared is 25\nEnter a number>6 squared is 36\nEnter a number>Bye\n’

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

>>> for line in output.splitlines(): print(line)

Hello stream world Enter a number>4 squared is 16 Enter a number>5 squared is 25 Enter a number>6 squared is 36 Enter a number>Bye

Еще лучше повторно использовать модуль more^Y, который мы написали в предыдущей главе (пример 2.1). При этом придется меньше запоминать и вводить с клавиатуры, а качество работы уже проверено нами (ниже, как и во всех примерах, где выполняется импортирование модулей из других каталогов, предполагается, что каталог, содержащий корневой подкаталог PP4E, находится в пути поиска модулей, - измените значение переменной окружения PYTHONPATH, если это необходимо):

>>> from PP4E.System.more import more >>> more(output)

Hello stream world Enter a number>4 squared is 16 Enter a number>5 squared is 25 Enter a number>6 squared is 36 Enter a number>Bye

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


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

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

>>> from io import StringIO

>>> buff = StringIO() # сохраняет записываемый текст в строке

>>> buff.write('spam\n')

5

>>> buff.write('eggs\n')

5

>>> buff.getvalue()

‘spam\neggs\n’

>>> buff = StringIO('ham\nspam\n') # возвращает входные данные из строки >>> buff.readline()

‘ham\n’

>>> buff.readline()

‘spam\n’

>>> buff.readline()

Экземпляры класса StringIO могут присваиваться переменным sys.stdin и sys.stdout, как демонстрировалось в предыдущем разделе, с целью перенаправить потоки для функций input и print, и использоваться в любом программном коде, выполняющем операции с настоящими объектами файлов. Напомню еще раз, что в языке Python правила игры определяются интерфейсом объекта, а не его конкретным типом:

>>> from io import StringIO

>>> import sys

>>> buff = StringIO()

>>> temp = sys.stdout >>> sys.stdout = buff

>>> print(42, 'spam', 3.141) # или print(..., file=buff)

>>> sys.stdout = temp # восстановит оригинальный поток

>>> buff.getvalue()

‘42 spam 3.141\n’

Следует также отметить, что существует класс io. BytesIO, обладающий похожим поведением, но он отображает операции с файлами не на строку типа str, а на буфер байтов типа bytes:

>>> from io import BytesIO >>> stream = BytesIO()

>>> stream.write(b'spam')

>>> stream.getvalue()

b’spam’

>>> stream = BytesIO(b'dpam')

>>> stream.read()

b’dpam’

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


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

Мы сосредоточились на перенаправлении stdin и stdout, но поток stderr также можно перенаправлять в файлы, каналы и объекты. Несмотря на то, что некоторые оболочки поддерживают возможность перенаправления этого потока, тем не менее это также можно сделаеть легко и просто в сценарии Python. Например, присвоение переменной sys. stderr экземпляра класса, такого как Output или StringIO из предыдущего примера, позволит сценарию перехватывать также текст, записываемый в стандартный поток ошибок.

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

Операция перенаправления стандартного потока ошибок из командной строки выглядит несколько сложнее и хуже переносится. В большинстве Unix-подобных систем перехватить вывод в поток stderr обычно можно с помощью операции перенаправления вида command > output 2>&1. Однако в некоторых версиях Windows она не действует, и даже в некоторых оболочках для Unix она может иметь другой вид - за дополнительной информацией обращайтесь к страницам справочного руководства по вашей оболочке.


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

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

print(stuff, file=afile) # afile - это объект, а не имя строковой переменной

выведет stuff в afile, а не в поток sys.stdout. По своему действию это напоминает присваивание объекта переменной sys.stdout, но в данном случае отпадает необходимость сохранять и восстанавливать первоначальное значение, чтобы вернуться к использованию оригинального потока вывода (как было показано в разделе, описывающем перенаправление потоков в объекты). Например:

import sys

print(‘spam’ * 2, file=sys.stderr)

выведет текст в объект стандартного потока ошибок, а не в sys.stdout, причем такое перенаправление будет действовать только для данного вызова функции print. Следующий вызов функции print (без аргумента file) выведет текст в стандартный поток вывода, как обычно. Точно так же в качестве выходного файла можно передать свой собственный объект или экземпляр класса из стандартной библиотеки:

>>> from io import StringIO >>> buff = StringIO()

>>> print(42, file=buff)

>>> print('spam', file=buff)

>>> print(buff.getvalue())

42

spam

>>> from redirect import Output >>> buff = Output()

>>> print(43, file=buff)

>>> print('eggs', file=buff)

>>> print(buff.text)

43

eggs


Другие варианты перенаправления: еще раз об os.popen и subprocess

Ближе к концу предыдущей главы мы впервые встретились с функцией os.popen и родственной ей subprocess.Popen, которые предоставляют возможность перенаправления потоков ввода-вывода других команд из программы на языке Python. Как мы видели, эти инструменты могут использоваться для выполнения команд оболочки (например, команд, которые обычно вводятся с клавиатуры в ответ на приглашение DOS или csh), и они возвращают объект Python, похожий на файл, соединенный с потоком вывода команды, - чтение из объекта файла позволяет сценарию принимать вывод другой программы. Однако эти инструменты могут также использоваться для соединения с потоками ввода.

Благодаря этому функцию os.popen и инструменты из модуля subprocess можно рассматривать как еще один способ перенаправления потоков порождаемых программ, родственный только что рассмотренным приемам. Их действие во многом похоже на действие оператора | объединения команд в конвейер (фактически имена этих инструментов означают «pipe open» - «открыть канал»), но они выполняются внутри сценария и предоставляют схожий с файлами интерфейс к потокам данных, связанных каналом. По духу они близки функции redirect, но запускают не функции, а программы, и потоки ввода-вывода обрабатываются в порождающем сценарии как файлы (не привязанные к объектам классов). Эти инструменты перенаправляют потоки ввода-вывода программ, запускаемых сценарием, а не самого сценария.

Перенаправление ввода или вывода с помощью os.popen

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

C:\...\PP4E\System\Streams> type hello-out.py print(‘Hello shell world’)

C:\...\PP4E\System\Streams> type hello-in.py inp = input()

open(‘hello-in.txt’, ‘w’).write(‘Hello ‘ + inp + ‘\n’)

Эти сценарии могут запускаться из командной строки, как обычно:

C:\...\PP4E\System\Streams> python hello-out.py Hello shell world

C:\...\PP4E\System\Streams> python hello-in.py Brian

C:\...\PP4E\System\Streams> type hello-in.txt Hello Brian

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

C:\...\PP4E\System\Streams> python >>> import os

>>> pipe = os.popen('python hello-out.py') # ‘r’ - по умолчанию, чтение stdout

>>> pipe.read()

‘Hello shell world\n’

>>> print(pipe.close()) # код завершения: None - успех

None

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

>>> pipe = os.popen('python hello-in.py', 'w') # ‘w’- запись в stdin программы

>>> pipe.write('Gumby\n')

6

>>> pipe.close() # символ \n в конце необязателен

>>> open('hello-in.txt').read() # вывод был отправлен в файл

‘Hello Gumby\n’

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

Перенаправление ввода и вывода с помощью модуля subprocess

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

Например, этот модуль позволяет множеством способов запускать программу, подключаться к ее стандартному потоку вывода и получать код завершения. Ниже демонстрируются три наиболее типичных способа использования этого модуля для запуска программы и перенаправления ее потока вывода (напомню, что для опробования примеров из этого раздела в Unix-подобных системах вам может потребоваться передавать функции Popen аргумент shell=True, как отмечалось в главе 2):

C:\...\PP4E\System\Streams> python

>>> from subprocess import Popen, PIPE, call

>>> X = call('python hello-out.py') # удобно

Hello shell world

>>> X 0

>>> pipe = Popen('python hello-out.py', stdout=PIPE)

>>> pipe.communicate()[0] # (stdout, stderr)

b’Hello shell world\r\n’

>>> pipe.returncode # код завершения

0

>>> pipe = Popen('python hello-out.py', stdout=PIPE)

>>> pipe.stdout.read()

b’Hello shell world\r\n’

>>> pipe.wait() # код завершения

0

Функция call, использованная в первом из этих трех способов, - это всего лишь функция-обертка, реализованная для удобства (существует несколько таких функций, о которых вы сможете прочитать в руководстве по библиотеке языка Python). Функция communicate делает второй способ немного удобнее третьего (она позволяет отправлять данные в stdin; читать данные из stdout, пока не будет достигнут конец файла; и ожидает завершения дочернего процесса).

Перенаправление и подключение к потоку ввода порождаемой программы реализуется так же просто, хотя и немного сложнее, чем при использовании функции os.popen с флагом режима ‘w’, как было показано в предыдущем разделе (как уже упоминалось в предыдущей главе, в настоящее время функция os.popen реализована с применением инструментов из модуля subprocess, и поэтому сама может считаться функцией-оберткой, реализованной для удобства):

>>> pipe = Popen('python hello-in.py', stdin=PIPE)

>>> pipe.stdin.write(b'Pokey\n')

6

>>> pipe.stdin.close()

>>> pipe.wait()

0

>>> open('hello-in.txt').read() # вывод был отправлен в файл

‘Hello Pokey\n’

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

C:\...\PP4E\System\Streams> type writer.py print("Help! Help! I’m being repressed!”) print(42)

C:\...\PP4E\System\Streams> type reader.py print(‘Got this: “%s”’ % input())

import sys

data = sys.stdin.readline()[:-1]

print(‘The meaning of life is’, data, int(data) * 2)

Следующий программный код демонстрирует возможность чтения и записи в потоки ввода-вывода сценария reader - объект pipe имеет два атрибута с объектами, похожими на файлы, один из которых подключается к потоку ввода, а другой - к потоку вывода (пользователи Python 2.X легко могут узнать в них эквивалент кортежа, возвращаемого функцией os.popen2, ныне исключенной из библиотеки):

>>> pipe = Popen('python reader.py', stdin=PIPE, stdout=PIPE)

>>> pipe.stdin.write(b'Lumberjack\n')

11

>>> pipe.stdin.write(b'12\n')

3

>>> pipe.stdin.close()

>>> output = pipe.stdout.read()

>>> pipe.wait()

0

>>> output

b’Got this: “Lumberjack”\r\nThe meaning of life is 12 24\r\n’

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

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

C:\...\PP4E\System\Streams> python writer.py | python reader.py

Got this: “Help! Help! I’m being repressed!”

The meaning of life is 42 84

C:\...\PP4E\System\Streams> python

>>> from subprocess import Popen, PIPE

>>> p1 = Popen('python writer.py', stdout=PIPE)

>>> p2 = Popen('python reader.py', stdin=p1.stdout, stdout=PIPE)

>>> output = p2.communicate()[0]

>>> output

b’Got this: “Help! Help! I\’m being repressed!”\r\nThe meaning of life is 42 84\r\n’ >>> p2.returncode

0

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

>>> import os

>>> p1 = os.popen('python writer.py', 'r')

>>> p2 = os.popen('python reader.py', 'w')

>>> p2.write( p1.read() )

36

>>> X = p2.close()

Got this: “Help! Help! I’m being repressed!”

The meaning of life is 42 84 >>> print(X)

None

С точки зрения более широкой перспективы, функция os.popen и модуль subprocess являются переносимыми эквивалентами механизма перенаправления потоков ввода-вывода порождаемых программ, реализованного в командных оболочках для Unix-подобных систем. Однако реализации на языке Python с таким же успехом работают в Windows и предоставляют более универсальный способ запуска других программ из сценариев на языке Python. Строки команд, передаваемые им, могут иметь свои особенности в зависимости от платформы (например, в Unix список содержимого каталога можно получить с помощью команды ls, а в Windows - с помощью команды dir), но сами инструменты могут применяться на всех платформах, поддерживающих Python.

Запуск новых, независимых программ и подключение к их потокам ввода-вывода из родительской программы в Unix-подобных системах можно также реализовать с помощью функций os.fork, os.pipe, os.dup и некоторых функций из семейства os.exec. Кроме того, они обеспечивают еще один способ перенаправления потоков ввода-вывода и являются низкоуровневыми эквивалентами таким инструментам, как os.popen (функция os.fork доступна в Windows, в версии Python для Cygwin).

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

Но перед этим, в главе 4, мы продолжим наше исследование системных интерфейсов, реализованных в библиотеке языка Python, и познако-

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


Python и csh

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

#!/bin/csh foreach x (*.py) echo $x

mail eric@halfabee.com -s $x < $x end

Ниже приводится эквивалентный сценарий на языке Python:

#!/usr/bin/python import os, glob for x in glob.glob(‘*.py’): print(x)

os.system(‘mail eric@halfabee.com -s %s < %s’ % (x, x))

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

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


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

Кроме того, сценарии на языке Python обычно легко могут переноситься на другие платформы, в отличие от csh. Например, задействовав для отправки электронной почты модуль Python, обеспечивающий интерфейс к SMTP, вместо утилиты mail командной строки, мы сможем использовать этот сценарий на любом компьютере, где установлен Python и имеется подключение к Интернету (как мы узнаем в главе 13, для работы с протоколом SMTP достаточно одних сокетов). Как и в языке C, нам нет необходимости использовать префикс $, чтобы получать значения переменных; что еще можно желать от свободного языка?


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


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

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


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

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

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

Другие связанные с файлами модули в Python позволяют, например, выполнять операции над файлами на низком уровне с использованием файловых дескрипторов (модуль os), перемещать файлы и группы файлов (модули os и shutil), сохранять в файлах данные и объекты по ключу (модули dbm и shelve) и обращаться к базам данных SQL (модуль sqlite3 и модули сторонних разработчиков). Последние две категории в большей степени относятся к обсуждению баз данных, которое ведется в главе 17.

В данном разделе мы кратко рассмотрим встроенный объект файла и несколько более сложных тем, относящихся к файлам. Как обычно, более подробное описание и методы, которые мы не имеем возможности разместить здесь, следует искать в руководстве по библиотеке или в справочниках, таких как «Python Pocket Reference». Не забывайте, что краткую справку можно получить в интерактивной оболочке: чтобы ознакомиться со списком атрибутов объекта файла, можно вызвать функцию dir(file) для объекта открытого файла; вызвав функцию help(file), можно получить справку более общего характера; а с помощью вызова help(file.read) - справку о конкретном методе, таком как read, хотя реализация объекта файла в версии 3.1 содержит меньше справочной информации, чем руководство по библиотеке и другие ресурсы.


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

Как и в случае со строковыми типами, о которых говорилось в главе 2, поддержка файлов в Python 3.X стала гораздо богаче, чем в предыдущих версиях. Как уже отмечалось ранее, в Python 3.X строки типа str всегда представляют текст Юникода (символы ASCII или многобайтовые символы), а строки типов bytes и bytearray представляют простые двоичные данные. Python 3.X проводит подобные различия между файлами, содержащими текст и двоичные данные:

Текстовые файлы, содержат текст, состоящий из символов Юникода. Содержимое текстовых файлов в сценариях всегда представляется в виде строк типа str - последовательностей символов (точнее, последовательностей «кодовых пунктов» Юникода). Для текстовых файлов автоматически выполняется преобразование символов конца строки, о котором рассказывается в этой главе, а к содержимому файлов автоматически применяются операции кодирования/деко-дирования: данные кодируются в двоичное представление при записи в файл и декодируются обратно в Юникод при чтении из файла, в соответствии с указанной или используемой по умолчанию кодировкой. Кодирование является тривиальной операцией для текста ASCII, но может быть весьма сложной в других случаях.

Двоичные файлы содержат обычные 8-битовые байты. Содержимое двоичных файлов в сценариях всегда представляется в виде строк байтов, обычно в виде объекта типа bytes - последовательности коротких целых чисел, которые поддерживают большинство операций, присущих типу str, и отображаются как последовательности символов ASCII, когда это возможно. Для двоичных файлов не предусматривается никаких преобразований данных при чтении или записи: ни преобразования символов конца строки, ни кодирования/ декодирования в Юникод.

На практике текстовые файлы используются для хранения действительно текстовых данных, а двоичные файлы - для хранения таких элементов, как упакованные двоичные данные, изображения, аудиоданные, выполняемый программный код и так далее. Программно эти два типа файлов различаются с помощью аргумента со строкой режима, который передается функции open: дополнительный символ «Ь» (например, ‘rb’, ‘wb’) означает, что файл содержит двоичные данные. Для создания нового содержимого текстовых файлов используются обычные строки (например, ‘spam’ или bytes.decode()), а для создания нового содержимого двоичных файлов - строки байтов (например, bspam’ или str.encode()).

Если в вашей практике область применения файлов не ограничивается использованием текста в кодировке ASCII, различия между представлением текстовых и двоичных данных в версии 3.X иногда будут сказываться на вашем программном коде. При работе с текстовыми файлами требуется использовать строки типа str, а с двоичными файлами - строки байтов. Поскольку вы не сможете смешивать эти типы в выражениях, вам придется внимательно подходить к вопросу выбора режима открытия файла. Многие встроенные инструменты, которые мы будем использовать в этой книге, делают этот выбор за нас - модули struct и pickle, например, в версии 3.X работают со строками байтов, а пакет xml - с Юникодом. Кроме того, о различиях между текстовыми и двоичными данными в версии 3.X необходимо помнить даже при использовании системных инструментов, таких как дескрипторы каналов и сокеты, потому что на сегодняшний день эти инструменты передают данные в виде строк байтов (впрочем, при необходимости эти данные можно кодировать и декодировать как текст Юникода).

Кроме того, при работе с текстовыми файлами выполняется обязательное декодирование их содержимого в Юникод в соответствии с выбранной кодировкой, поэтому вам придется использовать двоичный режим для чтения содержимого файлов, не поддающихся декодированию, в виде строк байтов (или обрабатывать исключения декодирования в Юникод с помощью инструкций try и пропускать такой файл целиком). Это относится и к собственно двоичным файлам, и к текстовым файлам, для представления текста в которых используется неподдерживаемая или неизвестная кодировка. Как мы увидим далее в этой главе, в версии 3.X строки типа str всегда содержат текст Юникода, поэтому иногда придется использовать строки байтов для представления имен файлов при использовании таких инструментов, как os.listdir, glob.glob и os.walk, если они не могут быть декодированы (передача в виде строки байтов фактически подавляет необходимость декодирования).

На протяжении всей книги мы будем видеть примеры влияния различий между текстовым и двоичным типами str и bytes в инструментах для работы с файлами: в главах 5 и 12, когда будем исследовать сокеты; в главах 6 и 11, когда нам потребуется игнорировать ошибки Юникода при поиске в файлах и каталогах; в главе 12, когда будем знакомиться с модулями поддержки протоколов Интернета на стороне клиента, таких как FTP и протоколы электронной почты, реализованные поверх сокетов, предполагающих определение режимов файлов и кодировок; и и во многих других местах.

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


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

Несмотря на различия между текстовыми и двоичными данными в Python 3.X, файлы по-прежнему очень просты в использовании. Для большинства задач обработки файлов в сценариях достаточно знать функцию open. Объект файла, возвращаемый функцией open, обладает методами для чтения данных (read, readline, readlines), записи данных (write, writelines), освобождения системных ресурсов (close), перемещения по файлу (seek), принудительного выталкивания выходных буферов на диск (flush), получения соответствующего дескриптора файла (fileno) и других. Но так как встроенный объект файла очень прост в использовании, давайте сразу рассмотрим несколько интерактивных примеров.

Вывод в файлы

Чтобы создать новый файл, следует вызвать функцию open с двумя аргументами: внешним именем создаваемого файла и строкой режима "w" (от write - запись). Чтобы сохранить данные в файле, нужно вызвать метод write объекта файла со строкой, содержащей данные, которые нужно сохранить, а затем метод close, чтобы закрыть файл. Метод write вернет количество символов или байтов, записанных в файл (о котором мы не всегда будем упоминать для экономии места в книге). Вызов метода close, как мы увидим далее, не является обязательным, если вам требуется открыть и прочитать файл повторно в той же программе или сеансе:

C:\temp> python

>>> file = open('data.txt', 'w') # откроет файл для вывода: создаст объект

>>> file.write('Heno file world!\n') # запишет строку, как есть

18

>>> file.write('Bye file world.\n') # вернет число символов/байтов

18

>>> file.close() # закрытие "сборщиком мусора" и выход

Вот и все - вы только что создали на своем компьютере, неважно каком, совершенно новый файл:

C:\temp> dir data.txt /B

data.txt

C:\temp> type data.txt

Hello file world!

Bye file world.

В новом файле нет ничего необычного. Здесь для показа имени файла и отображения его содержимого использованы команды DOS dir и type, но этот файл также будет виден в менеджере файлов с графическим интерфейсом.

Открытие файлов. В вызове функции open, показанном в предыдущем примере, первый аргумент может содержать необязательный полный путь к файлу. Если просто передать имя файла без указания пути, файл окажется в текущем рабочем каталоге Python. To есть он появится в том месте, откуда был запущен программный код, - в данном случае простое имя файла data.txt предполагает использование каталога C:\temp на моем компьютере, поэтому в реальности будет создан файл C:\temp\data.txt. Если быть более точным, в случае отсутствия абсолютного пути в имени файла путь к нему определяется относительно текущего рабочего каталога. Освежить эту тему в памяти можно с помощью раздела «Текущий рабочий каталог» (глава 3).

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

Запись. Обратите внимание, что в строки, записываемые в файл, был явно добавлен символ конца строки \n. В отличие от функции print, метод write объекта файла записывает в точности то, что ему передано, без дополнительного форматирования. Строка, переданная методу write, появляется во внешнем файле «символ в символ». При записи в текстовые файлы может выполняться преобразование символов конца строки или операция кодирования Юникода, о которых упоминалось выше, а когда позднее данные будут читаться из файла, автоматически будут выполнены обратные преобразования.

Для записи в файлы можно также использовать метод writelines, который просто записывает все строки из списка без дополнительного форматирования. Например, ниже приводится вызов writelines, эквивалентный двум вызовам write, показанным ранее:

file.writelines([‘Hello file world!\n’, ‘Bye file world.\n’])

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

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

open(‘somefile.txt’, ‘w’).write("G’day Bruce\n") # записать во временный файл open(‘somefile.txt’, ‘ r').read() # прочитать временный файл

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

Однако в некоторых контекстах вам может потребоваться явно закрывать файлы:

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

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

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

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

Гарантированное закрытие файлов: обработчики исключений и менеджеры контекста

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

Однако если необходимо обеспечить явное закрытие файла в любом случае, у вас есть два пути: наиболее типичный - использование инструкции try с предложением finally, потому что оно позволяет реализовать выполнение заключительных операций для любых типов исключений:

myfile = open(filename, ‘w’) try:

...обработка myfile... finally:

myfile.close()

В последних версиях Python появилась инструкция with, обеспечивающая более краткий способ реализации заключительных операций для объектов определенных типов, включая закрытие файлов:

with open(filename, ‘w’) as myfile:

... обработка myfile, закрывается автоматически после выхода...

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

Решение на основе инструкции with выглядит заметно короче (на 3 строки), чем альтернативное решение на основе конструкции try/finally, но оно является менее универсальным - инструкция with может применяться только к объектам, поддерживающим протокол менеджеров контекста, тогда как конструкция try/finally позволяет реализовать произвольные заключительные операции для произвольных контекстов исключений. Область применения инструкции with ограничена, несмотря на то, что у некоторых типов объектов также имеются менеджеры контекста (например, у блокировок потоков). Если вам хочется помнить только один вариант реализации заключительных операций, то конструкция try/finally выглядит наиболее объемлющей. При этом инструкция with позволяет уменьшить объем программного кода для файлов, которые должны быть закрыты в любом случае, и прекрасно справляется с этой конкретной задачей. Она позволяет сэкономить строку программного кода, когда обработка исключений не предусматривается (хотя и за счет добавления в логику обработки файла еще одного уровня вложенности и отступов):

myfile = open(filename, ‘w’) # традиционная форма

...обработка myfile...

myfile.close()

with open(filename) as myfile: # с применением менеджера контекста ... обработка myfile...

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

with A() as a, B() as b:

...инструкции...

действует так же, как программный код ниже, который можно использовать в версиях 3.1, 3.0 и 2.6:

with A() as a: with B() as b:

...инструкции...

Например, когда программа выходит из следующего блока инструкции with, автоматически выполняются действия по закрытию обоих файлов, независимо от того, возникло исключение или нет:

with open(‘data’) as fin, open(‘results’, ‘w’) as fout: for line in fin:

fout.write(transform(line))

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

Чтение из файлов

Чтение данных из внешних файлов осуществляется столь же просто, как запись, но при этом доступно большее количество методов, позволяющих загружать данные в разнообразных режимах. Входные текстовые файлы открываются с флагом режима "r" (от «read» - читать) либо вообще без флага режима ("r" - значение по умолчанию, и параметр часто пропускается). После открытия текстового файла его строки можно читать с помощью метода readlines:

C:\temp> python

>>> file = open('data.txt') # открыть входной файл: ‘r’ - по умолчанию

>>> lines = file.readlines() # прочитать в список строк

>>> for line in lines: # НО! использовать итератор файла!

... print(line, end='') # строки оканчиваются символом ‘\n’

Hello file world!

Bye file world.

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

file.read()

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

file.read(N)

Возвращает строку, содержащую очередные N символов (или байтов) из файла.

file.readline()

Читает содержимое файла до ближайшего символа \n и возвращает строку.

file.readlines()

Читает файл целиком и возвращает список строк.

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

>>> file.seek(0) # перейти в начало файла

>>> file.read() # прочитать в строку файл целиком

‘Hello file world!\nBye file world.\n’

>>> file.seek(0)

>>> file.readlines() # прочитать файл целиком в список строк

[‘Hello file world!\n’, ‘Bye file world.\n’]

>>> file.seek(0)

>>> file.readline() # читать по одной строке

‘Hello file world!\n’

>>> file.readline()

‘Bye file world.\n’

>>> file.readline() # конец файла - возвращается пустая строка

>>> file.seek(0) # прочитать N (или оставшиеся) символы/байты

>>> file.read(1), file.read(8) # конец файла - возвращается пустая строка (‘H’, ‘ello fil’)

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

• read() и readlines() загружают в память сразу весь файл. Это удобно, когда желательно получить содержимое файла, написав более короткий программный код. Кроме того, эти методы действуют очень быстро, но для больших файлов их применение накладно: загрузка гигабайтных файлов - обычно не самое лучшее решение (а кроме того, на некоторых компьютерах - просто невозможное).

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

• Кроме того, смотрите обсуждение итераторов файлов в следующем разделе. Как мы увидим, итераторы объединяют в себе удобство метода readlines() и экономное отношение к памяти метода readline(), и на сегодняшний день являются наиболее предпочтительным способом построчного чтения текстовых файлов.

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

Чтение строк с помощью итераторов файлов

В прежних версиях Python принятым способом построчного чтения информации из файла в цикле for было чтение файла в список, а затем обход этого списка в цикле:

>>> file = open('data.txt')

>>> for line in file.readlines(): # НЕ ДЕЛАЙТЕ ТАК БОЛЬШЕ!

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

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

>>> file = open('data.txt')

>>> for line in file: # нет необходимости вызывать readlines

... print(line, end='') # итератор каждый раз читает следующую строку

Hello file world!

Bye file world.

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

>>> for line in open('data.txt'): # еще короче: временный объект файла

... print(line, end='') # будет закрыт при утилизации автоматически

Hello file world!

Bye file world.

Кроме того, такая форма обхода строк в файле не вызывает загрузку всего содержимого файла в список строк, поэтому она более экономно расходует память при работе с большими текстовыми файлами. По этой причине данный способ построчного чтения файлов является наиболее предпочтительным на сегодняшний день. Если вам интересно узнать, что же в действительности происходит внутри цикла for, вы можете попробовать использовать итератор вручную. Итератор - это всего лишь метод__next__(вызываемый встроенной функцией next), который сво

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

>>> file = open('data.txt') # методы чтения: пустая строка в конце файла >>> file.readline()

‘Hello file world!\n’

>>> file.readline()

‘Bye file world.\n’

>>> file.readline()

>>> file = open('data.txt') # итераторы: исключение в конце файла

>>> file. next () # не нужно предварительно вызывать iter(file),

‘Hello file world!\n’ # потому что файлы имеют собственные итераторы

>>> file.__next__()

‘Bye file world.\n’

>>> file.__next__()

Traceback (most recent call last):

File "", line 1, in

StopIteration

Интересно отметить, что итераторы автоматически используются во всех итерационных контекстах, включая конструктор списка, генераторы списков, функцию map и оператор in проверки на вхождение:

>>> open('data.txt').readlines() # всегда читает строки

[‘Hello file world!\n’, ‘Bye file world.\n’]

>>> list(open('data.txt')) # выполняет обход строк

[‘Hello file world!\n’, ‘Bye file world.\n’]

>>> lines = [line.rstrip() for line in open('data.txt')] # генераторы >>> lines

[‘Hello file world!’, ‘Bye file world.’]

>>> lines = [line.upper() for line in open('data.txt')] # произв. действия >>> lines

[‘HELLO FILE WORLD!\n’, ‘BYE FILE WORLD.\n’]

>>> list(map(str.split, open('data.txt'))) # применение функции

[[‘Hello’, ‘file’, ‘world!’], [‘Bye’, ‘file’, ‘world.’]]

>>> line = 'Hello file world!\n'

>>> line in open('data.txt') # проверка на вхождение

True

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

Другие режимы открытия файлов

Помимо режимов открытия файлов "w" и "r" (по умолчанию) большинством платформ поддерживается строка режима открытия "а", означающая «append» (дополнение). В этом режиме вывода методы записи добавляют данные в конец файла, и вызов функции open не уничтожает текущее содержимое файла:

>>> file = open('data.txt', 'a') # для дополнения: содержимое не стирается

>>> file.write('The Life of Brian’) # добавит в конец существующих данных >>> file.close()

>>>

>>> open('data.txt').read() # открыть и прочитать весь файл

‘Hello file world!\nBye file world.\nThe Life of Brian’

Хотя в большинстве случаев для открытия файлов применяются уже рассмотренные нами формы вызовов, но кроме того, функция open может принимать дополнительные аргументы, позволяющие более точно определить потребности обработки файла. Чаще всего в практике используются первые три аргумента - имя файла, режим открытия и размер буфера. Все они, кроме первого, являются необязательными: если они опущены, принимается режим открытия по умолчанию "r" (ввод) и разрешается полная буферизация. Ниже приводятся некоторые сведения об этих трех аргументах функции open, которые вам следует знать:

Имя файла

Как уже говорилось, имена файлов могут включать путь к каталогу, что дает возможность ссылаться на файлы, находящиеся на компьютере в произвольном месте; если полный путь в имени файла отсутствует, считается, что путь к файлам указывается относительно текущего рабочего каталога (который описывался в предыдущей главе). В целом, любой формат имени файла, который можно ввести в системной оболочке, можно использовать и в вызове функции open. Например, аргумент имени файла r\.\temp\spam.txt’ в Windows соответствует файлу spam.txt в подкаталоге temp, находящемся в родительском каталоге текущего рабочего каталога, - на один шаг вверх и затем вниз в каталог temp.

Режим открытия

Функция open может принимать и другие режимы, часть из которых мы увидим далее в этой главе, "r+", "w+" и "a+", которые используются, чтобы открыть файл для чтения и записи, и "b" - для обозначения двоичного режима. В частности, режим "r+" означает, что файл доступен как для чтения, так и для записи, при этом содержимое существующих файлов сохраняется; "w+", позволяет выполнять операции чтения и записи, но создает файл заново, уничтожая прежнее его содержимое; режимы "rb" и "wb" разрешают читать и записывать данные в двоичном режиме без выполнения автоматических преобразований; наконец, режимы "wb+" и "r+b" объединяют возможность чтения и записи с двоичным режимом. Проще говоря, по умолчанию используется режим для чтения "r", но вы можете использовать режим "w" для записи и "a" для дополнения, можете добавлять символ +, чтобы обеспечить возможность изменения содержимого файла, а также указывать b и t, чтобы задать двоичный или текстовый режим. Порядок следования спецификаторов в строке режима не имеет значения.

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

Размер буфера

Функция open также принимает необязательный третий аргумент с размером буфера, позволяющий управлять буферизацией файла -способом размещения данных в очереди, позволяющим повысить производительность. Значение 0 в этом аргументе означает отсутствие буферизации (данные передаются немедленно, но это значение допустимо только для двоичных режимов), значение 1 означает построчную буферизацию, а любое другое положительное число означает использование режима полной буферизации (который используется по умолчанию, если третий аргумент отсутствует в вызове функции).

Как обычно, полные сведения о дополнительных аргументах функции

open, помимо этих трех, вы найдете в руководстве по библиотеке языка

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


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

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

>>> file = open('data.txt', 'wb') # откроет двоичный файл для записи >>> file = open('data.txt', 'rb') # откроет двоичный файл для чтения

После открытия двоичных файлов таким способом можно читать и записывать их содержимое с помощью представленных выше методов: read, write и так далее. Методы readline и readlines, как и построчные итераторы файлов, по-прежнему будут работать с текстовыми файлами, открытыми в двоичном режиме, но нет никакого смысла применять их к действительно двоичным данным, которые не имеют построчной организации (байты, обозначающие конец строки в текстовых данных, не имеют такого смысла в двоичных данных, да и вообще их может не быть в файле).

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

>>> open('data.txt').read() # текстовый режим: тип str

‘Hello file world!\nBye file world.\nThe Life of Brian’

>>> open('data.txt', 'rb').read() # двоичный режим: тип bytes b’Hello file world!\r\nBye file world.\r\nThe Life of Brian’

>>> file = open('data.txt', 'rb’)

>>> for line in file: print(line)

b’Hello file world!\r\n’ b’Bye file world.\r\n’ b’The Life of Brian’

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

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

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

>>> open('data.bin', 'wb').write(b'Spam\n')

5

>>> open('data.bin', 'rb').read()

b’Spam\n’

>>> open('data.bin', 'wb').write('spam\n')

TypeError: must be bytes or buffer, not str

(TypeError: аргумент должен иметь тип bytes или buffer, но не str)

Но обратите внимание, что в данном примере строки завершаются символом \n вместо последовательности \r\n, принятой в Windows, которая присутствовала в предыдущем примере работы с текстовым файлом в двоичном режиме. Строго говоря, двоичный режим запрещает не только кодирование символов Юникода, но и автоматическое преобразование символов конца строки, которые по умолчанию выполняются для файлов, открытых в текстовом режиме. Но прежде чем нам удастся полностью с этим разобраться, необходимо познакомиться с двумя основными отличиями между текстовыми и двоичными файлами.

Кодирование символов Юникода в текстовых файлах

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

>>> data = 'sp\xe4m'

>>> data

‘spam’

>>> 0xe4, bin(0xe4), chr(0xe4)

(228, ‘0b11100100’, ‘a’)

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

>>> data.encode('latin1') # 8-битовые символы: ascii + дополнительные b’sp\xe4m’

>>> data.encode('utf8') # 2 байта отводится только

b’sp\xc3\xa4m’ # для специальных символов

>>> data.encode('ascii') # кодирование в ascii невозможно

UnicodeEncodeError: ‘ascii’ codec can’t encode character ‘\xe4’ in position 2: ordinal not in range(128)

(UnicodeEncodeError: кодек ‘ascii’ не может преобразовать символ ‘\xe4’ в позиции 2: число выходит за пределы range(128) )

Интерпретатор Python отображает печатаемые символы в таких строках как обычно, а непечатаемые - в виде шестнадцатеричных экранированных значений \xNN, количество которых увеличивается при использовании некоторых более сложных схем кодирования (cp500 в следующем примере - это кодировка EBCDIC):

>>> data.encode('utf16') # по 2 байта на символ плюс преамбула

b’\xff\xfes\x00p\x00\xe4\x00m\x00’

>>> data.encode('cp500') # кодировка ebcdic: двоичное представление

b’\xa2\x97C\x94’ # строки существенно отличается

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

>>> open('data.txt', 'w', encoding='latin1').write(data)

4

>>> open('data.txt', 'r', encoding='latin1').read()

‘spam’

>>> open('data.txt', 'rb').read()

b’sp\xe4m’

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

>>> open('data.txt', 'w', encoding='utf8').write(data) # кодировка utf8 4

>>> open('data.txt', 'r', encoding='utf8').read() # декодирование: отменяет

‘spam’ # кодирование

>>> open('data.txt', 'rb').read() # преобразование

b’sp\xc3\xa4m’ # не производится

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

>>> open('data.txt', 'w’, encoding='ascii').write(data)

UnicodeEncodeError: ‘ascii’ codec can’t encode character ‘\xe4’ in position 2: ordinal not in range(128)

(UnicodeEncodeError: кодек ‘ascii’ не может преобразовать символ '\xe4' в позиции 2: число выходит за пределы range(128) )

>>> open(r'C:\Python31\python.exe', 'r').read()

UnicodeDecodeError: ‘charmap’ codec can’t decode byte 0x90 in position 2: character maps to

(UnicodeDecodeError: кодек ‘charmap’ не может преобразовать байт 0x90 в позиции 2: символ отображается в символ )

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

>>> open('data.txt', 'w’, encoding='cp500').writelines(['spam\n', 'ham\n'])

>>> open('data.txt', 'r’, encoding='cp500').readlines() [‘spam\n’, ‘ham\n’]

>>> open('data.txt', 'r').readlines()

UnicodeDecodeError: ‘charmap’ codec can’t decode byte 0x81 in position 2: character maps to

(UnicodeDecodeError: кодек ‘charmap’ не может преобразовать байт 0x81 в позиции 2: символ отображается в символ )

>>> open('data.txt', 'rb').readlines()

[b’\xa2\x97\x81\x94\r%\x88\x81\x94\r%’]

>>> open('data.txt', 'rb').read()

b’\xa2\x97\x81\x94\r%\x88\x81\x94\r%’

Если вы имеете дело только с текстом ASCII, вы можете пропустить все, что связано с кодировками, - данные в файлах будут один-в-один отображаться в символы в строках, потому что ASCII является подмножеством большинства кодировок, используемых по умолчанию. Если вам приходится обрабатывать файлы, созданные с применением других кодировок, и, возможно, на других платформах (например, файлы, полученные из Интернета), вам может потребоваться использовать двоичный режим, если кодировка заранее не известна. Однако имейте в виду, что текст в кодированном двоичном представлении не может обрабатываться так, как вам хотелось бы: текст, закодированный с применением определенной кодировки, не может сравниваться или объединяться с текстом, закодированным с применением других кодировок.

И снова за дополнительной информацией о Юникоде обращайтесь к другим ресурсам. Мы еще не раз будем возвращаться к теме Юникода в этой книге: в главе 9 будет показано, какое влияние оказывает Юникод на виджет Text из библиотеки tkinter, а в части IV, охватывающей вопросы программирования для Интернета, мы узнаем, как это отражается на данных, доставляемых по сети с использованием протоколов FTP, электронной почты и в Интернете в целом. Текстовые файлы обладают еще одной особенностью, отсутствующей у двоичных файлов: преобразование символов конца строки, что является темой следующего раздела.

Преобразование символов конца строки в Windows

По историческим причинам конец строки текста в файле представляется на разных платформах различными символами. В Unix и Linux -это одиночный символ \n, а в Windows - это последовательность из двух символов \r\n. В результате файлы, перемещаемые между Linux и Windows, могут после передачи странно выглядеть в текстовом редакторе - они могут сохранить окончание строки, принятое на исходной платформе.

Например, большинство текстовых редакторов для Windows обрабатывает текст в формате Unix, но Блокнот (Notepad) составляет заметное исключение - текстовые файлы, скопированные из Unix или Linux, обычно выглядят в Блокноте, как одна большая строка со странными символами внутри (\n). Точно так же при копировании файлов из Windows в Unix в двоичном режиме в них сохраняется символ \r (который в текстовых редакторах часто отображается как ^M).

Сценариям на языке Python это обычно безразлично, потому что объекты файлов автоматически отображают последовательность DOS \r\n в одиночный символ \n. При выполнении сценариев в Windows это действует так:

• Для файлов, открытых в текстовом режиме, при чтении \r\n преобразуется в \n.

• Для файлов, открытых в текстовом режиме, при записи \n преобразуется в \r\n.

• Для файлов, открытых в двоичном режиме, никакие преобразования не производятся.

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

Второе следствие из этого преобразования более тонкое: при обработке двоичных файлов использование двоичного режима (например, rb, wb) отключает механизм преобразования символов конца строки. Если выбрать неправильный режим, указанные преобразования вполне могут повредить данные, как при чтении, так и при записи, - случайно оказавшиеся среди двоичных данных байты \r могут быть ошибочно отброшены при чтении или ошибочно добавлены к байтам \n при записи. В итоге двоичные данные окажутся искаженными, что, вероятно, совсем не то, что вам хотелось бы получить при работе с изображениями или аудиоклипами!

В Python 3.X эта проблема ушла на задний план, потому что мы в принципе не можем использовать двоичные данные с файлами, открытыми в текстовом режиме, из-за того, что текстовый режим предполагает автоматическое применение кодировок Юникода к содержимому файлов. Операции чтения и записи просто будут терпеть неудачу, если данные не смогут быть декодированы при чтении или закодированы при записи. Использование двоичного режима позволяет избежать ошибок, связанных с преобразованием Юникода, и автоматически запрещает преобразование символов конца строки как таковое (ошибки, связанные с преобразованием Юникода, можно было бы перехватывать в инструкции try). Итак, стоит запомнить как отдельный факт, что двоичный ре-

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

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

>>> open('temp.txt', 'w').write('shrubbery\n') # запись в текстовом режиме:

10 # \n -> \r\n

>>> open('temp.txt', 'rb').read() # чтение двоичных данных:

b’shrubbery\r\n’ # фактические байты из файла

>>> open('temp.txt', 'r').read() # проверка чтением: \r\n -> \n

‘shrubbery\n’

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

>>> data = b'a\0b\rc\r\nd' # 4 байта, 4 обычных символа

>>> len(data)

8

>>> open('temp.bin', 'wb').write(data) # запись двоичных данных как есть 8

>>> open('temp.bin', 'rb').read() # чтение двоичных данных: b’a\x00b\rc\r\nd’ # без преобразования

Но при чтении двоичных данных в текстовом режиме, неважно, случайно или нет, механизм преобразования символов конца строки может повредить данные (предполагая, что вообще при декодировании не возникло ошибок, как с нашими ASCII-данными на платформе Windows):

>>> open('temp.bin', 'r').read() # чтение в текстовом режиме: искажены \r! ‘a\x00b\nc\nd’

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

>>> open('temp.bin', 'w').write(data) # в текстовом режиме должна TypeError: must be str, not bytes # передаваться строка типа str

# используйте bytes.decode()

# для преобразования типа

>>> data.decode()

‘a\x00b\rc\r\nd’

>>> open('temp.bin', 'w').write(data.decode())

8

>>> open('temp.bin', 'rb').read() # запись в текстовом режиме: добавит \r b’a\x00b\rc\r\r\nd’

>>> open('temp.bin', 'r').read() # опять искажение, изменит \r

‘a\x00b\nc\n\nd’

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

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

Загрузка...