[Python] Функция двойного назначения (generator/callback)
От: jazzer Россия Skype: enerjazzer
Дата: 04.05.20 12:54
Оценка:
Приветствую, коллеги!

Возник вопрос — можно ли написать функцию в Питоне, которая будет себя вести, в зависимости от аргумента, либо как генератор, либо как функция с коллбэком?
Что-то вроде:
def f(cb=None):
  x=1
  что-то(x, cb)
  x=2
  что-то(x, cb)

где "что-то" — это либо yield x, если cb is None, либо cb(x)
Соответственно, я ее зову либо
for x in f(): print(x)

либо
f(cb=print)

и в том, и в другом случае она должна напечатать 1 и 2.

Функция f длинная, поэтому писать две версии, различающихся только yield/cb, не хочется (ну и вообще, DRY)
Интересует не только Python3, но и Python2, потому что основная кодовая база до сих пор на 2.7.

(Сейчас складывается впечатление, что если в тексте функции есть yield, то функция автоматически становится генератором, независимо ни от чего вообще, даже если этот yield гарантированно ни разу не позовется)
jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got
  If you always do  what you always did
python yield callback generators
Re: [Python] Функция двойного назначения (generator/callback)
От: koenjihyakkei Россия  
Дата: 04.05.20 13:18
Оценка:
Здравствуйте, jazzer, Вы писали:

Можно сделать враппер:

def f_gen():
  x=1
  yield(x)
  x=2
  yield(x)

def f(cb=None):
  gen = f_gen()
  if cb:
    for x in gen:
      cb(x)
  else:
    return gen
Re[2]: [Python] Функция двойного назначения (generator/callback)
От: jazzer Россия Skype: enerjazzer
Дата: 04.05.20 13:24
Оценка:
Здравствуйте, koenjihyakkei, Вы писали:

K>Можно сделать враппер


Да, я сейчас именно так и делаю (у меня это обобщенная функция call_with_cb(cb, f, *args, **kwargs), которая работает с любой функцией-генератором), но это внешнее, а хотелось бы что-то внутри самой функции написать
jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got
  If you always do  what you always did
Re[3]: [Python] Функция двойного назначения (generator/callback)
От: jazzer Россия Skype: enerjazzer
Дата: 04.05.20 13:30
Оценка:
Здравствуйте, jazzer, Вы писали:

J>Здравствуйте, koenjihyakkei, Вы писали:


K>>Можно сделать враппер


J>Да, я сейчас именно так и делаю (у меня это обобщенная функция call_with_cb(cb, f, *args, **kwargs), которая работает с любой функцией-генератором), но это внешнее, а хотелось бы что-то внутри самой функции написать


причем желательно, чтобы это "что-то" из исходного поста выглядело как вызов функции (а не как yield).

То есть чтобы человек, не видевший никогда в глаза yield, не испугался бы и думал, что он зовет коллбэк.
jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got
  If you always do  what you always did
Re: [Python] Функция двойного назначения (generator/callback)
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 05.05.20 21:17
Оценка: 1 (1)
Здравствуйте, jazzer, Вы писали:

J>(Сейчас складывается впечатление, что если в тексте функции есть yield, то функция автоматически становится генератором, независимо ни от чего вообще, даже если этот yield гарантированно ни разу не позовется)


Так и есть.
https://docs.python.org/2/reference/simple_stmts.html#the-yield-statement

Using a yield statement in a function definition is sufficient to cause that definition to create a generator function instead of a normal function.


Почему — в PEP 255 объяснили такое решение.
The God is real, unless declared integer.
Re[3]: [Python] Функция двойного назначения (generator/callback)
От: watchmaker  
Дата: 05.05.20 23:50
Оценка: 11 (2)
Здравствуйте, jazzer, Вы писали:

J>Здравствуйте, koenjihyakkei, Вы писали:


K>>Можно сделать враппер


J>Да, я сейчас именно так и делаю (у меня это обобщенная функция call_with_cb(cb, f, *args, **kwargs), которая работает с любой функцией-генератором), но это внешнее, а хотелось бы что-то внутри самой функции написать


Казалось бы, чтобы снаружи не писать call_with_cb, придумали декораторы:
  def with_optional_callback(callback_argument_name):
      def decorator(f):
          @functools.wraps(f)
          def wrapper(*args, **kwargs):
              cb = kwargs.pop(callback_argument_name, None)
              seq = f(*args, **kwargs)
              if cb is None:
                  return seq
              else:
                  for item in seq:
                      cb(item)
          return wrapper
      return decorator


  @with_optional_callback("cb")
  def f():
      x=1
      yield x
      x=2
      yield x



Теперь вызов f() вернёт генератор, а вызов f(cb=print) позовёт callback.
Re[4]: [Python] Функция двойного назначения (generator/callback)
От: jazzer Россия Skype: enerjazzer
Дата: 08.05.20 15:35
Оценка:
Здравствуйте, watchmaker, Вы писали:

W>Здравствуйте, jazzer, Вы писали:


J>>Здравствуйте, koenjihyakkei, Вы писали:


K>>>Можно сделать враппер


J>>Да, я сейчас именно так и делаю (у меня это обобщенная функция call_with_cb(cb, f, *args, **kwargs), которая работает с любой функцией-генератором), но это внешнее, а хотелось бы что-то внутри самой функции написать


W>Казалось бы, чтобы снаружи не писать call_with_cb, придумали декораторы


Да, у меня такое есть уже. Хотелось избавиться от yield, чтоб вместо yield был типа вызов коллбека.
Я так понимаю, это невозможно по дизайну языка, чисто потому, что есть inspect.isgeneratorfunction.
jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got
  If you always do  what you always did
Re[5]: [Python] Функция двойного назначения (generator/callback)
От: watchmaker  
Дата: 08.05.20 19:20
Оценка:
Здравствуйте, jazzer, Вы писали:


J>Да, у меня такое есть уже. Хотелось избавиться от yield, чтоб вместо yield был типа вызов коллбека.


J>Я так понимаю, это невозможно по дизайну языка, чисто потому, что есть inspect.isgeneratorfunction.


Проблема в другом: если это callback, то функция f может его куда-то передать (например, в недра 3rd-party библиотеки, которая не обязательно даже на чистом python написана). Аналогично, полученный генератор тоже можно куда-то передать (в недра другой 3rd-party библиотеки).
Соответственно, каждый вызов next() от генератора должен приводить к переключению из одной части несвязанного кода в другую и обратно. Можно сказать, что нужно иметь одновременно два стека вызовов. В одном потоке CPython так не умеет.
Но это легко делается либо в Stackless Python: нет стека — нет проблем :)
Либо это делается запуском функции в отдельном потоке: два потока — два стека.
(во втором случае, просто создаём очередь из одного элемента, запускаем функцию в потоке и в качестве коллбека передаём метод "положи в очередь", после выхода из функции очередь закрываем; в основном же потоке сразу же возвращаем генератор, который вычитывает элементы из очереди пока они не закончатся)
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.