在本系列的前两篇文章中,我介绍了如何用IronPython模拟C#的语言特性、如何在IronPython中创建LINQ查询。本文将给出一个IronPython模块linq.py,用于在IronPython中提供流水线风格的LINQ to Objects查询。
以下就是linq.py的全部代码。
1: import clr
2: clr.AddReference('System.Core')
3:
4: import System
5: from System.Linq import Enumerable
6: from System import Func
7:
8:
9: # Pythonic wrappers around some common linq functions
10: def Count(col, fun = lambda x: True):
11: return Enumerable.Count[object](col, Func[object, bool](fun))
12:
13: def OrderBy(col, fun):
14: return Enumerable.OrderBy[object, object](col, Func[object, object](fun))
15:
16: def OrderByDesc(col, fun):
17: return Enumerable.OrderByDescending[object, object](col, Func[object, object](fun))
18:
19: def Select(col, fun):
20: return Enumerable.Select[object, object](col, Func[object, object](fun))
21:
22: def Single(col, fun):
23: return Enumerable.Single[object](col, Func[object, bool](fun))
24:
25: def Where(col, fun):
26: return Enumerable.Where[object](col, Func[object, bool](fun))
27:
28:
29: # Save all Pythonic wrappers into dict
30: this_module = __import__(__name__)
31: linqs = {}
32: for name in dir(this_module):
33: if name.startswith('__') or name in ('this_module', 'linqs'):
34: continue
35: linqs[name] = getattr(this_module, name)
36:
37:
38: def is_enumerable(obj):
39: ienum_object = System.Collections.Generic.IEnumerable[object]
40: return isinstance(obj, ienum_object)
41:
42:
43: class LinqAdapter(object):
44: def __init__(self, col):
45: self.col = col
46:
47: def __iter__(self):
48: return iter(self.col)
49:
50: def __str__(self):
51: return '[%s]' % ', '.join( (str(v) for v in self) )
52:
53: def __repr__(self):
54: return str(self)
55:
56: def __getattr__(self, attr):
57: def decorator(*arg, **kws):
58: result = linqs[attr](self.col, *arg, **kws)
59: if is_enumerable(result):
60: return LinqAdapter(result)
61: else:
62: return result
63: return decorator
64:
65:
66: def From(col):
67: if (is_enumerable(col)):
68: return LinqAdapter(col)
69: else:
70: return LinqAdapter( (c for c in col) )
第一部分,加载程序集,导入名字。
• 第1~2行:从GAC中加载System.Core.dll。该程序集定义了LINQ to Object的查询操作符,它们都是定义在System.Linq.Enumerable中的扩展方法。
• 第4~6行:导入名空间System,导入类Enumerable和Func
• 第9~26行:定义了一组辅助函数,以方便调用查询操作符。Enumerable中的扩展方法很多,这里只是示例性地提供了6个辅助函数。读者可以定义更多的辅助函数,以调用其他扩展方法。
第二部分,将辅助方法加入字典对象linqs中。其中,字典的键是辅助函数名(如“Where”),对应的值是方法对象(如Where)。
• 第30行:获取当前模块(即linq.py)的引用:__name__返回当前模块的名字,__import__(__name__)返回当前模块的引用,该引用保存于this_module。
• 第31行:创建字典对象linqs。
• 第32行:迭代当前模块(this_module)的所有成员的名字(name)。
• 第33~34行:如果name代表内建函数(以"__"开始)或字典对象(linqs),则忽略该name。 由于linq.py在该语句之前只定义了一批辅助函数,因此没有被过滤的name对应于辅助函数名。
• 第35行:getattr(this_module, name)获取name对应的辅助函数,并保存在字典项linqs[name]中。
第三部分,定义辅助方法is_enumerable和辅助类LinqAdapter。
• 第38~40行:定义辅助方法is_enumerable,用于判断一个对象是否实现了接口System.Collections.Generic.IEnumerable[object]。这样的对象可以直接传递给查询操作符。
• 第43行:定义了类LinqAdapter
• 第44~45行:实现了初始化方法。客户端程序需要保证输入参数col是一个实现了接口IEnumerable[object]的对象。
• 第47~48行:实现了迭代子协议(iterator protocol),使得LinqAdapter对象可以直接用于for语句。函数__iter__的返回值是一个迭代子,它迭代self.col的元素。
• 第53~54行:实现了特殊函数__repr__,它返回一个字符串。该字符串表示了self.col的所有元素。
• 第56~63行:实现了特殊函数__getattr__。它根据客户端指定的属性名attr,返回一个函子decorator。对于LinqAdapter对象adapter,表达式adapter.Where(condition)等同于adapter.__getattr__(‘Where’)(condition),等同于Where(adapter.col, condition)。这是一个非常有用的Python惯用法,是构建流水线风格的查询的关键。
• 第58行:该行要求__getattr__的输入参数attr必须是一个在linqs中可以找到的辅助函数名。如果attr不是辅助函数名,那么该行将抛出异常KeyError。linqs[attr](self.col, *arg, **kws)的操作分为两步。第一步,linqs[attr]返回attr对应的辅助函数。第二步,调用该辅助函数,其中第一个参数是self.col,其余参数由客户端提供。函数调用的返回值保存在result中。
• 第59~62行:判断result是否实现了接口IEnumerable[object]。如果实现了,则说明result的元素可以继续在流水线中传递,于是将其配接为LinqAdapter(result),并返回配接对象。如果没有实现(通常是Count、Max、Min等查询操作符的返回值),则说明result无法在流水线中传递,直接将其返回。
第四部分,定义了辅助函数From,配接可迭代对象,返回LinqAdapter对象。
• 第67行:判断输入参数col是否实现了接口IEnumerable[object]。
• 第68行:col实现了IEnumerable[object],将其直接传递给LinqAdapter。
• 第70行:col没有实现IEnumerable[object],用生成器(Generator)(c for c in col) 生迭代子,将迭代子传递给LinqAdapter。
实现了linq.py,就可以在IronPython中方便地调用LINQ to Objects。首先,导入linq.py,构建辅助类DataObject。
1: import linq
2:
3: class DataObject(object):
4: def __init__(self, **kws):
5: self.__dict__.update(kws)
然后,调用linq.From获得LinqAdapter对象。利用该对象构建流水线风格的查询。
1: import System
2: from System.Diagnostics import Process
3:
4: processes = (
5: linq.From(Process.GetProcesses())
6: .Where(lambda p: p.WorkingSet64 > 20*1024*1024)
7: .OrderBy(lambda p: p.WorkingSet64)
8: .Select(lambda p: DataObject(Id = p.Id, Name = p.ProcessName))
9: )
10:
11: for p in processes:
12: print p.Name, p.Id
13:
14: print processes.Count()
• 第1~2行:导入名空间System和类Process。
• 第4~9行:构建LINQ to Objects查询,将查询保存在processes中。
• 第5行:获得LinqAdapter对象,该对象的成员col引用了Process.GetProcesses()的返回值(一个Process数组)。
• 第6行:调用Where辅助函数,其过滤条件是进程的WorkingSet64大于20M。
• 第7行:调用OrderBy辅助函数,按照WorkingSet64的大小排序。
• 第8行:调用Select辅助函数,生成新的对象。该对象的数据成员Id对应进程号,数据成员Name对应进程名。
• 第11~12行:执行查询processes,并输出进程名和进程Id。
• 第14行:再次执行查询processes,输出满足过滤条件的进程个数。
从这个例子可以看出,linq.py使得IronPython程序员可以方便地构造LINQ查询。这种流水线风格的语法,虽然没有C#和VB.NET中的查询表达式简单,但也非常直观、易懂。对于一些问题,其表达力甚至强于查询表达式。此外,linq.py保留了LINQ流式供应、延迟求值的特点,充分发挥了LINQ to Objects的能力。
由本系列的第二篇文章可知,IronPython的字典(dict)对象没有实现接口IEnumerable[object]。From利用生成器配接字典对象,使字典对象也可以用作LINQ to Objects的数据源。
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
• 第1行:构建字典对象d。
• 第2~6行:构建LINQ to Objects查询,将查询保存在query中。
• 第3行:获得LinqAdapter对象,该对象的成员col引用了一个迭代子。该迭代子返回字典d的所有键。
• 第4行:调用Where辅助函数,其过滤条件是键key的字面值大于2。
• 第5行:调用Select辅助函数,获得键对应的值。
• 第8~9行:执行查询query,输出4和5。
实际上,From利用生成器,可以配接IronPython中所有实现了迭代子协议的对象。
1: class Range(object):
2: def __init__(self, start, end):
3: assert start < end
4: self.start = start
5: self.end = end
6:
7: def next(self):
8: if self.start < self.end:
9: value = self.start
10: self.start += 1
11: return value
12: else:
13: raise StopIteration
14:
15: def __iter__(self):
16: return self
17:
18: r = Range(1, 5)
19: print linq.is_enumerable(r) # print "False"
20: query = linq.From(r).Where(lambda x: x > 2)
21: for i in query:
22: print i
• 第1~16行:定义了类Range,它实现了迭代子协议。
• 第18行:定义了对象r。如果执行for i in r: print i,将输出1, 2, 3, 4。
• 第19行:检查对象r是否实现了接口IEnumerable[object]。该语句输出False。
• 第20行:构建LINQ to Objects查询,将查询保存在query中。该查询返回序列r中大于2的值。
• 第21~22行:执行查询query,输出3和4。
linq.py展示了IronPython的威力。一方面,Python语言提供了简洁、灵活、有表达力的程序;另一方面,IronPython无缝地与CLR集成,充分地利用.NET的强大能力。对于.NET平台上的程序员,IronPython确实是值得关注的动态语言。