代码改变世界

IronPython and LINQ to Objects (IV): 实现IEnumerable<Object>

2010-05-03 16:08  liangshi  阅读(1055)  评论(0编辑  收藏  举报

在本系列的前三篇文章中,我介绍了如何用IronPython模拟C#的语言特性如何在IronPython中创建LINQ查询如何在IronPython中实现流水线风格的LINQ to Objects查询。本文将根据IronPython 2.6来进一步完善linq.py,并介绍Iterator模式在Python和C#中的应用,以及如何在IronPython中实现C#接口。

 

1. 问题

 

LINQ to Object要求被查询的对象实现了System.Collections.Generic.IEnumerable<T>接口。IronPython提供的标准容器列表(list)、元组(tuple)、集合(set)都实现了IEnumerable<Object>,可以被LINQ to Object的查询操作符(定义在System.Linq.Enumerable的扩展方法)直接使用。然而,字典(dict)和其他一些可迭代的对象(如函数open()返回的File对象)并没有实现IEnumerable<Object>。

 

为了将IronPython中的可迭代对象配接到IEnumerable<T>接口,LINQ构建块一文建议使用生成器(Generator)。这个建议对于IronPython 2.6 Beta 2 (2.6.0.20)是正确的,对于IronPython 2.6 Final (2.6.10920.0)是错误的。

 

1:  import System
2:  ie_object = System.Collections.Generic.IEnumerable[System.Object]
3:  generator = (i for i in range(1))

4:  print isinstance(generator, ie_object)   

 

对于上述程序,IronPython 2.6 Beta 2的输出是True,表明generator实现了接口IEnumerable<Object>。但是,IronPython 2.6 Final 输出的是False,表明generator没有实现该接口。可见,linq.py需要一种新的方法来配接IEnumerable<Object>。

 

在Python中,可迭代对象必须支持迭代协议(iteration protocal,实现函数__iter__())或序列协议(sequence protocol,实现函数__getitem__())。本文给出了一种方法,将支持迭代协议的对象配接到接口IEnumerable<Object>。该方法很容易扩展到配接支持序列协议的对象。

 

2. 迭代协议(iteration protocal

  

支持迭代协议的对象必须实现函数__iter__(),该函数返回一个迭代子对象(iterator)。迭代子对象必须实现函数next()。每调用一次next(),该函数就返回一个可访问的元素(element)。当迭代结束时,next()不返回元素,而是抛出StopIteration异常。这就是GoF的迭代子模式(Iterator Pattern)在Python语言中的实现。

 

Python对于迭代子模式提供了语言级别的支持。任何一个支持迭代协议的对象,都可以被用于for语句和其他迭代表达式中。

 

1:  # seq supports iteration protocal
2:  for i in seq:
3:      do_something_to(i)
4:     
5:  # the implementation of for-statement
6:  fetch = iter(seq)
7:  while True:
8:      try:
9:          i = fetch.next()
10:      except StopIteration:
11:          break
12:      do_something_to(i)

 

  • 第2~3行:for语句迭代seq,并访问其元素。其中,seq实现了迭代协议。
  • 第6~12行:Python引擎对for语句的一种可能的实现方式。
    • 第6行:iter(seq)等价于seq.__iter__()。该函数调用获得seq的迭代子,并将其赋给fetch。
    • 第9行:调用迭代子的next(),获得可访问的元素。
    • 第10、11行:当next()抛出StopIteration异常时,捕获该异常,利用break语句退出while循环。

  

3. IEnumerable<T>

  

Design Patterns 15 Years Later: An Interview with Erich Gamma, Richard Helm, and Ralph Johnson一文中,Richard Helm提到

 

I think there has been an evolution in level of sophistication. Reusable software has migrated to the underlying system/language as toolkits or frameworks—and mostly should be left to the experts. 我认为,复杂性的层次发生了演变。可复用软件已经以工具包或框架的形式迁移到底层系统和语言中——大部分工作应该由专家完成。

 

这一趋势在C#和.NET这样快速演进的语言和平台中得到印证。例如,C#通过IEnumerable<T>接口,对迭代子模式提供了语言级别的支持。

 

1:  public interface IEnumerable<T>: IEnumerable
2:  {
3:      IEnumerator<T> GetEnumerator();
4:  }
5:   
6:  public interface IEnumerator<T> : IEnumerator, IDisposable
7:  {
8:      T Current { get; }
9:  }
10:   
11:  public interface IEnumerator
12:  {
13:      bool MoveNext();
14:      object Current { get; }
15:      void Reset();
16:  }
17:   
18:  public interface IDisposable
19:  {
20:      void Dispose();
21:  }


以上就是C#中一个强类型的迭代器所需要实现的4个接口。一个实现了IEnumerable<T>的对象,提供了函数GetEnumerator(),它返回一个实现了IEnumerator<T>的迭代子对象。调用迭代子对象的函数MoveNext(),可以将迭代子移动到下一个元素,并可以利用属性Current访问该元素。当没有更多的元素可以访问时,MoveNext()返回false,表示迭代结束。以下代码展示了C#的foreach语句是如何迭代一个实现了IEnumerable<T>的对象。

 

1:  // container implements IEnumerable<T>
2:  foreach (var elem in container)
3:  {
4:      DoSomethingTo(elem);
5:  }
6:   
7:  // the implementation of foreach-statement
8:  var foreach enumerator = container.GetEnumerator();
9:  while (enumerator.MoveNext())
10:  {
11:      var elem = enumerator.Current;
12:      DoSomethingTo(elem);
13:  }

  • 第2~5行:foreach语句迭代container,并访问其元素。其中,container实现了接口IEnumerable<T>。
  • 第8~13行:C#编译器对foreach语句的一种可能的实现方式。
    • 第8行:调用IEnumerable<T>.GetEnumerator()获得实现了IEnumerator<T>的迭代子。
    • 第9行:调用IEnumerator.MoveNext(),将迭代子移动到下一个位置。如果MoveNext()返回false,则迭代结束。
    • 第11行:访问属性IEnumerator<T>.Current获得当前元素。

由以上代码可见,C#和Python对迭代子模式的实现思路大致相同。其重要区别在于,Python利用异常来跳转控制流,C#则利用MoveNext()的返回值对控制流进行分支选择。这体现了动态语言和静态语言对于异常的不同态度。

 

4. 将支持序列协议的IronPython对象配接到IEnumerable<T>

 

我利用配接器模式(Adapter Pattern),将支持序列协议的IronPython对象配接到IEnumerable<T>。

 

1:  class IEnumerableAdapter(System.Collections.Generic.IEnumerable[object],
2:      System.Collections.Generic.IEnumerator[object]):
3:      def __init__(self, iteration):
4:          self.iter = iter(iteration)
5:          self.element = None
6:         
7:      def GetEnumerator(self):
8:          return self
9:   
10:      def get_Current(self):
11:          return self.element
12:         
13:      def MoveNext(self):
14:          try:
15:              self.element = self.iter.next()
16:              return True
17:          except StopIteration:
18:              return False
19:     
20:      def Dispose(self):
21:          pass

 

感谢IronPython Team的卓越工作,在IronPython中实现C#接口是一件很容易的事情。

  • 第1~2行:定义了类IEnumerableApater,它继承了接口IEnumerable[object]和IEnumerator[object]。在IronPython中,object是System.Object,IEnumerable[object]等价于C#中的IEnumerable<Object>。
  • 第3~5行:定义了初始化函数__init__()。它接纳一个支持迭代协议的对象iteration,调用iter(iteration)获得iteration的迭代子,并保持在self.iter中。
  • 第7~8行:实现了IEnumerable<Object>.GetEnumerator()函数。该函数返回对象自身。这是一个Python的惯用法:用同一个对象实现可迭代对象和迭代子对象。
  • 第10~11行:实现了IEnumerable<Object>.Current属性。C#中的属性一般由get操作和set操作组成。IEnumerable<T>只要求实现Current属性的get操作,所以只要在IEnumerableAdapter定义函数get_Current()即可。该函数返回当前可访问的元素self.element。
  • 第13~18行:实现了IEnumerable.MoveNext()函数。它调用迭代子self.iter的next()函数,以设置当前可访问元素self.element。如果存在可访问元素,返回True。如果不存在可访问元素,next()会抛出StopIteration异常。该异常被捕获后,MoveNext()返回False,表示迭代应该结束。
  • 第20~21行:实现了IDisposable.Dispose()函数。

值得注意的是,IEnumerableApater没有实现IEnumerator.Reset()函数。在C#中,如果类不实现接口中所有的成员,那么无法实例化该类的对象。IronPython则没有这个限制。IEnumerator的客户可以正常访问IEnumerableApater提供的MoveNext()和get_Current()。如果客户访问IEnumerator.Reset(),将遭遇运行时异常。由于LINQ查询操作符不调用IEnumerator.Reset(),因此IEnumerableApater不实现该函数是可行的。

 

将IEnumerableApater加入linq.py。在IronPython控制台(console)上测试该类,可知它满足我的需求。

 

>>> import linq

>>> d = d = {'1':1, '2':2, '3':4, '4':5}

>>> linq.is_enumerable(d)

False

>>> a = linq.IEnumerableAdapter(d)

>>> linq.is_enumerable(a)

True

>>> for key in a: print key, d[key]    # IronPython can iterate IEnumerable.

...

4 5

3 4

2 2

1 1

 

5. 应用linq.py

 

基于IEnumerableApater,将linq.py中的From()修改为:

1:  def From(col):
2:      if not is_enumerable(col):
3:          col = IEnumerableAdapter(col)
4:      return LinqAdapter(col)


这样,就可以用LINQ操作符去处理字典和其他可迭代对象了。在以下代码中,query筛选出字典d中键(key)大于2的项(item)所对应的值(value)。最后的输出结果是4和5。

1:  d = {'1':1, '2':2, '3':4, '4':5}
2:  query = (
3:      linq.From(d)
4:      .Where(lambda key: int(key) > 2)
5:      .Select(lambda key: d[key])
6:      )
7:      
8:  for i in query:
9:      print i

 

大功告成!linq.py的完整代码可以从这里下载。

 

6. Q & A

 

回答网友DS所提出的一些关于linq.py的问题。

Q: 我看到IronPython and LINQ to Objects(III),是不是也相当于用IronPython完成了Linq的句法?
A: 《LINQ in Action》指出,LINQ由两个互为补充的部分组成:处理数据的工具和对编程语言的扩展。

 

所谓“处理数据的工具”是.NET Framework提供的一组程序库(例如,定义在System.Linq.Enumerable的扩展方法)和一些代码生成工具。这些程序库和工具可以被.NET平台上的语言所共享。C#、VB.NET、F#或IronPython都可以使用它们。

 

但是,“使用”并不代表“好用”。为了提高LINQ的可用性,C#和VB.NET对编程语言进行了扩展。这就是互为补充的第二部分:“语言扩展”。C# 3.0可以使用类似SQL的语法去调用程序库,这极大地简化了程序员的工作。这种类似SQL的语法被称为“查询表达式”。不幸的是,这种语言扩展只在C#和VB.NET被实现。其他.NET平台上的语言都不支持查询表达式。

 

linq.py是利用Python语法的灵活性来调用.NET Framework提供的LINQ程序库。它没有实现C#中的“查询表达式”。从这个角度,它没有实现C#的Linq语法。

 

Q: 更多的感觉是用IronPython完成了一种“包装”?
A: linq.py是一种“包装”。在IronPython中,直接调用System.Linq.Enumerable的函数是比较繁琐的。linq.py可以视作Facade模式的一个应用:它提供了一个简化的接口来调用System.Linq.Enumerable的函数。

 

Q: 使得开发者可以用IronPython来写效果等同于C# Linq?
A: 对于LINQ to Object,IronPython和C#都是调用System.Linq.Enumerable来完成计算。从这个角度,两者的计算基础是相同的,计算结果也是相同的。

 

Q: 既然MS提出IronPython为什么不完成给出像类似于您的Linq.py呢?
A: IronPython团队的任务是在.NET平台上构建符合Python标准的Python语言实现,其余的大部分工作应该由IronPython社区来完成。也就是说,他们的任务是构建良好的IronPython引擎,使得linq.py这样的模块可以很容易实现。我觉得,他们的工作很棒!此外,IronPython的官方网站www.ironpython.net(国内访问有困难)有一篇关于IronPython和.NET集成的文档。非常值得一读。