|
| 1 | +# itr - a powerful `Iterable` wrapper |
| 2 | + |
| 3 | +Heavily inspired by rust's [Iterator trait](https://doc.rust-lang.org/std/iter/trait.Iterator.html), |
| 4 | +with added pythonicness (such as `starmap`) |
| 5 | + |
| 6 | + |
| 7 | +### Functionality |
| 8 | + |
| 9 | +`Itr` wraps python Iterators, Iterables and Generators providing the ability to arbitrarily chain (and lazily evaluate) methods, e.g. |
| 10 | + |
| 11 | +```py |
| 12 | +>>> from itr import Itr |
| 13 | +>>> Itr(range(100)).rev().step_by(4).skip(10).map(lambda x: x * x).filter(lambda x: x % 3 == 0).for_each(print) |
| 14 | +2304 |
| 15 | +1296 |
| 16 | +576 |
| 17 | +144 |
| 18 | +0 |
| 19 | +>>> |
| 20 | +``` |
| 21 | + |
| 22 | +For reference, the equivalent expression using builtins and `itertools`: |
| 23 | + |
| 24 | +```py |
| 25 | +from iterools import islice |
| 26 | + |
| 27 | +for item in filter( |
| 28 | + lambda x: x % 3 == 0, |
| 29 | + map(lambda x: x * x, islice(islice(reversed(range(100)), None, None, 4), 10, None)), |
| 30 | +): |
| 31 | + print(item) |
| 32 | +``` |
| 33 | + |
| 34 | + |
| 35 | +In many cases the `Itr` simply provides a wrapper around `functools` enabling chaining of operations in a left-to-right syntax. |
| 36 | + |
| 37 | +Whilst many methods return either `self` or another instance/instances of `Itr` (thus allowing for chaining and lazy evaluation), some methods consume and return actual values (such as `collect`, `next`, `next_chunk`) or aggregates (such as `count`,`reduce`,`max`) from the iterable (i.e. eager evaluation). Note that: |
| 38 | + |
| 39 | +- iterators can only be consumed once (use `copy`, `cycle` or `repeat` as necessary) |
| 40 | +- it's not possible to rewind, but the next value in the sequence can be "previewed" using the `peek` method. Doing this a lot is unlikely to be efficent though, as it copies the iterator. |
| 41 | +- be wary of open-ended iterators, eager evaluation can lead to infinite loops. |
| 42 | + |
| 43 | +Methods implemented (see the docstrings for more details): |
| 44 | + |
| 45 | +`__iter__`, `__next__`, `all`, `any`, `batched`, `chain`, `collect`, `copy`, `count`, `cycle`, `diff`, `enumerate`, `filter`, `find`, `flat_map`, `flatten`, `fold`, `for_each`, `groupby`, `interleave`, `intersperse`, `last`, `map`, `max`, `min`, `next`, `next_chunk`, `nth`, `pairwise`, `partition`, `peek`, `product`, `reduce`, `repeat`, `rev`, `rolling`, `skip`, `skip_while`, `starmap`, `step_by`, `take`, `take_while`, `unzip`, `zip` |
| 46 | + |
| 47 | +This list is auto-generated by `Itr` introspecting itself, which also generates [more detailed documentation](autodoc.md) |
| 48 | + |
| 49 | +The code is here: [introspect.py](introspect.py) |
| 50 | + |
| 51 | +## Examples |
| 52 | + |
| 53 | +### Inputs |
| 54 | + |
| 55 | +Accepts iterables (sequences and ranges), iterators or generators: |
| 56 | + |
| 57 | +```py |
| 58 | +from itr import Itr |
| 59 | + |
| 60 | +it1 = Itr([0, 1, 2, 3, 4]) |
| 61 | +assert it1.collect() == (0, 1, 2, 3, 4) |
| 62 | + |
| 63 | +it2 = Itr(range(5)) |
| 64 | +assert it2.collect() == (0, 1, 2, 3, 4) |
| 65 | + |
| 66 | +class Fib(Iterator): |
| 67 | + def __init__(self) -> None: |
| 68 | + self.a = 0 |
| 69 | + self.b = 1 |
| 70 | + |
| 71 | + def __iter__(self) -> Self: |
| 72 | + return self |
| 73 | + |
| 74 | + def __next__(self) -> int: |
| 75 | + ret = self.a |
| 76 | + self.a, self.b = self.b, self.a + self.b |
| 77 | + return ret |
| 78 | + |
| 79 | +it3 = Itr(Fib()) |
| 80 | +assert it3.take(10).collect() == (0, 1, 1, 2, 3, 5, 8, 13, 21, 34) |
| 81 | + |
| 82 | +def fibgen() -> Generator[int, None, None]: |
| 83 | + a, b = 0, 1 |
| 84 | + while True: |
| 85 | + yield a |
| 86 | + a, b = b, a + b |
| 87 | + |
| 88 | +it4 = Itr(fibgen()) |
| 89 | +assert it4.take(10).collect() == (0, 1, 1, 2, 3, 5, 8, 13, 21, 34) |
| 90 | +``` |
| 91 | + |
| 92 | +### Outputs |
| 93 | + |
| 94 | +By default, the `collect()` method returns a `tuple`, but this can be changed to `list`, `set` or `dict`. Here's how to group a words by their length into a dictionary: |
| 95 | + |
| 96 | +```py |
| 97 | +>>> from itr import Itr |
| 98 | +>>> Itr(("apple", "banana", "carrot")).groupby(len).collect(dict) |
| 99 | +{5: ('apple',), 6: ('banana', 'carrot')} |
| 100 | +>>> |
| 101 | +``` |
| 102 | + |
| 103 | +```py |
| 104 | +>>> {k: tuple(v) for k, v in itertools.groupby(("apple", "banana", "carrot"), key=len)} |
| 105 | +``` |
| 106 | + |
| 107 | + |
| 108 | +NB using `dict` requires an iterable that produces 2-tuples. |
| 109 | + |
| 110 | +### Composition |
| 111 | + |
| 112 | +Numerical differentiation and integration: |
| 113 | + |
| 114 | +```py |
| 115 | +from itr import Itr |
| 116 | +import numpy as np |
| 117 | + |
| 118 | +x = Itr(np.linspace(0, 1, 11)) |
| 119 | +# need a copy as the original x is consumed in the zip method |
| 120 | +xy = x.copy().zip(x.map(lambda xi: xi * xi / 2)) |
| 121 | + |
| 122 | +def diff1central(xy): |
| 123 | + (x0, y0), (x1, _y1), (x2, y2) = xy |
| 124 | + return x1, (y2 - y0) / (x2 - x0) |
| 125 | + |
| 126 | +print( |
| 127 | + xy.copy() # need a copy as reusing xy below |
| 128 | + .rolling(3) |
| 129 | + .map(diff1central) # x, dy/dx pairs |
| 130 | + .collect() |
| 131 | +) |
| 132 | + |
| 133 | +# similarly, the definite integral x**3/6 -> using the trapezoidal rule over 0,1 = 1/6 |
| 134 | +def trapz(sum, xy): |
| 135 | + (x0, y0), (x1, y1) = xy |
| 136 | + return sum + 0.5 * (y0 + y1) * (x1 - x0) |
| 137 | + |
| 138 | +print(xy.pairwise().fold(0, trapz)) |
| 139 | + |
| 140 | +# ...or an alternative implementation using starmap and two levels of flattening |
| 141 | + |
| 142 | +def trapz2(x0, y0, x1, y1): # type: ignore[no-untyped-def] # noqa: D103 |
| 143 | + return (y0 + y1) * (x1 - x0) / 2 |
| 144 | + |
| 145 | +print(xy.copy().pairwise().flatten().flatten().batched(4).starmap(trapz2).reduce(add)) |
| 146 | + |
| 147 | +``` |
0 commit comments