Файлы и Дескрипторы

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

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

  1. Текстовые, содержат текст, разбитый на строки.
  2. Двоичные, могут содержать любые данные без ограничений, в двоичных файлах хранятся рисунки, звуки, видео и т. д.
Работа с файлом в программе на языке Python включает три основных этапа.

  1. Открытие файла.
  2. Работа с файлом: чтение/запись.
  3. Закрытие файла.
file = open('book.txt')
text = file.read() # читаем содержимое файла в переменную
file.close()
Пример записи в текстовый файл:
text = """
Я узнал что меня есть огромная семья
И тропинка и лесок, в поле каждый колосок,
Речка, небо голубое - это все мое родное!
Всех люблю на свете я, это Родина моя!
"""
file = open('book.txt', 'w')
file.write(text)
file.close()
Функция open
Открывает файл с указанными правами и возвращает соответствующий объект файла. Если файл не может быть открыт, возникает ошибка OSError.

file = open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

Обязательные аргументы:

file - это одно из двух значений:

  • path - строка, абсолютный или относительный путь до файла с именем и расширением файла
  • дескриптор файла, целое число (рассмотрим далее)
Пример:
# linux OS
file_1 = open('lib/my_book.txt')             # относительный путь
file_2 = open('/home/user/lib/my_book.txt')  # абсолютный путь
# Windows OS
file_3 = open('lib\\my_book.txt')     # относительный путь
file_4 = open('D:\\lib\\my_book.txt')  # абсолютный путь
# ... закрытие файлов
Необязательные аргументы:
mode - определяет режим открытия файла, по умолчанию используется режим открытия rt (текстовый+чтение):

Группа по правам:
r - открыть на чтение (default) существующий файл
w - открыть на запись, создаёт новый файл, если он не существует, или стирает содержимое существующего.
x - создать и открыть на запись, если файл существует поднимает исключение FileExistsError
a - открыть на запись в конец файла (создаст файл, если не существует)

Группа по типу содержимого файла:
b - бинарный режим
t - текстовый режим (default)

Расширенные права:
+ - открыть для чтения и записи

Режимы открытия можно объединять по одному значению из группы, например:
file_1 = open('book.txt', mode='r')    # по умолчанию
file_2 = open('book.txt', mode='rb')   # чтение бинарного файла
file_3 = open('book.txt', mode='rb+')  # чтение и запись бинарного файла

file_4 = open('book.txt', mode='w')    # пезапись текстового файла
file_5 = open('book.txt', mode='w+')   # пезапись и чтение текстового файла
file_6 = open('book.txt', mode='wb+')  # пезапись и чтение бинарного файла
file_7 = open('book.txt', mode='a+')   # чтение и дозапись в конец текстового файла
# ... закрытие файлов
Файлы, открытые в двоичном режиме (включая «b» в аргументе mode), возвращают содержимое в виде строки bytes без декодирования.
В текстовом режиме (по умолчанию, или когда в аргумент режима включен 't') содержимое файла возвращается как str, причем байты будут сначала декодированы с использованием указанной кодировки, если она задана (или кодировки системы).

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

  • -1 - использование системного значения по умолчанию. Размер буфера зависит от системы и обычно это 4096 или 8192 байт.
  • 0 - отключает буферизацию для бинарного режима.
  • 1 - включает буферизацию строки для текстового режима.
  • Положительное число - устанавливает размер буфера в байтах, python выберет подходящее близкое к указанному число значение.
import sys

file = open('book.txt')
print('Размер буфера для чтения по умолчанию:', sys.getsizeof(file.buffer), 'байт') 
file.close()

file = open('book.txt', buffering=8192)
print('Размер буфера для чтения:', sys.getsizeof(file.buffer), 'байт')  # ~ 8192 байт
file.close()
На самом деле объект файла это синтаксический сахар, в зависимости от режима открытия объект файла будет иметь разный тип, каждый из которых реализует по своему методы read, write и остальные. Подробнее в документации

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

Принудительная запись данных из буфера происходит в трех случаях:

  1. В буфере слишком мало места для ожидающих записи данных и его нужно освободить.
  2. Вызван метод file.flush() принудительно записывает данные буфера в файл.
  3. Вызван метод file.close() записывает оставшиеся в буфере данные перед закрытием файла.
Если верить документации, то дефолтный размер буферов можно узнать вызвав io.DEFAULT_BUFFER_SIZE, но все немного сложнее и это не всегда так.


encoding - это имя кодировки, используемой для кодирования / декодирования файла (только для текстового режима). Кодировка по умолчанию зависит от платформы ( Windows – это 'cp1252', а в Linux 'utf-8'), ее можно узнать вызвав locale.getpreferredencoding(). Таким образом файл созданный в Windows откроется не корректно на Linux, если не указывать конкретную кодировку ('cp1252' или 'utf-8' например). Можно использовать любую из поддерживаемых кодировок текста.

errors - указывает, как должны обрабатываться ошибки кодирования и декодирования (только для текстового режима). Режимы обработки смотри в документции.
import locale

print('Дефолтная кодировка ОС:', locale.getpreferredencoding())

# Записали текстовый файл в linux  под дефолтной кодировкой UTF-8
file = open('book.txt', mode='w', encoding='UTF-8')
file.write('Вышел ежик из тумана')
file.close()

# Открыли текстовый файл в windows под дефолтной кодировкой cp1252
file = open('book.txt', encoding='cp1252', errors='ignore')
print(file.read())  # получили "Кракозябры"
file.close()

# Открыли текстовый файл в windows под кодировкой UTF-8
file = open('book.txt', encoding='UTF-8')
print(file.read())  # нормальный текст
file.close()
newline - политика обработки универсальных переносов строк, только для текстового режима. В разных ОС используются разные символы переноса строки, например в Unix это симовол '\n', в Windows это пара '\r\n', а в старых Macintosh символ '\r'. Все эт создает большую путаницу, для правильного распознавания существует "режим универсальных переносов". Аргумент принимает следующие значения:

  • None - (по умолчанию) режим универсальных переносов строк активирован. При чтении файла символы '\n', '\r', или '\r\n' транслируются в '\n', а при записи в файл каждый символ '\n' преобразуется в системный знак переноса строки (его можно узнать командой os.linesep) - в таком режиме осуществляется полная совместимость для всех ОС.

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

  • Один из символов '\n', '\r', или '\r\n' - символ в который будут преобразованы знаки переноса перед записью или чтением файла.
import os

print('Системный разделитель строк:', repr(os.linesep))

# Пример №1 
file = open('book.txt', mode='w+', encoding='UTF-8', newline=None)
file.write('Я узнал что у меня\r\nесть огромная семья\r\n...')  # Преобразование \r\n в \n
file.seek(0)
print(repr(file.read()))  # Преобразование \n в \n
file.close()

# Пример №2 
file = open('book.txt', mode='w', encoding='UTF-8', newline='\r\n')
file.write('Я узнал что у меня\nесть огромная семья\n...')  # Преобразование \n в \r\n
file.close()

file = open('book.txt', encoding='UTF-8', newline='')
print(repr(file.read()))  # Преобразования не произошло и будет выведено \r\n
file.close()
closefd - boolean флаг необходимости закрытия файлового дескриптора. Если в аргумент file передать дескриптор вместо имени файла то при closefd=False закрытие файла не закроет переданный дескриптор. Если file это имя файла то closefd должен быть True.
# Открываем файл в первый раз
sys_logfile = open('sys.log', mode='a')
sys_logfile.write('Старт лога..\n')

# Получаем дескриптор файла
descriptor = sys_logfile.fileno()

# Открываем файл в второй раз
process_log = open(descriptor, mode='a', closefd=False)
process_log.write('Некий процесс запущен...\n')
process_log.close() 
# если не указать closefd=False то sys_logfile.close() вызовет ошибку, т.к. общий дескриптор уже закрыт

sys_logfile.close()
opener - это объект, реализующий функционал открытия файла. Аргумент принимает значение None (тогда используется системный "открыватель" os.open) или пользовательский объект, который реализует интерфейс "открывателя", возвращающий файловый объект.
Дескрипторы
Программа может взаимодействовать с файлом только через открытый дескриптор файла.

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

Давайте подробнее разберемся что такое дескриптор и что он дает. Когда вы запускаете Python программу, ОС создает новый процесс и сразу создает для него 3 дескриптора: 0 (stdin), 1 (stdout), 2 (stderr) - потоки стандартного ввода, вывода и вывода отладочных сообщений, например Python использует их для команд input() и print().

Дескрипторы процесса (т.е вашей программы) хранятся в дескрипторной таблице LDT (Local Descriptor Table) которая связывает адрес в памяти и целое число, идентификатор - дескриптора. Например при открытии файла из программы берется следующее свободное число в дескрипторной таблице (например 3) и ему сопоставляется адрес файла в памяти. Таким образом в программе через значение дескриптора "3" можно работать с файлом в памяти.

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

Давайте по пробуем по работать со стандартными дескрипторами stdout и stderr
stdout = 1
stderr = 2

out_stream = open(stdout, mode='w', encoding='UTF-8')
out_stream.write('Выводим текст в консоль..\n')
out_stream.close()

err_stream = open(stderr, mode='w', encoding='UTF-8')
err_stream.write('Отладочня информация...\n')
err_stream.close()
В примере выше out_stream открывает файловый объект на дескриптор стандартного вывода, и потому вы видите в консоли все что мы записываем в этот файл, аналогично с err_stream.

На этом примере очень удобно продемонстрировать как работает буферизация при записи в файл. Рассмотрим пример, когда мы записываем в файл 3 короткие строки: после выполнения каждой инструкции write данные будут записаны в буфер в оперативной памяти и только после закрытия будут записаны в файл одной операцией.
import time

stderr = 2

file = open(stderr, mode='w', encoding='UTF-8')
file.write('Первая строка\n') # -> запись в буфер
file.write('Вторая строка\n') # -> запись в буфер
time.sleep(2)                 # (^-^) отдохнем 2 сек
file.write('Третья строка\n') # -> запись в буфер
file.close()                  # <- запись буфера в файл и мы видим результат в консоли
А теперь давайте заставим python выгрузить буфер в файл два раза, это можно сделать заполнив буфер или вызвав метод flush
import time

stdout = 1

file = open(stdout, mode='w', encoding='UTF-8')
file.write('Первая строка\n') # -> запись в буфер
file.write('Вторая строка\n') # -> запись в буфер
file.flush()                  # <- запись буфера в файл и мы видим результат в консоли
time.sleep(2)                 # (^-^) отдохнем 2 сек
file.write('Третья строка\n') # -> запись в буфер
file.close()                  # <- запись буфера в файл и мы видим результат в консоли
Обработка исключений и закрытие файла
Когда файл открыт и мы работаем с его содержимым в коде может возникнуть исключение и выполнение программы прервется без закрытия файла, теоретически такой открытый файл должен быть закрыт сборщиком мусора, т.к. связанная с ним переменная будет удалена, но так не всегда происходит, и не нужно надеяться на сборщик мусора в этой ситуации. Наличие открытых файлов (декскрипторов) чревато двумя проблемами - ресурсы не осовобождаются + накапливая много открытых файлов вы можете поймать ошибку ОС о превышении лимита открытых файлов.
Для гарантированного закрытия файлов можно использовать инструкцию обработки исключений, например так:
file = None
try:
   file = open('sys.log') # Внезапно получаем FileNotFoundError - файл не найден
finally:
   if file is not None:
       file.close() # Отрабатываем закрытие файла
Но эта конструкция реализет инструкцию контекстного менеджера, которую и следует применять вместо обработки исключений try / finally. Контекстные менеджеры уже были подробно рассмотрены, если вы пропустили этот материал - ознакомьтесь самостоятельно.

Как только интерпреттор покинет блок кода with, независимо было исключение или нет - файл гарантированно будет закрыт.
with open('sys.log') as file:
   text = file.read()
Итерирование по файлу
Файловый объект поддерживает протокол итерирования, это значит его можно обходить построчно в цикле, пример:
with open('book.txt') as file:
    for line in file:
        print(line, end='')
Методы работы с файлами

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

Задача: Мы веб-программист, обслуживаем простой интернет магазин. Клиент периодически дает нам информацию о наличии товаров на складе. Нам необходимо загрузить ее на наш сайт.
Входные данные: файл в формате csv
Требования к результату: файл в формате json, который содержит только те товары у которых есть цена и она целое число. Все товары не удовлетворяющие требованию надо записать в лог файл, чтобы передать эту информацию заказчику.
Используйте докуметацию для работы с модулями csv и json
# Содержимое файла products.csv
id|name|price
1|стол|5000
7|Неопознанный товар|сто рублей
2|стул|2500
5|скатерть|1000
6|Неопознанный товар|
import csv
import json


with open('products.csv') as csv_file, \
     open('err.log', 'w', encoding='UTF-8') as log_file, \
     open('result.json', 'w') as json_file:
    reader = csv.reader(csv_file, delimiter='|')
    headers = next(reader)  # Пропускаем первую строку с названием колонок
    json_data = []
    for row in reader:
        i, name, price = row  # распаковываем список в переменные 
        if price.isdigit():  # при чтении из файла всегда всегда получаем значение типа str  
            json_data.append({
                "id": i,
                "name": name,
                "price": price
            })
        else:
            log_file.write(f'{i}\n') # если значение price не число то пишем в лог
    if json_data:
        json_str = json.dumps(json_data, ensure_ascii=False) # Создаем JSON-строку и записываем ее в файл
        json_file.write(json_str)
Обратите внимание что это демонстрационный пример. По условию задачи мы считаем что формат csv-файла всегда корректный. Для больших csv-файлов при чтении нужно ограничивать кол-во данных которые мы можем держать в памяти: при чтении достаточно большого файла выгрузки есть риск заполнить память машины.