Python: LINQ capable class supporting simultaneous

2019-06-10 00:59发布

问题:

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.

回答1:

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


回答2:

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