Pythonic方法链接python生成器函数以形成管道

时间:2016-08-04 00:29:37

标签: python generator

我正在使用python进行管道代码重构。

假设我们有一系列生成器函数,我们希望将它们链接起来形成数据处理管道。

示例:

#!/usr/bin/python
import itertools

def foo1(g):
    for i in g:
        yield i + 1

def foo2(g):
    for i in g:
        yield 10 + i

def foo3(g):
    for i in g:
        yield 'foo3:' + str(i)

res = foo3(foo2(foo1(range(0, 5))))

for i in res:
    print i

输出:

foo3:11
foo3:12
foo3:13
foo3:14
foo3:15

我认为foo3(foo2(foo1(range(0, 5))))不是实现我的管道目标的pythonic方式。特别是当管道中的阶段数量很大时。

我希望我能像jquery中的链一样重写它。类似于:

range(0, 5).foo1().foo2().foo3()

或者

l = [range(0, 5), foo1, foo2, foo3]
res = runner.run(l)

但我是生成器主题的新手,无法找到实现此目的的方法。

欢迎任何帮助。

5 个答案:

答案 0 :(得分:2)

跟进你的runner.run方法,让我们定义这个效用函数:

def recur(ops):
    return ops[0](recur(ops[1:])) if len(ops)>1 else ops[0]

举个例子:

>>> ops = foo3, foo2, foo1, range(0, 5)
>>> list( recur(ops) )
['foo3:11', 'foo3:12', 'foo3:13', 'foo3:14', 'foo3:15']

替代方案:向后排序

def backw(ops):
    return ops[-1](backw(ops[:-1])) if len(ops)>1 else ops[0]

例如:

>>> list( backw([range(0, 5), foo1, foo2, foo3]) )
['foo3:11', 'foo3:12', 'foo3:13', 'foo3:14', 'foo3:15']

答案 1 :(得分:2)

您可以使用PyMonad编写咖喱生成器函数:

def main():
    odds = list * \
         non_divisibles(2) * \
         lengths * \
         Just(["1", "22", "333", "4444", "55555"])
    print(odds.getValue())    #prints [1, 3, 5]


@curry
def lengths(words: Iterable[Sized]) -> Iterable[int]:
    return map(len, words)


@curry
def non_divisibles(div: int, numbers: Iterable[int]) -> Iterable[int]:
    return (n for n in numbers if n % div)

另一种替代方法是从Monad开始,并使用fmap调用组合生成器-Java 8 Stream用户熟悉此语法:

def main():
    odds = Just(["1", "22", "333", "4444", "55555"]) \
        .fmap(lengths) \
        .fmap(non_divisibles(2)) \
        .fmap(list) \
        .getValue()
    print(odds)   #prints [1, 3, 5]


def lengths(words: Iterable[Sized]) -> Iterable[int]:
    return map(len, words)


@curry
def non_divisibles(div: int, numbers: Iterable[int]) -> Iterable[int]:
    return (n for n in numbers if n % div)

请注意,在这种情况下,无需使用@curry修饰函数。直到终端getValue()调用时,才评估整个转换链。

答案 2 :(得分:0)

如果您的示例中的函数是一次性(或一次性使用)函数,则这是另一个答案。一些不错的变量命名和生成器表达式的使用可以帮助小型操作。

>>> g = range(0, 5)
>>> foo1 = (x+1 for x in g)
>>> foo2 = (x+10 for x in foo1)
>>> foo3 = ('foo3:' + str(x) for x in foo2)
>>> for x in foo3:
...     print x
...
foo3:11
foo3:12
foo3:13
foo3:14
foo3:15

答案 3 :(得分:0)

我不认为foo3(foo2(foo1(range(0,5))))是实现管道目标的一种Python方法。尤其是当管道中的阶段数很大时。

链接生成器的方法很简单,我认为很清楚:将每个结果分配给一个变量,每个变量都可以有一个描述性名称。

range_iter = range(0, 5)
foo1_iter = foo1(range_iter)
foo2_iter = foo2(foo1_iter)
foo3_iter = foo3(foo2_iter)

for i in foo3_iter:
  print(i)

相对于使用高阶函数的东西,例如reduce或类似的内容:

  • 在我的实际情况下,每个foo *生成器函数通常都需要其自己的其他参数,如果使用reduce,这会很棘手。

  • 在我的实际情况下,管道中的步骤在运行时不是动态的:(对于我来说)似乎更适合动态情况的模式似乎有点奇怪/意外。

  • 这与通常在将每个函数显式调用的地方编写常规函数的方式以及将每个结果传递给下一个调用的方式有点不一致。是的,我猜有点重复,但是我很高兴对“调用函数”进行重复,因为(对我来说)这很清楚。

  • 无需导入:它使用核心语言功能。

答案 4 :(得分:0)

对于未来的读者:另一个非常pythonic的解决方案(恕我直言):

steps = [
    foo1, 
    foo2, 
    foo3
    ]

res = range(0, 5)
for step in steps:
    res = step(res)

for i in res:
    print(i)

foo3:11
foo3:12
foo3:13
foo3:14
foo3:15

这本质上与 functools.reduce 一样,就像在 maxymoo 的回答中一样。生成器的惰性允许这种简单的公式化而无需 functools。