Lazy Evaluation
Lazy evaluation refers to a programming technique that postpones the execution of a specific operation until the result is actually needed. It plays a vital role in controlling the execution flow and is closely associated with concepts such as functional programming, coroutines, generators, and asynchronous processing. Lazy evaluation helps to avoid unnecessary computations and is primarily used to enhance performance.
Lazy Evaluation and Other Programming Concepts.
Functional Programming
The concept of pure functions in functional programming doesn't rely on external state, allowing for calculations to be postponed, making it compatible with lazy evaluation. Pure functions in functional programming always return the same output for the same input, and they neither depend on nor modify external states. This means the time of call is independent of external conditions, offering flexibility in when it's executed.
Coroutines
Coroutines are programming components that can independently manage their execution flow, making them well-suited for lazy evaluation. Coroutines can be executed when necessary and paused in the middle, making them useful for resuming or postponing tasks. This feature aligns well with the fundamental concept of lazy evaluation.
Generators
Generators are functions that create iterators, generating values one by one upon request. Owing to this characteristic, generators can be considered quintessential examples of lazy evaluation. They can postpone the generation of values until they are genuinely needed.
Asynchronous Processing
Although asynchronous processing might not be directly related to lazy evaluation, in certain situations, they can be used together to complement each other effectively.
Asynchronous processing is a method to continue performing other tasks in parallel without waiting for a specific task to complete, reducing wait times. In contrast, lazy evaluation postpones data computations until necessary, avoiding unnecessary calculations. When combined, these two techniques can optimize the overall resource usage of a system.
Utilization of Lazy Evaluation
Performance Enhancement and Memory Savings
It prevents unnecessary calculations and memory allocations, thereby improving performance.
Lazy evaluation vs Eager evaluation
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = []
for i in numbers:
print("i < 6")
if i < 6:
print("i%2 == 0")
if i % 2 == 0:
print("i = i*10")
result.append(i * 10)
print(result)
result
i < 6
i%2 == 0
i < 6
i%2 == 0
i = i*10
i < 6
i%2 == 0
i < 6
i%2 == 0
i = i*10
i < 6
i%2 == 0
i < 6
i < 6
i < 6
i < 6
i < 6
[20, 40]
The result follows flow of Lazy evaluation.
The program consists of two if statements and operates in the following sequence
1. It checks if the value is less than 6. (If not, it skips steps 2 and 3 and moves on to the next element.)
2. It determines if the value is even. (If not, it skips step 3 and continues to the next element.)
3. It multiplies the element by 10.
These steps are sequentially applied to each element. Because each element goes through the entire process only once, it saves on additional memory and avoids unnecessary computation time.
On the other hand, consider a method where each if statement iterates through all the elements of the list, checking and storing the results in a data structure before proceeding to the next if statement. This approach is known as Eager Evaluation. This method consumes more memory and also involves redundant computations.
Infinite Data Structures
With lazy evaluation, only the necessary values are computed when needed, making it useful for representing infinite data structures.
Infinite sequences
def infinite_numbers():
num = 1
while True:
yield num
num += 1
gen = infinite_numbers()
# first five numbers
for _ in range(5):
print(next(gen))
(yield is one of Python's keywords. When used inside a function, that function behaves as a generator.
This means it returns num with each call. If called infinitely, it can return data indefinitely.
Infinite binary tree
class TreeNode:
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left if left is not None else TreeNode(value * 2)
self.right = right if right is not None else TreeNode(value * 2 + 1)
root = TreeNode(1)
# Starting from the root, print the value of the left child node 3 times.
node = root
for _ in range(3):
print(node.value)
node = node.left
In cases where a child node is not provided, the code automatically creates a new node. Only the parts that are actually accessed or used get instantiated in memory.
Drawbacks of Lazy Evaluation
Complexity in Debugging
def ex_generator():
print("Start")
yield 1
print("After first yield")
yield 2
print("End")
Taking generators as an example, due to their ability to pause and resume, they don't follow a sequential flow. This means that with each call, they return a value and save their current position. Because of this, it's challenging to pinpoint exactly where a problem occurs. One must fully understand the current state and execution flow of the generator to identify the actual cause of an issue.
Increased Memory Usage
def infinite_numbers():
num = 0
while True:
yield num
num += 1
generator = infinite_numbers()
# Creation of an infinite list. The program crashes.
numbers_list = list(generator)
There can be issues when processing large data using lazy evaluation. Imagine continuously generating data and maintaining it in memory. The program might be interrupted due to insufficient memory.
Latency
Lazy evaluation postpones data or computations until the point when they're actually needed. However, when that moment comes, there might be a need for significant data processing and computations, requiring a lot of resources. This can potentially lead to performance degradation in both the system and the program.
댓글