Работа с файлами

Некоторые свойства файлов

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

  • у каждого файла есть хотя бы одно имя
  • файл может позволять записывать в него данные
  • файл может позволять считывать из него данные

При этом, например, не гарантируется, что:

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

Связано это с тем, что операционные системы обычно позволяют произвольной программе изображать из себя файл.

Общие принципы работы с файлами

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

Первая — направление:

  • чтение
  • запись
  • дозапись

Вторая — тип файла:

  • текстовый
  • бинарный

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

В Python есть второе (менее существенное) различие: бинарный режим выдаёт данные как элемент типа bytes (иммутабельный массив байт), а текстовый режим выдаёт данные как элемент типа str.

Работа с файлами в Python

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

  • 'r' или 'rb' — чтение в текстовом/бинарном режиме
  • 'w' или 'wb' — запись в текстовом/бинарном режиме
  • 'a' или 'ab' — дозапись в текстовом/бинарном режиме
  • 'r+', 'r+b', 'w+', 'w+b', 'a+', 'a+b' — чтение И запись

Чтение выдаёт ошибку, если файл с таким названием не существует. Режимы записи/дозаписи пытается файл создать. Также режим записи стирает старое содержимое файла (если для этого файла вообще определено понятие «содержимое»).

Режимы «с плюсом» отличаются от таковых «без плюса» только дополнительной возможностью записи/чтения. В остальном они ведут себя так же, как и соответствующие режимы «без плюса».

Функция open возвращает объект, посредством которого можно производить действия с файлом. Самые полезные:

  • метод read — считывает данные из файла
  • метод write — записывает данные в файл
  • метод close — завершает работу с файлом («закрывает» его)

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

with open(...) as f:
  какой-то код

Это следует писать вместо:

f = open(...)
какой-то код
f.close()

Первая конструкция работает даже если на участке (какой-то код) присутствует return или возникают исключения.

Сравните две эквивалентных функции (складывающие все числа из указанного файла):

def short_sum(filename):
  with open(filename, 'r') as f:
    return sum([int(x) for x in f.read().split()])

def long_sum(filename):
  f = open(filename, 'r')
  try:
    result = sum([int(x) for x in f.read().split()])
  except:
    f.close()
    raise

  f.close()
  return result

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

with open('files_list', 'r') as f:
  filenames = [name for line in f 
                    for name in [line.strip()]
                    if  name]

for filename in filenames:
  with open(filename, 'w') as f:
    f.write(str(len(filenames)))

Стандартные файлы

В подавляющем большинстве ОС каждой запущенной программе по умолчанию предоставляются три (уже открытых) файла — т.н. стандартный вход, стандартный выход, стандартный поток ошибок. В Python они доступны посредством переменных sys.stdin, sys.stdout, sys.stderr модуля sys.

Такие функции, как input и print работают как раз с этими файлами (впрочем у print можно и поменять используемый файл на произвольный).

Например, input (почти) эквивалентен следующему определению:

import sys

def input(prompt):
    sys.stdout.write(prompt)
    sys.stdout.flush()
    line = sys.stdin.readline()
    if len(line) > 0 and line[-1] == '\n': return line[:-1]
    return line

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

Пример бинарного формата

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

В качестве примера бинарного формата рассмотрим растровый формат TGA (если быть более точным, TGA второго типа).

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

Заголовок устроен так (побайтово):

  1. Длина (в байтах) идентификатора.
  2. Ноль.
  3. Два.
  4. Ноль.
  5. Ноль.
  6. Ноль.
  7. Ноль.
  8. Ноль.
  9. Младший байт горизонтальной координаты начала изображения.
  10. Старший байт горизонтальной координаты начала изображения.
  11. Младший байт вертикальной координаты начала изображения.
  12. Старший байт вертикальной координаты начала изображения.
  13. Младший байт ширины изображения.
  14. Старший байт ширины изображения.
  15. Младший байт высоты изображения.
  16. Старший байт высоты изображения.
  17. Количество бит в пикселе (16, 24 или 32).
  18. Битовое поле.

Младшие 4 бита 18-го байта — ширина альфа канала (для 24-битных изображений это 0, для 32-битных это 8). Пятый бит — ноль. Шестой бит отвечает за ориентацию изображения. Ноль в шестом бите означает, что строчки изображения начинаются с самой нижней. Единица — с самой верхней. Старшие два бита — нули.

Пиксели хранятся построчно, слева направо. В 24-битном формате каждый пиксель задаётся тремя байтами: интенсивностями синей, зелёной и красной компонент соответственно. В 32-битном формате к ним добавляется четвёртый байт — значение альфа-канала.

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

def create_image(width, height):
    return (width, [(0,0,0)]*(width*height))

def image_width(image):
    width, pixels = image
    return width

def image_height(image):
    width, pixels = image
    return len(pixels) // width

def put_pixel(image, x, y, r, g, b):
    width, pixels = image
    pixels[width*y + x] = (r,g,b)

def write_image_tga(image, filename):
    width, pixels = image
    height = image_height(image)

    header = bytes([
        0, 0, 2,
        0, 0, 0, 0, 0,
        0, 0, 0, 0,
        width  % 256, width  // 256,
        height % 256, height // 256,
        24, 0
    ])

    with open(filename, 'wb') as f:
        f.write(header)
        for r,g,b in pixels:
            f.write( bytes([b,g,r]) )

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