Глава 4. Применение оператора with в потоках

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

В этой главе будут рассмотрены следующие темы:

  • Само понятие управления контекстом и те варианты, которые данный оператор with предоставляет в качестве некоего диспетчера контекста, в особенности при совместном и параллельном программировании.

  • Собственно синтаксис оператора with и как его действенно и эффективно применять.

  • Различные способы использования оператора with при совместном программировании.

Технические требования

Вот список предварительных требований для данной главы:

  • Убедитесь что на вашем компьютере уже установлен Python 3

  • Выгрузите необходимый репозиторий из GitHub

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

  • Ознакомьтесь со следующими видеоматериалами Code in Action

Управление контекстом

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

Начинаем с управляющих файлов

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

Давайте рассмотрим по- быстрому некий пример, чтобы проиллюстрировать этот момент далее. Давайте рассмотрим файл Chapter04/example1.py, который показан в следующем коде:


# Chapter04/example1.py

n_files = 10
files = []

for i in range(n_files):
    files.append(open('output1/sample%i.txt' % i, 'w'))
 	   

Эта программа на скорую руку просто создаёт 10 текстовых файлов внутри соответствующей папки output1: sample0.txt, sample1.txt, ..., sample9.txt. Что может быть для нас более интересным, так это тот факт, что эти файлы были открыты внутри нашего цикла for, но не были закрыты - это плохая практика при программировании, которая будет обсуждена далее. Теперь, допустим, мы пожелали переназначить значение переменной n_files на большее число - скажем, 10 000 - как это показано в таком коде:


# Chapter4/example1.py

n_files = 10000
files = []

# method 1
for i in range(n_files):
    files.append(open('output1/sample%i.txt' % i, 'w'))
 	   

Мы получим некую ошибку, подобную следующей:


> python example1.py
Traceback (most recent call last):
  File "example1.py", line 7, in <module>
OSError: [Errno 24] Too many open files: 'output1/sample253.txt'
		

Пристальнее взглянув на данное сообщение об ошибке, мы можем увидеть, что мой ноутбук способен обрабатывать только 253 открытых файла одновременно (в качестве примечания: если вы работаете в UNIX- подобной операционной системе, вызов ulimit -n выдаст вам то число файлов, которое может обрабатывать ваша система {Прим. пер.: для Windows воспользуйтесь Testlimit.exe -h, подробнее...}). В более общем смысле, данная ситуация возникла из- за того, что именуется утечкой файловых дескрипторов. Когда Python открывает некий файл внутри программы, данный открытый файл на самом деле представлен неким целым значением. Это целое действует как некий указатель, который программа может применять чтобы получить доступ к этому файлу до тех пор, пока данная программа не завершит управление лежащим в его основе самим файлом.

Открывая одновременно слишком много файлов, наша программа назначает слишком много файловых дескрипторов для управления такими открытыми файлами, отсюда и соответствующее сообщение об ошибке. Утечка файловых дескрипторов может повлечь за собой множество сложных проблем - в особенности при совместном или параллельном программировании - а именно, не авторизованные операции ввода/ вывода в уже открытых файлах. Основным решением для этого является простое закрытие открытых файлов неким согласованным образом. Давайте рассмотрим свой файл Chapter04/example1.py с его вторым методом. В своём цикле for мы будем делать следующее:


# Chapter04/example1.py

n_files = 1000
files = []

# method 2
for i in range(n_files):
    f = open('output1/sample%i.txt' % i, 'w')
    files.append(f)
    f.close()
 	   

Оператор with в качестве диспетчера контекста

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

Одним из возможных решений данной проблемы, которое также распространено и в прочих языках программирования, это применение блока try...except...finally всякий раз когда мы желаем взаимодействовать с неким внешним файлом. Данное решение всё ещё требует того же самого уровня управления и существенных накладных расходов и даже не предоставляет достойного улучшения относительно простоты и читаемости наших программ. Именно тут вступает в игру оператор Python with.

Этот оператор with даёт нам некий простой способ гарантии того, что все открытые файлы управляются надлежащим образом и очищаются когда наша программа прекращает их применять. Наиболее заметное преимущество использования оператора with проявляется в том факте, что даже если наш код успешно завершён или он возвращает некую ошибку, сам оператор with всегда обрабатывает открытые файлы и управляет ими надлежащим образом через контекст. Например, давайте взглянем на файл Chapter04/example1.py более подробно:


# Chapter04/example1.py

n_files = 254
files = []

# method 3
for i in range(n_files):
    with open('output1/sample%i.txt' % i, 'w') as f:
        files.append(f)
 	   

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

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

Синтаксис оператора with

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


with [expression] (as [target]):
    [code]
 	   

Заметим, что часть as [target] обсуждаемого оператора with на самом деле не требуется, как мы обнаружим позднее. Кроме того, данный оператор with может также обрабатывать более одного элемента в одной и той же строке. В частности, соответствующие диспетчеры контекста рассматриваются как множество операторов with, вкладываемых {матрёшкой} один в другой. Например, следующий код:


with [expression1] as [target1], [expression2] as [target2]:
    [code]
 	   

Может интерпретироваться так:


with [expression1] as [target1]:
    with [expression2] as [target2]:
        [code]
 	   

Оператор with при параллельном программировании

Очевидно, что открытие и закрытие внешних файлов не очень напоминает совместную обработку. Тем не менее, мы уже упоминали ранее, что наш оператор with, выступая в роли диспетчера контекста, не только используется для управления файловыми дескрипторами, но обычно и большинством ресурсов. А если вы уже в действительности обнаружили что управление блокировкой объектов из класса threading.Lock() похоже на управление внешними файлами при изучении Главы 2, Закон Амдала, тогда именно тут удобно применять их сравнение.

В качестве напоминания, блокирования являются механизмами в совместном и параллельном программировании, которые обычно применяются для синхронизации потоков в многопоточных приложениях (то есть для предотвращения того, чтобы к критическому разделу одновременно осуществляли доступ более одного потока). Тем не менее, как мы обсудим снова в Главе 12, Зависание, блокировки также являются распространённым источником взаимной блокировки, в процессе которой некий поток овладевает каким- то блокированием, но никогда не высвобождает его по причине не подлежащих обработке обстоятельств, посредством чего останавливает всю программу целиком.

Пример обработки взаимной блокировки

Давайте рассмотрим некий пример по- быстрому. Для этого заглянем в соответствующий файл Chapter04/example2.py, который показан в следующем коде:


# Chapter04/example2.py

from threading import Lock

my_lock = Lock()

def get_data_from_file_v1(filename):
    my_lock.acquire()

    with open(filename, 'r') as f:
        data.append(f.read())

    my_lock.release()

data = []

try:
    get_data_from_file('output2/sample0.txt')
except FileNotFoundError:
    print('Encountered an exception...')

my_lock.acquire()
print('Lock can still be acquired.')
 	   

В этом примере у нас имеется некая функция get_data_from_file_v1(), которая получает соответствующий путь к некоторому внешнему файлу, считывает из него данные и дописывает к его конец данные из ранее определённого списка с названием data. Внутри данной функции при помощи my_lock вызывается некое блокирование объекта, которое также определено до вызова данной функции, достигается и освобождается когда данный параметрический файл соответственно считывается до и после.

В своей основной программе мы попытаемся вызвать get_data_from_file_v1() для некоторого не существующего файла, что является одной из наиболее распространённых ошибок при программировании. В самом конце этой программы мы также завладеем соответствующей блокировкой объекта вновь. Основной момент состоит в том, чтобы обнаружить способно ли наше программирование обработать такую ошибку чтения не существующего файла надлежащим образом и аккуратно всего лишь применяя тот блок try...except, который есть у нас.

После запуска данного сценария вы отметите, что ваша программа выведет на печать некое сообщение об ошибке, которое определено в блоке try...except, Encountered an exception..., как и ожидалось, так как данный файл не может быть обнаружен. Тем не менее, данная программа также откажет в исполнении всего оставшегося кода; она никогда не достигнет самой последней строки кода - print('Lock acquired.') - и повиснет навсегда (или пока вы не нажмёте Ctrl + C чтобы принудительно покинуть эту программу).

Именно это и является ситуацией взаимной блокировки {тупика}, которая, опять- таки, происходит когда мы завладели my_lock внутри своей функции get_data_from_file_v1(), но так как наша программа столкнулась с некоторой ошибкой прежде чем исполнить my_lock.release(), данное блокирование никогда не высвобождается. Это в свою очередь вызывает зависание соответствующей строки my_lock.acquire() в самом конце нашей программы, так как блокирование не может быть получено ни коим образом. Следовательно, наша программа не сможет достичь своей самой последней строки кода, print('Lock acquired.').

Тем не менее, данная проблема может быть решена быстро и без усилий при помощи некоторого оператора with. В своём файле example2.py просто установите комментарий на строку вызова get_data_from_file_v1() и уберите комментарий со строки get_data_from_file_v2(), и вы получите следующее:


# Chapter04/example2.py

from threading import Lock

my_lock = Lock()

def get_data_from_file_v2(filename):
    with my_lock, open(filename, 'r') as f:
        data.append(f.read())

data = []

try:
    get_data_from_file_v2('output2/sample0.txt')
except:
    print('Encountered an exception...')

my_lock.acquire()
print('Lock acquired.')
 	   

В нашей функции get_data_from_file_v2() мы имеем необходимый эквивалент пары таких вложенных операторов with:


with my_lock:
    with open(filename, 'r') as f:
        data.append(f.read())
 	   

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


> python example2.py
Encountered an exception...
Lock acquired.
		

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

Выводы

Оператор Python with предлагает некий интуитивно понятный способ управления ресурсами и при этом гарантировать корректную обработку ошибок и исключительных ситуаций. Такая возможность управлять ресурсами даже ещё больше важна при совместном и параллельном программировании, пр и которых совместно применяются различные ресурсы в различных сущностях - в частности, применяя соответствующий оператор with с объектами threading.Lock, которые используются для синхронизации различных потоков в некотором многопоточном приложении.

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

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

Вопросы

  • Что такое файловый дескриптор и как он может обрабатываться в Python?

  • Какие проблемы появляются, когда файловые дескрипторы не обрабатываются надлежащим образом?

  • Что из себя представляет блокирование и какими путями оно может обрабатываться в Python?

  • Какие проблемы возникают при не аккуратной обработке блокировок?

  • Что стоит за основной идеей диспетчеров контекста?

  • Какие варианты предоставляет оператор Python with в терминах управления контекстом?

Дальнейшее чтение

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