Разбираемся с "with" в питоне (перевод)
Данная статья - перевод 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
Это не было очень сложно, правда?