Created
November 22, 2024 10:16
-
-
Save EnriqueSoria/f2bc427a2e6f7b2d8aa6b07265498e75 to your computer and use it in GitHub Desktop.
A (Python) list-like interface for iterables, that is lazy, a.k.a: only evaluates the iterable when needed
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from __future__ import annotations | |
import itertools | |
import collections | |
from typing import TypeVar | |
from typing import Iterable | |
from typing import Iterator | |
T = TypeVar("T") | |
class LazyList(collections.Sequence, collections.Sized): | |
"""A lazy list-like collection built upon an iterable, that only gets evaluated if needed""" | |
def __init__(self, original_iterable: Iterable[T]): | |
self._original_iterable = original_iterable | |
self._data: list[T] | None = None | |
def _get_iterable_copy(self) -> Iterable[T]: | |
self._original_iterable, iterable_copy = itertools.tee(self._original_iterable, 2) | |
return iterable_copy | |
@property | |
def data(self) -> list[T]: | |
if self._data is None: | |
self._data = list(self._get_iterable_copy()) | |
return self._data | |
def __eq__(self, other) -> bool: | |
if isinstance(other, LazyList): | |
return self.data == other.data | |
if isinstance(other, list): | |
return self.data == other | |
return NotImplemented | |
def __iter__(self) -> Iterator[T] | list[T]: | |
return iter(self._get_iterable_copy()) | |
def __getitem__(self, item) -> T: | |
return self.data[item] | |
def __contains__(self, item) -> bool: | |
return item in self.data | |
def __len__(self) -> int: | |
return len(self.data) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from unittest.mock import Mock | |
import pytest | |
from lazy_list import LazyList | |
def make_iterator_with_consumption_spy(iterator, exhausted_spy): | |
# consume items from given iterator | |
yield from iterator | |
# call spy so we know iterator has been exhausted | |
exhausted_spy() | |
class TestLazyListBehavesAsList: | |
def test_item_access(self): | |
lazy_list = LazyList(range(2)) | |
assert lazy_list[0] == 0 | |
assert lazy_list[1] == 1 | |
with pytest.raises(IndexError): | |
lazy_list[2] | |
def test_len(self): | |
lazy_list = LazyList(range(2)) | |
assert len(lazy_list) == 2 | |
def test_contains(self): | |
lazy_list = LazyList(range(2)) | |
assert 0 in lazy_list | |
assert 1 in lazy_list | |
assert 2 not in lazy_list | |
def test_equals_to_another_list(self): | |
lazy_list = LazyList(range(3)) | |
assert lazy_list == [0, 1, 2] | |
def test_multiple_iterations_produce_same_value(self): | |
# given | |
# a LazyList created using an iterator | |
original_iterator = (x for x in range(3)) | |
lazy_list = LazyList(original_iterator) | |
# when | |
# evaluating lazy_list multiple times | |
first_evaluation = list(lazy_list) | |
second_evaluation = list(lazy_list) | |
# then | |
# original iterator has been exhausted, as expected in an iterator | |
assert list(original_iterator) == [] | |
# but both lazy_list evaluations have the same values, as expected in a regular list | |
assert first_evaluation == second_evaluation == [0, 1, 2] | |
class TestLazyList: | |
def test_it_does_not_get_evaluated_when_generating_another_iterator(self): | |
# given | |
# a LazyList created using an iterator with a spy so we can know when it gets exhausted | |
end_spy = Mock() | |
iterator_with_spies = make_iterator_with_consumption_spy(range(10), end_spy) | |
lazy_list = LazyList(iterator_with_spies) | |
# when | |
# generating another iterator using it | |
(x for x in lazy_list) | |
# then | |
# it doesn't get evaluated | |
end_spy.assert_not_called() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment