Python的列表推导式与.NET LINQ比较

56
以下是简单的LINQ代码
string[] words = { "hello", "wonderful", "linq", "beautiful", "world" };

// Get only short words
var shortWords =
  from word in words
  where word.Length <= 5
  select word;

// Print each word out
shortWords.Dump();

可以使用列表推导式将其翻译为Python,如下所示。

words = ["hello", "wonderful", "linq", "beautiful", "world"]
shortWords = [x for x in words if len(x) <=5]
print shortWords
  • LINQ只是另一种实现列表推导的想法吗?
  • LINQ能做但列表推导无法完成的示例有哪些?

6
如果你在使用 C# 时感到懒惰,可以考虑写成这样:words.Where(w => w.Length < 5).Dump(); 这样做可以使代码更简洁易懂。 - Moberg
正如这个关于单子的史诗级解释所解释的那样,LINQ被精心设计为单子,因此可以以简单、安全和有效的方式构建。单子和函数式编程正在迅速增长并变得越来越重要。因此,我建议大家在他们的答案中涉及到这些内容。 - nealmcb
1
@nealmcb 在不支持共享可变状态的语言中,单子模式才是重要的,就像静态方法只在不支持一级函数的语言中有用一样。这是一种强制意识形态纯洁性的方式,允许编程范例做一些它不适合做的事情,只为了完整性。因此,单子模式永远不会增加重要性;它是一个绕过自我限制的支撑。泛型与动态类型是另一个例子。 - alcalde
@alcalde - 静态方法在支持一阶函数的语言中非常有用。我不知道你是怎么得出结论的。 - Davor
1
@Davor 一个没有数据的类是不合逻辑的;对象是数据和作用于它们的方法。静态方法只是为了在那些意识形态纯洁禁止这种事情的语言中允许独立函数而存在的“漏洞”。如果我可以在任何地方定义一等函数,我就不需要把它塞进一个类里;我可以编写任何使用静态方法完成的代码,而不会失去表达能力或简洁性。在这种情况/语言下,它们是无用的。 - alcalde
1
@alcalde - 如果你有一个一等函数只在一个类中需要,你不认为将其设置为私有静态并以此限制其可见性是有意义的吗? - Davor
4个回答

59

(警告:下面有一篇庞大的回答。我想,前面到第一个水平线的部分可以作为一个很好的 tl;dr 部分)

我不确定自己是否有资格成为 Python 大师……但我对 Python 中的迭代有着扎实的掌握,所以让我们来试试吧 :)

首先:据我所知,LINQ 查询是惰性执行的——如果是这样的话,生成器表达式就是更接近 Python 概念的东西了(无论如何,列表、字典和集合推导式在概念上只是被馈送到列表/字典/集合构造函数的生成器表达式!)。

此外,还存在一个概念上的区别:LINQ 用于查询数据结构,正如其名称所示。列表/字典/集合推导式是这个概念的可能应用(例如,过滤和投影列表项)。因此,它们实际上不太通用(正如我们将看到的,许多内置于 LINQ 中的东西并没有内置于它们中)。同样,生成器表达式是一种在原地形成一次性前向迭代器的方式(我喜欢把它看作是生成器函数的 lambda,只是没有一个丑陋而冗长的关键字 ;)),而不是描述复杂查询的方式。它们有重叠,是的,但它们并不相同。如果你想在 Python 中拥有 LINQ 的全部功能,你将不得不编写一个完整的生成器。或者结合内置的众多强大的生成器和 itertools


现在,Python的LINQ功能对应如下:

投射: (x.foo for ...)

过滤: (... if x.bar > 5)

  • 连接(x join y on x.foo equals y.bar)

最接近的事情可能是((x_item, next(y_item for y_item in y if x_item.foo == y_item.bar)) for x_item in x)

请注意,这不会迭代整个y以匹配每个x_item,它只会获取第一个匹配项。

  • 组连接(x join y on x.foo equals y.bar into g)

这更难。 Python没有匿名类型,虽然如果您不介意使用__dict__,则可以轻松完成:

class Anonymous(object):
    def __init__(self, **kwargs):
        self.__dict__ = kwargs

然后,我们可以执行 (Anonymous(x=x, y=y) for ...) 以获取一个对象列表,这些对象具有相应值的 xy 成员。 通常正确的做法是将结果提供给适当类的构造函数,比如 XY。

  • 分组(按 x.bar 分组 x.foo)

现在变得复杂了... 据我所知,没有内置的方法。但如果需要,我们可以自己定义它:

from collections import defaultdict

def group_by(iterable, group_func):
    groups = defaultdict(list)
    for item in iterable:
        groups[group_func(item)].append(item)
    return groups

例子:

>>> from operator import attrgetter
>>> group_by((x.foo for x in ...), attrgetter('bar'))
defaultdict(<class 'list'>, {some_value_of_bar: [x.foo of all x where x.bar == some_value_of_bar], some_other_value_of_bar: [...], ...})

这需要我们分组的任何内容都是可哈希的。虽然可以避免这种情况,但如果公众需要,我会尝试一下。但目前,我有点懒 :)

我们还可以通过在结果上调用.values()来返回一个没有我们分组的值的可迭代组,当然,我们可以将提供给list,以获取可以索引并多次迭代的内容。但谁知道我们是否需要组值...

  • 排序(按 x.foo 升序排列,y.bar降序排列)

排序需要特殊的语法吗?内置的sorted也适用于可迭代对象:sorted(x % 2 for x in range(10))sorted(x for x in xs, key=attrgetter('foo'))。默认升序排序,关键字参数reverse按降序排序。

不幸的是,据我所知,按多个属性进行排序并不容易,特别是当混合升序和降序时。嗯...这是一篇食谱的主题吗?

  • 中间变量(let tmp = x.foo)

不,列表推导式或生成器表达式中不可能实现这个功能 - 正如名称所示,它们应该是表达式(通常只跨越一两行)。但在生成器函数中完全可以实现:

(x * 2 for x in iterable)

使用中间变量重写为生成器:

def doubles(iterable):
    for x in iterable:
        times2 = x * 2
        yield times2

展开: (c for s in ("aa","bb") for c in s )


请注意,虽然LINQ to Objects处理委托,但其他查询提供程序(例如LINQ to SQL)可以处理表达式树,这些表达式树描述查询而不仅仅是呈现可执行的委托。这使得查询可以被翻译成SQL(或其他查询语言)-同样,我不知道Python是否支持这种功能。但这是LINQ的一个重要部分。
Python绝对不会做这样的事情。列表表达式与在(可能嵌套的)for循环中累积普通列表一一对应,生成器表达式与生成器一一对应。 鉴于解析器和ast模块,理论上可以编写一个库,将推导转换为例如SQL查询。但没有人关心这个。

2
为了使这些更具有“Pythonic”(即本地/惯用)的特性,请查看Python的collections.namedtupleitertools.groupby - Ghopper21
2
另外,对于展平操作:itertools.chain - Ghopper21

25

那么,需要区分一些不同的东西:

  • LINQ标准查询操作符
  • C#中的LINQ查询表达式
  • VB中的LINQ查询表达式

C#在查询表达式中支持的比VB少得多,但是以下是它支持的内容:

  • 投影 (select x.foo)
  • 过滤 (where x.bar > 5)
  • 连接 (x join y on x.foo equals y.bar)
  • 组合连接 (x join y on x.foo equals y.bar into g)
  • 分组 (group x.foo by x.bar)
  • 排序 (orderby x.foo ascending, y.bar descending)
  • 中间变量 (let tmp = x.foo)
  • 展平 (from x in y from z in x)

我不知道Python的列表推导式中有多少直接支持这些功能。

请注意,尽管LINQ to Objects处理委托,其他查询提供程序(例如LINQ to SQL)可以处理描述查询而不仅仅是呈现可执行委托的表达式树。 这允许查询被转换为SQL(或其他查询语言) - 再次声明,我不知道Python是否支持这种事情。 但它是LINQ的一个重要部分。


1
“你能做的,我能做得更好……”请参考Pynq(https://github.com/heynemann/pynq/wiki)。为什么不拿橙子和橙子比较呢?无论如何,列表推导式与LINQ不可比较,因为它们具有完全不同的目的。在LINQ强调表现力胜过性能的同时,列表推导式则强调性能胜过表现力。它们的工作方式类似于通过传递IEqualsComparator来覆盖C#中的.Equals()方法,只是你将一个生成器函数传递到迭代器(例如列表)的构造函数中。虽然我不是专家,但这在Python中是相当基础的东西。 - Evan Plaice
1
@EvanPlaice 列表推导式不是生成器,区别在于使用方括号和圆括号,请参见 https://dev59.com/AHVD5IYBdhLWcg3wOo9h。而在 .Net 中,Linq 总是在内存中,并且可以使用 yield 关键字生成(类似于生成器但没有单次使用限制)。 - yoel halb

17

通过使用asq Python 包,您可以轻松地在Python中执行大多数C#中使用LINQ-for-objects的操作。 使用asq,您的Python示例将变为:

from asq.initiators import query
words = ["hello", "wonderful", "linq", "beautiful", "world"]
shortWords = query(words).where(lambda x: len(x) <= 5)

4
我不是Python专家,但我认为Python实际上支持它们所有,因为你可以嵌套列表推导式并包含所有想要的lambda表达式。(如果列表推导式过于复杂,阅读起来会很困难...),但是它没有包括任何"特定的语法"来完成所有这些。
大多数功能可以使用以下方式重现: - 列表推导式或生成器 - lambda函数或内置函数(如filter()或map())或itertools模块中的函数
例如,如果你想复制以下行为:
  • 投影:这将是列表推导式的左部分...可以是单个值,也可以是元组。例如:[ (k,v) for k,v in my_dict.items() if k.startswith("abc"]。您还可以使用map()
  • 过滤:这将是if之后右侧的表达式。您还可以使用filter()
  • 排序:只需使用内置的sorted()
  • 分组聚合:使用内置的min()max()itertools.groupby()

关于连接展开,我认为您需要“手动完成”...

(随时查看Python快速参考非常好)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接