<div>
<p>
For more details, please visit <strong><a href="https://edube.org/learn/pe-2/python-essentials-2-module-4-1">this link</a></strong> to access documents and practice.
</p>
</div>
# Miscellaneous
### Generators
* Generators is a piece of specialized code able to produce a series of values, and to control the iteration process
```python
for i in range(5):
print(i)
# The range function is a generator
```
* Function returns 1 | Generator returns a series of values
**The iterator protocol is a way in which an object should behave to conform to the rules imposed by the context of the for and in statements.** An object conforming to the iterator protocol is called an **iterator**.

```python
class MyIterable:
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current >= self.end:
raise StopIteration
else:
self.current += 1
return self.current - 1
# Using the custom iterable
my_iterable = MyIterable(1, 5)
for i in my_iterable:
print(i)
```
* Explain: In this example, MyIterable is a class that implements the iterator protocol. It has ```__iter__``` which returns self (since the object itself is the iterator), and ```__next__``` which returns the current value and increments it until it reaches the end value. When you use a for loop to iterate over my_iterable, it will call iter(my_iterable) to get an iterator and then repeatedly call next(iterator) to get the values until a StopIteration is raised.
#### yield
```python
def fun(n):
for i in range(n):
return i
# the function above is not a generator
def fun(n):
for i in range(n):
yield i
# the function above is a generator
```
* yield is a keyword in Python that is used in the body of a function like a return statement, but instead of terminating the function, it temporarily suspends the function's execution and sends a value back to the caller. This allows the function to be resumed from where it left off the next time it's called.
#### list comprehensions
* list() function
```python
def powers_of_2(n):
power = 1
for i in range(n):
yield power
power *= 2
t = list(powers_of_2(3))
print(t)
# the list() function is used to convert the output of the generator function powers_of_2(n) into a list.
```
* in operator
```python
def powers_of_2(n):
power = 1
for i in range(n):
yield power
power *= 2
for i in range(20):
if i in powers_of_2(4):
print(i)
# the in operator allows you to use a generator
```
#### example of list comprehension
```python
list_1 = []
for ex in range(6):
list_1.append(10 ** ex)
list_2 = [10 ** ex for ex in range(6)]
print(list_1)
print(list_2)
#[1, 10, 100, 1000, 10000, 100000]
#[1, 10, 100, 1000, 10000, 100000]
```
* Use if statement in list comprehension
>expression_one if condition else expression_two
```python
# Example 1: Filtering even numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = [x for x in numbers if x % 2 == 0]
print(even_numbers)
```
#### List comprehensions vs. generators
* Just one change can turn any list comprehension into a generator
```python
the_list = [1 if x % 2 == 0 else 0 for x in range(10)]
the_generator = (1 if x % 2 == 0 else 0 for x in range(10))
for v in the_list:
print(v, end=" ")
print()
for v in the_generator:
print(v, end=" ")
print()
# output: 1 0 1 0 1 0 1 0 1 0
# this difference is the parentheses. The brackets make a comprehension, the parentheses make a generator.
```
#### The lambda function
>lambda arguments: expression
```python
add = lambda x, y: x + y
add(1,2)
#output = 3
```
**Using map with a lambda function**
```python
numbers = [1, 2, 3, 4, 5]
squared_numbers = map(lambda x: x**2, numbers)
print(list(squared_numbers)) # Output: [1, 4, 9, 16, 25]
# The map() function applies the lambda function to each element in the numbers list, returning an iterator of the results.
```
**Using filter with a lambda function**
```python
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = filter(lambda x: x % 2 == 0, numbers)
print(list(even_numbers)) # Output: [2, 4, 6, 8, 10]
# The filter() function applies the lambda function to each element in the numbers list and returns an iterator containing only the elements for which the lambda function returns True.
```
**Sorting a list of strings by their length**
```python
words = ["apple", "banana", "cherry", "date", "fig"]
sorted_words_by_length = sorted(words, key=lambda x: len(x))
print(sorted_words_by_length) # Output: ['fig', 'date', 'apple', 'cherry', 'banana']
# The sorted() function sorts the words list based on the length of each word. The key parameter specifies a function that calculates a "sort key" for each element. Here, it uses the lambda function to get the length of each word as the sort key.
```
### Closures
* **closure** is a technique which allows the storing of values in spite of the fact that the context in which they have been created does not exist anymore
# Understand basic Input/Output terminology
## • I/O modes