Дев’ятимильна прогулянка - це не жарт, особливо під час дощу.
Гаррі Кемелман[1]

Мій ноутбук з Windows містив 38493 файли ще до того як я поставив хоч одну програму. Встановлення Python додало майже 3000 файлів до загальної суми. Файли є основною парадигмою зберігання даних в будь-якій поширеній операційній системі, ця ідея настільки вкоренилась, що більшості людей проблематично уявити альтернативу. Ваш комп'ютер, метафорично кажучи, потопає в файлах.

Читання текстових файлів

ред.

Перед тим як зчитати щось з файла потрібно його відкрити. Відкриття файлів у Python не може бути простіше:

a_file = open('examples/chinese.txt', encoding='utf-8')

Python має вбудовану функцію open(), яка приймає як параметр ім'я файла. В цьому прикладі це 'examples/chinese.txt'. Назва файла має 5 цікавих особливостей:

  1. Це не просто ім'я файла — це комбінація шляху та назви файла. Гіпотетично функція могла б мати 2 параметри — шлях та назву файла, але open() приймає лише один параметр. За потреби можна включити до імені файла частковий або повний шлях.
  2. Шлях використувує прямий слеш, але я не сказав яку операційну систему використовуємо. Windows використовує зворотні слеші для позначення директорій, а Mac OS X та Linux використовують прямі слеші. Але в Python, прямі слеші завжди Просто Працюють, навіть у Windows.
  3. Шлях не починається зі слешу або літери диску, тобто це відносний шлях. Ви можливо запитаєте відносно чого? Терпіння.
  4. Це рядок. Всі сучасні операційні системи (навіть Windows!) використовують Unicode для зберігання імен файлів та директорій. Python 3 повністю підтримує не-ASCII імена.
  5. Не потрібно щоб файл був на вашому локальному диску. Це може бути змонтований мережевий диск. Файл може бути елементом повністю віртуальної файлової системи. Якщо ваш комп'ютер вважає його файлом і має доступ до нього у вигляді файла, Python може спокійно відкрити його.

Але виклик функції open() не зупиняється на імені файла. Там ще один аргумент — encoding (кодування). Ой мамо, це звучить жахливо знайомо.

Кодування символів показує свою потворну голову

ред.

Байти це байти, а символи це абстракція. Рядок — це послідовність символів Unicode. Але файл на диску не є послідовністю символів Unicode. Файл на диску — послідовність байтів. Отже, якщо ви читаєте текстовий файл з диску, як Python конвертує послідовність байтів у послідовність символів? Він декодує байти відповідно до певного алгоритму кодування символів і повертає послідовність символів Unicode (також відому як рядок).

# Цей приклад був створенний на Windows. Інші платформи
# можуть поводити себе інакше (описано нижче)
>>> file = open('examples/chinese.txt')
>>> a_string = file.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Python31\lib\encodings\cp1252.py", line 23, in decode
    return codecs.charmap_decode(input,self.errors,decoding_table)[0]
UnicodeDecodeError: 'charmap' codec can't decode byte 0x8f in position 28: character maps to <undefined>
>>>
Кодування за замовчуванням платформозалежне.

Що трапилось? Ви не вказали кодування символів, тому Python використав кодування за замовчування. Що це за кодування? Якщо придивитись до повідомлення про помилку, то можна побачити, що це cp1252, звідки випливає що Python використовує кодування CP-1252 за замовчуванням (CP-1252 — кодування, яке часто використовується на комп'ютерах з Microsoft Windows). Кодування CP-1252 не підтримує символи, які знаходяться в цьому файлі, тому читання завершується з потворним UnicodeDecodeError.

Але зачекайте, тут ще гірше! Кодування за замовчуванням залежить від платформи, так що цей код може працювати на вашому комп'ютері (якщо за замовчуванням кодування UTF-8), але він не буде виконаний, якщо ви передасте його комусь ще (кодування за замовчуванням може відрізнятись від CP-1252).

Якщо вам потрібно отримати кодування за замовчуванням, імпортуйте модуль locale та зробіть виклик locale.getpreferredencoding(). На моєму ноутбуці Windows, вона повертає 'cp1252', але на моєму комп'ютері з Linux, вона повертає 'utf8'. Я навіть не можу підтримувати консистентність в своєму власному домі! Ваші результати можуть відрізнятися (навіть на Windows) залежно від версії операційної системи, яку ви встановили і як ваш регіон або мови налаштовані. Ось чому так важливо вказувати кодування щоразу, коли ви відкриваєте файл.

Потоковий об'єкт

ред.

Всі ми знаємо, що Python має вбудовану функцію open(). Ця функція повертає потоковий об'єкт, який має атрибути та методи для отримання інформації про потік символів файла, а також для маніпуляції над ним.

>>> a_file = open('examples/chinese.txt', encoding='utf-8')
>>> a_file.name
'examples/chinese.txt'

Атрибут name містить ім'я файла, який ми передали як параметр у функцію open(). Ім'я файла не нормалізується до абсолютного шляху.

>>> a_file.encoding
'utf-8'

Крім того, атрибут encoding містить кодування, яке ви передали як параметр у фінкцію open(). Якщо при відкритті файла ви не вказали кодування (поганий розробник!), то атрибут буде містити значення locale.getpreferredencoding().

>>> a_file.mode
'r'

Атрибут mode містить інформацію про те, в якому режимі був відкритий файл. Ви можете передати необов'язковий параметр у функцію open(). Якщо ви не вказали режим, коли відкривали файл, то Python використає значення за замовчуванням 'r', яке означає "відкрити лише для читання, в текстовому режимі". Далі у цьому розділі, ви переконаєтесь, що режим файла служить для декількох цілей. Різні режими дозволяють вам перезаписувати файл, додавати дані до файла, чи відкрити файл у двійковому режимі (в цьому режимі ви маєте справу з байтами, а не рядками)

Документація функції open() містить список всіх можливих режимів роботи з файлами.

Читання даних з текстових файлів

ред.

Якщо ви відкрили файл для читання, то можливо в певний момент ви захочете з нього щось прочитати.

>>> a_file = open('examples/chinese.txt', encoding='utf-8')
>>> a_file.read()
'Dive Into Python 是为有经验的程序员编写的一本 Python 书。\n'

Коли ви відкрили файл (з правильним кодуванням), читання з нього здійснюється простим викликом методу read() потокового об'єкту. Результатом є текст.

>>> a_file.read()
''

Можливо дещо несподівано, але повторне прочитання файла не створює винятку. Python не розглядає читання даних після закінчення файла помилкою, він просто повертає порожній рядок.

При відкритті файла завжди передавайте параметр encoding

А що якщо ми хочемо перечитати файл?

>>> a_file.read()
''

Так як ми все ще знаходимось в кінці файла, наступні виклики методу потоку read() просто повернуть порожній рядок.

>>> a_file.seek(0)
0

Метод seek() пененосить нас до вказаної в байтах позиції у файлі.

>>> a_file.read(16)
'Dive Into Python'

Метод read() приймає необов’язковий параметр - кількість символів для читання.

>>> a_file.read(1)
' '

Якщо хочете, можна навіть читати по одному символу за раз.

>>> a_file.read(1)
'是' 
>>> a_file.tell()
20

16 + 1 + 1 = … 20?

Давайте спробуємо спочатку.

>>> a_file.seek(17)
17
>>> a_file.read(1)
'是'
>>> a_file.tell()
20

Переміщуємось на 17-тий байт. Читаємо один символ. Після цього ми опиняємось на 20-тому байті.

Ви бачите? Методи seek() та tell() рахують байти, але тому що ми відкрили файл як текст, метод read() рахує символи. Китайські ієрогліфи потребують кількох байт для того щоб бути закодованими в UTF-8. Кожен з латинських символів потребує лише одного байта в файлі, тому це може переконати вас що seek() та read() рахують одне й те ж, але це вірно лише для деяких символів.

Але зачекайте, буде ще гірше!

>>> a_file.seek(18)
18
>>> a_file.read(1)
Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    a_file.read(1)
  File "C:\Python31\lib\codecs.py", line 300, in decode
    (result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf8' codec can't decode byte 0x98 in position 0: unexpected code byte

Перемістіться на 18-тий байт та спробуйте прочитати один символ. Чому це не працює? Тому що на 18-тому байті немає символа. Найближчий символ починається на 17-тому байті та простягається на три байти. Спроба почати читати символ з його середини призводить до UnicodeDecodeError.

Закривання файлів

ред.

Відкриті файли споживають системні ресурси, та залежно від режиму, інші програми можуть не отримати до них доступу. Тому важливо закривати файли, як тільки ви закінчите роботу з ними.

>>> a_file.close()

Потоковий об’єкт a_file досі існує, виклик методу close() не знищує його. Але цей об’єкт не є надто корисним.

>>> a_file.read()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
ValueError: I/O operation on closed file.

Ви не можете читати закритий файл, це призводить до винятку.

>>> a_file.seek(0)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
ValueError: I/O operation on closed file.

Не можна переміщуватись по закритому файла.

>>> a_file.tell()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
ValueError: I/O operation on closed file.

В закритому файлі немає позиції, тому tell() теж не працює.

>>> a_file.close()

Хоча й дещо несподівано, але виклик методу close() над потоковим об’єктом чий файл вже був закритий не створює винятку. Просто нічого не відбувається.

>>> a_file.closed
True

Закритий потоковий об’єкт має один корисний атрибут: closed який підтверджує факт закривання файла.

Автоматичне закривання файлів

ред.
try..finally - це добре. А with - ще краще.

Потокові об’єкти мають явний метод close(), але що станеться якщо у вашому коді є помилка, і програма завалюється ще до того як ви викличите close()? Цей файл теоретично може залитатись відкритим набагато довше ніж необхідно. Поки ви тестуєте програму на своєму комп’ютері, це не така вже й проблема. Але на працюючому сервері - може бути проблемою.

Python 2 мав для розв’язок цієї проблеми: блок try..finally. Таке все ще працює в Python 3 і ви могли бачити це в коді інших людей, чи в старому коді який перенесли на Python 3. Але починаючи з Python 2.6 було введено більш акуратний спосіб, якому потрібно віддавати перевагу - конструкція with

with open('examples/chinese.txt', encoding='utf-8') as a_file:
    a_file.seek(17)
    a_character = a_file.read(1)
    print(a_character)

Цей код викликає open(), але ніде не видно a_file.close(). Слово with починає блок, аналогічно до подібного в конструкції if чи for. Всередині цього блоку коду ви можете використовувати змінну a_file в якості потокового об’єкта що повертається функцією open(). Звичайні методи потокового об’єкта все ще доступні — seek(), read(), будь-що що вам потрібно. Коли блок with закінучєеться, Python викликає a_file.close() автоматично.

Ось що цікаво: незважаючи на те як, чи коли ви вийдете з блоку with, Python закриє файл. Навіть якщо ви "вийдете" з блоку за допомогою необробленого винятку. Так, це правда, навіть якщо ваш код згенерує виняток, і вся програма завершить роботу, файл все одно буде закритим. Гарантовано.

В технічних термінах, конструкція with створює контекст виконання. В цих прикладах потоковий об’єкт працює як менеджер контексту. Python створює потоковий об’єкт a_file і каже що ми входимо в контекст виконання. Коли блок коду закінучується, Python каже потоковому об’єкта що ми виходимо з контексту виконання і потоковий об’єкт сам викликає свій метод close(). Для деталей прочитайте про Класи що можуть використовуватись в блоці with.

Немає нічого специфічного для файлів у конструкції with, це просто загальний спосіб створення контекстів виконання та повідомлення об’єктам що вони входять та виходять з цього контексту. Якщо наш об’єкт є потоковим об’єктом, тоді він виконує корисні для файла речі (такі як його автоматичне закривання). Але така поведінка описана в потоковому об’єкті, а не в самій конструкції with. Існує багато інших способів використовувати менеджери контексту які жодним чином не пов’язані з файлами. Ви навіть можете створити свій власний, як буде показано далі в цьому розділі.

Читання даних по рядку за раз

ред.

Рядок тексту - це те про що ви подумали - ви набираєте кілька слів, натискаєте ↵ Enter на опиняєтесь на новому рядку. Рядок тексту це послідовність символів, обмежена... чим саме? Ну, це складно сказати, тому що текстові файли можуть використовувати різні символи для позначення кінця рядка. Кожна операційна система має свої власні правила. Одні використовують символ повернення каретки, інші використовують символ нового рядка, а деякі використовують обидва символи для кінця кожного рядка.

Тепер зітхніть з полегшенням, тому що Python автоматично обробляє закінчення рядків. Якщо ви скажете "Я хочу читати цей текстовий файл порядково", Python з’ясує який вид закінчень рядків використовується, і все буде Просто Працювати.

Якщо вам потрібен точний контроль над тим що вважається кінцем рядка, можете передати в функцію open() необов’язковий параметр newline. Дивіться документацію функції open() аби дізнатись всі криваві подробиці.

Отож, як це зробити? Прочитати файл по рядку за раз, це дуже просто і дуже красиво.

line_number = 0
with open('examples/favorite-people.txt', encoding='utf-8') as a_file:  
    for a_line in a_file:                                               
        line_number += 1
        print('{:>4} {}'.format(line_number, a_line.rstrip()))          

① Використовуючи конструкцію with ми безпечно відкриваємо файл, і покладаємо обов’язок його закривання на Python.

② Щоб прочитати файл по рядку за раз, використовуйте цикл for. Це все. Потоковий об’єкт не тільки має явні методи на зразок read(), він також є ітератором що повертає наступний рядок щоразу коли ми про нього просимо.

③ Використовуючи метод format(), ви можете надрукувати номер рядка та сам рядок. Специфікатор формату {:>4} означає "надрукуй цей аргумент вирівняним по правому краю з шириною в чотири символи.” Змінна a_line містить ввесь рядок, символ повернення каретки, та інше. Метод rstrip() видаляє зайві невидимі символи, включаючи символи повернення каретки.


* * *


Запис в текстові файли

ред.
Просто відкрийте файл і почніть запис

Ви можете писати в файли майже аналогічно тому як ви з них читаєте. Спершу потрібно відкрити файл та отримати потоковий об’єкт, потім використати методи потокового об’єкта для запису даних, після чого закрити файл.

Для відкривання файла використовується функція open() якій передають режим запису файла. Є два режими відкривання файла на запис:

  • Режим "Write", який повністю перепише файл з початку. Передайте функції open() параметр mode='w'.
  • Режим "Append", який додаватиме дані в кінець файла. Передайте функції open() параметр mode='a'.

Кожен режим створюватиме файли автоматично, якщо вони не існують, тому ніколи не виникає потреба в таких незручностях як "якщо файл ще не існує, створити його, аби його можна було відкрити". Просто відкривайте файл і починайте запис.

Ви повинні завжди закривати файл, як тільки закінчите записувати дані, щоб звільнити файловий дескриптор і гарантувати що дані справді потраплять на диск. Як і при читанні, можна викликати метод потокового об’єкта close(), або використати конструкцію with дозволивши Пайтону зробити це за вас. Закладаюсь що ви знаєте який підхід я рекомендую.

>>> with open('test.log', mode='w', encoding='utf-8') as a_file:
...     a_file.write('test succeeded')                          
>>> with open('test.log', encoding='utf-8') as a_file:
...     print(a_file.read())                              
test succeeded

Ми рішуче починаємо створюючи новий файл test.log (чи стираючи існуючий), та відкриваємо його для запису, на що вказує праметр mode='w'. Так, це настільки небезпечно як звучить. Я сподіваюсь ви не сильно переживаєте за попередній вміст того файла (якщо такий був), тому що зараз дані пропали. Можна додати дані в щойновідкритий файл методом потокового об’єкта write(). Після закінчення блоку Python автоматично закриє файл.

>>> with open('test.log', mode='a', encoding='utf-8') as a_file:
...     a_file.write('and again')
>>> with open('test.log', encoding='utf-8') as a_file:
...     print(a_file.read())                              
test succeededand again

Це було так весело, що я захотів ще. Але цього разу з режимом mode='a' для того аби додавати дані до файла, замість того аби його переписувати. Додавання не повинно шкодити існуючим даним файла.

Як і рядок що ми записали трохи раніше, так і той рядок що ми записали щойно зараз знаходяться в файлі test.log. Також зауважте що не було додано ні символів повернення каретки, ні символів переходу на новий рядок. Так як ми явно не сказали записати їх у файл, файл їх не містить. Можна записувати повернення каретки з допомогою символа \r, а перехід на новий рядок з допомогою \n, але так як ми не вказали жодного з них, все що ми записували опинилось в одному рядку.

Знову кодування символів

ред.

Ви зауважили параметр encoding що передається в функцію open() при відкриванні файла на запис? Це важливо, ніколи його не забувайте! Як ви вже побачили на початку цього розділу, файли не містять рядків, вони містять байти. Читання "рядка" з текстового файла працює лише тому, що ви сказати Python яке кодування використовувати аби прочитати послідовність байтів і перетворити її в рядок. Запис тексту у файл являє собою цю ж проблему, тільки з іншого боку. Ви не можете записати символи в файл, символи це абстракція. Щоб записувати в файл, Python повинен знати як перетворити ваш рядок в послідовність байтів. Єдиний спосіб бути впевненим що перетворення відбувається правильно - явно передати параметр encoding при відкриванні файла на запис.


* * *


Двійкові файли

ред.
 
Собака

Не всі файли містять текст. Деякі з них містять зображення собаки.

>>> an_image = open('dog.jpg', mode='rb')

Відкривати двійковий файл так само просто як і текстовий, з однією тонкою відмінністю: параметр mode містить символ 'b'.

>>> an_image.mode
'rb'

Потоковий об’єкт який ми отримуємо при відкриванні файла в двійковому режимі має багато атрибутів що є й у текстовому режимі, включаючи mode, який відповідає параметру mode переданому в функцію open().

>>> an_image.name
'dog.jpg'

Як і в текстових потокових об’єктах, атрибут name містить назву відкритого файла.

>>> an_image.encoding
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: '_io.BufferedReader' object has no attribute 'encoding'

А ось одна відмінність: двійковий потоковий об’єкт не має атрибута encoding. Це й логічно, хіба ні? Ми читаємо (чи пишемо) байти, а не рядки, тому не потрібно ніяких перетворень. Те що ви отримуєте з двійкового файла - точно те саме що ви в нього записуєте, тому перетворення не є обов’язковим.


Я вже згадував що ви читаєте байти? О, так, ви читаєте байти.

>>> an_image.tell()
0
>>> data = an_image.read(3)
>>> data
b'\xff\xd8\xff'

Як і текстові, двійкові файли можна читати по шматочку за раз. Але є одна критична відмінність...

>>> type(data)
<class 'bytes'>

... ми читаємо байти, а не рядки. Так як файл відкрито в двійковому режимі, метод read() приймає кількість байт для прочитання, а не кількість символів.

>>> an_image.tell()
3
>>> an_image.seek(0)
0
>>> data = an_image.read()
>>> len(data)
3150

Це означає що ніколи не виникне неочікуваної невідповідності між числом яке ми передали в метод read() та індексом позиції яку нам після цього повертає метод tell(). Метод read() читає байти, а метод seek() та tell() рахують клількість прочитаних байтів. Для двійкових файлів вони завжди зходяться.


* * *


Потокові об'єкти з нефайлових джерел

ред.
Щоб читати з фальшивого файла, просто викличте функцію read().

Уявіть собі що ви пишете бібліотеку, і одна з функцій вашої бібліотеки збирається прочитати деякі дані з файла. Функція може просто отримати назву файла в рядку, відкрити файл для читання, прочитати його, та закрити перед завершенням. Але ви не повинні цього робити. Замість цього ваше API повинно приймати довільний потоковий об’єкт.

В найпростішому випадку, потоковий об’єкт це будь-що з методом read(), який приймає необов’язковий параметр size та повертає рядок. При виклику без параметра size, метод read() повинен прочитати все що ще залишилось на вході і повернути ці дані єдиним значенням. При виклику з параметром size він читає і повертає стільки даних скільки попросили. При повторному виклику він починає читати з того місця де зупинився, і повертає наступний шматок даних.

Звучить майже так само як потоковий об’єкт який ми отримаємо від відкривання справжнього файла. Тільки відмінність в тому, що ми не обмежуємо себе лише файлами. Джерелом даних для читання може бути будь-що: веб-сторінка, рядок в пам’яті, навіть вивід іншої програми. Якщо ваша функція приймає потоковий об’єкт і просто викликає метод read(), ви можете працювати з будь-яким видом даних що поводяться як файл, без написання спеціального коду для кожного виду.

>>> a_string = 'PapayaWhip is the new black.'
>>> import io

Модуль io описує клас StringIO який можна використати аби поводитись з рядком в пам’яті як з файлом.

>>> a_file = io.StringIO(a_string)

Щоб створити потоковий об’єкт з рядка, створіть екземпляр класу io.StringIO() і передайте його конструктору рядок який ви хочете використати як дані вашого "файла". Тепер ми маємо потоковий об’єкт і можемо робити з ним все що дозволяє потоковий об’єкт.

>>> a_file.read()
'PapayaWhip is the new black.'

Виклик методу read() "читає" увесь "файл", що в даному випадку з StringIO просто повертає початковий рядок.

>>> a_file.read()
''

Як і зі справжнім файлом, повторний виклик read() повертає порожній рядок.

>>> a_file.seek(0)
0

Ви можете переміститись на початок рядка, так само як ви робили б це з файлом, використовуючи метод seek() об’єкта класу StringIO.

>>> a_file.read(10)
'PapayaWhip'
>>> a_file.tell()
10
>>> a_file.seek(18)
18
>>> a_file.read()
'new black.'

Також можна читати рядок по шматочках, передавши параметр size методу read().

io.StringIO дозволяє вам поводитись з рядком як з текстовим файлом. Також існує клас io.BytesIO, який дозволяє вам поводитись з масивом байтів як з двійковим файлом.

Обробка стиснутих файлів

ред.

Стандартна бібліотека мови Python містить модулі що підтримують читання та запис стиснених файлів. Існує багато різних форматів стискання, два найбільш популярні на системах без Windows - це gzip та bzip2. (Ви могли також зустрічатись з архівами PKZIP та GNU Tar. Python має модулі і для них.)

Модуль gzip дозволяє вам створювати потоковий об’єкт для читання чи запису файла стисненого алгоритмом gzip. Потоковий об’єкт що ним надається підтримує метод read() (якщо ви відкрили його для читання) чи метод write() (якщо ви відкрили його для запису). Тобто ви можете використовувати методи для звичайних файлів, які ви вже вивчили, щоб прямо писати чи читати з файла gzip, без створення тимчасового файла для збереження розархівованих даних.

Як додатковий бонус, він також підтримує конструкцію with, тому ви можете дозволити Python-ну автоматично закривати ваш стиснений файл.

you@localhost:~$ python3

>>> import gzip
>>> with gzip.open('out.gz', mode='wb') as z_file:                                      
...   z_file.write('Дев’ятимильна прогулянка - це не жарт, особливо під час дощу.'.encode('utf-8'))
... 
>>> exit()

Ви повинні завжди відкривати стиснені gzip-файли в двійковому режимі. (Зверніть увагу на символ 'b' в аргументі mode)

you@localhost:~$ ls -l out.gz 
-rw-rw-r-- 1 bunyk bunyk 117 кві  5 19:52 out.gz

Я створив цей приклад на Linux. Якщо ви не знайомі з командним рядком, ця команда показує "розширений лістинг" для стисненого файла який ми щойно створили з директорії Python. Цей лістинг показує що файл існує (це добре), і що він має довжину 117 байт. Це насправді більше ніж рядок з якого ми почали! Формат файла gzip включає заголовок фіксованої довжини, який містить деякі метадані про файл, тому є неефективним для надто малих файлів.

you@localhost:~$ gunzip out.gz

Команда gunzip (читається як "джі-анзіп") розархівовує файл, і зберігає його вміст в новому файлі, який називається так само як стиснений файл, але без розширення .gz.

you@localhost:~$ cat out
Дев’ятимильна прогулянка - це не жарт, особливо під час дощу.

Команда cat показує вміст файла. Цей файл містить рядок який ми раніше записали прямо в стиснений файл out.gz прямо з оболонки Python.


* * *


Стандартні потоки вводу, виводу та помилок

ред.
sys.stdin,
sys.stdout,
sys.stderr.

Гуру командного рядка вже знайомі з концепцією стандартного вводу, стандартного виводу, та стандартного виводу помилок. Цей розділ для решти з вас.

Стандартний вивід, та вивід помилок (зазвичай скорочуються до stdout та stderr) - це канали що вбудовані в кожну UNIX-подібну систему, включаючи Mac OS X та Linux. Коли ви викликаєте функцію print(), все що ви друкуєте відправляється у канал stdout. За замовчуванням обидва ці канали просто прив’язані до вікна терміналу, і коли програма завершує роботу з помилкою, ви бачите цю помилку в тому ж терміналі що й вивід програми. В графічній оболонці Python, канали stdout та stderr за замовчуванням прив’язані до "Інтерактивного вікна".

>>> for i in range(3):
...     print('PapayaWhip')
PapayaWhip
PapayaWhip
PapayaWhip

Просто функція print() в циклі. Поки що нічого незвичайного.

>>> import sys
>>> for i in range(3):
...     l = sys.stdout.write('is the')
is theis theis the

stdout описаний в модулі sys, і є потоковим об’єктом. Виклик його методу write() надрукує рядок що йому передали, після чого поверне довжину виводу. Насправді це те що робить функція print() - додає символ нового рядка до кінця рядка який потрібно надрукувати і викликає sys.stdout.write().

>>> for i in range(3):
...     l = sys.stderr.write('new black')
new blacknew blacknew black

В найпростішому випадку sys.stdout та sys.stderr відправляють свій вивід в одне й те ж місце: IDE Python (якщо ви ним користуєтесь), чи термінал (якщо використовуєте Python в командному рядку). Як і стандартний вивід, стандартний вивід помилок не додаватиме символи нового рядка за вас. Якщо вам потрібно закінчити рядок - потрібно додати символ переходу самому.

sys.stdout та sys.stderr - потокові об’єкти, але вони призначені тільки для запису. Якщо ви спробуєте викликати їх метод read() - завжди отримаєте IOError.

>>> import sys
>>> sys.stdout.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: not readable

Перенаправлення стандартного потоку виводу

ред.

sys.stdout та sys.stderr є потоковими об’єктами, хоча й такими що підтримують лише запис. Але вони не константи, вони змінні. Це означає що їм можна присвоїти нове значення - будь-який інший потоковий об’єкт, щоб перенаправити їх вивід.

import sys

class RedirectStdoutTo:
    def __init__(self, out_new):
        self.out_new = out_new

    def __enter__(self):
        self.out_old = sys.stdout
        sys.stdout = self.out_new

    def __exit__(self, *args):
        sys.stdout = self.out_old

print('A')
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):
    print('B')
print('C')

Зацініть:

you@localhost:~$ python3 stdout.py
A
C
you@localhost:~$ cat out.log
B

Давайте спершу розберемось з останньою частиною:

print('A')
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):
    print('B')
print('C')

Це складна конструкція with. Давайте я перепишу її в щось більш знайоме:

with open('out.log', mode='w', encoding='utf-8') as a_file:
    with RedirectStdoutTo(a_file):
        print('B')

Як демонструє переписаний варіант, насправді тут є дві вкладені конструкції with. "Зовнішня" конструкція with повинна бути вам вже знайомою: вона відкриває текстовий файл в кодуванні UTF-8, який називається out.log для запису, і присвоює утворений потоковий об’єкт змінній a_file. Але це не настільки дивно, як те що нижче.

with RedirectStdoutTo(a_file):

Де ключове слово as? Насправді, конструкція with не вимагає цього слова. Так само як ми можемо викликати функцію, та ігнорувати її повернене значення, ми можемо мати конструкцію with яка не присвоює свій менеджер контексту змінній. В даному випадку ми зацікавлені лише в побічних ефектах контексту RedirectStdoutTo.

Що це за побічні ефекти? Давайте подивимось всередину класу RedirectStdoutTo. Цей клас є менеджером контексту. Кожен клас може стати менеджером контексту, якщо опише два спеціальні методи: __enter__() та __exit__().

class RedirectStdoutTo:
    def __init__(self, out_new):
        self.out_new = out_new

Метод __init__() викликається одразу після того як створюється екземпляр класу. Він приймає один параметр - потоковий об’єкт який ми хочемо використовувати в якості стандартного потоку виводу протягом часу життя контексту. Цей метод просто зберігає потоковий об’єкт в атрибуті екземпляра, аби потім його могли використовувати інші методи.

    def __enter__(self):
        self.out_old = sys.stdout
        sys.stdout = self.out_new

Метод __enter__() - це спеціальний метод, який викликається коли інтерпретатор входить в контекст (на початку блоку with). Цей метод зберігає поточне значення sys.stdout в self.out_old, після чого перенаправляє стандартний вивід присвоюючи змінній sys.stdout значення змінної self.out_new.

    def __exit__(self, *args):
        sys.stdout = self.out_old

Метод __exit__() - це інший спеціальний метод, який викликається при виході з контексту (в кінці блоку with). Цей метод відновлює початкове значення стандартного потоку виводу, присвоюючи змінній sys.stdout її старе значення збережене у змінній self.out_old.

Тепер подивимось як це все разом працює.

print('A')
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):
    print('B')
print('C')

Перший рядок надрукує щось в інтерактивне вікно Python, чи термінал, якщо ви запускаєте скрипт з командного рядка).

Другий рядок починається ключовим словом with, після якого йде список менеджерів контексту, розділених комами. Цей список працює так само як послідовність вкладених блоків with. Перший з перелічених менеджерів контексту створює "зовнішній" блок, останній "внутрішній". Перший контекст відкриває файл, другий контекст перенаправляє sys.stdout в потоковий об’єкт що був створений в першому контексті.

Тому що функція print() в блоці with знаходиться в контексті що перенаправляє стандартний вивід, вона не виводиться на екран, а записується в файл out.log.

Коли блок with закінчується, Python каже кожному менеджеру контексту робити те що вони повинні робити при виході з контексту. Менеджери контексту формують стек "останній зайшов - перший вийшов". Перед виходом, другий контекст замінює sys.stdout назад в початкове значення, потім перший контекст закриває файл названий out.log. Так як стандартний потік виводу отримав початкове значення, наступний виклик функції print знову друкуватиме на екран.

Перенаправлення стандартного потоку помилок працює так само, просто замість sys.stdout потрібно використати sys.stderr.


* * *


Для подальшого читання

ред.

Рефакторинг · XML