"Lazy" list item evaluation in Python

Viewed 623

For a lack of better words, I went with this title.

What I want is to be able to do something like this:

>>> from random import randint
>>> fruits = [
...     "Orange",
...     "Banana",
...     f"{randint(2,5)} Cherries",
... ]

>>> fruits[2]
'3 Cherries'

>>> fruits[2]
'5 Cherries'

>>> fruits[2]
'2 Cherries'

But instead, the literal expression inside the string gets evaluated once upon the creation of the list and gives the same result each time I access it.

I was wondering whether there was an easier/smarter way to approach this other than writing some complicated edge case handling (we're programmers after all; who doesn't like writing nice code and being all elegant and fancy?). I speak of edge case handling because only 6 of my 49 strings require this kind of "special" behaviour.

What I've tried so far is making a lambda function out of the randint call, this doesn't help though; same result. Maybe this is a case for lazy evaluation, but I need a little guidance on how (or whether?) to use it with a list.

4 Answers

If you want some of your items to be literal, but some be callable, you could create your own version of list:

from random import randint

class LazyList(list):
    'Like a list, but invokes callables'
    def __getitem__(self, key):
        item = super().__getitem__(key)
        if callable(item):
            item = item()
        return item

fruits = LazyList([
    "Orange",
    "Banana",
    lambda: f"{randint(2,5)} Cherries",
])

print(fruits[2])
print(fruits[2])

You're on the right track, wanting a lambda:

from random import randint

fruits = [
        lambda: "Orange",
        lambda: "Banana",
        lambda: f"{randint(2, 5)} Cherries",
]

print(fruits[2]())
print(fruits[2]())
print(fruits[2]())

There are ways to elide the () but it's probably not worth it.

Putting a lambda in a list sounds nice at first, but then you have to call this special (or all) indexes of your list. You cant iterate the list, you cant sort the list, ...

Its imho easier to create a function that provides you with lists:

from random import randint

def gimme_fruits ():
    return [ "Orange", "Banana", f"{randint(2,5)} Cherries",]

print(gimme_fruits())

Output:

['Orange', 'Banana', '3 Cherries']

['Orange', 'Banana', '4 Cherries']

['Orange', 'Banana', '2 Cherries']

Each single list is "fixed" in the amount of cherries - but you can do this:

for f in gimme_fruits() + gimme_fruits() + gimme_fruits():
    print(f)

To get your vitamins:

Orange
Banana
3 Cherries
Orange
Banana
3 Cherries
Orange
Banana
3 Cherries

Disclaimer: I'm the author of the library

You can use seqtools to do lazy mapping, it supports iterating and slicing:

>>> fruits = seqtools.smap(lambda x: x if isinstance(x, str) else x(), 
...                        fruits)
>>> fruits[0]
'Orange'
>>> fruits[-1]
'3 Cherries'

If you don't want to re-evaluate the function elements on each call, you can add some cache:

fruits = seqtools.add_cache(fruits)

Item assignment doesn't work through mappings obviously, so you will have to trick your way around for that.

Sorting will eventually evaluate all items in the list though, so you may as well do that upfront and call the regular list.sort() method.

Related