Chapter 5_Control Flow & Iteration
I. Conditional Statements
1. if … else
1) if :
if cond:
# T1
2) elif:
if cond:
# T1
elif cond:
# T2
3) else:
if cond:
# T1
else:
# T2
if cond:
# T1
elif cond:
# T2
else:
# T3
4) Shorthand if (ternary operator)
# One-line if statement:
if cond: action
# Short Hand If ... else
value/action_if_true if condition else value/action_if_false
# -----------------------------------------------------------
# Examples:
if a > b: print("a is greater than b")
print("A") if a > b else print("B")
bigger = a if a > b else b
5) Pass Statement
Note if statements cannot be empty, but if you for some reason have an
ifstatement with no content,put in the
passstatement to avoid getting an error.it’s also commonly used with loops, functions, and classes.
# Examples:
if age < 18:
pass # TODO: Add underage logic later
def calculate_discount(price):
pass # TODO: Implement discount logic
2. Match
match expression:
case x:
code block
case y:
code block
case z:
code block
Note (Default Value)
Use the underscore character
_as the last case value if you want a code block to execute whenthere are no other matches.
If
_is at the top an exception will raise called:SyntaxError: wildcard makes remaining patterns unreachable.
day = 4
match day:
case 6:
print("Today is Saturday")
case 7:
print("Today is Sunday")
case _:
print("Looking forward to the Weekend")
Note (Combine Values)
Use the pipe character
|as an or operator in the case evaluation to check for more than one value match in one case.
day = 4
match day:
case 1 | 2 | 3 | 4 | 5:
print("Today is a weekday")
case 6 | 7:
print("I love weekends!")
Note (If Statements as Guards)
You can add
ifstatements in the case evaluation as an extra condition-check.
month = 5
day = 4
match day:
case 1 | 2 | 3 | 4 | 5 if month == 4:
print("A weekday in April")
case 1 | 2 | 3 | 4 | 5 if month == 5:
print("A weekday in May")
case _:
print("No match")
II. Loops
3. While loops:
while cond:
# T1
4. For loops:
for item in iterable:
# T1
5. do .. while equivalent:
while True:
# code runs at least once
x = int(input("Enter a number: "))
if not cond:
break
# This behaves exactly like do { ... } while (condition);
6. range():
range(stop) # [0,...., stop-1]
range(start, stop) # [start, ... ,stop-1 ]
range(start, stop, step)
range() does NOT create a list in memory. It returns a range object (lazy, iterable).
7. enumerate() (Loop Power Tool):
# instead of :
for i in range(len(items)):
print(i, items[i])
# better:
for i, val in enumerate(items):
print(i, val)
8. zip() (Iterating Multiple Iterables):
names = ["A", "B"]
ages = [20, 30]
for name, age in zip(names, ages):
print(name, age)
9. break, continue, else in loops:
9.1 break:
Note With the break statement we can stop the loop before it has looped through all the items.
# for loop:
for x in fruits:
print(x)
if x == "banana":
break
# while loop:
i = 1
while i < 6:
print(i)
if i == 3:
break
i += 1
9.2 continue:
Note With the continue statement we can stop the current iteration, and continue with the next.
# for loop:
for x in fruits:
if x == "banana":
continue
print(x)
# while loop:
i = 0
while i < 6:
i += 1
if i == 3:
continue
print(i)
9.3 else:
Note Specifies a block of code to be executed when the loop is finished
# for loop:
for x in range(6):
print(x)
else:
print("Finally finished!")
# while loop:
i = 1
while i < 6:
print(i)
i += 1
else:
print("i is no longer less than 6")
Note The else block will NOT be executed if the loop is stopped by a
breakstatement.
III. Iterators:
1. Iterator vs Iterable
Note: (Iterable):
An object is iterable if it has
__iter__()method.iter(list1) # works → iterableIf
iter(obj)works →objis iterable.
⚠️ Iterables can create multiple iterators
Note: (Iterator)
An iterator is an object that:
- Remembers its current position
- Returns elements one at a time (
next()method)- Stops when exhausted. ( ⚠️ When finished →
StopIterationis raised. )
It implements two methods:
__iter__()__next__()
⚠️ Iterators are single-use
# Examples:
nums = [1, 2] # iterable
it = iter(nums) # iterator
print(next(it)) # 1
print(next(it)) # 2 / iterator consumed ( single use )
# iterable is NOT consumed
for x in nums:
print(x)
# The for loop actually creates an iterator object and executes the next() method for each loop.
#Example 2:
# or unpack the iterator at once using *
it2 = iter(nums)
print(*it2) # 1 2
1.1 Create an Iterator
To create an object/class as an iterator you have to implement the methods iter() and next() to your object.
The iter() method acts similar, you can do operations (initializing etc.), but must always return the iterator object itself.
The next() method also allows you to do operations, and must return the next item in the sequence.
Note: (Example1)
class MyNumbers: def __iter__(self): self.a = 1 return self def __next__(self): x = self.a self.a += 1 return x myclass = MyNumbers() myiter = iter(myclass) print(next(myiter))
The example above would continue forever if you had enough
next()statements, or if it was used in aforloop.To prevent the iteration from going on forever, we can use the
StopIterationstatement.In the
__next__()method, we can add a terminating condition to raise an error if the iteration is done a specified number of times
Note: (Example2)
# Stop after 20 iterations class MyNumbers: def __iter__(self): self.a = 1 return self def __next__(self): if self.a > 20: raise StopIteration else: x = self.a self.a += 1 return x
IV. Generators:
1. generator function:
Generators are functions that can pause and resume their execution. ( lazy evaluation )
When a generator function is called, it returns a generator object, which is an iterator.
The code inside the function is not executed yet, it is only compiled. The function only executes when you iterate over the generator.
The
yieldkeyword is what makes a function a generator.When
yieldis encountered, the function’s state is saved, and the value is returned.The next time the generator is called, it continues from where it left off.
# Generators Saves Memory
def large_sequence(n):
for i in range(n):
yield i
# This doesn't create a million numbers in memory
gen = large_sequence(1000000)
# You can manually iterate through a generator using the next() function:
print(next(gen))
print(next(gen))
print(next(gen))
# When there are no more values to yield, the generator raises a StopIteration exception:
# Or using a for loop (lazy evaluated)
for gen in large_sequence(1000000):
print(gen)
2. Generator Expressions:
Similar to list comprehensions, you can create generators using generator expressions with parentheses instead of square brackets.
same as iterator: Once a generator is exhausted, it cannot be reused.
Syntax:
(expression for item in iterable if condition)
gen = (x * x for x in range(5))
print(gen) # <generator object ...>
print(next(gen)) # 0
print(next(gen)) # 1
print(next(gen)) # 4
# This generates squares one by one, not all at once. (better memory efficiency, lazy evaluation)
3. Generator Methods:
Generators have special methods for advanced control:
3.1 send() Method
The send() method allows you to send a value to the generator:
def echo_generator():
while True:
received = yield # stops here ( I’m now waiting for someone to send me a value )
print("Received:", received)
gen = echo_generator()
next(gen) # Prime the generator
gen.send("Hello")
gen.send("World")
next(gen) # this one here in this context is equivalent to: gen.send(None)
next(gen) starts the generator and runs it until the first yield, so it’s ready to receive values via send().
3.2 close() Method
The close() method stops the generator :
def my_gen():
try:
yield 1
yield 2
yield 3
finally:
print("Generator closed")
gen = my_gen()
print(next(gen))
gen.close()
V. Advanced Generators:
1. yield from:
Used to hand over part (or all) of a generator’s work to another iterable or generator. It automatically:
- Iterates over the sub-generator
- Propagates values
- Handles StopIteration internally
yield from iterable is equivalent to:
for item in iterable:
yield item
# Example:
def gen1():
yield 1
yield 2
def gen2():
yield from gen1()
yield 3
for x in gen2():
print(x)
2. throw() Method:
Injects an exception into a running generator.
This allows error handling inside the generator.
If not caught inside the generator, the exception propagates to the caller.
def my_gen():
try:
yield 1
yield 2
except ValueError:
print("Exception caught inside generator")
gen = my_gen()
print(next(gen))
gen.throw(ValueError)
3. Generator Cleanup (finally):
When a generator is closed (explicitly or implicitly), the finally block is always executed.
This makes generators suitable for resource management.
def my_gen():
try:
yield 1
finally:
print("Cleanup executed")
gen = my_gen()
gen.close()