Step 1: Iterators
Iterators allow sequential access to elements in a collection.
numbers = [1, 2, 3] # a list of numbers
it = iter(numbers) # create an iterator object from the list
print(next(it)) # prints 1, moves pointer to next element
print(next(it)) # prints 2
print(next(it)) # prints 3
Real-life Example: Reading lines from a large log file:
# Open a large log file and read line by line
with open("system.log", "r") as f:
lines = iter(f) # create iterator over file lines
for i in range(5): # print first 5 lines
print(next(lines).strip())
Step 2: Custom Iterators
Create your own iterator by implementing __iter__() and __next__().
class Counter:
def __init__(self, low, high):
self.current = low
self.high = high
def __iter__(self):
return self
def __next__(self):
if self.current > self.high:
raise StopIteration
else:
self.current += 1
return self.current - 1
for num in Counter(1, 5):
print(num)
Real-life Example: Paginated API request:
class PageIterator:
def __init__(self, pages):
self.pages = pages
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index >= len(self.pages):
raise StopIteration
page = self.pages[self.index]
self.index += 1
return page
pages = ["Page1 data", "Page2 data", "Page3 data"]
for page in PageIterator(pages):
print(page)
Step 3: Generators
Generators yield values lazily using yield, saving memory for large datasets.
def my_generator(n):
for i in range(n):
yield i
for val in my_generator(5):
print(val)
Real-life Example: Streaming sensor data:
def sensor_stream(n):
"""Simulate reading sensor values""
for i in range(n):
yield f"Sensor reading {i}: {i*0.5}"
for reading in sensor_stream(5):
print(reading)
Step 4: Generator Expressions
Compact way to create generators similar to list comprehensions.
squares = (x*x for x in range(5))
for val in squares:
print(val)
Real-life Example: Calculating squared values of user IDs:
user_ids = range(1, 1000000)
squared_ids = (uid*uid for uid in user_ids)
for i, val in enumerate(squared_ids):
if i < 5: # just print first 5 to test
print(val)
Step 5: Decorators
Decorators modify or enhance functions without changing their code.
def decorator(func):
def wrapper():
print("Before function")
func()
print("After function")
return wrapper
@decorator
def say_hello():
print("Hello!")
say_hello()
Real-life Example: Logging function calls:
def log_calls(func):
def wrapper():
print(f"Function {func.__name__} started")
func()
print(f"Function {func.__name__} ended")
return wrapper
@log_calls
def process_data():
print("Processing data...")
process_data()
Step 6: Decorators with Arguments
def repeat(n):
def decorator(func):
def wrapper():
for _ in range(n):
func()
return wrapper
return decorator
@repeat(3)
def say_hi():
print("Hi!")
say_hi()
Real-life Example: Retry network requests automatically:
import random
def retry(n):
def decorator(func):
def wrapper():
for _ in range(n):
try:
func()
break
except Exception as e:
print("Retrying due to:", e)
return wrapper
return decorator
@retry(3)
def fetch_data():
if random.random() < 0.7:
raise Exception("Network Error")
print("Data fetched successfully")
fetch_data()
Step 7: PyCharm Project Layout
IterGenDecorators
├── iterators_demo.py
├── generators_demo.py
└── decorators_demo.py
# Example: generators_demo.py
def squares(n):
"""Yield square numbers from 0 to n-1"""
for i in range(n):
yield i*i
for val in squares(5):
print(val)
Step 8: Tips & Best Practices
- Use iterators and generators for memory-efficient loops.
- Generators are ideal for streaming data or large sequences.
- Decorators improve code reuse and readability.
- Keep decorator logic simple and document it.
- Test custom iterators carefully to avoid infinite loops.
✔ End of Day 7 – You now understand Iterators, Generators, and Decorators in Python with full real-life examples ready for practical use!