Ehouarn Perret Ehouarn Perret - 2 months ago 5
Python Question

Python: LINQ capable class supporting simultaneously laziness and fluent design

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):
                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, _iter):
            self._iter = _iter
        def __iter__(self):
            return self._iter()
        def intersect(self, second):
            yield from set(self._iter()).intersection(second)
        def select(self, selector):
            yield from map(selector, self._iter())
        def union(self, second):
            yield from set(self._iter()).union(second)
        def where(self, predicate):
            yield from filter(predicate, self._iter())
        def take(self, count):
            yield from (x for x, i in enumerate(self._iter()) if i < count)
        def take_while(self, predicate):
            for x in self._iter():
                if not predicate(x): break
                yield x
        def zip(self, second, result_selector=None):
            zipped = zip(self._iter(), second)
            if result_selector is None:
                yield from zipped
                yield from map(lambda x: result_selector(*x), zipped)
        def to_dict(self, key_selector, element_selector=None):
            element_selector = element_selector or (lambda x: x)
            return dict(
                (key_selector(x), element_selector(x))
                for x in self._iter()
        def to_list(self):
            return list(self._iter())
    return Enumerable(iterable.__iter__)

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()