【问题标题】:How do I form multiple pipelines with generators?如何使用生成器形成多个管道?
【发布时间】:2018-02-06 00:34:12
【问题描述】:

我正在使用 python,并且正在尝试找到一种将多个生成器优雅地链接在一起的方法。问题的一个例子是有一个根生成器,它提供某种数据,并且每个值都像级联一样传递给它的“子级”,而级联又可以修改它们接收到的对象。我可以走这条路:

for x in gen1:
    gen2(x)
    gen3(x)

但它丑陋且不优雅。我正在考虑一种更实用的做事方式。

【问题讨论】:

    标签: python generator


    【解决方案1】:

    您可以将生成器转换为协程,以便它们可以send() 并从彼此接收值(使用(yield) 表达式)。这将使每个人都有机会更改他们收到的值,和/或将它们传递给下一个生成器/协程(或完全忽略它们)。

    请注意,在下面的示例代码中,我使用了一个名为 coroutine 的装饰器来“启动”生成器/协程函数。这会导致它们在第一个 yield 表达式/语句之前执行。这是 youtube 视频中显示的一个稍微修改的版本,该视频是 Dave Beazley 在 PyCon 2009 上发表的题为 A Curious Course on Coroutines and Concurrency 的非常有启发性的演讲。

    正如您应该能够从生成的输出中看到的那样,每个管道正在处理数据值,这些管道通过单个 send() 配置到头协程,然后有效地将其“多路复用”到每个管道。由于每个子协程也这样做,因此可以建立一个精心制作的进程“树”。

    import sys
    
    def coroutine(func):
        """ Decorator to "prime" generators used as coroutines. """
        def start(*args,**kwargs):
            cr = func(*args,**kwargs)  # Create coroutine generator function.
            next(cr)                   # Advance to just before its first yield.
            return cr
        return start
    
    def pipe(name, value, divisor, coroutines):
        """ Utility function to send values to list of coroutines. """
        print('  {}: {} is divisible by {}'.format(name, value, divisor))
        for cr in coroutines:
            cr.send(value)
    
    def this_func_name():
        """ Helper function that returns name of function calling it. """
        frame = sys._getframe(1)
        return frame.f_code.co_name
    
    
    @coroutine
    def gen1(*coroutines):
        while True:
            value = (yield)     # Receive values sent here via "send()".
            if value % 2 == 0:  # Only pipe even values.
                pipe(this_func_name(), value, 2, coroutines)
    
    @coroutine
    def gen2(*coroutines):
        while True:
            value = (yield)     # Receive values sent here via "send()".
            if value % 4 == 0:  # Only pipe values divisible by 4.
                pipe(this_func_name(), value, 4, coroutines)
    
    @coroutine
    def gen3(*coroutines):
        while True:
            value = (yield)     # Receive values sent here via "send()".
            if value % 6 == 0:  # Only pipe values divisible by 6.
                pipe(this_func_name(), value, 6, coroutines)
    
    # Create and link together some coroutine pipelines.
    g3 = gen3()
    g2 = gen2()
    g1 = gen1(g2, g3)
    
    # Send values through both pipelines (g1 -> g2, and g1 -> g3) of coroutines.
    for value in range(17):
        print('piping {}'.format(value))
        g1.send(value)
    

    输出:

    piping 0
      gen1: 0 is divisible by 2
      gen2: 0 is divisible by 4
      gen3: 0 is divisible by 6
    piping 1
    piping 2
      gen1: 2 is divisible by 2
    piping 3
    piping 4
      gen1: 4 is divisible by 2
      gen2: 4 is divisible by 4
    piping 5
    piping 6
      gen1: 6 is divisible by 2
      gen3: 6 is divisible by 6
    piping 7
    piping 8
      gen1: 8 is divisible by 2
      gen2: 8 is divisible by 4
    piping 9
    piping 10
      gen1: 10 is divisible by 2
    piping 11
    piping 12
      gen1: 12 is divisible by 2
      gen2: 12 is divisible by 4
      gen3: 12 is divisible by 6
    piping 13
    piping 14
      gen1: 14 is divisible by 2
    piping 15
    piping 16
      gen1: 16 is divisible by 2
      gen2: 16 is divisible by 4
    

    【讨论】:

      【解决方案2】:

      管道可能看起来更像这样:

      for x in gen3(gen2(gen1())):
          print x
      

      例如:

      for i, x in enumerate(range(10)):
          print i, x
      

      没有办法在 Python 中分叉(或“tee”)管道。如果您需要多个管道,则必须复制它们:gen2(gen1())gen3(gen1())

      【讨论】:

      • 我需要 gen1 向多个生成器提供数据。 gen1(gen2(gen3, gen4),gen5) 的顺序呢?
      • 我认为您必须创建多个管道:gen1(gen2(gen3(gen5())))gen1(gen2(gen4(gen5())))
      【解决方案3】:

      Dave Beazley gave this example in a talk he did in 2008。目标是总结在 Apache Web 服务器日志中传输了多少字节的数据。假设日志格式如:

      81.107.39.38 -  ... "GET /ply/ HTTP/1.1" 200 7587
      81.107.39.38 -  ... "GET /favicon.ico HTTP/1.1" 404 133
      81.107.39.38 -  ... "GET /admin HTTP/1.1" 403 -
      

      传统(非发电机)解决方案可能如下所示:

      with open("access-log") as wwwlog:
          total = 0
          for line in wwwlog:
              bytes_as_str = line.rsplit(None,1)[1]
              if bytes_as_str != '-':
                  total += int(bytes_as_str)
      print("Total: {}".format(total))
      

      为此使用生成器表达式的生成器管道可以可视化为:

      access-log => wwwlog => bytecolumn => bytes => sum() => total
      

      可能看起来像:

      with open("access-log") as wwwlog:
          bytecolumn = (line.rsplit(None,1)[1] for line in wwwlog)
          bytes = (int(x) for x in bytecolumn if x != '-')
      print("Total: {}".format(sum(bytes)))
      

      Dave Beazley 的幻灯片和更多示例可在 on his website 获取。 His later presentations elucidate this further

      如果不确切知道您要做什么,很难说更多,因此我们可以评估您正在做的每件事是否甚至需要自定义生成器(生成器表达式/理解可以在没有需要声明生成器函数)。

      【讨论】:

        【解决方案4】:

        下面是一个简洁的例子:

        def negate_nums(g):
            for x in g:
                yield -x
        
        def square_nums(g):
            for x in g:
                yield x ** 2
        
        def half_num(g):
            for x in g:
                yield x / 2.0
        
        def compose_gens(first_gen,*rest_gens):
            newg = first_gen(compose_gens(*rest_gens)) if rest_gens else first_gen
            return newg
        
        for x in compose_gens(negate_nums,square_nums,half_num,range(10)):
            print(x)
        

        在这里,您正在编写生成器,以便在最终的 compose_gens 调用中从右到左调用它们。您可以通过反转 args 将其更改为管道。

        【讨论】:

          猜你喜欢
          • 2021-09-25
          • 1970-01-01
          • 2020-07-16
          • 1970-01-01
          • 1970-01-01
          • 2016-12-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多