October 28, 2012

Зачем в python with

Долгое время при работе с файлами из python я писал примерно следующий код:

Без подсветки синтаксиса
def some_func(fname):
    fd = open(fname)
    some_data_processing(fd.read())
    return result

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

Но что будет если в some_data_processing произойдет исключение?

Например так:

Без подсветки синтаксиса
import sys

class TestClass(object):
    def __del__(self):
        print "I'm deleted"

def data_process():
    obj = TestClass()
    raise IndexError()

try:
    data_process()
except:
    print "In exception handler"

print "after except"

На консоли появляется:

    In exception handler
    after except
    I'm deleted

Почему-то "In exception handler" и "after except" выводятся раньше "I'm deleted".

Первая проблема в том, что вместе с исключением питон хранит и трейс стека, содержащий все фреймы вплоть до породившего исключение. А внутри фрейма живет f_locals - словарь локальных переменных, и именно он имеет ссылку на экземпляр класса TestClass. Таким образом до окончания обработки исключения obj будет жить точно. Почему же "after except" появляется раньше чем "I'm deleted"? Было бы логично чистить трейс после успешного выхода из блока try. Дело в том что 2.X питон не всегда чистит внутренние структуры после обработки исключения и в общем случае вы должны явно вызывать функцию sys.exc_clear чтобы очистить их. Когда я подошел с этим вопросом к Larry Hastings (одному из основных разработчиков ядра питона) ему потребовалось около 20ти минут, что-бы понять что происходит и найти в документации sys.exc_clear. (Правда стоит отметить, что он давно использует 3.X, где это поведение стало адекватнее.) В 3.X это поведение улучшили, и теперь sys.exc_clear автоматически вызывается окончанию обработки исключения.

Кстати, если вы напишете примерно такой код:

Без подсветки синтаксиса
try:
    data_process()
except:
    fr = sys.exc_info()[2]
    del fr

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

Стоит отметить, что подобное поведение проявляется не всегда. Например следующий код исполняется более предсказуемо:

Без подсветки синтаксиса
import sys

class TestClass(object):
    def __del__(self):
        print "I'm deleted"

def data_process():
    fd = TestClass()
    try:
        raise IndexError()
    except:
        print "In internal exception handler"

data_process()
print "after except"
    In internal exception handler
    I'm deleted
    after except

В общем что-бы гарантированно избавить себя от этих проблем нужно явно закрывать все файлы и прочие объекты или так:

Без подсветки синтаксиса
fd = open(fname)
try:
    process_code()
finally:
    fd.close()

или так:

Без подсветки синтаксиса
with open(fname) as fd:
    process_code()

собственное with именно для этого и был сделан. Без его использования вы рискуете исчерпать лимит на дескрипторы или что-там-еще в зависимости от объектов. Впрочем это только начало печальной истории, продолжение дальше.

Ссылки:
          docs.python.org/2/library/sys.html#sys.exc_clear
          nuitka.net

Исходники этого и других постов со скриптами лежат тут - github.com/koder-ua. При использовании их, пожалуйста, ссылайтесь на koder-ua.blogspot.com.

2 comments:

Goran said...

Хм... попробовал под Ubuntu 12.10 (там как раз оба Python 2.x и 3.x из коробки) - результат один и тот же

igor@ubuntu:~/Dev/PythonResearch$ python except.py
In internal exception handler
I'm deleted
after except
igor@ubuntu:~/Dev/PythonResearch$ python3 except.py
In internal exception handler
I'm deleted
after except

версия 2.x Python 2.7.3 - либо очень произвольно зависит от сборщика мусора (от его выполнения), либо в самой последней версии 2.7.3 пофиксили до нормального поведения :) Вообще интересный момент

konstantin danilov said...

В случае если исключение отлавливается внутри функциимне не удалось воспроизвести это поведение.
В случае же, если исключение отлавливается на уровне модуля(первый пример) - востпроизводится на 100% на 2.7