代码改变世界

IronPython and LINQ to Objects (II): LINQ 构建块

2010-05-03 15:48  liangshi  阅读(1575)  评论(0编辑  收藏  举报

在第一篇文章中,我讨论了如何用IronPython来模拟C#的语言扩展。在这篇文章中,我将进一步讨论如何用IronPython来构造LINQ查询。如果您读过《LINQ in Action》,您会发现我是依据此书来组织本系列文章的。我的第一篇文章对应《LINQ in Action》的第2章“C#和VB的语言增强”,本文对应第3章“LINQ构建块”。

  

1. IEnumerable<T>

在意图上,LINQ to Objects旨在查询内存中的数据源;在技术上,这些数据源是实现了System.Collections.Generic.IEnumerable<T>接口的对象。C#程序员常常使用.NET Framework提供了泛型容器作为数据源对象,它们位于System.Collections.Generic名空间下并实现了IEnumerable<T>。那么,IronPython程序员经常使用Python标准容器,包括列表(list)、元组(tuple)、集合(set)和字典(dict),实现了IEnumerable<T>吗?在IronPython 2.x的命令行上运行如下语句,就可以知道答案。

 

>>> import System

>>> ie_object = System.Collections.Generic.IEnumerable[System.Object]

>>> isinstance(list(),ie_object)

True

>>> isinstance(tuple(), ie_object)

True

>>> isinstance(set(), ie_object)

True

>>> isinstance(dict(), ie_object)

False

 

由运行结果可知,list、tuple和set实现了IEnumerable<Object>,而dict没有实现。实际上,C#也面临相似的问题。在.NET Framework中存在大量没有实现IEnumerable<T>的非泛型容器,程序员也会实现自定义的容器。如何将这些容器方便地配接(adapt)到IEnumerable<T>呢?C#设计者给出的答案是迭代器。

  

2. 迭代器(Iterator

迭代器是一个用于遍历集合元素的对象。由于这是一种非常有用的设计模式,.NET Framework提供了迭代器接口IEnumerable(以及相应的范型接口IEnumerable<T>),C#语言则提供了关键字yield,以便直接构造实现了IEnumerable的迭代器类型。利用迭代器,C#程序就可以将非泛型容器、自定义容器和序列配接到IEnumerable<T>上。例如,Enumerable为非泛型容器提供了扩展方法Cast,它的一种可能实现如下。

 

  1: public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source)

  2: {

  3:     foreach (object elem in source)

  4:     {

  5:         yield return (TResult)elem;

  6:     }

  7: }

 

在该实现中,Cast利用foreach迭代非泛型容器,将其中的元素强制转换为目标类型TResult的对象,然后用yield构造出实现了IEnumerable<T>的迭代器。这段代码简单明了,也很容易应用到其他需要IEnumerable<T>的情景中。

 

与C#类似,Python提供了关键字yield用于生成迭代器。此外,Python还提供迭代器的另一种形式:生成器(Generator)。利用它们可以将IronPython中的dict和自定义序列配接到IEnumerable<T>。在IronPython 2.x的命令行上运行如下语句,可知yield和生成器所返回的对象都实现了IEnumerable<Object>。

 

  1: >>> import System

  2: >>> ie_object = System.Collections.Generic.IEnumerable[System.Object]

  3: >>> def seq(num):

  4: ...     for i in range(num):

  5: ...         yield i

  6: ...

  7: >>> isinstance(seq(1), ie_object)

  8: True

  9: >>> isinstance((i for i in range(1)) # (i for i in range(1)) is a generator,

 10: ...                                  # whose result is the same with seq(1).

 11: ... , ie_object)

 12: True

 

3. 查询操作符(Query Operator

LINQ to Objects的查询操作符是定义在System.Linq.Enumerable类中的扩展方法。其签名形如:

 

  1: public static int Count<TSource>

  2:     (this IEnumerable<TSource> source);

  3:

  4: public static IEnumerable<TResult> Select<TSource, TResult>

  5:     (this IEnumerable<TSource> source, Func<TSource, TResult> selector);

  6:

  7: public static IEnumerable<TSource> Where<TSource>

  8:     (this IEnumerable<TSource> source, Func<TSource, bool> predicate);

 

由于它们都是泛型函数,IronPython在调用它们时必须指定TSource等类型参数的具体类型。Harry Peirson在他的IronPython and Linq to XML中提供了一组辅助函数来简化IronPython的调用代码。

 

  1: def Count(col):

  2:     return Enumerable.Count[object](col)

  3:      

  4: def Select(col, fun):

  5:     return Enumerable.Select[object, object](col, Func[object, object](fun))

  6:    

  7: def Where(col, fun):

  8:     return Enumerable.Where[object](col, Func[object, bool](fun))

 

以辅助方法Where(col, fun)为例,它将Enumerable.Where的类型参数TSource指定为object(即System.Object),也就是将该函数的第一个参数具现为IEnumerable<Object>,这样就可以将IronPython中的容器和生成器传递给该参数。然后它把IronPython中的函子fun包装为System.Func的对象,并将该对象传递给Enumerable.Where的第二个参数。这样包装的原因是,Enumerable.Where只接受Func对象,而不接受IronPython定义的函数和lambda表达式。

 

有了这样一组辅助方法,我们就可在IronPython中调用LINQ to Objects了。例如,以下这条C#语句

 

int count = Process.GetProcesses()

    .Where(process => process.WorkingSet64 > 20*1024*1024)

    .Count();

 

就可以用IronPython实现为

 

processes = Process.GetProcesses()

processes = Where(processes, lambda p: p.WorkingSet64 > 20*1024*1024)

cnt = Count(processes)

 

4. 查询表达式(Query Expression

查询表达式是C#编译器提供的用于简化查询代码的语法糖。C#编译器会将查询表达式翻译为对扩展方法的调用。例如查询表达式

 

var processes =

    from process in Process.GetProcesses()

    where where process.WorkingSet64 > 20*1024*1024

    orderby process.WorkingSet64

    select new { process.Id, Name = process.ProcessName };

 

会被翻译为     

                                                                    

var processes =

     Process.GetProcesses()

    .Where(process => process.WorkingSet64 > 20 * 1024 * 1024)

    .OrderBy(process => process.WorkingSet64)

    .Select(process => new { process.Id, Name = process.ProcessName });

 

由于IronPython编译器不支持查询表达式,在IronPython中无法写出SQL-Like的查询语句。但是,利用powershell.py(在IronPython自带的Tutorial目录下可以找到该文件)所提供的思路,我们可以写出“流水线”风格的查询。

 

processes = (

    From(Process.GetProcesses())

    .Where(lambda p: p.WorkingSet64 > 20*1024*1024)

    .OrderBy(lambda p: p.WorkingSet64)

    .Select(lambda p: makeobj(Id = p.Id, Name = p.ProcessName))

    )

 

流水线是一种非常有用的“隐喻”,在Unix Shell、Windows PowerShell等环境中得到广泛的使用。程序员们熟悉它、喜爱它。更重要的是,它符合LINQ流式供应、延迟求值(deferred evaluation)的特点。从某种角度,它比查询表达式更好地表现了查询操作的语义。

 

在本系列的第三篇文章中,我将基于已有的讨论给出完整的解决方案,在IronPython中提供流水线风格的LINQ to Objects查询。