# Python Generators: Documentation & MCQs
## Introduction to Generators
Generators are a special type of function in Python that return an iterator and allow you to iterate over a series of values **lazily**. Unlike normal functions that return a single value using `return`, **generators use the `yield` keyword** to produce a series of values one by one, without storing them all in memory at once.
---
## How Generators Work
Generators are created using functions and the `yield` keyword. When a generator function is called, it does **not execute the function body immediately**. Instead, it returns a **generator object** that can be iterated using the `next()` function or a `for` loop.
---
## Create Python Generator
In Python, similar to defining a normal function, we can define a generator function using the `def` `keyword`, but instead of the `return` statement we use the `yield` statement.
```python
def generator_name(arg):
# statements
yield something
```
Here, the `yield` keyword is used to produce a value from the generator.
When the generator function is called, it does not execute the function body immediately. Instead, it returns a generator object that can be iterated over to produce the values.
## Example of a Basic Generator
Here is a simple example of a generator that yields the first three numbers:
```python
def simple_generator():
yield 1
yield 2
yield 3
gen = my_generator()
print(next(gen)) # Output: 1
print(next(gen)) # Output: 2
print(next(gen)) # Output: 3
```
# Python Generator Expression
In Python, a generator expression is a concise way to create a generator object. It is similar to a list comprehension, but instead of creating a list, it creates a generator object that can be iterated over to produce the values in the generator.
## Generator Expression Syntax
A generator expression has the following syntax:
Here, `expression` is a value that will be returned for each item in the `iterable`.
`(expression for item in iterable)`
The generator expression creates a generator object that produces the values of `expression` for each item in the iterable, one at a time, when iterated over.
## Example: Python Generator Expression
```python
# create the generator object
squares_generator = (i * i for i in range(5))
# iterate over the generator and print the values
for i in squares_generator:
print(i)
```
**outputs**
0
1
4
9
16
In this example, we have created a generator object that will produce the squares of the numbers 0 through 4 when iterated over. Then, to iterate over the generator and get the values, we have used the for loop.
# Differences Between `return` and `yield`
- - - -
| Feature | `return` | `yield` |
|--------------------------|---------------------------------|-------------------------------------|
| **Function Behavior** | Ends the function execution | Pauses function and saves state |
| **Number of Returns** | One value only | Multiple values in succession |
| **State Retention** | No | Yes, state is retained between calls|
| **Memory Efficiency** | Less efficient (stores all at once) | More efficient (yields lazily) |
------
# Use Cases of Generators
1. **Processing large datasets:**
Generators allow you to read large files line by line without loading the entire file into memory.
2. **Infinite sequences:**
Generators can create infinite sequences without running out of memory.
**Represent Infinite Stream**
Generators are excellent mediums to represent an infinite stream of data. Infinite streams cannot be stored in memory, and since generators produce only one item at a time, they can represent an infinite stream of data.
The following generator function can generate all the even numbers (at least in theory).
```python
def all_even():
n = 0
while True:
yield n
n += 2
```
3. **Data pipelines:**
Generators can be used to stream data through multiple stages of processing lazily.
4. **Pipelining Generators:**
You can use multiple generators together to perform a series of tasks. For example, we can create one generator that generates Fibonacci numbers and another generator that squares those numbers.
To find the sum of the squares of Fibonacci numbers, we can connect these two generators so that the output from the Fibonacci generator is directly used by the squaring generator.
```python
def fibonacci_numbers(nums):
x, y = 0, 1
for _ in range(nums):
x, y = y, x + y
yield x
def square(nums):
for num in nums:
yield num ** 2
print(sum(square(fibonacci_numbers(10))))
```
**output:** 4895
----------
## Advantages of Generators
- **Memory efficient:** Items are produced only when needed.
- **Faster execution:** Useful for large datasets and infinite streams.
- **Cleaner code:** Avoids managing state variables manually.
- **Composable:** Can be used with other generators to build data pipelines.
----------
## Limitations of Generators
- **One-time use:** Generators can be iterated only once. After being exhausted, they cannot be reused.
- **Difficult to debug:** Since generators are executed lazily, it can be harder to trace errors.
- **No random access:** Unlike lists, you cannot access elements by index.
## Statefulness of Generators
Generators retain their state between calls. This makes them useful for iterating over large datasets without recalculating everything from scratch.
### Example:
```python
def countdown(n):
while n > 0:
yield n
n -= 1
gen = countdown(3)
print(next(gen)) # Output: 3
print(next(gen)) # Output: 2`
```
## Advanced Generator Concepts
- **Using `yield from`**: This allows one generator to delegate part of its operations to another generator, simplifying the code structure.
```python
`def subgenerator():
yield from range(3)
def main_generator():
yield from subgenerator()`
```
- **The `send()` Method**: You can send values into a generator, which can affect its behavior and control flow.
- **The `throw()` Method**: This allows you to raise exceptions inside the generator.
- **The `close()` Method**: This stops the generator's execution.
- ---
## Exceptions in Python Generators
In Python, exceptions related to generators are similar to those found in regular functions and iterators. Here are some key exceptions that you might encounter when working with generators:
### 1. StopIteration
- **Description**: This exception is raised to signal that there are no further items to be produced by a generator.
- **When It Occurs**: It occurs automatically when a generator function runs out of `yield` statements, indicating that the iteration is complete.
- **Example**:
```python
def my_generator():
yield 1
yield 2
gen = my_generator()
print(next(gen)) # Output: 1
print(next(gen)) # Output: 2
print(next(gen)) # Raises StopIteration
```
## 2. ValueError
- **Description**: This exception may occur when using the `next()` function with a generator if it’s called after the generator is exhausted.
- **When It Occurs**: It can also occur if you try to send a value to a generator that does not accept it (when using `yield` in conjunction with `send()`).
- **Example**:
```python
`def simple_gen():
value = yield
return value
gen = simple_gen()
next(gen) # Start the generator
print(gen.send(10)) # Raises ValueError`
```
## 3. TypeError
- **Description**: This exception can be raised when you try to iterate over a generator using an incompatible type or if you try to use `yield` in a non-generator function.
- **When It Occurs**: It happens if you misuse the generator in ways that violate the expected types.
- **Example**:
```python
`def incorrect_gen():
yield 1
yield "string"
for item in incorrect_gen():
print(item + 1) # Raises TypeError when "string" is encountered`
```
## 4. GeneratorExit
- **Description**: This exception is raised when a generator's `close()` method is called.
- **When It Occurs**: It allows the generator to perform cleanup actions if necessary.
- **Example**:
```python
`def gen():
try:
yield 1
finally:
print("Generator closed!")
g = gen()
next(g) # Output: 1
g.close() # Output: Generator closed!`
```
## Handling Exceptions
You can handle these exceptions in your code using `try` and `except` blocks to ensure your program behaves gracefully in case of errors.
## Example of Handling Exceptions
```python
`def safe_gen():
try:
yield 1
yield 2
except StopIteration:
print("Generator has been exhausted.")
except ValueError:
print("Value error occurred.")
except TypeError:
print("Type error occurred.")
gen = safe_gen()
try:
print(next(gen))
print(next(gen))
print(next(gen)) # This will raise StopIteration
except StopIteration:
print("Caught StopIteration.")`
```
## MCQs on Python Generators
### 1. What is the primary use of generators in Python?
- a) To return a single value
- b) To generate a sequence of values lazily
- c) To store data in memory
- d) To process files line by line without reading them
**Answer:** b) To generate a sequence of values lazily
----------
### 2. Which keyword is used to create a generator in Python?
- a) `return`
- b) `yield`
- c) `continue`
- d) `break`
**Answer:** b) `yield`
----------
### 3. What happens when a generator is exhausted?
- a) It raises a `MemoryError`
- b) It raises a `StopIteration` exception
- c) It resets the generator to the initial state
- d) It continues indefinitely
**Answer:** b) It raises a `StopIteration` exception
----------
### 4. How do you retrieve the next value from a generator?
- a) `get()`
- b) `yield()`
- c) `next()`
- d) `iterate()`
**Answer:** c) `next()`
----------
### 5. Which of the following is NOT true about generators?
- a) They yield values lazily
- b) They retain state between yields
- c) They can be reused after being exhausted
- d) They use the `yield` keyword
**Answer:** c) They can be reused after being exhausted
----------
### 6. What will the following code output?
```python
`def gen():
yield 10
return 20
g = gen()
print(next(g))`
```
- a) 10
- b) 20
- c) None
- d) StopIteration
**Answer:** a) 10
----------
### 7. What is the purpose of the `yield` statement?
- a) To generate a random number
- b) To pause the function and return a value
- c) To terminate the function immediately
- d) To return all values at once
**Answer:** b) To pause the function and return a value
----------
### 8. Which of the following best describes how a generator works?
- a) It loads all elements into memory before returning them
- b) It processes and yields values one by one, on demand
- c) It returns values in reverse order
- d) It uses `yield` to return a list of values
**Answer:** b) It processes and yields values one by one, on demand
----------
### 9. What does the `yield from` statement do?
- a) Stops the generator
- b) Delegates iteration to another iterable
- c) Converts a generator into a list
- d) Stores all elements in memory
**Answer:** b) Delegates iteration to another iterable
----------
### 10. Which of the following operations is invalid on a generator?
- a) Using `next()`
- b) Using a `for` loop
- c) Converting to a list with `list()`
- d) Accessing elements by index
**Answer:** d) Accessing elements by index
### 11. What is the output of the following code?
```python
def generator():
yield 1
yield 2
gen = generator()
print(list(gen))
print(list(gen))`
```
- a) [1, 2], [1, 2]
- b) [1, 2], []
- c) [], [1, 2]
- d) Error
**Answer:** b) [1, 2], []
----------
### 12. Which built-in function can be used to convert a generator into a list?
- a) `list()`
- b) `tuple()`
- c) `set()`
- d) `dict()`
**Answer:** a) `list()`
----------
### 13. Which of the following is true about generators?
- a) Generators are always faster than lists
- b) Generators can produce infinite sequences
- c) Generators must contain at least one `return` statement
- d) Generators can be accessed randomly using an index
**Answer:** b) Generators can produce infinite sequences
----------
### 14. Which exception is raised when a generator finishes yielding values?
- a) `ValueError`
- b) `IndexError`
- c) `StopIteration`
- d) `KeyError`
**Answer:** c) `StopIteration`
----------
### 15. Which Python keyword allows a generator to delegate part of its operations to another generator?
- a) `yield from`
- b) `yield`
- c) `return`
- d) `break`
**Answer:** a) `yield from`
----------
### 16. What is a key difference between an iterator and a generator?
- a) Generators store all values in memory
- b) Generators are created using the `class` keyword
- c) Generators use `yield` to return values lazily
- d) Iterators cannot be used with `for` loops
**Answer:** c) Generators use `yield` to return values lazily
----------
### 17. What will the following code output?
```python
`def gen():
yield 1
yield 2
yield 3
g = gen()
for i in g:
print(i)
for i in g:
print(i)`
```
- a) 1, 2, 3
- b) 1, 2, 3, 1, 2, 3
- c) 1, 2, 3, (and then nothing)
- d) None
**Answer:** a) 1, 2, 3
----------
### 18. Can a generator function have both `yield` and `return` statements?
- a) Yes, but `return` terminates the generator
- b) No, only one of them is allowed
- c) Yes, but they must alternate
- d) No, `yield` replaces `return` entirely
**Answer:** a) Yes, but `return` terminates the generator
----------
### 19. Which of the following can NOT be used to iterate over a generator?
- a) `for` loop
- b) `while` loop with `next()`
- c) List comprehension
- d) Accessing elements by index
**Answer:** d) Accessing elements by index
----------
### 20. What will happen if a generator function has no `yield` statement?
- a) It will return an empty generator
- b) It will raise a `TypeError`
- c) It will return `None`
- d) It will run but not produce any values
**Answer:** b) It will raise a `TypeError`
----------
### 21. How can you restart a generator after it has been exhausted?
- a) Use `reset()` method
- b) Use `next()` on the same generator object
- c) Create a new instance of the generator
- d) Use `seek(0)` method
**Answer:** c) Create a new instance of the generator
----------
### 22. Which of the following will **exhaust** a generator immediately?
- a) `for i in generator()`
- b) `sum(generator())`
- c) `next(generator())`
- d) Both (a) and (b)
**Answer:** d) Both (a) and (b)
----------
### 23. What will be the result of running the following code?
```python
`def gen():
yield from range(3)
g = gen()
print(next(g))
print(next(g))`
```
- a) 0, 1
- b) 1, 2
- c) 2, 3
- d) 0, 2
**Answer:** a) 0, 1
----------
### 24. Which of the following is the **main reason** to use generators?
- a) To store large datasets in memory
- b) To produce values lazily and efficiently
- c) To avoid writing functions
- d) To reduce code size
**Answer:** b) To produce values lazily and efficiently
----------
### 25. What will the following code output?
```python
`def gen():
for i in range(3):
yield i
g = gen()
print(next(g))
print(next(g))
g = gen()
print(next(g))`
```
- a) 0, 1, 0
- b) 0, 1, 2
- c) 0, 1, StopIteration
- d) 0, 1, Error
**Answer:** a) 0, 1, 0
----
### 26. What is the correct way to check if a generator is exhausted?
- a) Use the `len()` function
- b) Use `next()` and handle `StopIteration`
- c) Check with `if generator:`
- d) Use `is_empty()` method
**Answer:** b) Use `next()` and handle `StopIteration`
---
### 27. What happens if a generator has multiple `yield` statements in a loop?
```python
`def gen():
for i in range(2):
yield i
yield i + 10
g = gen()
print(list(g))`
```
- a) [0, 1, 10, 11]
- b) [0, 10, 1, 11]
- c) [0, 1, 10]
- d) [1, 10, 11]
**Answer:** b) [0, 10, 1, 11]
----------
### 28. Which of the following can **terminate** a generator?
- a) Reaching the end of the function
- b) Encountering a `return` statement
- c) Raising an exception
- d) All of the above
**Answer:** d) All of the above
----------
### 29. What will be the output of the following code?
```python
`def gen():
yield from [1, 2]
yield 3
g = gen()
print(list(g))`
```
- a) [1, 2]
- b) [1, 2, 3]
- c) [2, 3]
- d) None
**Answer:** b) [1, 2, 3]
----------
### 30. Can a generator contain both `yield` and `yield from` statements?
- a) Yes, but only one can be used at a time
- b) Yes, both can be used within the same generator function
- c) No, `yield from` must replace all `yield` statements
- d) No, it's not allowed in Python
**Answer:** b) Yes, both can be used within the same generator function
----------
### 31. Which of the following is true about the `yield from` statement?
- a) It only works with lists
- b) It allows delegation to sub-generators
- c) It can only yield one value
- d) It raises an error if used in a generator
**Answer:** b) It allows delegation to sub-generators
----------
### 32. What will the following code output?
```python
`def counter():
i = 0
while i < 2:
yield i
i += 1
gen = counter()
print(sum(gen))
print(sum(gen))`
```
- a) 1, 1
- b) 1, 0
- c) 0, 0
- d) 3, 0
**Answer:** b) 1, 0
----------
# MCQs on Exceptions and Handling in Python Generators
### Question 1
What exception is raised when a generator has no further items to yield?
- A) ValueError
- B) StopIteration
- C) TypeError
- D) GeneratorExit
**Answer**: B) StopIteration
---
### Question 2
Which of the following exceptions can occur if you attempt to send a value to a generator that does not accept it?
- A) StopIteration
- B) TypeError
- C) ValueError
- D) GeneratorExit
**Answer**: C) ValueError
---
### Question 3
What exception will be raised when you call the `close()` method on a generator?
- A) StopIteration
- B) ValueError
- C) GeneratorExit
- D) TypeError
**Answer**: C) GeneratorExit
---
### Question 4
Which exception is raised when trying to iterate over a generator using an incompatible type?
- A) StopIteration
- B) TypeError
- C) ValueError
- D) GeneratorExit
**Answer**: B) TypeError
---
### Question 5
In which part of the generator's code should you typically handle exceptions?
- A) Inside the `__init__` method
- B) Inside the `yield` statement
- C) Using `try` and `except` blocks
- D) In the main program where the generator is called
**Answer**: C) Using `try` and `except` blocks
---
### Question 6
What will happen if you attempt to call `next()` on an exhausted generator?
- A) It will return None.
- B) It will raise a TypeError.
- C) It will raise a StopIteration exception.
- D) It will return the last yielded value.
**Answer**: C) It will raise a StopIteration exception
---
### Question 7
What does the `finally` block in a generator do?
- A) It stops the generator from yielding any more values.
- B) It performs cleanup actions when the generator is closed.
- C) It raises an exception.
- D) It returns a value from the generator.
**Answer**: B) It performs cleanup actions when the generator is closed.
---
### Question 8
How can you handle exceptions raised by a generator in your code?
- A) By ignoring them
- B) By using `if` statements
- C) By using `try` and `except` blocks
- D) By restarting the generator
**Answer**: C) By using `try` and `except` blocks