Code source de teachcompute.fctmr.simplefctmr

"""
Simple *mapper* and *reducer* implemented in :epkg:`Python`
"""

from typing import Callable, Iterable
from itertools import groupby


[docs] def mapper(fct: Callable, gen: Iterable) -> Iterable: """ Applies function *fct* to a generator. :param fct: function :param gen: generator :return: generator .. exref:: :title: mapper :tag: progfonc .. runpython:: :showcode: from teachcompute.fctmr import mapper res = mapper(lambda x: x + 1, [4, 5]) print(list(res)) .. faqref:: :title: Différence entre un itérateur et un générateur ? :tag: faqprogfonc Un :epkg:`itérateur` et un :epkg:`générateur` produisent tous deux des éléments issus d'un ensemble. La différence vient du fait que qu'un :epkg:`itérateur` parcourt les éléments d'un ensemble qui existe en mémoire. Un :epkg:`générateur` produit ou calcule des éléments d'un ensemble qui n'existe pas en mémoire. Par conséquent, parcourir deux fois un ensemble avec un itérateur a un coût en :math:`O(n)` alors que pour un générateur, il faut ajouter le calcul de l'élément une seconde fois. Le coût est imprévisible et parfois il est préférable de :epkg:`cacher` les éléments pour le parcourir plusieurs fois : cela revient à transformer un :epkg:`générateur` en :epkg:`itérateur`. Un générateur est souvent défini comme suit en :epkg:`Python` : .. runpython:: :showcode: def generate(some_iterator): for el in some_iterator: yield el g = generate([4, 5]) print(list(g)) print(g.__class__.__name__) """ return map(fct, gen)
[docs] def take(gen: Iterable, count: int = 5, skip: int = 0) -> Iterable: """ Skips and takes elements from a generator. :param gen: generator :param count: number of elements to consider :param skip: skip the first elements :return: generator .. exref:: :title: take :tag: progfonc .. runpython:: :showcode: from teachcompute.fctmr import take res = take([4, 5, 6, 7, 8, 9], 2, 2) print(list(res)) """ took = 0 for i, el in enumerate(gen): if i < skip: continue if took >= count: break yield el took += 1
[docs] def ffilter(fct: Callable, gen: Iterable) -> Iterable: """ Filters out elements from a generator. :param fct: function :param gen: generator :return: generator .. exref:: :title: filter :tag: progfonc .. runpython:: :showcode: from teachcompute.fctmr import ffilter res = ffilter(lambda x: x % 2 == 0, [4, 5]) print(list(res)) """ return filter(fct, gen)
[docs] def reducer( fctkey: Callable, gen: Iterable, asiter: bool = True, sort: bool = True ) -> Iterable: """ Implements a reducer. :param fctkey: function which returns the key :param gen: generator :param asiter: returns an iterator on each element of the group of the group itself :param sort: sort elements by key before grouping :return: generator .. exref:: :title: reducer :tag: progfonc .. runpython:: :showcode: from teachcompute.fctmr import reducer res = reducer(lambda x: x[0], [ ('a', 1), ('b', 2), ('a', 3)], asiter=False) print(list(res)) """ if sort: new_gen = [x[1] for x in sorted((fctkey(el), el) for el in gen)] gr = groupby(new_gen, fctkey) else: gr = groupby(gen, fctkey) if asiter: # Cannot return gr. Python is confused when yield and return # are used in the same function. for _ in gr: yield _ else: for key, it in gr: yield key, list(it)
[docs] def combiner( fctkey1: Callable, gen1: Iterable, fctkey2: Callable, gen2: Iterable, how: str = "inner", ) -> Iterable: """ Joins (or combines) two generators. The function is written based on two reducers. The function is more efficient if the groups of the second ensemble *gen2* are shorter as each of them will be held in memory. :param fctkey1: function which returns the key for gen1 :param gen1: generator for the first element :param fctkey2: function which returns the key for gen2 :param gen2: generator for the second element :param how: *inner*, *outer*, *left*, right* :return: generator .. exref:: :title: combiner or join :tag: progfonc .. runpython:: :showcode: from teachcompute.fctmr import combiner def c0(el): return el[0] ens1 = [('a', 1), ('b', 2), ('a', 3)] ens2 = [('a', 10), ('b', 20), ('a', 30)] res = combiner(c0, ens1, c0, ens2) print(list(res)) """ gr1 = reducer(fctkey1, gen1, asiter=True, sort=True) gr2 = reducer(fctkey2, gen2, asiter=False, sort=True) def fetch_next(it): "local function" try: return next(it) except StopIteration: return None, None k1, g1 = fetch_next(gr1) k2, g2 = fetch_next(gr2) while k1 is not None or k2 is not None: if k1 is None or (k2 is not None and k2 < k1): if how in ("outer", "right"): for el in g2: yield None, el k2, g2 = fetch_next(gr2) else: break elif k2 is None or k1 < k2: if how in ("outer", "left"): for el in g1: yield el, None k1, g1 = fetch_next(gr1) else: break elif k1 == k2: for el1 in g1: for el2 in g2: yield el1, el2 k1, g1 = fetch_next(gr1) k2, g2 = fetch_next(gr2)