среда, 17 апреля 2013 г.

Использование try-finally

Хочу обратить внимание на маленькую особенность написания конструкции try-finally.

Возьмём для примера многопоточность, а конкретно блокировки.

Где-то (наверное, в конструкторе класса) мы создали объект блокировки:

self.locker = threading.RLock()

Затем в каком-то методе мы пытаемся использовать эту блокировку в try-finally statement. Да, я знаю что RLock поддерживает context manager protocol и может использоваться в with statement. Так будет даже лучше, но мы сейчас говорим о другом варианте использования.

try:
    self.locker.acquire()
    do_some_work()
finally:
    self.locker.release()

В чём ошибка? .acquire() может выбросить исключение. Блокировка не будет захвачена и попытка её освободить в .release() выбросит новое (другое) исключение. Что крайне нежелательно. Особенно в python 2.x, где нет цепочек исключений. Т.е. ошибка в .acquire() будет просто скрыта, понять в чём было дело невозможно.

Правильно писать так:

self.locker.acquire()
try:
    do_some_work()
finally:
    self.locker.release()

Если было исключение в .acquire() — то блокировка не захвачена и освобождать её не нужно. Пусть обработка исключения разворачивается своим ходом, .release() в finally block совершенно не нужен.

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

Проблема усугубляется тем, что обычно .acquire() работает успешно, и лишь в редких случаях выбрасывает исключение. Которое мы видим в логах (все используют логи, верно?) и недоумеваем, что именно произошло.

Это замечание относится к любому коду, выполняемому в finally block.

Переменные, блокировки, захват ресурсов, открытие файлов и т.д. должны быть выполнены перед try.

P.S.

На открытие файлов хочу обратить особое внимание как на самый частый случай. Куда более частый чем работа с многопоточностью. Правильно писать:

f = open('filename')
try:
    f.read()
finally:
    f.close()

Надеюсь, последний пример запомнится хорошо и внесёт ясность в головы уважаемых молодых коллег.

8 комментариев:

  1. Это, кстати, актуально не только для Python.

    ОтветитьУдалить
    Ответы
    1. Это актуально для всех языков, имеющих конструкцию try-finally.

      Но я пишу о своём любимом Python, поэтому и текст соответствующий.

      Удалить
  2. При работе с файлом я бы написал:

    with open('filename') as f:
    pass

    ОтветитьУдалить
    Ответы
    1. with statement намного лучше, как я уже писал. Но статья не о with, а о том как использовать try-finally правильно.

      Удалить
  3. Пример с open():

    open(name[, mode[, buffering]])

    Open a file, returning an object of the file type described in section File Objects. If the file cannot be opened, IOError is raised.


    f = open('filename') <--- если здесь будет IOError, то весь код ниже будет бесполезен, нет?

    Либо я не понимаю, чего хочется добиться используя try-finally

    ОтветитьУдалить
    Ответы
    1. В try-finally нужно закрыть уже открытый файл если работе с ним была ошибка. Если open выбросил IOError то файл не открыт и в finally его закрывать не нужно.

      Удалить
  4. Спасибо большое за пример try-finally с файлом.
    Можно теперь починить свой костыльный код =)

    ОтветитьУдалить