Skip to content

Commit d758f2a

Browse files
committed
doc stuff
1 parent c96b252 commit d758f2a

8 files changed

Lines changed: 1296 additions & 100 deletions

File tree

README.md

Lines changed: 114 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,115 @@
1-
# itr - a powerful `Iterable` wrapper
1+
# itr - A Powerful Iterable Wrapper
22

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`)
3+
`itr` is a powerful Python library that wraps iterators, iterables, and generators, providing a Rust-inspired `Iterator` trait experience with added Pythonic conveniences. It enables developers to build complex data processing pipelines with a fluent, chainable, and lazy API. In most cases, it simply wraps itertools in syntactic sugar.
54

5+
Heavily inspired by Rust's [Iterator trait](https://doc.rust-lang.org/std/iter/trait.Iterator.html), `itr` offers a familiar and robust pattern for sequence manipulation.
66

7-
### Functionality
7+
## Key Features & Why `Itr`?
88

9-
`Itr` wraps python Iterators, Iterables and Generators providing the ability to arbitrarily chain (and lazily evaluate) methods, e.g.
9+
* **Fluent & Chainable API:** Write expressive, left-to-right sequences of operations on your data.
10+
* **Lazy Evaluation:** Operations are only performed when their results are needed, improving efficiency for large or infinite sequences.
11+
* **Rust-Inspired Patterns:** Bring the power and clarity of Rust's `Iterator` design to your Python projects.
12+
* **Pythonic Additions:** Includes convenient methods like `starmap` that integrate smoothly with Python's ecosystem.
13+
* **Simplifies Complex Logic:** Often provides a more readable and concise alternative to nested `itertools` or built-in functions.
14+
15+
## Quick Start & Core Functionality
16+
17+
`Itr` wraps Python Iterators, Iterables, and Generators, allowing you to arbitrarily chain methods for data transformation.
1018

1119
```py
1220
>>> from itr import Itr
21+
>>> # Process a range of numbers with a concise, chainable syntax
1322
>>> 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-
>>>
23+
2601
24+
1521
25+
729
26+
225
27+
9
28+
2029
```
2130

22-
For reference, the equivalent expression using builtins and `itertools`:
31+
For reference, the equivalent expression using built-ins and `itertools` can be significantly less readable:
2332

24-
```py
25-
from iterools import islice
33+
```python
34+
from itertools import islice
2635

2736
for item in filter(
2837
lambda x: x % 3 == 0,
2938
map(lambda x: x * x, islice(islice(reversed(range(100)), None, None, 4), 10, None)),
3039
):
3140
print(item)
41+
3242
```
3343

44+
## How `Itr` Works: Lazy vs. Eager
45+
46+
Most `Itr` methods are **lazy transformations**, meaning they return a new `Itr` instance without immediately processing any data. This allows for arbitrary chaining and efficient memory usage, as items are only processed as they are requested. In many cases, `Itr` acts as a convenient wrapper around `itertools`, enabling this left-to-right chaining syntax.
47+
48+
However, some methods are **eager consumers**. These methods iterate over and consume the underlying data, returning concrete values, collections, or aggregates. Examples include:
49+
* **Collection methods:** `collect`, `next`, `next_chunk`
50+
* **Aggregation methods:** `count`, `reduce`, `max`, `min`, `all`, `any`, `last`, `find`, `fold`, `product`
3451

35-
In many cases the `Itr` simply provides a wrapper around `functools` enabling chaining of operations in a left-to-right syntax.
52+
### Important Considerations
3653

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:
54+
When working with `Itr`, keep these points in mind:
3855

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.
56+
* **Single-Pass Iterators:** Like all Python iterators, `Itr` instances (and their underlying iterators) can generally only be consumed once. If you need to process the same sequence multiple times, use methods like `copy()`, `cycle()`, or `repeat()` as necessary.
57+
* **No Rewinding:** It's not possible to rewind an `Itr` to an earlier state. You can "preview" the next value using the `peek()` method, but be aware that `peek()` often copies the iterator internally, which can be inefficient if used excessively.
58+
* **Infinite Iterators:** Be cautious with open-ended iterators (e.g., those from `itertools.count()` or custom generators). Eager evaluation methods (like `collect()`, `count()`, `reduce()`) will attempt to consume the entire sequence, potentially leading to infinite loops or out-of-memory errors if applied to an infinite source.
4259

43-
Methods implemented (see the docstrings for more details):
60+
## API Reference
61+
62+
`Itr` provides a comprehensive set of methods for various iterable operations. For a complete list of methods and their detailed descriptions, please refer to the [full documentation](autodoc.md).
63+
64+
*Note: The detailed list of methods in `autodoc.md` is automatically generated by [introspect.py], where `Itr` introspects itself.*
65+
66+
```py introspect.py
67+
from operator import add
68+
69+
from itr import Itr
70+
71+
# use Itr to introspect itself for methods and their docstrings
72+
methods = (
73+
Itr(dir(Itr))
74+
.filter(lambda m: m in ("__init__", "__iter__", "__next__") or not m.startswith("_"))
75+
.map(lambda m: (m, getattr(Itr, m).__doc__))
76+
.collect(dict)
77+
)
4478

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`
79+
# use Itr to make some documentation
80+
method_template = """
81+
### `{method_name}`
4682
47-
This list is auto-generated by `Itr` introspecting itself, which also generates [more detailed documentation](autodoc.md)
83+
{method_doc}
84+
"""
4885

49-
The code is here: [introspect.py](introspect.py)
86+
with open("doc/autodoc.md", "w") as fd:
87+
fd.write("# `Itr` class documentation\n")
88+
fd.write(Itr.__doc__ or "")
89+
fd.write("## Public methods\n")
90+
fd.write(Itr(methods.items()).map(lambda m: method_template.format(method_name=m[0], method_doc=m[1])).reduce(add))
91+
```
5092

5193
## Examples
5294

53-
### Inputs
95+
### Input Flexibility
5496

55-
Accepts iterables (sequences and ranges), iterators or generators:
97+
`Itr` is designed to work seamlessly with various Python iterable types, including sequences, ranges, custom iterators, and generators.
5698

5799
```py
100+
from collections.abc import Iterator
101+
from typing import Self, Generator
58102
from itr import Itr
59103

104+
# From a list (iterable)
60105
it1 = Itr([0, 1, 2, 3, 4])
61106
assert it1.collect() == (0, 1, 2, 3, 4)
62107

108+
# From a range (iterable)
63109
it2 = Itr(range(5))
64110
assert it2.collect() == (0, 1, 2, 3, 4)
65111

112+
# From a custom iterator (implementing __iter__ and __next__)
66113
class Fib(Iterator):
67114
def __init__(self) -> None:
68115
self.a = 0
@@ -79,6 +126,7 @@ class Fib(Iterator):
79126
it3 = Itr(Fib())
80127
assert it3.take(10).collect() == (0, 1, 1, 2, 3, 5, 8, 13, 21, 34)
81128

129+
# From a generator function
82130
def fibgen() -> Generator[int, None, None]:
83131
a, b = 0, 1
84132
while True:
@@ -89,59 +137,78 @@ it4 = Itr(fibgen())
89137
assert it4.take(10).collect() == (0, 1, 1, 2, 3, 5, 8, 13, 21, 34)
90138
```
91139

92-
### Outputs
140+
### Output Options with `collect()`
93141

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:
142+
While `Itr` methods typically return another `Itr` instance, the `collect()` method allows you to materialize the results into various Python collections: `tuple` (default), `list`, `set`, or `dict`.
95143

96-
```py
144+
Here's how to group words by their length into a dictionary:
145+
146+
```python
97147
>>> from itr import Itr
98148
>>> Itr(("apple", "banana", "carrot")).groupby(len).collect(dict)
99149
{5: ('apple',), 6: ('banana', 'carrot')}
100-
>>>
150+
101151
```
102152

153+
For reference, an equivalent using `itertools` directly:
154+
103155
```py
156+
>>> import itertools
104157
>>> {k: tuple(v) for k, v in itertools.groupby(("apple", "banana", "carrot"), key=len)}
158+
{5: ('apple',), 6: ('banana', 'carrot')}
159+
105160
```
106161

162+
*Note: Using `collect(dict)` requires an iterable that produces 2-tuples (key-value pairs).*
107163

108-
NB using `dict` requires an iterable that produces 2-tuples.
164+
### More examples
109165

110-
### Composition
166+
More examples can be found [here](./doc/examples.md).
111167

112-
Numerical differentiation and integration:
113168

114-
```py
169+
170+
171+
172+
Beyond basic transformations, `Itr` enables powerful composition of operations, making complex data processing more manageable. This example demonstrates numerical differentiation and integration using `Itr`.
173+
174+
```python
115175
from itr import Itr
116176
import numpy as np
177+
from operator import add
117178

118179
x = Itr(np.linspace(0, 1, 11))
119-
# need a copy as the original x is consumed in the zip method
180+
# Need a copy of 'x' as the original 'x' is consumed in the zip method.
120181
xy = x.copy().zip(x.map(lambda xi: xi * xi / 2))
121182

122-
def diff1central(xy):
123-
(x0, y0), (x1, _y1), (x2, y2) = xy
183+
# Numerical differentiation (central difference)
184+
def diff1central(xy_segment):
185+
# Unpack the 3-element rolling window: (x0, y0), (x1, y1_ignored), (x2, y2)
186+
(x0, y0), (x1, _y1), (x2, y2) = xy_segment
124187
return x1, (y2 - y0) / (x2 - x0)
125188

189+
print("Numerical Derivatives:")
126190
print(
127-
xy.copy() # need a copy as reusing xy below
191+
xy.copy() # Need a copy as xy will be reused below for integration
128192
.rolling(3)
129-
.map(diff1central) # x, dy/dx pairs
193+
.map(diff1central) # Produces (x, dy/dx) pairs
130194
.collect()
131195
)
132196

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))
197+
# Numerical integration (trapezoidal rule)
198+
# Integrates x**2 / 2, resulting in x**3 / 6. Over 0,1 this is 1/6.
199+
def trapz_accumulate(current_sum, xy_pair):
200+
# Unpack (x0, y0) and (x1, y1) from the pairwise iteration
201+
(x0, y0), (x1, y1) = xy_pair
202+
return current_sum + 0.5 * (y0 + y1) * (x1 - x0)
139203

140-
# ...or an alternative implementation using starmap and two levels of flattening
204+
print("\nNumerical Integral (Trapezoidal Rule - Fold):")
205+
print(xy.pairwise().fold(0, trapz_accumulate))
141206

142-
def trapz2(x0, y0, x1, y1): # type: ignore[no-untyped-def] # noqa: D103
207+
# ...or an alternative integration implementation using starmap and flattening
208+
def trapz2(x0, y0, x1, y1): # type: ignore[no-untyped-def]
209+
"""Calculates trapezoidal area for two points."""
143210
return (y0 + y1) * (x1 - x0) / 2
144211

212+
print("\nNumerical Integral (Trapezoidal Rule - Starmap):")
145213
print(xy.copy().pairwise().flatten().flatten().batched(4).starmap(trapz2).reduce(add))
146-
147214
```

autodoc.md renamed to doc/autodoc.md

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,21 +114,12 @@ Example:
114114
>>> itr = Itr([1, 2, 3])
115115
>>> cycler = itr.cycle()
116116
>>> cycler.take(5).collect()
117-
[1, 2, 3, 1, 2]
118-
119-
120-
### `diff`
121-
122-
123-
Convenience method for differencing a sequence
124-
T must have a "-" operator
125-
n must be != 0
126-
if n is negative, diff is reversed
117+
(1, 2, 3, 1, 2)
127118

128119

129120
### `enumerate`
130121

131-
Yield pairs of (index, item) for each item in the iterator.
122+
Yield pairs of (index, item) for each item in the iterator, where index starts at 0 or the value provided
132123

133124
Returns:
134125
Itr[tuple[int, T]]: An iterator of (index, item) pairs.

0 commit comments

Comments
 (0)