7

Is it possible to have a list be evaluated lazily in Python?

For example

a = 1
list = [a]
print list
#[1]
a = 2
print list
#[1]

If the list was set to evaluate lazily then the final line would be [2]

Mike
  • 58,961
  • 76
  • 175
  • 221
  • 2
    This isn't what I've understood lazy evaluation to be. I'd say it's more like this: we want to find the first 30 positive numbers whose square is divisible by 3. With an eager language, you either code for performance - `while(list.length < 30) if(i*i % 3 == 0) list += i++;` or expressiveness (but throwing away huge unnecessary lists along the way) - `list1 = range 0..10000; for i in list1 if i * i % 3 == 0 list2.add i; list2.trim(30)`. With lazy you write something more like the latter, but get the performance of the former. Daniel Tao [explains it brilliantly](http://danieltao.com/lazy.js/). – Rob Grant Aug 14 '14 at 11:39
  • A lazy way might look like this: `range().filter(var where var*var % 3 == 0).take(30)` - it's written very expressively, but the code isn't run imperatively. It's run: `range` generates 1. `Filter` - fails because `1*1%3 != 0`. Then `range` generates 2. `Filter` fails. Then `range` generates 3. `Filter` passes because `3*3%3==0`. `Take` queues it as an answer. After `take` has taken 30, the algorithm stops. It only uses as much memory as it needs to, but it's also highly readable (and parallelisable) code. – Rob Grant Aug 14 '14 at 11:46

4 Answers4

11

The concept of "lazy" evaluation normally comes with functional languages -- but in those you could not reassign two different values to the same identifier, so, not even there could your example be reproduced.

The point is not about laziness at all -- it is that using an identifier is guaranteed to be identical to getting a reference to the same value that identifier is referencing, and re-assigning an identifier, a bare name, to a different value, is guaranteed to make the identifier refer to a different value from them on. The reference to the first value (object) is not lost.

Consider a similar example where re-assignment to a bare name is not in play, but rather any other kind of mutation (for a mutable object, of course -- numbers and strings are immutable), including an assignment to something else than a bare name:

>>> a = [1]
>>> list = [a]
>>> print list
[[1]]
>>> a[:] = [2]
>>> print list
[[2]]

Since there is no a - ... that reassigns the bare name a, but rather an a[:] = ... that reassigns a's contents, it's trivially easy to make Python as "lazy" as you wish (and indeed it would take some effort to make it "eager"!-)... if laziness vs eagerness had anything to do with either of these cases (which it doesn't;-).

Just be aware of the perfectly simple semantics of "assigning to a bare name" (vs assigning to anything else, which can be variously tweaked and controlled by using your own types appropriately), and the optical illusion of "lazy vs eager" might hopefully vanish;-)

Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
11

Came across this post when looking for a genuine lazy list implementation, but it sounded like a fun thing to try and work out.

The following implementation does basically what was originally asked for:

from collections import Sequence

class LazyClosureSequence(Sequence):
    def __init__(self, get_items):
        self._get_items = get_items

    def __getitem__(self, i):
        return self._get_items()[i]

    def __len__(self):
        return len(self._get_items())

    def __repr__(self):
        return repr(self._get_items())

You use it like this:

>>> a = 1
>>> l = LazyClosureSequence(lambda: [a])
>>> print l
[1]
>>> a = 2
>>> print l
[2]

This is obviously horrible.

Jamie Cockburn
  • 7,379
  • 1
  • 24
  • 37
7

Python is not really very lazy in general.

You can use generators to emulate lazy data structures (like infinite lists, et cetera), but as far as things like using normal list syntax, et cetera, you're not going to have laziness.

Amber
  • 507,862
  • 82
  • 626
  • 550
0

That is a read-only lazy list where it only needs a pre-defined length and a cache-update function:

import copy
import operations
from collections.abc import Sequence
from functools import partialmethod
from typing import Dict, Union

def _cmp_list(a: list, b: list, op, if_eq: bool, if_long_a: bool) -> bool:
    """utility to implement gt|ge|lt|le class operators"""
    if a is b:
        return if_eq
    for ia, ib in zip(a, b):
        if ia == ib:
            continue
        return op(ia, ib)

    la, lb = len(a), len(b)
    if la == lb:
        return if_eq
    if la > lb:
        return if_long_a
    return not if_long_a


class LazyListView(Sequence):
    def __init__(self, length):
        self._range = range(length)
        self._cache: Dict[int, Value] = {}

    def __len__(self) -> int:
        return len(self._range)

    def __getitem__(self, ix: Union[int, slice]) -> Value:
        length = len(self)

        if isinstance(ix, slice):
            clone = copy.copy(self)
            clone._range = self._range[slice(*ix.indices(length))]  # slicing
            return clone
        else:
            if ix < 0:
                ix += len(self)  # negative indices count from the end
            if not (0 <= ix < length):
                raise IndexError(f"list index {ix} out of range [0, {length})")
            if ix not in self._cache:
                ...  # update cache
            return self._cache[ix]

    def __iter__(self) -> dict:
        for i, _row_ix in enumerate(self._range):
            yield self[i]

    __eq__ = _eq_list
    __gt__ = partialmethod(_cmp_list, op=operator.gt, if_eq=False, if_long_a=True)
    __ge__ = partialmethod(_cmp_list, op=operator.ge, if_eq=True, if_long_a=True)
    __le__ = partialmethod(_cmp_list, op=operator.le, if_eq=True, if_long_a=False)
    __lt__ = partialmethod(_cmp_list, op=operator.lt, if_eq=False, if_long_a=False)

    def __add__(self, other):
        """BREAKS laziness and returns a plain-list"""
        return list(self) + other

    def __mul__(self, factor):
        """BREAKS laziness and returns a plain-list"""
        return list(self) * factor

    __radd__ = __add__
    __rmul__ = __mul__


Note that this class is discussed also in this SO.

ankostis
  • 8,579
  • 3
  • 47
  • 61