How to create an Python class which would be a iterable wrapper with LINQ-like methods (select, where, orderby, etc.) without using extension methods or monkey patching.
?
That is this LinqCapable class would be capable to return its own type when it is relevant (i.e. fluent design) and support lazy evaluation.
I'm just looking here for a snippet as a starting point.
It's not enough to just return a generator for the implemented linq methods, you need to have it return an instance of the wrapper to be able to chain additional calls.
You can create a metaclass which can rewrap the linq implementations. So with this, you can just implement the methods you want to support and use some special decorators to ensure it remains chainable.
def linq(iterable):
from functools import wraps
def as_enumerable(f):
f._enumerable = True
return f
class EnumerableMeta(type):
def __new__(metacls, name, bases, namespace):
cls = type.__new__(metacls, name, bases, namespace)
def to_enumerable(f):
@wraps(f)
def _f(self, *args, **kwargs):
return cls(lambda: f(self, *args, **kwargs))
return _f
for n, f in namespace.items():
if hasattr(f, '_enumerable'):
setattr(cls, n, to_enumerable(f))
return cls
class Enumerable(metaclass=EnumerableMeta):
def __init__(self, _iterable):
self._iterable = _iterable
def __iter__(self):
return iter(self._iterable())
@as_enumerable
def intersect(self, second):
yield from set(self._iterable()).intersection(second)
@as_enumerable
def select(self, selector):
yield from map(selector, self._iterable())
@as_enumerable
def union(self, second):
yield from set(self._iterable()).union(second)
@as_enumerable
def where(self, predicate):
yield from filter(predicate, self._iterable())
@as_enumerable
def skip(self, count):
yield from (x for x, i in enumerate(self._iterable()) if i >= count)
@as_enumerable
def skip_while(self, predicate):
it = iter(self._iterable())
for x in it:
if not predicate(x):
yield x
break
yield from it
@as_enumerable
def take(self, count):
yield from (x for x, i in enumerate(self._iterable()) if i < count)
@as_enumerable
def take_while(self, predicate):
for x in self._iterable():
if not predicate(x): break
yield x
@as_enumerable
def zip(self, second, result_selector=lambda a, b: (a, b)):
yield from map(lambda x: result_selector(*x), zip(self._iterable(), second))
def single(self, predicate=lambda _: True):
has_result = False
for x in self._iterable():
if predicate(x):
if has_result:
raise TypeError('sequence contains more elements')
value = x
has_result = True
if not has_result:
raise TypeError('sequence contains no elements')
return value
def sum(self, selector=lambda x: x):
return sum(map(selector, self._iterable()))
def to_dict(self, key_selector, element_selector=lambda x: x):
return {
(key_selector(x), element_selector(x))
for x in self._iterable()
}
def to_list(self):
return list(self._iterable())
return Enumerable(lambda: iterable)
So you'd be able to do things like this with any iterable sequence as you might do it in C#.
# save a linq query
query = linq(range(100))
# even numbers as strings
evenstrs = query.where(lambda i: i%2 == 0).select(str)
# build a different result using the same query instances
odds = query.where(lambda i: i%2 != 0)
smallnums = query.where(lambda i: i < 50)
# dynamically build a query
query = linq(some_list_of_objects)
if some_condition:
query = query.where(some_predicate)
if some_other_condition:
query = query.where(some_other_predicate)
result = query.to_list()
You should return a "Linq capable class" to achieve chaining and furthermore your implementation is not lazy: look at asq where method: it is based on the I filter and it appears correct...
Anyway, here it is a very basic implementation based on my understanding of your question and comments
class LinqCapable(object):
def __init__(self, iterable=None):
self._iterable = iterable
self._predicates = []
def where(self, predicate):
chain = LinqCapable(self._iterable)
chain._predicates = self._predicates
chain._predicates.append(predicate)
return chain
def toArray(self):
for item in self._iterable:
isOk = True
for predicate in self._predicates:
if (not predicate(item)):
isOk = False
break
if (isOk):
yield item
Usage
test = LinqCapable([1,2,3])
def pred1(l: int) -> bool:
return l>1
chain1 = test.where(pred1)
def pred2(l: int) -> bool:
return l<3
chain2 = chain1.where(pred2)
list(chain2.toArray())
Edit (adding also a select method)
I've added also a simple select method.
The objective here is to aggregate predicates and selectors when possible, to avoid inefficient nested loops.
class LinqCapable(object):
def __init__(self, iterable=None, predicates = [], selectors = [], tree=None):
self._iterable = iterable
self._predicates = list(predicates)
self._selectors = list(selectors)
self._tree = tree
def select(self, selector):
if (len(self._predicates) == 0):
chain = LinqCapable(self._iterable, [], self._selectors, self._tree)
chain._selectors.append(selector)
else:
chain = LinqCapable(None, [], [])
if (len(self._selectors) == 0):
chain._tree = LinqCapable(self._iterable, self._predicates, [], self._tree)
else:
chain._tree = self
chain._selectors.append(selector)
return chain
def where(self, predicate):
chain = LinqCapable(self._iterable, self._predicates, self._selectors, self._tree)
chain._predicates.append(predicate)
return chain
def enumerate(self):
if (self._tree != None):
self._iterable = list(self._tree.enumerate())
return self._cycle()
def _cycle(self):
for item in self._iterable:
for selector in self._selectors:
item = selector(item)
isOk = True
for predicate in self._predicates:
if (not predicate(item)):
isOk = False
break
if (isOk):
yield item
so an example would be
test = LinqCapable([1,2, 20,200, 300])
def pred1(l: int) -> bool:
return l>1
chain1 = test.where(pred1)
def pred2(l: int) -> bool:
return l<300
def sel1(l: int) -> str:
return str(l)
def sel2(l: str) -> str:
return '<' + l + '>'
def pred3(l: str) -> bool:
return len(l) > 3
def sel3(l: str) -> str:
return l[1:-1]
def sel4(l: str) -> int:
return int(l)
chain2 = chain1.where(pred2).select(sel1).select(sel2).where(pred3).select(sel3).select(sel4)
print(list(chain2.enumerate()))