组成包含分支逻辑的生成器函数而不会丢失生成器状态

时间:2019-06-04 13:10:24

标签: python functional-programming

我有一个从模块化功能集合构建数据管道的过程。

其中一个功能是 switch ,它应该允许根据数据内容执行不同的功能。

在下面的代码中,这是switcheroo函数,该函数执行测试(通过字典查找),并尝试将适当的函数插入生成器管道。

def switcheroo(x, field, s_dict, default):
    for content in x:
        yield next(s_dict.get(content[field], default)([content]))

我可以成功运行下面的代码,这样做会生成3个文件-但是,test_a.txt文件应该包含两个结果。相反,它仅包含一个,因为save函数/生成器将丢失其占位符,并在每次调用时从头开始重新评估-在这种情况下,将重新从头开始打开文件。

如果我在没有switcheroo的情况下运行类似的管道,则save生成器将保留其内部状态,并将多行保存到一个文件中。

我尝试了其他方法来组成switcheroo函数,但是我的约束是我需要将其与我拥有的所有其他函数一起组合到一个要迭代的单个pipeline生成器中在运行时结束。

其他限制是我想保持我所有功能的模块化,以便可以按任何顺序组合它们。


from collections import OrderedDict
from functools import partial, reduce

data_source = [ OrderedDict({"id" : "1", "name" : "Tom",      "sync" : "a" }),
                OrderedDict({"id" : "2", "name" : "Steve",    "sync" : "a" }),
                OrderedDict({"id" : "3", "name" : "Ulrich",   "sync" : "b" }),
                OrderedDict({"id" : "4", "name" : "Victor",   "sync" : "b" }),
                OrderedDict({"id" : "5", "name" : "Wolfgang", "sync" : "c" }),
                OrderedDict({"id" : "6", "name" : "Xavier",   "sync" : "c" }),
                OrderedDict({"id" : "7", "name" : "Yves",     "sync" : "c" }),
                OrderedDict({"id" : "8", "name" : "Zaphod",   "sync" : "d" }),
               OrderedDict({ "id" : "9", "name" : "Albert",   "sync" : "d" })]


def od_to_str(od):
    return ",".join((str(v) for v in od.values()))

def filt(x, field, filt):
    for content in x:
        if content[field] in filt:
            yield content

def save(x, filename):
    with open(filename, "w") as out:
        for content in x:
            out.write(od_to_str(content)+"\n")
            yield content

p_save_a = partial(save, filename="test_a.txt")
p_save_b = partial(save, filename="test_b.txt")
p_save_c = partial(save, filename="test_c.txt")
p_save_d = partial(save, filename="test_d.txt")

switch_d = { "a" : p_save_a, 
             "b" : p_save_b, 
             "c" : p_save_c, 
             "d" : p_save_d, 
           }

def switcheroo(x, field, s_dict, default):
    for content in x:
        yield next(s_dict.get(content[field], default)([content]))

p_filt=partial(filt, field="name", filt=["Tom", "Steve", "Victor", "Xavier"])
p_switcheroo = partial(switcheroo, field="sync", s_dict=switch_d, default=lambda x : x)

dsc=[d.copy() for d in data_source.copy()]
iterator=(d for d in dsc)

def compose(*functions):
    return lambda x: reduce(lambda λ, f : f(λ), functions, x)

pipeline = [p_filt, p_switcheroo]

funcs = [p_filt, p_switcheroo]
pipeline=compose(*funcs)

for result in pipeline(iterator):
    print (result)

作为参考,以上代码生成了3个文件,其内容应为:

test_a.txt

1,Tom,a
2,Steve,a

test_b.txt

4,Victor,b

test_c.txt

6,Xavier,c

但是,在 test_a.txt 中只有

2,Steve,a

由于save函数从一开始就被评估了两次。即每次将记录插入文件时,它都会从头开始重新创建文件。因此,保存了“ Tom”记录,但是当重新运行文件设置代码时,“ Steve”记录将覆盖它。

我想要做的是,这种初始设置只有一次按照正常的生成/屈服模式进行。

我可以将文件设置为“追加”,但是我对保留生成器模式比对序列化的细节更感兴趣-我想以一个有效的 pattern 作为结束我可以放心地应用于以不同组合排列的任意模块化组件集合。就线性管道而言,我已经做到了。但是,这种分支功能(管道变成树状结构)正在将扳手投入工作。

2 个答案:

答案 0 :(得分:1)

基于chepner的评论:

def dup(src):  # to allow "peeking"
  for x in src: yield x; yield x
def switcheroo(x, field, s_dict, default):
  # Make generators that share x:
  x=dup(x)
  d={k:v(x) for k,v in s_dict.items()}
  for content in x:  # peek
    yield next(d.get(content[field], default))

请注意,default现在必须本身就是一个生成器,并且从s_dict生成的每个生成器必须表现良好,就其在每次收益之前从其输入流中读取一次的意义而言。可以使用itertools.tee代替dup;虽然不那么骇人听闻,但却无法放松任何假设。

答案 1 :(得分:1)

嗯。

您的代码不起作用,因为您尝试将迭代迭代到更多内容。需要一些推动。

考虑一下,当switcheroo两次执行相同的save *功能时会发生什么。它将调用这些函数两次(从而创建两个生成器,每个生成器在其中打开用于写入的文件并覆盖前一个生成器)。是的,您可以存储它们,但是然后遇到第二个问题-您已经传递了数据,p_save_a会在该数据上进行迭代([content])。您无法扩展它,当您使用简单的列表对象遇到第二个p_save_a调用时,您需要一个包装器。

如果您想要一些可行的方法,请尝试以下操作:

def switcheroo(x, field, s_dict, default):
    iterators = {}
    class placeholder:
        def __init__(self):
            self.values = []
        def append(self, v):
            self.values.append(v)
        def __iter__(self):
            return self
        def __next__(self):
            if not self.values:
                raise StopIteration
            v = self.values.pop(0)
            return v
    for content in x:
        val = content[field]
        try:
            iter, iter_val = iterators[val]
        except Exception:
            iter_val = placeholder()
            iter = s_dict.get(val, default)(iter_val)
            iterators[val] = iter, iter_val
        iter_val.append(content)
        yield next(iter)

placeholder类充当代理通信服务,是一个向量,可以在迭代过程中进行迭代和扩展。当然,这是糟糕的性能,但这只是概念的证明。