Skip to content

Instantly share code, notes, and snippets.

@EnriqueSoria
Created November 22, 2024 10:16
Show Gist options
  • Save EnriqueSoria/f2bc427a2e6f7b2d8aa6b07265498e75 to your computer and use it in GitHub Desktop.
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
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)
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