pyochain 0.5.0__py3-none-any.whl → 0.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyochain might be problematic. Click here for more details.

pyochain/_iter/_main.py CHANGED
@@ -1,14 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Callable, Collection, Generator, Iterable, Iterator
4
- from typing import TYPE_CHECKING, Any, Concatenate
3
+ import itertools
4
+ from collections.abc import (
5
+ Callable,
6
+ Generator,
7
+ Iterable,
8
+ Iterator,
9
+ Sequence,
10
+ )
11
+ from typing import TYPE_CHECKING, Any, Concatenate, overload, override
12
+
13
+ import cytoolz as cz
5
14
 
6
15
  from ._aggregations import BaseAgg
7
16
  from ._booleans import BaseBool
8
- from ._constructors import IterConstructors
17
+ from ._dicts import BaseDict
9
18
  from ._eager import BaseEager
10
19
  from ._filters import BaseFilter
11
- from ._groups import BaseGroups
12
20
  from ._joins import BaseJoins
13
21
  from ._lists import BaseList
14
22
  from ._maps import BaseMap
@@ -21,8 +29,11 @@ if TYPE_CHECKING:
21
29
  from .._dict import Dict
22
30
 
23
31
 
32
+ class CommonMethods[T](BaseAgg[T], BaseEager[T], BaseDict[T]):
33
+ pass
34
+
35
+
24
36
  class Iter[T](
25
- BaseAgg[T],
26
37
  BaseBool[T],
27
38
  BaseFilter[T],
28
39
  BaseProcess[T],
@@ -32,18 +43,15 @@ class Iter[T](
32
43
  BaseTuples[T],
33
44
  BasePartitions[T],
34
45
  BaseJoins[T],
35
- BaseGroups[T],
36
- BaseEager[T],
37
- IterConstructors,
46
+ CommonMethods[T],
38
47
  ):
39
48
  """
40
- A wrapper around Python's built-in iterable types, providing a rich set of functional programming tools.
49
+ A wrapper around Python's built-in Iterators/Generators types, providing a rich set of functional programming tools.
41
50
 
42
- It supports lazy evaluation, allowing for efficient processing of large datasets.
51
+ It's designed around lazy evaluation, allowing for efficient processing of large datasets.
43
52
 
44
- It is not a collection itself, but a wrapper that provides additional methods for working with iterables.
45
-
46
- It can be constructed from any iterable, including `lists`, `tuples`, `sets`, and `generators`.
53
+ - To instantiate from a lazy Iterator/Generator, simply pass it to the standard constructor.
54
+ - To instantiate from an eager Sequence (like a list or set), use the `from_` class method.
47
55
  """
48
56
 
49
57
  __slots__ = ("_data",)
@@ -51,8 +59,160 @@ class Iter[T](
51
59
  def __init__(self, data: Iterator[T] | Generator[T, Any, Any]) -> None:
52
60
  self._data = data
53
61
 
54
- def __repr__(self) -> str:
55
- return f"{self.__class__.__name__}({self.unwrap().__repr__()})"
62
+ @staticmethod
63
+ def from_count(start: int = 0, step: int = 1) -> Iter[int]:
64
+ """
65
+ Create an infinite iterator of evenly spaced values.
66
+
67
+ **Warning** ⚠️
68
+ This creates an infinite iterator.
69
+ Be sure to use `Iter.take()` or `Iter.slice()` to limit the number of items taken.
70
+
71
+ Args:
72
+ start: Starting value of the sequence. Defaults to 0.
73
+ step: Difference between consecutive values. Defaults to 1.
74
+ Example:
75
+ ```python
76
+ >>> import pyochain as pc
77
+ >>> pc.Iter.from_count(10, 2).take(3).into(list)
78
+ [10, 12, 14]
79
+
80
+ ```
81
+ """
82
+
83
+ return Iter(itertools.count(start, step))
84
+
85
+ @staticmethod
86
+ def from_func[U](func: Callable[[U], U], input: U) -> Iter[U]:
87
+ """
88
+ Create an infinite iterator by repeatedly applying a function on an original input.
89
+
90
+ **Warning** ⚠️
91
+ This creates an infinite iterator.
92
+ Be sure to use `Iter.take()` or `Iter.slice()` to limit the number of items taken.
93
+
94
+ Args:
95
+ func: Function to apply repeatedly.
96
+ input: Initial value to start the iteration.
97
+
98
+ Example:
99
+ ```python
100
+ >>> import pyochain as pc
101
+ >>> pc.Iter.from_func(lambda x: x + 1, 0).take(3).into(list)
102
+ [0, 1, 2]
103
+
104
+ ```
105
+ """
106
+
107
+ return Iter(cz.itertoolz.iterate(func, input))
108
+
109
+ @overload
110
+ @staticmethod
111
+ def from_[U](data: Iterable[U]) -> Iter[U]: ...
112
+ @overload
113
+ @staticmethod
114
+ def from_[U](data: U, *more_data: U) -> Iter[U]: ...
115
+ @staticmethod
116
+ def from_[U](data: Iterable[U] | U, *more_data: U) -> Iter[U]:
117
+ """
118
+ Create an iterator from any Iterable, or from unpacked values.
119
+
120
+ - An Iterable is any object capable of returning its members one at a time, permitting it to be iterated over in a for-loop.
121
+ - An Iterator is an object representing a stream of data; returned by calling `iter()` on an Iterable.
122
+ - Once an Iterator is exhausted, it cannot be reused or reset.
123
+
124
+ If you need to reuse the data, consider collecting it into a list first with `.collect()`.
125
+
126
+ In general, avoid intermediate references when dealing with lazy iterators, and prioritize method chaining instead.
127
+
128
+ Args:
129
+ data: Iterable to convert into an iterator, or a single value.
130
+ more_data: Additional values to include if 'data' is not an Iterable.
131
+ Example:
132
+ ```python
133
+ >>> import pyochain as pc
134
+ >>> data: tuple[int, ...] = (1, 2, 3)
135
+ >>> iterator = pc.Iter.from_(data)
136
+ >>> iterator.unwrap().__class__.__name__
137
+ 'tuple_iterator'
138
+ >>> mapped = iterator.map(lambda x: x * 2)
139
+ >>> mapped.unwrap().__class__.__name__
140
+ 'map'
141
+ >>> mapped.collect(tuple).unwrap()
142
+ (2, 4, 6)
143
+ >>> # iterator is now exhausted
144
+ >>> iterator.collect().unwrap()
145
+ []
146
+ >>> # Creating from unpacked values
147
+ >>> pc.Iter.from_(1, 2, 3).collect(tuple).unwrap()
148
+ (1, 2, 3)
149
+
150
+ ```
151
+ """
152
+ if cz.itertoolz.isiterable(data):
153
+ return Iter(iter(data))
154
+ else:
155
+ d: Iterable[U] = (data, *more_data) # type: ignore[assignment]
156
+ return Iter(iter(d))
157
+
158
+ @staticmethod
159
+ def unfold[S, V](seed: S, generator: Callable[[S], tuple[V, S] | None]) -> Iter[V]:
160
+ """
161
+ Create an iterator by repeatedly applying a generator function to an initial state.
162
+
163
+ The `generator` function takes the current state and must return:
164
+
165
+ - A tuple `(value, new_state)` to emit the `value` and continue with the `new_state`.
166
+ - `None` to stop the generation.
167
+
168
+ This is functionally equivalent to a state-based `while` loop.
169
+
170
+ **Warning** ⚠️
171
+ If the `generator` function never returns `None`, it creates an infinite iterator.
172
+ Be sure to use `Iter.take()` or `Iter.slice()` to limit the number of items taken if necessary.
173
+
174
+ Args:
175
+ seed: Initial state for the generator.
176
+ generator: Function that generates the next value and state.
177
+
178
+ Example:
179
+ ```python
180
+ >>> import pyochain as pc
181
+ >>> # Example 1: Simple counter up to 5
182
+ >>> def counter_generator(state: int) -> tuple[int, int] | None:
183
+ ... if state < 5:
184
+ ... return (state * 10, state + 1)
185
+ ... return None
186
+ >>> pc.Iter.unfold(seed=0, generator=counter_generator).into(list)
187
+ [0, 10, 20, 30, 40]
188
+ >>> # Example 2: Fibonacci sequence up to 100
189
+ >>> type FibState = tuple[int, int]
190
+ >>> def fib_generator(state: FibState) -> tuple[int, FibState] | None:
191
+ ... a, b = state
192
+ ... if a > 100:
193
+ ... return None
194
+ ... return (a, (b, a + b))
195
+ >>> pc.Iter.unfold(seed=(0, 1), generator=fib_generator).into(list)
196
+ [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
197
+ >>> # Example 3: Infinite iterator (requires take())
198
+ >>> pc.Iter.unfold(seed=1, generator=lambda s: (s, s * 2)).take(5).into(list)
199
+ [1, 2, 4, 8, 16]
200
+
201
+ ```
202
+ """
203
+ from ._main import Iter
204
+
205
+ def _unfold() -> Iterator[V]:
206
+ current_seed: S = seed
207
+ while True:
208
+ result: tuple[V, S] | None = generator(current_seed)
209
+ if result is None:
210
+ break
211
+ value, next_seed = result
212
+ yield value
213
+ current_seed = next_seed
214
+
215
+ return Iter(_unfold())
56
216
 
57
217
  def itr[**P, R, U: Iterable[Any]](
58
218
  self: Iter[U],
@@ -86,9 +246,9 @@ class Iter[T](
86
246
  """
87
247
 
88
248
  def _itr(data: Iterable[U]) -> Generator[R, None, None]:
89
- return (func(Iter.from_(x), *args, **kwargs) for x in data)
249
+ return (func(Iter(iter(x)), *args, **kwargs) for x in data)
90
250
 
91
- return self.apply(_itr)
251
+ return self._lazy(_itr)
92
252
 
93
253
  def struct[**P, R, K, V](
94
254
  self: Iter[dict[K, V]],
@@ -100,6 +260,7 @@ class Iter[T](
100
260
  Apply a function to each element after wrapping it in a Dict.
101
261
 
102
262
  This is a convenience method for the common pattern of mapping a function over an iterable of dictionaries.
263
+
103
264
  Args:
104
265
  func: Function to apply to each wrapped dictionary.
105
266
  *args: Positional arguments to pass to the function.
@@ -151,74 +312,158 @@ class Iter[T](
151
312
  def _struct(data: Iterable[dict[K, V]]) -> Generator[R, None, None]:
152
313
  return (func(Dict(x), *args, **kwargs) for x in data)
153
314
 
154
- return self.apply(_struct)
315
+ return self._lazy(_struct)
155
316
 
156
- def with_keys[K](self, keys: Iterable[K]) -> Dict[K, T]:
317
+ def apply[**P, R](
318
+ self,
319
+ func: Callable[Concatenate[Iterable[T], P], Iterator[R]],
320
+ *args: P.args,
321
+ **kwargs: P.kwargs,
322
+ ) -> Iter[R]:
157
323
  """
158
- Create a Dict by zipping the iterable with keys.
324
+ Apply a function to the underlying Iterator and return a new Iter instance.
325
+
326
+ Allow to pass user defined functions that transform the iterable while retaining the Iter wrapper.
159
327
 
160
328
  Args:
161
- keys: Iterable of keys to pair with the values.
329
+ func: Function to apply to the underlying iterable.
330
+ *args: Positional arguments to pass to the function.
331
+ **kwargs: Keyword arguments to pass to the function.
332
+
162
333
  Example:
163
334
  ```python
164
335
  >>> import pyochain as pc
165
- >>> keys = ["a", "b", "c"]
166
- >>> values = [1, 2, 3]
167
- >>> pc.Iter.from_(values).with_keys(keys).unwrap()
168
- {'a': 1, 'b': 2, 'c': 3}
169
- >>> # This is equivalent to:
170
- >>> pc.Iter.from_(keys).zip(values).pipe(
171
- ... lambda x: pc.Dict(x.into(dict)).unwrap()
172
- ... )
173
- {'a': 1, 'b': 2, 'c': 3}
336
+ >>> def double(data: Iterable[int]) -> Iterator[int]:
337
+ ... return (x * 2 for x in data)
338
+ >>> pc.Iter.from_([1, 2, 3]).apply(double).into(list)
339
+ [2, 4, 6]
174
340
 
175
341
  ```
176
342
  """
177
- from .._dict import Dict
343
+ return self._lazy(func, *args, **kwargs)
178
344
 
179
- return Dict(dict(zip(keys, self.unwrap())))
180
-
181
- def with_values[V](self, values: Iterable[V]) -> Dict[T, V]:
345
+ def collect(self, factory: Callable[[Iterable[T]], Sequence[T]] = list) -> Seq[T]:
182
346
  """
183
- Create a Dict by zipping the iterable with values.
347
+ Collect the elements into a sequence, using the provided factory.
184
348
 
185
349
  Args:
186
- values: Iterable of values to pair with the keys.
350
+ factory: A callable that takes an iterable and returns a Sequence. Defaults to list.
351
+
187
352
  Example:
188
353
  ```python
189
354
  >>> import pyochain as pc
190
- >>> keys = [1, 2, 3]
191
- >>> values = ["a", "b", "c"]
192
- >>> pc.Iter.from_(keys).with_values(values).unwrap()
193
- {1: 'a', 2: 'b', 3: 'c'}
194
- >>> # This is equivalent to:
195
- >>> pc.Iter.from_(keys).zip(values).pipe(
196
- ... lambda x: pc.Dict(x.into(dict)).unwrap()
197
- ... )
198
- {1: 'a', 2: 'b', 3: 'c'}
355
+ >>> pc.Iter.from_(range(5)).collect().unwrap()
356
+ [0, 1, 2, 3, 4]
199
357
 
200
358
  ```
201
359
  """
202
- from .._dict import Dict
360
+ return self._eager(factory)
361
+
362
+ @override
363
+ def unwrap(self) -> Iterator[T]:
364
+ """
365
+ Unwrap and return the underlying Iterator.
203
366
 
204
- return Dict(dict(zip(self.unwrap(), values)))
367
+ ```python
368
+ >>> import pyochain as pc
369
+ >>> iterator = pc.Iter.from_([1, 2, 3])
370
+ >>> unwrapped = iterator.unwrap()
371
+ >>> list(unwrapped)
372
+ [1, 2, 3]
205
373
 
374
+ ```
375
+ """
376
+ return self._data # type: ignore[return-value]
206
377
 
207
- class Seq[T](BaseAgg[T], BaseEager[T]):
378
+
379
+ class Seq[T](CommonMethods[T]):
208
380
  """
209
- pyochain.Seq represent an in memory collection.
381
+ pyochain.Seq represent an in memory Sequence.
210
382
 
211
383
  Provides a subset of pyochain.Iter methods with eager evaluation, and is the return type of pyochain.Iter.collect().
212
384
  """
213
385
 
214
386
  __slots__ = ("_data",)
215
387
 
216
- def __init__(self, data: Collection[T]) -> None:
388
+ def __init__(self, data: Sequence[T]) -> None:
217
389
  self._data = data
218
390
 
391
+ @overload
392
+ @staticmethod
393
+ def from_[U](data: Sequence[U]) -> Seq[U]: ...
394
+ @overload
395
+ @staticmethod
396
+ def from_[U](data: U, *more_data: U) -> Seq[U]: ...
397
+ @staticmethod
398
+ def from_[U](data: Sequence[U] | U, *more_data: U) -> Seq[U]:
399
+ """
400
+ Create a Seq from a Sequence or unpacked values.
401
+
402
+ Args:
403
+ data: Sequence of items or a single item.
404
+ more_data: Additional item to include if 'data' is not a Sequence.
405
+
406
+ Example:
407
+ ```python
408
+ >>> import pyochain as pc
409
+ >>> pc.Seq.from_([1, 2, 3]).unwrap()
410
+ [1, 2, 3]
411
+ >>> pc.Seq.from_(1, 2).unwrap()
412
+ (1, 2)
413
+
414
+ ```
415
+
416
+ """
417
+ if cz.itertoolz.isiterable(data):
418
+ return Seq(data)
419
+ else:
420
+ return Seq((data, *more_data))
421
+
219
422
  def iter(self) -> Iter[T]:
220
423
  """
221
424
  Get an iterator over the sequence.
222
425
  Call this to switch to lazy evaluation.
223
426
  """
224
- return Iter.from_(self.unwrap())
427
+ return self._lazy(iter)
428
+
429
+ def apply[**P, R](
430
+ self,
431
+ func: Callable[Concatenate[Iterable[T], P], Sequence[R]],
432
+ *args: P.args,
433
+ **kwargs: P.kwargs,
434
+ ) -> Seq[R]:
435
+ """
436
+ Apply a function to the underlying Sequence and return a Seq instance.
437
+
438
+ Allow to pass user defined functions that transform the Sequence while retaining the Seq wrapper.
439
+
440
+ Args:
441
+ func: Function to apply to the underlying Sequence.
442
+ *args: Positional arguments to pass to the function.
443
+ **kwargs: Keyword arguments to pass to the function.
444
+
445
+ Example:
446
+ ```python
447
+ >>> import pyochain as pc
448
+ >>> def double(data: Iterable[int]) -> Sequence[int]:
449
+ ... return [x * 2 for x in data]
450
+ >>> pc.Seq([1, 2, 3]).apply(double).into(list)
451
+ [2, 4, 6]
452
+
453
+ ```
454
+ """
455
+ return self._eager(func, *args, **kwargs)
456
+
457
+ @override
458
+ def unwrap(self) -> Sequence[T]:
459
+ """
460
+ Unwrap and return the underlying Sequence.
461
+
462
+ ```python
463
+ >>> import pyochain as pc
464
+ >>> pc.Seq([1, 2, 3]).unwrap()
465
+ [1, 2, 3]
466
+
467
+ ```
468
+ """
469
+ return self._data # type: ignore[return-value]
pyochain/_iter/_maps.py CHANGED
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import itertools
4
- from collections.abc import Callable, Collection, Generator, Iterable, Iterator, Mapping
4
+ from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence
5
5
  from functools import partial
6
- from typing import TYPE_CHECKING, Any, Concatenate, Self, overload
6
+ from typing import TYPE_CHECKING, Any, Concatenate, overload
7
7
 
8
8
  import cytoolz as cz
9
9
  import more_itertools as mit
@@ -20,29 +20,31 @@ class BaseMap[T](IterWrapper[T]):
20
20
  func: Callable[Concatenate[T, P], Any],
21
21
  *args: P.args,
22
22
  **kwargs: P.kwargs,
23
- ) -> Self:
23
+ ) -> None:
24
24
  """
25
- Apply a function to each element in the iterable.
25
+ Consume the Iterator by applying a function to each element in the iterable.
26
26
 
27
27
  Args:
28
28
  func: Function to apply to each element.
29
29
  args: Positional arguments for the function.
30
30
  kwargs: Keyword arguments for the function.
31
31
 
32
- Can be used for side effects such as printing or logging.
32
+ Is a terminal operation, and is useful for functions that have side effects, or when you want to force evaluation of a lazy iterable.
33
33
  ```python
34
34
  >>> import pyochain as pc
35
- >>> pc.Iter.from_([1, 2, 3]).for_each(lambda x: print(x)).collect().unwrap()
35
+ >>> pc.Iter.from_([1, 2, 3]).for_each(lambda x: print(x))
36
36
  1
37
37
  2
38
38
  3
39
- []
40
39
 
41
40
  ```
42
41
  """
43
- for v in self.unwrap():
44
- func(v, *args, **kwargs)
45
- return self
42
+
43
+ def _for_each(data: Iterable[T]) -> None:
44
+ for v in data:
45
+ func(v, *args, **kwargs)
46
+
47
+ return self.into(_for_each)
46
48
 
47
49
  def map[R](self, func: Callable[[T], R]) -> Iter[R]:
48
50
  """
@@ -58,7 +60,7 @@ class BaseMap[T](IterWrapper[T]):
58
60
 
59
61
  ```
60
62
  """
61
- return self.apply(partial(map, func))
63
+ return self._lazy(partial(map, func))
62
64
 
63
65
  @overload
64
66
  def flat_map[U, R](
@@ -94,7 +96,7 @@ class BaseMap[T](IterWrapper[T]):
94
96
  def _flat_map(data: Iterable[U]) -> map[R]:
95
97
  return map(func, itertools.chain.from_iterable(data))
96
98
 
97
- return self.apply(_flat_map)
99
+ return self._lazy(_flat_map)
98
100
 
99
101
  def map_star[U: Iterable[Any], R](
100
102
  self: IterWrapper[U], func: Callable[..., R]
@@ -123,11 +125,12 @@ class BaseMap[T](IterWrapper[T]):
123
125
  ['blue-S', 'blue-M', 'red-S', 'red-M']
124
126
 
125
127
  ```
128
+
126
129
  - Use map_star when the performance matters (it is faster).
127
130
  - Use map with unpacking when readability matters (the types can be inferred).
128
131
  """
129
132
 
130
- return self.apply(partial(itertools.starmap, func))
133
+ return self._lazy(partial(itertools.starmap, func))
131
134
 
132
135
  def map_if[R](
133
136
  self,
@@ -165,7 +168,7 @@ class BaseMap[T](IterWrapper[T]):
165
168
 
166
169
  ```
167
170
  """
168
- return self.apply(mit.map_if, predicate, func, func_else=func_else)
171
+ return self._lazy(mit.map_if, predicate, func, func_else=func_else)
169
172
 
170
173
  def map_except[R](
171
174
  self, func: Callable[[T], R], *exceptions: type[BaseException]
@@ -192,17 +195,17 @@ class BaseMap[T](IterWrapper[T]):
192
195
  def _map_except(data: Iterable[T]) -> Iterator[R]:
193
196
  return mit.map_except(func, data, *exceptions)
194
197
 
195
- return self.apply(_map_except)
198
+ return self._lazy(_map_except)
196
199
 
197
200
  def repeat(
198
- self, n: int, factory: Callable[[Iterable[T]], Collection[T]] = tuple
201
+ self, n: int, factory: Callable[[Iterable[T]], Sequence[T]] = tuple
199
202
  ) -> Iter[Iterable[T]]:
200
203
  """
201
- Repeat the entire iterable n times (as elements) and return Iter.
204
+ Repeat the entire iterable n times (as elements).
202
205
 
203
206
  Args:
204
207
  n: Number of repetitions.
205
- factory: Factory to create the repeated collection (default: tuple).
208
+ factory: Factory to create the repeated Sequence (default: tuple).
206
209
 
207
210
  ```python
208
211
  >>> import pyochain as pc
@@ -217,7 +220,7 @@ class BaseMap[T](IterWrapper[T]):
217
220
  def _repeat(data: Iterable[T]) -> Iterator[Iterable[T]]:
218
221
  return itertools.repeat(factory(data), n)
219
222
 
220
- return self.apply(_repeat)
223
+ return self._lazy(_repeat)
221
224
 
222
225
  @overload
223
226
  def repeat_last(self, default: T) -> Iter[T]: ...
@@ -247,7 +250,7 @@ class BaseMap[T](IterWrapper[T]):
247
250
 
248
251
  ```
249
252
  """
250
- return self.apply(mit.repeat_last, default)
253
+ return self._lazy(mit.repeat_last, default)
251
254
 
252
255
  def ichunked(self, n: int) -> Iter[Iterator[T]]:
253
256
  """
@@ -275,7 +278,7 @@ class BaseMap[T](IterWrapper[T]):
275
278
 
276
279
  ```
277
280
  """
278
- return self.apply(mit.ichunked, n)
281
+ return self._lazy(mit.ichunked, n)
279
282
 
280
283
  @overload
281
284
  def flatten[U](
@@ -297,7 +300,7 @@ class BaseMap[T](IterWrapper[T]):
297
300
 
298
301
  ```
299
302
  """
300
- return self.apply(itertools.chain.from_iterable)
303
+ return self._lazy(itertools.chain.from_iterable)
301
304
 
302
305
  def pluck[U: Mapping[Any, Any]](
303
306
  self: IterWrapper[U], *keys: str | int
@@ -333,17 +336,16 @@ class BaseMap[T](IterWrapper[T]):
333
336
  """
334
337
 
335
338
  getter = partial(cz.dicttoolz.get_in, keys)
336
- return self.apply(partial(map, getter))
339
+ return self._lazy(partial(map, getter))
337
340
 
338
341
  def round[U: float | int](
339
342
  self: IterWrapper[U], ndigits: int | None = None
340
343
  ) -> Iter[float]:
341
344
  """
342
- Round each element in the iterable to the given number of decimal places and return Iter.
345
+ Round each element in the iterable to the given number of decimal places.
343
346
 
344
347
  Args:
345
348
  ndigits: Number of decimal places to round to.
346
-
347
349
  ```python
348
350
  >>> import pyochain as pc
349
351
  >>> pc.Iter.from_([1.2345, 2.3456, 3.4567]).round(2).into(list)
@@ -355,4 +357,4 @@ class BaseMap[T](IterWrapper[T]):
355
357
  def _round(data: Iterable[U]) -> Generator[float | int, None, None]:
356
358
  return (round(x, ndigits) for x in data)
357
359
 
358
- return self.apply(_round)
360
+ return self._lazy(_round)