Python 列表推导与 .NET LINQ

56 投票
4 回答
19889 浏览
提问于 2025-04-16 05:25

下面这段简单的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可以做到,但列表推导式做不到的呢?

4 个回答

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)
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的一个重要部分。

59

(警告:接下来是个大长篇。第一部分到第一条横线的内容可以算作简要概述,应该还不错)

我不确定自己是否算得上Python高手……但我对Python中的迭代有一定了解,所以我们来试试吧 :)

首先:据我所知,LINQ查询是懒执行的——如果是这样的话,生成器表达式在Python中更接近这个概念(无论如何,列表、字典和集合推导本质上就是将生成器表达式传递给列表、字典或集合构造函数!)。

另外,还有一个概念上的区别:LINQ是用来查询数据结构的,正如名字所示。列表、字典和集合推导是这种查询的应用(例如,过滤和投影列表中的项目)。所以它们实际上不够通用(正如我们将看到的,很多LINQ内置的功能在这些推导中并不存在)。同样,生成器表达式是一种在本地创建一次性前向迭代器的方法(我喜欢把它看作是生成器函数的lambda,只是没有那么丑、那么长的关键字 ;)),而不是描述复杂查询的方式。它们有重叠,但并不相同。如果你想在Python中使用LINQ的所有功能,你需要写一个完整的生成器,或者结合使用内置的和在itertools中的众多强大生成器。


现在,Python中与Jon Skeet提到的LINQ功能对应的内容:

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

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

  • 连接(x连接y,条件是x.foo等于y.bar)

最接近的写法可能是((x_item, next(y_item for y_item in y if x_item.foo == y_item.bar)) for x_item in x)吧。

注意,这不会对每个x_item遍历整个y,只会获取第一个匹配项。

  • 分组连接(x连接y,条件是x.foo等于y.bar,结果放入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循环中累积一个普通列表一一对应,生成器表达式与生成器一一对应。考虑到parserast模块,理论上可以写一个库,将推导式转换为例如SQL查询。但没有人愿意去做。

撰写回答