2

我正在尝试对 Python 中的一组对象编写一个简单的查询,这在 C# 或 Ruby 中将是微不足道的和优雅的,但我很难在 Python 中使它优雅。我想我做错了什么。

在 C# 中:

list.Where(x => x.Foo > 10).Select(x => x.Bar).Where(x => x.Baz.StartsWith("/"))

这将创建一个枚举,包括为列表中的所有其他项目list[0].Bar提供list[0].Foo> 10 并以 等list[0].Bar.Baz开头。'/'数据从左到右清晰流动,右侧可以附加进一步的过滤/投影/聚合。

在红宝石中:

list.select { |x| x.foo > 10 }.map(&:bar).select { |x| x.baz.starts_with? '/' }

同样,从左到右的流程相当清晰,可以轻松附加进一步的操作。

但我在 Python 中的尝试似乎是倒退的,由内而外,而且通常很丑:

[x for x in (x.bar for x in (x for x in list if x.foo > 10)) if x.baz.startswith('/')]

现在我知道我可以在一个步骤中将地图和过滤器与列表理解结合起来,并且上面可以重写为:

[x.bar for x in list if x.foo > 10 and x.bar.baz.startswith('/')]

但这反而没有抓住重点。一方面,投影 x.bar 可能很昂贵,我不想对其进行两次评估;另一方面,投影和过滤只是我对流应用的两个潜在操作,我可以是排序、聚合、分页等,并不是所有的投影和过滤器都需要相邻,过滤器也不需要在投影之前应用而不是之后。

我是否试图将 Python 扭曲成它不是的东西?我通常会尽可能尝试以这种风格进行编程,无论是命令行(shell 管道)、C#、Ruby 还是 Java(比 Python 更痛苦)。我应该停止戳痛的地方吗?

4

2 回答 2

4

您可以使用生成器来生成bar值;你有一个不需要的生成器级别:

[bar for bar in (x.bar for x in somelist if x.foo > 10) if bar.baz.startswith('/')]

您可以先将该嵌套生成器分配给一个变量:

bars = (x.bar for x in somelist if x.foo > 10)
[bar for bar in bars if bar.baz.startswith('/')]

如果您想将内容保持在行长限制内。.bar生成器将只被使用一次,仅对 的每个元素访问一次昂贵的属性somelist

如果您想复制 C# 和 Ruby 代码的阅读顺序,您可以进一步执行此操作,方法是使用单独的生成器执行以下步骤:

filtered_on_foo = (x for x in somelist if x.foo > 10)
bar_selected = (x.bar for x in filtered_on_foo)
filtered_on_baz = [bar for bar in bar_selected if bar.baz.startswith('/')]

但是现在您通过单独选择会产生额外的循环。

于 2013-09-14T17:48:18.140 回答
1

实际上,我是 C# 开发人员,我非常喜欢 LINQ(虽然不如 Python 多:)),我一直想知道为什么没有 Python 版本的 LINQ。

但我从来没有时间正确检查这一点,因为我使用 Python 只是为了好玩。因此,在您提出问题之后,我开始搜索 Python 是否存在类似 LINQ 的东西(如果不存在这样的模块,我实际上正在考虑自己编写类似的东西)。

我认为这个很好 - LINQ to objects 和 Parallel LINQ to objects (ASQ) 的 Python 实现

对于您的情况,它可以像这样工作:

from asq.initiators import query

a = [{"foo":1, "bar": {"baz":"aaaa"}}, {"foo": 11, "bar": {"baz":"/ddddd"}}]

q = query(a).where(lambda x: x["foo"] > 10).select(lambda x: x["bar"]).where(lambda x: x['baz'].startswith('/'))

q.to_list()
# gives [{'foo': 11, 'bar': {'baz': '/ddddd'}}]

我发现唯一的缺点是不可能像这样格式化这个查询:

q = query(a).where(lambda x: x["foo"] > 10)
            .select(lambda x: x["bar"])
            .where(lambda x: x['baz'].startswith('/'))

您还可以以函数式样式进行此处理:

q = ifilter(lambda x: x["foo"] > 10, a)
q = imap(lambda x: x["bar"], q)
q = ifilter(lambda x: x["baz"].startswith('/'), q)
于 2013-09-14T18:56:23.903 回答