Только что прочитал у Pythy, что вышел Python 2.5. В нем много всего нового и змечательного, и что приятно, я даже не нашел на первый взгляд ничего, что вызвало бы отрицательную реакцию.

Но я больше всего рад появлению одной конкретной вещи — оператора with.

with в Delphi

Я очень любил оператор with, когда активно писал на Delphi, даже несмотря на опасность попутать переменные из разных областей видимости. Во-первых, потому что удобство от неписания постоянно одного и то же имени переменной у меня перекрывало редкие случаи багов, но главное, потому что он обладает очень четкой узнаваемой семантикой: локальный блок операций надо одним главенствующим объектом.

Из этой семантики естественным образом вытекает то, что with удачно сочетается с блоком try..finally, который гарантирует правильное создание и уничтожение объекта:

With TFileStream.Create(FileName) Do
Try
  { ... }
Finally
  Free;
End;

И можно обойтись без объявления переменной.

Я, кстати, именно так без отступа писал try..finally, чтобы подчеркнуть, что эта конструкция составляет с with одно логическое целое.

with в Питоне

И вот именно потому мне так и нравится новый питоновский with, что он реализует как раз такое поведение: не просто блок с "главным" объектом, но еще и с автоматической его инициализацией и гарантированным завершением. То есть вся эта катавасия с громоздким try..finally сведена в короткую синтаксическую конструкцию:

with open(filename) as f:  # файл открывается
  f.read(100)

# файл закрывается, даже после exception'ов

или

with threading.Lock():
  # код критической секции

# lock освободится после выхода из with

Определение того, что именно происходит при инициализации и завершении, сделано чисто по-питоновски: методами __enter__() и __exit__() объекта, который стоит под with. То есть можно легко делать и свои объекты, которые будут способны создать такой блок (который теперь называется контекстом, а объекты, соответственно, менеджерами контекста).

И что еще приятно! Той самой опасной особенности паскалевского with в Питоне как раз и нет :-). Переменную, которая представляет объект в контексте, нужно указывать явно.

Вложенные try..finally

Еще одна штука, которой сильно не хватало в Delphi — это красивая синтаксическая возможность управлять уничтожением нескольких объектов.

Простой пример — блок кода, который работает с двумя файлами. Если написать так:

InFile := TFileStream.Create('...', fmOpenRead);
OutFile := TFileStream.Create('...', fmOpenWrite);
Try
  { процесс }
Finally
  InFile.Free;
  OutFile.Free;
End;

... то это, вообще-то, будет некорректно: при ошибке на второй строчке, которая идет до try, finally никогда не выполнится, и первый файл останется открытым. Поэтому более корректным будут два вложенных блока:

InFile := TFileStream.Create('...', fmOpenRead);
Try
  OutFile := TFileStream.Create('...', fmOpenWrite);
  Try
    { процесс }
  Finally
    OutFile.Free;
  End;
Finally
  InFile.Free;
End;

Но это уже дикость нечитаемая :-(.

В Питоне 2.5 для оформления таких вещей появилась библиотечная функция:

from contextlib import nested

with nested(open('...', 'r'), open('...', 'w')) as (in_file, out_file):
  # процесс

# по выходу гарантирует закрытие и одного, и другого файлов

Ну и еще там есть всякие полезные утилитки, которые упрощают создание своих менеджеров контектса, используя распространенные паттерны и соглашения языка (как например то, что у подавляющего большинства объектов, которые можно "закрыть", этот метод называется close() и никак иначе).


Эх... Приятно все-таки работать с языком, который так хорошо ложится на мозги!

Комментарии: 13

  1. CR

    Одно не могу понять: зачем вводить слова __enter__() и __exit__(), когда достаточно обойтись del'ом ?

  2. Иван Сагалаев
        <p>Мне видятся сразу несколько причин.</p>
    
    1. Эта пара методов позволяет не привязывать время жизни объекта к with. Например, если есть какой-то большой и сложный объект, который живет в программе от начала и до конца, но который в том числе может работать временами и как менеджер контекста.

    2. Вызов __del__ не гарантируется при выходе из with, он вызовется когда-нибудь потом, когда будет удобно garbage collector'у. А закрывать файл или освобождать lock как раз и нужно обычно делать сразу и надежно.

    3. В __exit__ передаются еще и параметры возникшего exception'а, если оно есть, чтобы его можно было обработать.

  3. Шаген Оганджанян

    А мне очень понравилась конструкция

    x = true_value if condition else false_value

  4. [...] Ruby конечно хорош. Тут язык у меня ничего против сказать не повернется. Но жена его не знает, зато на первом курсе слушала краткое введение в Python. Понятное дело, его и выбрала в качестве языка для курсовой работы. Я соответственно, как защитник и помошник, пользуясь получасовым затишьем любезно предоставленным дочкой Анечкой, решил подчитать что интересного в мире удава произошло с тех пор, как я последний раз интересовался им. Оказывается вышла новая версия, которая несет некоторые очень толковые изменения, в частности with и nested. Подробнее о with в python-e пишет в своем блоге Иван Сагалаев, куда я и рекоммендую глянуть всем заинтересовавшимся. [...]

  5. declonter

    А почему бы не так:

    Try
      InFile := TFileStream.Create('...', fmOpenRead);
      OutFile := TFileStream.Create('...', fmOpenWrite);
      { процесс }
    Finally
      InFile.Close;
      OutFile.Close
    End;
    

    ?

  6. Иван Сагалаев

    Наверное имелся в виду Free вместо Close, но смысл понятен.

    InFile и OutFile до начала этого блока содержат мусор. Поэтому если подорвется создание InFile, то OutFile так и не будет инициализирован, и OutFile.Free вероятнее всего вызовет access violation. А писать

    InFile := Nil;
    OutFile := Nil;
    

    до начала тоже не особенно приятно.

  7. Иван Сагалаев

    Ой... А я и в статье везде Close вместо Free понаписал :-). Все позабыл напрочь :-). Пошел исправлять...

  8. declonter

    Иван Сагалаев, да, действительно access violation. А Close был получен методом copy-paste :)

  9. DemonZLa

    По мне так лучше access violation... чем допустить работать проге тогда когда файл не открыт и следовательно вся дальнейшая логика проги идёт к чертям собачьим...

  10. [...] Python, как генераторы, декораторы, list comprehensions, оператор with… Но есть в Питоне и некоторые возможности, которые [...]

  11. [...] Python, как генераторы, декораторы, list comprehensions, оператор with… Но есть в Питоне и некоторые возможности, которые [...]

  12. zahardzhan

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

    (defmacro with-open-file [[name file] & body]
      `(let [~name ~file]
         (try (do ~@body)
              (finally (close ~name)))))
    
    (with-open-file [f filename]
      (read f 100))
    
  13. Ivan Sagalaev

    Даже удивлен, как в комменты не понабежали лисперы и не начали срать кирпичами

    Видимо, большинство лисперов более воспитанны. Кроме того, бесполезные комментарии я просто не пропускаю.

Добавить комментарий