портрет у стены

Python Fun #2: generators and exceptions

Возмём простую задачу, но заюзаем прикольную фичу Python'а -- генераторы, которые, согласно документации, позволяют "замораживать выполнение функции до обращения за следующим результатом" (примерно так). Не, это не многопоточное выполнение; это лишь синтаксическая условность, обозначающая что на операторе yield мы как будто выходим из функции с результатом, делаем что-то снаружи, и потом опять возвращаемся в функцию для продолжения.

Напишем функцию, которая возвращает последовательность трёх элементов: чисел 1,2,3. Простейшее решение -- return [1,2,3]. Но мы напишем функцию, которая возвращает результат с помощью генераторов, а также имеет инициализатор в начале (строка 2). И пройдёмся по её результату для вывода на stdout (строки 19-22).

Забавы ради, иницилизатор сделаем "глючным", и он нам будет выкидывать исключение (строка 2). Но мы умные, мы исключения от этой функции ожидаем, и будем их ловить (строка 14), и в таком случае результат подразумевать пустым (строка 16).

Просто, да? Вот код:

  1. def generator():
  2.     raise Exception("dieplz")
  3.     yield 1
  4.     yield 2
  5.     yield 3
  6.  
  7.  
  8. try:
  9.     print('==============================')
  10.     try:
  11.         print("Calling function")
  12.         items = generator()
  13.         print("Call succeded")
  14.     except Exception, e:
  15.         print("Call failed with exception: %s" % e)
  16.         items = []
  17.     print('==============================')
  18.    
  19.     print("Iterating over items")
  20.     for item in items:
  21.         print("Item: %s" % item)
  22.     print("Iteration finished")
  23. except Exception, e:
  24.         print("Failed with unhandled exception: %s" % e)
  25. finally:
  26.         print("Done")
  27. print('==============================')
  28.  



Но не тут-то было! Эта функция НЕ выполняется в момент вызова функции (на строке 12). Вместо этого на невидимой прослойке интерпретатора возвращается некий объект-генератор, который попадает в переменную items. И только попытка итерации по нему (строка 20) уже реально вызывает функцию.

И исключение, которое в функции должно происходить даже до первой "заморозки выполнения", случается не в том месте, где его ждут (на строке 20 вместо ожидаемой 12). И мы ловим unhandled exception вместо итерации по пустому анти-ошибочному списку.

>python generators.py
==============================
Calling function
Call succeded
==============================
Iterating over items
Failed with unhandled exception: dieplz
Done
==============================


Решение проблемы простое - всегда пригонять результаты генератором в известный тип: items = list(generator()). Но это решение неудобное. А если эта функция - callback, который задаётся кем-то снаружи? А если им захочется передать не колбек, а просто экземпляр итерабельного объекта? А если этот объект ещё изменит своё состояние до цикла for, и итерация действительно должна быть только в том месте?

Не без изъянов, в общем, язычок-то.
Метки:

Comments

Нет, ну чего ты ждал? Там ясно, человечьим языком, написано, что при вызове функции, определённой с yield вернётся generator iterator, генерирующий итератор, его за ногу. Тело генератора исполняется при вызове .next() - собственно тогда и вернётся твой эксепшн, а вовсе не во время вызова generator().

Так что твоя проблема - в недопонимании того, что вызывая generator() ты вызваешь не тело функции, а всего лишь создаёшь генератор, т.к. функция, определённая с yield превращается в функцию-генератор )


Using a yield statement in a function definition is sufficient to cause that definition to create a generator function instead of a normal function. When a generator function is called, it returns an iterator known as a generator iterator, or more commonly, a generator.


Вот там курили:
http://www.python.org/dev/peps/pep-0255/
http://www.python.org/dev/peps/pep-0342
Я мануал читал, знаю это. Проблема в неочевидности синтаксиса. Если бы функция отрабатывала хотя бы до первого yield - было бы уже куда очевиднее.
Я перл уже два раза забыть успел, причём буквально :-)

Один раз учил и практиковал когда админил/со-админил у провайдера и для себя. Всякие скрипты, парсеры, и пр.

Второй раз учил когда думал туда переползти с пхп; но, увидев тамошний "ООП", испугался и вытер из памяти чтоб не повредить своё мировоззрение %-)
Это да, может быть и плохая. Недаром от неё норовят отпочковаться всякие аспектные и прочие парадигмы. Но она такая живучая, потому что лучших решений пока нет. Функциональное программирование - удел касты математиков с вывихнутыми на рекурсии мозгами. Массовому программисту "только что из школы" оно не по силам. Вот и живёт ООП монопольно.

Замыкания - ок, есть в питоне красивые, есть в JS. Если JS не пошёл в массы поскольку сфера применения ограничена по факту (хотя что мешает расширить), то почему питон не идёт - непонятно. Видимо, замыкания - удел гуи и прочего интерактивного, как альтернативное решение для передачи состояния. Не вебовское точно, не скриптовое.

Декларативное ждём-с. Пока что это вещь в себе, судить не о чем. Хотя, кстати... А чё ждать? У меня с 2002 года в голове концепты и наброски как программировать на естественном языке и как это парсить и интерпретировать. Надо брать и разрабатывать. Ай, лентяй :-)
Дай бог, но это всё на годы и годы вперёд. Не здесь, не сейчас. А пока что ООП рулит, потому что ООП рулит :-)
Пообщался с другом

xxx: можешь сказать в чем главная ошибка
yyy: он изначально рассматривает генератор как функцию и ожидает что она сразу начнет выполняться
xxx: хм, а как надо?
yyy: вызов генератора лучше рассматривать как создание объекта
xxx: понятно, спасиб
Ну это понятно. Если знать заранее. Но синтаксис fn() не намекает на создание объекта. Пример:

def doit(callback):
  try: items = callback()
  except: items = []
  for item in items:
    print(item)

def fn1(): return [1,2,3]
def fn2(): yield 10

doit(fn1)
doit(fn2)
 


Простой, казалось бы, синтаксис у doit. Вменяемый. А вот юз-кейс подкачал. Потому что "фокус".
PS: То есть проблема в том, что fn2 объявлена как "def" (фукнция, а не что-то там типа "class" или т.п.). И внутренности у неё не являются фабрикой, кроме как за счёт yield'а, который по сути более похож на "return value", чем на "return lazy_iterator([value])".
портрет у стены

Октябрь 2011

Вс Пн Вт Ср Чт Пт Сб
      1
2345678
9101112131415
16171819202122
23242526272829
3031     

Метки

Разработано LiveJournal.com