Python 中链式列表表达式的正确样式



我正在尝试对 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'/' 开头,依此类推,用于列表中的所有其他项目。数据从左到右清晰地流动,右侧可以附加进一步的过滤/投影/聚合。

在 Ruby 中:

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更痛苦)。我应该停止戳它疼的地方吗?

您可以使用生成器生成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('/')]

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

如果要复制 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('/')]

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

实际上我是

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

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

我认为这个很好 - LINQ to 对象和 Parallel LINQ to 对象 (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)

相关内容

  • 没有找到相关文章

最新更新