Background
Lazy evaluation is a programming technique where computations are deferred until their results are needed. In Python, this paradigm became popular thanks to generators and special functions from the standard library, such as itertools. Originally, such techniques came from functional languages, but Python provides its native and third-party tools for lazy processing.
The Problem
Traditional eager evaluation requires loading and calculating all data at once (for example, when using list comprehensions), which can lead to high memory consumption and performance degradation when dealing with large or infinite sequences. Lazy evaluation allows for "loading" and processing elements only as needed, avoiding unnecessary resource expenditure.
The Solution
In Python, lazy evaluation is implemented not only through generators (yield), but also via special lazy functions in the itertools module, standard functions like map, filter, as well as generator expressions. For example, the map() function returns a lazy iterator that computes values only when accessed:
# Example of lazy evaluation: squaring each number squares = map(lambda x: x ** 2, range(10**10)) # no memory used for the list print(next(squares)) # 0 print(next(squares)) # 1
Key Features:
Does the map() function in Python always implement lazy processing? How can one tell which standard functions are lazy?
No, starting from Python 3, the functions map(), filter(), zip() return iterators, thus implementing lazy evaluation. In Python 2, these functions returned lists. To determine if an object is lazy, one must check its type or consult the documentation:
result = map(lambda x: x+1, range(5)) print(type(result)) # 'map' is an iterator
Will lazy evaluation work when using a generator expression inside the sum() function?
The sum() function needs to iterate through the entire iterator until the end. The generator expression itself is lazy, but sum() ultimately consumes the entire sequence:
s = sum(x**2 for x in range(1000000)) # the generator is fully consumed
Can lazy evaluation be applied to regular lists and tuples, for instance, through map/lambda?
Yes, but lists and tuples are still loaded in memory. map will return a lazy iterator over them, but the original data is still fully in memory. For a fully lazy chain, it's preferable to work with generators at every stage:
def gen(): for i in range(1, 100): yield i squares = map(lambda x: x**2, gen()) # all lazy
map(...)) can lead to unexpected data absence.list()), negating the memory savings.A developer writes a handler for a large log file, using a generator expression to filter lines, but accidentally immediately converts it to a list:
with open('biglog.txt') as f: important_lines = [line for line in f if 'ERROR' in line] # loads the entire file
Pros:
Cons:
Another team uses a lazy approach with a generator expression and processes lines as they come:
with open('biglog.txt') as f: for line in (l for l in f if 'ERROR' in l): process(line)
Pros:
Cons: