Using generator send() within a for loop

时间:2016-04-25 08:47:43

标签: python python-3.x generator python-3.5

I implemented graph traversal as a generator function which yields the node being visited.

Sometimes the user needs to tell the traversal function that the edges outgoing from a particular node shouldn't be followed; in order to support that, the traversal checks the value sent back to it (using generator send() method), and if it's True, regards the node as a leaf for traversal purposes.

The problem is that the simplest user loop is kinda long:

# 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

Is there any way to improve this?

I thought something like this should work:

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

but I feel I'm missing the knowledge of some python syntax.

7 个答案:

答案 0 :(得分:5)

I don't see a way to do this in a regular for loop. However, you could create another generator, that iterates another generator, using some "follow-function" to determine whether to follow the current element, thus encapsulating the tricky parts of your code into a separate function.

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)

答案 1 :(得分:3)

要简化客户端代码,您可以使用普通的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 -- Change StopIteration handling inside generators。您可以将其隐藏在辅助函数中:

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)

答案 2 :(得分:1)

我不认为这是一个常见的用例,将其视为原始生成器:

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)

答案 3 :(得分:0)

我发现我的问题会有一个单行的答案,使用扩展的"继续" earlier version of PEP 342中提出的陈述:

RewriteEngine On

RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)/$ /$1 [L,R=301]

RewriteCond %{REQUEST_URI} !(\.css|\.js|\.png|\.jpg|\.gif|robots\.txt)$ [NC]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !^/public/
RewriteRule ^(css|js|images)/(.*)$ public/$1/$2 [L,NC]

然而,当PEP 342被接受时,该特定功能在Raymond和Guido之间this June 2005 discussion之后被撤销:

  Raymond Hettinger说:

     
    

让我作为一个强大的-1记录下来,继续EXPR"。该     for循环是我们最基本的结构,很容易理解     现在的形式。对于"继续"同样可以说和"休息"哪一个     为人们提供近零学习曲线的额外优势     从其他语言迁移。

         

任何使这些基本陈述复杂化的冲动都应该是认真的     仔细检查并保持高标准的清晰度,可解释性,     显而易见,有用和必要。国际海事组织,它失败了大部分     测试

         

我不希望解释"继续EXPR"在教程中     并认为它会成为一个反特征。

  
     

[...]正确反对"继续EXPR"是那里   还没有用例;如果有一个好的用例,解释   会轻易跟进。

     

如果python核心开发人员已经改变了他们关于扩展"继续"的有用性的想法,也许这可以重新引入到未来的PEP中。但是,鉴于这个问题几乎完全相同的用例已在引用的帖子中讨论过,并且没有发现具有说服力,但似乎不太可能。

答案 4 :(得分:0)

这可能是该主题的问题的答案。

看看traversal函数和自定义send函数中其他空的yields语句,它可以完成神奇的工作。

# 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)

答案 5 :(得分:0)

我编写了一个small class SettableGenerator,它使用一种方法来接收要发送的值,然后在调用__next__时将其转发给实际的生成器。

您可以这样写:

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

答案 6 :(得分:0)

让我们考虑以下生成器。它生成从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并从中获取结果。

干杯!