Изображение гика

Блог питониста

Разбираемся с "with" в питоне

18 февраля 2018 г.

Данная статья - перевод c моим небольшим дополнением, оригинал

Часто ключевое слово with не до конца понятно даже опытным разработчикам.

Как и многие другие вещи в Python, ключевое слово with на самом деле очень просто устроено, это станет очевидно, как только вы поймете какую проблему оно решает. Посмотрите на данный код:

1 set things up
2 try:
3     do something
4 finally:
5     tear things down

Здесь под "set things up" подразумевается открытие файла, подключение какого-то внешнего ресурса, а под "tear things down" - закрытие файла, отключение от внешнего ресурса. Конструкция try-finally гарантирует, что "tear things down" часть будет всегда исполнена, даже если код, делающий что-либо вызовет ошибку или не завершится.

Если это часто ипользуется, то было бы удобно вынести код  “set things up” и “tear things down” в библиотечную функцию, чтобы легко ее использовать. Конечно, вы можете сделать что-то вроде:

 1 def controlled_execution(callback):
 2     set things up
 3     try:
 4         callback(thing)
 5     finally:
 6         tear things down
 7 
 8 def my_function(thing):
 9     do something
10 
11 controlled_execution(my_function)

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

1 def controlled_execution():
2     set things up
3     try:
4         yield thing
5     finally:
6         tear things down
7 
8 for thing in controlled_execution():
9     do something with thing

Но yield нельзя было использовать внутри try-finally в 2.4 и раньше. И немного странно использовать loop для чего-то, что вы хотите выполнить один раз.

Поэтому, после рассмотрения нескольких вариантов, Гвидо Ван Россум и python-dev команда наконец решили использовать объект вместо генератора, чтобы контролировать поведение данного кода:

1 class controlled_execution:
2     def __enter__(self):
3         set things up
4         return thing
5     def __exit__(self, type, value, traceback):
6         tear things down
7 
8 with controlled_execution() as thing:
9     some code

Теперь, когда "with" выражение исполняется, Python исполняет выражение, вызывает метод __enter__ с полученным значением (которое называется "context guard"), затем присваивает переменной переданной словом as (в данном случае thing) то, что возвращает метод __enter__. Далее, Python исполняет тело (в данное случае some code), и в любом случае вызывает метод __exit__.

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

1 def __exit__(self, type, value, traceback):
2     return isinstance(value, TypeError)

Например:

 1 class controlled_execution:
 2     def __enter__(self):
 3         print('in enter')
 4         print(self)
 5         return ('test')
 6 
 7     def __exit__(self, type, value, traceback):
 8         print('in exit')
 9 
10 
11 with controlled_execution() as thing:
12     print('in here')
13     print(thing)
14     raise TypeError
alex@vostro:~/projects/blog_materials$ python test.py
in enter
<__main__.controlled_execution instance at 0x7f338bcb6a70>
in here
test
in exit
Traceback (most recent call last):
  File "test.py", line 14, in <module>
    raise TypeError
TypeError

Но:

 1 class controlled_execution:
 2     def __enter__(self):
 3         print('in enter')
 4         print(self)
 5         return ('test')
 6 
 7     def __exit__(self, type, value, traceback):
 8         print('in exit')
 9         return isinstance(value, TypeError)
10 
11 
12 with controlled_execution() as thing:
13     print('in here')
14     print(thing)
15     raise TypeError
alex@vostro:~/projects/blog_materials$ python test.py
in enter
<__main__.controlled_execution instance at 0x7ff3fac74a70>
in here
test
in exit

В Python 2.5 у объекта типа file появились методы __enter__ и __exit__, первый просто возвращает сам объект, а второй закрывает файл:

>>> f = open("x.txt")
>>> f
<open file 'x.txt', mode 'r' at 0x00AE82F0>
>>> f.__enter__()
<open file 'x.txt', mode 'r' at 0x00AE82F0>
>>> f.read(1)
'X'
>>> f.__exit__(None, None, None)
>>> f.read(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file

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

1 with open("x.txt") as f:
2     data = f.read()
3     do something with data

Это не было очень сложно, правда?

Метки

перевод python
Если вам понравился пост, можете поделиться им в соцсетях: