• IronPython and LINQ to Objects (V): 支持PLINQ


    当IronPython 2.6 Beta发布时,我阅读了《LINQ in Action》和DevHawkIronPython and LINQ to XML系列文章。受他们的启发,我撰写了三篇博文介绍了如何在IronPython中实现流水线风格的LINQ to Objects查询。

    当IronPython 2.6发布时,我发现IronPython改变了生成器(generator)的实现,于是撰写了第四篇博文,介绍如何利用Adapter模式和Iterator模式来配接IronPython生成器和LINQ的IEnumerable<Object>接口。

    不久前,IronPython 2.6.1和.NET Framework 4发布。为了支持.NET 4引入的PLINQ,我重构了linq.py,并在IronPython 2.6.1上测试通过。本文将介绍这次更新的实作方法。

     

    1. 调用扩展函数

    LINQ的底层基础是查询操作符,它们被实现为一批扩展函数。LINQ to Objects的查询操作符是定义在System.Linq.Enumerable中的扩展函数;PLINQ的查询操作符是定义在Sytem.Linq.ParallelEnumerable中的扩展函数。这些扩展函数是有数个重载的范型函数,例如Enumerable.Where是两个范型函数:

    static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

    static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, int, bool> predicate);

    对于C#这样的静态语言,范型函数的实例化和重载函数的选择是在编译时完成的。编译器利用静态类型系统提供的类型信息,使用类型推演生成具现函数,并使用重载规则将函数调用绑定到具现函数。IronPython是动态语言,范型函数具现和重载绑定是在运行时由IronPython引擎完成的。如果IronPython引擎不能完成类型推演或重载绑定,它会抛出运行时异常。

    1: Enumerable.Where([1, 2, 3], lambda x: x > 1)
    2: Enumerable.Where((i for i in [1, 2, 3]), lambda x: x > 1)

    在IronPython 2.6.1中,第一行的代码可以正确执行,说明IronPython引擎可以对列表(list)完成类型推演和重载绑定。但是,第2行代码将抛出异常:”TypeError: The type arguments for method 'Where' cannot be inferred from the usage. Try specifying the type arguments explicitly.” 这说明IronPython引擎对生成器(generator)无法完成类型推演,它要求程序员显式地指定范型函数的参数类型。

    考虑到生成器是Python实现“延迟求值”最重要的惯用法,而“延迟求值”是LINQ最重要的优点之一,我为每一个扩展函数都进行了如下包装,以显式指定范型函数的类型参数。因为IronPython的所有类型都继承自object(即System.Object),所以我将Where的类型参数TSource绑定到object,而IronPython引擎会完成剩下的工作。

    def Where(col, fun):

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

    在我看来,以上代码展示了IronPython的本质困难之一:如何将动态类型配接到静态类型系统、如何在动态语言中调用为静态语言编写的程序库。IronPython团队给出了一个较好的解决方案:IronPython引擎在大多数情况下可以完成类型推演和重载绑定;当它需要程序员帮助时,程序员只要编写少量代码就可以完成任务。

     

    2. 构建流水线风格的查询

    C#允许对象像调用成员函数一样调用扩展函数,从而方便地编写出流水线风格的代码。IronPython将扩展函数视为普通的静态函数,不支持使用成员函数的语法调用扩展函数。为了构建流水线风格的查询,linq.py提供了一个包装类LinqWrapper,其核心代码如下。

    1:  def pipeline(func):
    2:      def wrapper(self, *args, **kws):
    3:          result = func(self, *args, **kws)
    4:          return LinqWrapper(self.ext, result)
    5:      return wrapper
    6:   
    7:  class LinqWrapper(object):
    8:      def __init__(self, ext, col):
    9:          self.ext = ext
    10:          self.col = col
    11:   
    12:      def Count(self, fun = lambda x: True):
    13:          return self.ext.Count[object](self.col, Func[object, bool](fun))
    14:   
    15:      @pipeline
    16:      def Where(self, fun):
    17:          return self.ext.Where[object](self.col, Func[object, bool](fun))
    18:   
    19:  assert (LinqWapper(Enumerable, [1,2,3])
    20:      .Where(lambda x: x > 1)
    21:      .Count()) == 2

    LinqWarpper用于包装Enumerable、ParallelEnumerable等实现了LINQ扩展函数的静态类。它的初始化函数有两个参数:ext代表被包装的静态类,col代表待处理的数据源。在LINQ to Objects中,数据源是实现了接口IEnumerable<TSource>的对象;在PLINQ中,数据源是实现了接口ParallelQuery<TSource>的对象。函数pipeline是修饰器(decorator),用于修饰LinqWarpper中返回数据源的函数。它将数据源封装为一个新的LinqWarpper对象,这样便可以实现19~21行的流水线风格的代码

    • 第19行,LinqWarpper包装了LINQ to Objects的静态类Enumerable,并作用于列表[1, 2, 3]。
    • 第20行,调用LinqWrapper.Where,该函数调用Enumerable.Where,并返回查询结果。由于LinqWrapper.Where被装饰器pipeline修饰,LinqWrapper.Where的返回值是pipeline的内嵌函数wrapper的返回值:一个新的LinqWrapper对象,它包装了Eunumerable,并作用于查询结果。
    • 第21行,调用LinqWrapper.Count,该函数调用Enumerable.Count,并返回查询结果。

     

    3. 一些辅助函数

    为了简化流水线查询的构建,linq.py提供了一批辅助函数。

    1:  def From(col):
    2:      if is_parallel_enumerable(col):
    3:          return LinqWrapper(ParallelEnumerable, col)
    4:      else:
    5:          return LinqWrapper(Enumerable, get_enumerable(col))
    6:     
    7:  def PFrom(col):
    8:      col = get_enumerable(col)
    9:      col = ParallelEnumerable.AsParallel(col)
    10:      return LinqWrapper(ParallelEnumerable, col)
    11:   
    12:  def get_enumerable(col):
    13:      return col if is_enumerable(col) else (c for c in col)
    14:   
    15:  def is_enumerable(obj):
    16:      return isinstance(obj, System.Collections.IEnumerable)
    17:     
    18:  def is_parallel_enumerable(obj):
    19:      return str(type(obj)) == "<type 'ParallelEnumerable'>"

     

    • 第1~5行,函数From的输入是IronPython中的可迭代对象,输出是LinqWrapper对象,其目的是简化LINQ to Objects查询的构造。
    • 第7~10行,函数PFrom的输入是IronPython的可迭代对象,输出是封装了ParrallelEnumerable的LinqWrapper对象,其目的是简化PLINQ查询的构造。
    • 第12~13行,函数get_enumerable检查可迭代对象col是否实现了接口IEnumerable,如果没有实现,则使用生成器将其配接到IEnumerable。在IronPython 2.6.1中,生成器对象实现了接口IEnumerable,可作为LINQ查询操作符的输入。

    有了以上辅助函数,我们就可以方便地构造LINQ查询。

    From([1, 2, 3]).Where(lambda x: x > 1).Count()

    PFrom([1, 2, 3]).Count(lambda x: x > 1)

    4. 测试

     

    本系列的最大缺点是没有提供测试用例以构建安全网。这使得linq.py在IronPython版本升级和重构过程中显得非常脆弱。为了亡羊补牢,我在最新的linq.py中增加了一批测试用例。详细测试逻辑可参考源代码,这里仅列出被测试覆盖的对象。

    li = [1, 2, 3, 4, 5]

    test(lambda: System.Array[System.Int32](li))

    test(lambda: System.Collections.ArrayList(li))

    test(lambda: li)

    test(lambda: tuple(li))

    test(lambda: set(li))

    test(lambda: dict((i, i) for i in li))

    test(lambda: (i for i in li))

    test(lambda: MyContainer(li))

    由以上代码可知,目前的测试覆盖了.NET强类型容器(整型数组)、弱类型容器(ArrayList)、Python内建容器(列表、元组、集合、字典)、生成器、以及自定义容器(参考源代码可知,该容器支持序列协议(sequence protocol),即实现了函数__getitem__)。

     

    5. linq.py

    最后列出linq.py的全部源代码。感谢IronPython团队将IronPython平滑地集成于CLR,使得我们可以用简短的代码将LINQ的威力引入Python的世界。

       1:  import clr
       2:  clr.AddReference('System.Core')
       3:   
       4:  import System
       5:  from System.Linq import Enumerable, ParallelEnumerable
       6:  from System import Func
       7:
       8:  def pipeline(func):
       9:      def wrapper(self, *args, **kws):
      10:          result = func(self, *args, **kws)
      11:          return LinqWrapper(self.ext, result)
      12:      return wrapper
      13:   
      14:  class LinqWrapper(object):
      15:      def __init__(self, ext, col):
      16:          self.ext = ext
      17:          self.col = col
      18:   
      19:      def __iter__(self):
      20:          return iter(self.col)
      21:         
      22:      def __str__(self):
      23:          return '[%s]' % ', '.join( (str(v) for v in self) )  24:         
      25:      def __repr__(self):
      26:          return str(self)
      27:         
      28:      def Count(self, fun = lambda x: True):
      29:          return self.ext.Count[object](self.col, Func[object, bool](fun))
      30:   
      31:      @pipeline
      32:      def Distinct(self):
      33:          return self.ext.Distinct[object](self)
      34:   
      35:      def First(self):
      36:          return self.ext.First[object](self)
      37:   
      38:      @pipeline
      39:      def GroupBy(self, fun):
      40:          return self.ext.GroupBy[object, object](self.col, Func[object, object](fun))
      41:   
      42:      @pipeline
      43:      def Join(outer, inner, outerKey, innerKey, fun):
      44:          return self.ext.Join[object, object, object, object](outer, inner
      45:              , Func[object, object](outerKey), Func[object, object](innerKey)
      46:              , Func[object, object, object](fun))

      48:      @pipeline
      49:      def OrderBy(self, fun):
      50:          return self.ext.OrderBy[object, object](self.col, Func[object, object](fun))
      51:   
      52:      @pipeline
      53:      def OrderByDesc(self, fun):
      54:          return self.ext.OrderByDescending[object, object](self.col, Func[object, object](fun))
      55:     
      56:      @pipeline
      57:      def ThenBy(self, fun):
      58:          return self.ext.ThenBy[object, object](self.col, Func[object, object](fun))
      59:   
      60:      @pipeline
      61:      def ThenByDesc(self, fun):
      62:          return self.ext.ThenByDescending[object, object](self.col, Func[object, object](fun))
      63:   
      64:      @pipeline
      65:      def Take(self, count):
      66:        return self.ext.Take[object](self.col, count)
      67:   
      68:      @pipeline
      69:      def Select(self, fun):
      70:          return self.ext.Select[object, object](self.col, Func[object, object](fun))
      71:   
      72:      def Single(self, fun):
      73:          return self.ext.Single[object](self.col, Func[object, bool](fun))
      74:   
      75:      @pipeline
      76:      def Where(self, fun):
      77:          return self.ext.Where[object](self.col, Func[object, bool](fun))
      78:   
      79:  def From(col):
      80:      if is_parallel_enumerable(col):
      81:          return LinqWrapper(ParallelEnumerable, col)
      82:      else:
      83:          return LinqWrapper(Enumerable, get_enumerable(col))
      84:     
      85:  def PFrom(col):
      86:      col = get_enumerable(col)
      87:      col = ParallelEnumerable.AsParallel(col)
      88:      return LinqWrapper(ParallelEnumerable, col)
      89:   
      90:  def get_enumerable(col):
      91:      return col if is_enumerable(col) else (c for c in col)
      92:   
      93:  def is_enumerable(obj):
      94:      return isinstance(obj, System.Collections.IEnumerable)
      95:     
      96:  def is_parallel_enumerable(obj):
      97:      return str(type(obj)) == "<type 'ParallelEnumerable'>"
      98:   
      99:  if __name__ == '__main__':
     100:      def test_query(ctr, from_):
     101:          print 'test_query', from_.__name__
     102:          query = (
     103:              from_(ctr())
     104:              .Where(lambda x: x > 2)
     105:              .Select(lambda x: x * 2)
     106:          )
     107:          expect = set(r for r in query)
     108:          actual = set(x * 2 for x in (y for y in ctr() if y > 2))
     109:          assert expect == actual, expect
     110:         
     111:      def test_count(ctr, from_):
     112:          print 'test_count', from_.__name__
     113:          expect = from_(ctr()).Where(lambda x: x > 2).Count()
     114:          actual = len([x for x in ctr() if x > 2])
     115:          assert expect == actual, expect
     116:         
     117:      def show(ctr):
     118:          col = ctr()
     119:          print 'type', type(col)
     120:          print 'clrtype', clr.GetClrType(type(col))
     121:          print 'is_enumerable', isinstance(col, System.Collections.IEnumerable)
     122:          print 'is_enumerable[object]', isinstance(col, System.Collections.Generic.IEnumerable[object])
     123:          print 'is_enumerable[Int32]', isinstance(col, System.Collections.Generic.IEnumerable[System.Int32])
     124:   
     125:      def test(ctr):
     126:          show(ctr)
     127:          for from_ in (From, PFrom):
     128:              test_query(ctr, from_)
     129:              test_count(ctr, from_)
     130:             
     131:      class MyContainer(object):
     132:          def __init__(self, col):
     133:              self.col = col
     134:             
     135:          def __getitem__(self, idx):
     136:              return self.col[idx]
     137:   
     138:      li = [1, 2, 3, 4, 5]
     139:      test(lambda: System.Array[System.Int32](li))
     140:      test(lambda: System.Collections.ArrayList(li))
     141:      test(lambda: li)
     142:      test(lambda: tuple(li))
     143:      test(lambda: set(li))
     144:      test(lambda: dict((i, i) for i in li))
     145:      test(lambda: (i for i in li))
     146:      test(lambda: MyContainer(li))

  • 相关阅读:
    一个js选项卡
    郁闷
    Object Literals
    typeof and instanceof
    比较好用的日期控件
    很炫的分页
    [转]ASP.NET Web.config配置文件的基本使用方法
    正则匹配 获取QQ空间日志
    漂浮窗(层)JS
    ASP.NET页面事件执行过程(完整版)
  • 原文地址:https://www.cnblogs.com/liangshi/p/1726413.html
Copyright © 2020-2023  润新知