【问题标题】:Expressive way compose generators in Python表达方式在 Python 中编写生成器
【发布时间】:2018-01-12 19:07:08
【问题描述】:

我真的很喜欢 Python 生成器。特别是,我发现它们正是连接到 Rest 端点的正确工具——我的客户端代码只需要在连接端点的生成器上进行迭代。但是,我发现 Python 的生成器没有我想要的那样富有表现力。通常,我需要过滤从端点获得的数据。在我当前的代码中,我将一个谓词函数传递给生成器,它将谓词应用于它正在处理的数据,并且仅当谓词为 True 时才产生数据。

我想转向生成器的组合 - 例如 data_filter(datasource( ))。这是一些演示代码,显示了我尝试过的内容。很清楚为什么它不起作用,我想弄清楚的是什么是最有表现力的解决方案:

# Mock of Rest Endpoint: In actual code, generator is 
# connected to a Rest endpoint which returns dictionary(from JSON).
def mock_datasource ():
    mock_data = ["sanctuary", "movement", "liberty", "seminar",
                 "formula","short-circuit", "generate", "comedy"]
    for d in mock_data:
        yield d

# Mock of a filter: simplification, in reality I am filtering on some
# aspect of the data, like data['type'] == "external" 
def data_filter (d):
    if len(d) < 8:
        yield d

# First Try:
# for w in data_filter(mock_datasource()):
#     print(w)
# >> TypeError: object of type 'generator' has no len()

# Second Try 
# for w in (data_filter(d) for d in mock_datasource()):
#     print(w)
# I don't get words out, 
# rather <generator object data_filter at 0x101106a40>

# Using a predicate to filter works, but is not the expressive 
# composition I am after
for w in (d for d in mock_datasource() if len(d) < 8):
    print(w)

【问题讨论】:

  • 你觉得内置的filter()怎么样?
  • 好建议 - 如果我使用谓词函数,我会编写 filter(data_predicate, mock_datasource())。但是,我确实更喜欢可以编写生成组合的方法,例如 f(g(x))
  • @Kevin 在这种情况下 filter 调用 lambda 现在你有一个笨拙的表达式。 filter 在过滤功能已经存在时很好(比如str.isdigitNone 用于测试真值等,
  • @Jean-FrançoisFabre,同意,filter 是“有时”的解决方案。这就是为什么我没有努力围绕它建立一个完整的答案:-P
  • filter 在 python 2 中对字符串非常有用,因为它节省了对 str.join 的需求。现在快乐消失了:)

标签: python generator function-composition


【解决方案1】:

data_filter 应将len 应用于d元素 而不是d 本身,如下所示:

def data_filter (d):
    for x in d:
        if len(x) < 8:
            yield x

现在你的代码:

for w in data_filter(mock_datasource()):
    print(w)

返回

liberty
seminar
formula
comedy

【讨论】:

  • 谢谢,这似乎让我最接近我所要求的。话虽如此,我想知道组合生成器是否会带来我没有考虑的性能成本。
  • 是的,你链接的函数/生成器调用越多,你的应用程序就会越慢。在 python 中调用函数比在编译语言中更昂贵,部分原因是编译语言能够内联一些调用。
  • 到目前为止,在测试比较过滤与谓词的执行时间与使用组合生成器过滤(即基于您的答案)时,我没有看到组合方法的巨大性能损失。通常情况下,需要进行更多的测试。“第一条原则是,你不能欺骗自己,你是最容易被欺骗的人。”理查德·费曼
  • 确实如此。您使用相关的数据大小(大小和内容)为您的各种方法提供了很多基准。
【解决方案2】:

更简洁地说,您可以直接使用生成器表达式来做到这一点:

def length_filter(d, minlen=0, maxlen=8):
    return (x for x in d if minlen <= len(x) < maxlen)

像常规函数一样将过滤器应用于您的生成器:

for element in length_filter(endpoint_data()):
    ...

如果你的谓词真的很简单,内置函数filter 也可能满足你的需求。

【讨论】:

    【解决方案3】:

    您可以传递您为每个项目应用的过滤器函数:

    def mock_datasource(filter_function):
        mock_data = ["sanctuary", "movement", "liberty", "seminar",
                 "formula","short-circuit", "generate", "comedy"]
    
        for d in mock_data:
            yield filter_function(d)
    
    def filter_function(d):
        # filter
        return filtered_data
    

    【讨论】:

    • 对 - 您建议的方法类似于我正在使用的代码。我试图将过滤器放在数据源的输出端。我想将过滤器完全从生成器的代码中提取出来。我最接近的是在我给出的最后一个示例中使用谓词。无论如何感谢您的建议!
    【解决方案4】:

    我要做的是定义filter(data_filter) 接收一个生成器作为输入,并返回一个生成器,其值由data_filter 谓词(常规谓词,不知道生成器接口)过滤。

    代码是:

    def filter(pred):
        """Filter, for composition with generators that take coll as an argument."""
        def generator(coll):
            for x in coll:
                if pred(x):
                    yield x
        return generator
    
    def mock_datasource ():
        mock_data = ["sanctuary", "movement", "liberty", "seminar",
                     "formula","short-circuit", "generate", "comedy"]
        for d in mock_data:
            yield d
    
    def data_filter (d):
        if len(d) < 8:
            return True
    
    
    gen1 = mock_datasource()
    filtering = filter(data_filter)
    gen2 = filtering(gen1) # or filter(data_filter)(mock_datasource())
    
    print(list(gen2)) 
    

    如果您想进一步改进,可以使用compose,这是我认为的全部意图:

    from functools import reduce
    
    def compose(*fns):
        """Compose functions left to right - allows generators to compose with same
        order as Clojure style transducers in first argument to transduce."""
        return reduce(lambda f,g: lambda *x, **kw: g(f(*x, **kw)), fns)
    
    gen_factory = compose(mock_datasource, 
                          filter(data_filter))
    gen = gen_factory()
    
    print(list(gen))
    

    PS:我使用了一些在here 找到的代码,Clojure 人在其中表达了生成器的组合,灵感来自于他们通常使用转换器进行组合的方式。 PS2:filter 可以写成更 Python 的方式:

    def filter(pred):
        """Filter, for composition with generators that take coll as an argument."""
        return lambda coll: (x for x in coll if pred(x))
    

    【讨论】:

      【解决方案5】:

      这是我一直用来组合生成器的函数。

      def compose(*funcs):
          """ Compose generators together to make a pipeline.
          e.g.
              pipe = compose(func1, func2, func3)
              result = pipe(range(0, 5))
          """
          return lambda x: reduce(lambda f, g: g(f), list(funcs), x)
      

      funcs 是生成器函数的列表。所以你的例子看起来像

      pipe = compose(mock_datasource, data_filter)
      print(list(pipe))
      

      This is not original

      【讨论】:

        猜你喜欢
        • 2017-12-08
        • 2014-04-02
        • 2021-12-27
        • 1970-01-01
        • 1970-01-01
        • 2016-10-12
        • 2021-10-17
        • 2021-04-26
        • 2012-08-20
        相关资源
        最近更新 更多