Использование генератора send() в цикле for

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

Иногда пользователю нужно сообщить функции обхода, что ребра, исходящие из определенного узла, не следует отслеживать; чтобы поддерживать это, обход проверяет значение, отправленное ему обратно (используя метод генератора send()), и, если это True, рассматривает узел как лист для целей обхода.

Проблема в том, что самый простой пользовательский цикл довольно длинный:

# simplified thanks to @tobias_k
# bfs is the traversal generator function
traversal = bfs(g, start_node)
try:
  n = next(traversal)
  while True:
    # process(n) returns True if don't want to follow edges out of n
    n = traversal.send(process(n))
except StopIteration:
    pass

Есть ли способ улучшить это?

Я думал, что что-то вроде этого должно работать:

for n in bfs(g, start_node):
  ???.send(process(n))

но я чувствую, что мне не хватает знаний о синтаксисе Python.


person max    schedule 25.04.2016    source источник
comment
Ну, вы можете сделать его намного короче, поместив try/except вне цикла; это сэкономит вам один набор try/except и условие if.   -  person tobias_k    schedule 25.04.2016
comment
@tobias_k исправлено, спасибо.   -  person max    schedule 25.04.2016


Ответы (7)


Я не вижу способа сделать это в обычном цикле for. Однако вы можете создать еще один генератор, который повторяет другой генератор, используя некоторую «функция следования», чтобы определить, следует ли следовать текущему элементу, тем самым инкапсулируя сложные части вашего кода в отдельную функцию.

def checking_generator(generator, follow_function):
    try:
      x = next(generator)
      while True:
        yield x
        x = generator.send(follow_function(x))
    except StopIteration:
        pass

for n in checking_generator(bfs(g, start_node), process):
    print(n)
person tobias_k    schedule 25.04.2016
comment
Это работает! Я предполагаю, что единственный недостаток, помимо необходимости создания дополнительной вспомогательной функции, заключается в том, что гипотетическая ???.send() может использоваться во многих местах цикла, заставляя цикл продолжаться. При таком подходе значение может быть отправлено только в самом конце цикла. Жаль, что у python нет синтаксиса для поддержки такого базового варианта использования. - person max; 25.04.2016
comment
@max вы можете отправить дополнительные значения в генератор, сохранив ссылку на исходное: traversal = bfs(g, start_node) ; for n in checking_generator(traversal,process): ... traversal.send(...) хотя в этом случае checking_generator все равно будет обрабатываться на основе последнего обработанного узла. - person Tadhg McDonald-Jensen; 26.04.2016
comment
@tobias_k, когда функция генерации завершается, вызывает StopIteration. Таким образом, обернуть весь код в try, чтобы подавить StopIteration и выйти из функции... которая затем вызывает StopIteration, кажется немного глупым. :) - person Tadhg McDonald-Jensen; 26.04.2016
comment
Вы должны обрабатывать StopIteration явно, иначе может произойти RuntimeError, если действует from __future__ import generator_stop. - person jfs; 26.04.2016
comment
@tobias_k был представлен в Python 3.5. См. ссылку на поддержку и код обходного пути в мом ответе. - person jfs; 26.04.2016
comment
@ J.F.Sebastian Спасибо за предупреждение. Вернулся к моей (немного более уродливой, но в данном случае, вероятно, более безопасной) первой версии. - person tobias_k; 26.04.2016

Я обнаружил, что на мой вопрос можно было бы ответить одной строкой, используя расширенный оператор «продолжить», предложенный в более ранняя версия PEP 342:

for n in bfs(g, start_node):
  continue process(n)

Однако, несмотря на то, что PEP 342 был принят, эта конкретная функция была отозвана после того, как -342" rel="noreferrer">это обсуждение в июне 2005 года между Рэймондом и Гвидо:

Рэймонд Хеттингер сказал:

Позвольте мне заявить, что я получил сильную -1 за «продолжить EXPR». Цикл for является нашей основной конструкцией, и его легко понять в его нынешнем виде. То же самое можно сказать о «продолжить» и «перерыв», которые имеют дополнительное преимущество почти нулевой кривой обучения для людей, переходящих с других языков.

Любое стремление усложнить эти базовые утверждения следует тщательно изучить и придерживаться высоких стандартов ясности, объяснимости, очевидности, полезности и необходимости. ИМО, он не проходит большинство этих тестов.

Я бы не стал с нетерпением ждать объяснения «продолжить EXPR» в учебнике и думаю, что это будет выделяться как анти-функция.

[...] Правильный аргумент против «продолжить EXPR» заключается в том, что вариантов использования еще нет; если бы был хороший вариант использования, объяснение последовало бы легко.

Гвидо

Если с тех пор разработчики ядра Python изменили свое мнение о полезности расширенного «продолжения», возможно, это можно было бы повторно ввести в будущий PEP. Но, учитывая почти идентичный вариант использования, поскольку этот вопрос уже обсуждался в цитируемой ветке и не был сочтен убедительным, это кажется маловероятным.

person max    schedule 01.05.2016
comment
это может быть generator.send_wait или что-то в этом роде вместо continue generator. это решило бы многие проблемы. - person Jules G.M.; 04.07.2021
comment
Проблемы связаны с не усложнением продолжения, а не с функциональностью отправки без следующего - person Jules G.M.; 04.07.2021

Для упрощения клиентского кода можно использовать обычный генератор bsf() и проверять в нем атрибут node.isleaf:

 for node in bfs(g, start_node):
     node.isleaf = process(node) # don't follow if `process()` returns True

Недостатком является то, что node является изменяемым. Или вам нужно передать общую структуру данных, которая отслеживает конечные узлы: leaf[node] = process(node) где leaf словарь передается в bfs() ранее.

Если вы хотите явно использовать метод .send(); вы должны справиться с StopIteration. См. PEP 479 -- Изменение обработки StopIteration внутри генераторов. Вы можете скрыть это во вспомогательной функции:

def traverse(tree_generator, visitor):
    try:
        node = next(tree_generator)
        while True:
             node = tree_generator.send(visitor(node))
    except StopIteration:
        pass

Пример:

traverse(bfs(g, start_node), process)
person jfs    schedule 26.04.2016

Я не считаю это частым вариантом использования, считайте это исходным генератором:

def original_gen():
    for x in range(10):
        should_break = yield x
        if should_break:
            break

Если значение should_break всегда вычисляется на основе вызова какой-либо функции с x, то почему бы просто не написать генератор следующим образом:

def processing_gen(check_f):
    for x in range(10):
        yield x
        should_break = check_f(x)
        if should_break:
            break

Однако я обычно думаю о коде, который обрабатывает сгенерированные значения, как о написанном внутри цикла (иначе какой смысл вообще иметь цикл?)

На самом деле кажется, что вы хотите создать генератор, в котором вызов метода __next__ действительно подразумевает send(process(LAST_VALUE)), который можно реализовать с помощью класса:

class Followup_generator(): #feel free to use a better name
    def __init__(self,generator,following_function):
        self.gen = generator
        self.process_f = following_function
    def __iter__(self):
        return self
    def __next__(self):
        if hasattr(self,"last_value"):
            return self.send(self.process_f(self.last_value))
        else:
            self.last_value = next(self.gen)
            return self.last_value
    def send(self,arg):
        self.last_value = self.gen.send(arg)
        return self.last_value
    def __getattr__(self,attr):
        "forward other lookups to the generator (.throw etc.)"
        return getattr(self.gen, attr) 

# call signature is the exact same as @tobias_k's checking_generator
traversal = Followup_generator(bfs(g, start_node), process)
for n in traversal: 
    print(n)
    n = traversal.send(DATA) #you'd be able to send extra values to it

Однако это по-прежнему не так часто используется, я бы прекрасно справился с циклом while, хотя я бы поместил вызов .send вверху:

traversal = bfs(g, start_node)
send_value = None
while True:
    n = traversal.send(send_value)
    #code for loop, ending in calculating the next send_value
    send_value = process(n)

И вы можете обернуть это в try: ... except StopIteration:pass, хотя я считаю, что простое ожидание возникновения ошибки лучше выражается с помощью менеджера контекста:

class Catch:
    def __init__(self,exc_type):
        if issubclass(exc_type,BaseException):
            self.catch_type = exc_type
        else:
            raise TypeError("can only catch Exceptions")
    def __enter__(self):
        return self
    def __exit__(self,exc_type,err, tb):
        if issubclass(exc_type, self.catch_type):
            self.err = err
            return True


with Catch(StopIteration):
    traversal = bfs(g, start_node)
    send_value = None
    while True:
        n = traversal.send(send_value)
        #code for loop, ending in calculating the next send_value
        send_value = process(n)
person Tadhg McDonald-Jensen    schedule 26.04.2016

Вероятно, это ответ на вопрос из темы ветки.

Взгляните на дополнительные пустые операторы yields внутри функции traversal и пользовательской функции send, которые делают волшебную работу.

# tested with Python 3.7

def traversal(n):
    for i in range(n):
        yield i, '%s[%s] %s' % (' ' * (4 - n), n, i)
        stop = yield
        if stop:
            yield  # here's the first part of the magic
        else:
            yield  # the same as above
            yield from traversal(int(n / 2))


def send(generator, value):
    next(generator)   # here's the second part of the magic
    generator.send(value)


g = traversal(4)

for i, (num, msg) in enumerate(g):
    print('>', i, msg)
    stop = num % 2 == 0
    send(g, stop)
person Tomasz Kurgan    schedule 28.03.2020

Я написал небольшой класс SettableGenerator, который использует метод для получения значения для отправки, а затем пересылает его фактическому генератору при вызове __next__.

При этом вы можете написать:

gen = SettableGenerator(bfs(g, start_node))
for n in gen:
  gen.set(process(n))
person unique2    schedule 08.06.2020

Рассмотрим следующий генератор. Он генерирует числа от 0 до 9. Для каждого сгенерированного числа он получает ввод и сохраняет его в ret:

def count_to_nine():
    # Output: numbers from 0 to 9
    # Input: converted numbers
    ret = []
    for i in range(10):
        # Yield a number, get something back
        val = (yield i)
        # Remember that "something"
        ret.append(val)
    return ret

Вы действительно можете выполнить итерацию, используя next() + send(), но лучше всего итерировать, используя только send():

g = count_to_nine()
value = None  # to make sure that the first send() gives a None
while True:
    value = g.send(value)  # send the previously generated value, get a new one
    value = f'#{value}'

Вот результат:

StopIteration: ['#0', '#1', '#2', '#3', '#4', '#5', '#6', '#7', '#8', '# 9']

Если вам нужен этот вывод, поймайте StopIteration и получите от него результат.

Ваше здоровье!

person kolypto    schedule 03.09.2020