Python: Intermediate 1

Contents

Functions: Default Args, Keyword Args, *args, **kwargs

Master Python function arguments: default values, keyword arguments, variable-length arguments.

# Default arguments
def greet(name, msg="Hello"):
    print(f"{msg}, {name}!")
greet("Alice")
greet("Bob", "Hi")

# Keyword arguments
def add(a, b):
    return a + b
print(add(2, 3))
print(add(b=5, a=7))

# *args and **kwargs
def demo(*args, **kwargs):
    print("args:", args)
    print("kwargs:", kwargs)
demo(1, 2, 3, x=10, y=20)

# Argument unpacking
nums = (4, 5)
print(add(*nums))
params = {'a': 8, 'b': 9}
print(add(**params))

# Function with all types of arguments
def all_args(a, b=2, *args, c=3, **kwargs):
    print(f"a={a}, b={b}, args={args}, c={c}, kwargs={kwargs}")
all_args(1)
all_args(1, 10, 20, 30, c=99, x=5, y=6)

# Mutable default argument pitfall
def append_to_list(val, lst=[]):
    lst.append(val)
    return lst
print(append_to_list(1))
print(append_to_list(2))  # Same list reused!

# Correct way
def append_to_list_safe(val, lst=None):
    if lst is None:
        lst = []
    lst.append(val)
    return lst
print(append_to_list_safe(1))
print(append_to_list_safe(2))

Lambda, map, filter, reduce

Use anonymous functions and functional programming tools in Python.

# Lambda
square = lambda x: x**2
print(square(5))

# map
nums = [1, 2, 3, 4]
squares = list(map(lambda x: x**2, nums))
print(squares)

# filter
evens = list(filter(lambda x: x%2==0, nums))
print(evens)

# reduce
from functools import reduce
product = reduce(lambda x, y: x*y, nums)
print(product)

# Named vs lambda
def cube(x):
    return x**3
print(list(map(cube, nums)))

# filter with condition
greater_than_two = list(filter(lambda x: x > 2, nums))
print(greater_than_two)

# reduce for sum
total = reduce(lambda x, y: x + y, nums)
print(total)

# Sorting with lambda
words = ['apple', 'banana', 'pear', 'grape']
words_sorted = sorted(words, key=lambda w: len(w))
print(words_sorted)

Iterators & Generators

Understand iterators, the yield keyword, and generator expressions.

# Iterator
lst = [10, 20, 30]
itr = iter(lst)
print(next(itr))
print(next(itr))
print(next(itr))

# Custom iterator
class CountDown:
    def __init__(self, start):
        self.n = start
    def __iter__(self):
        return self
    def __next__(self):
        if self.n <= 0:
            raise StopIteration
        val = self.n
        self.n -= 1
        return val
for x in CountDown(3):
    print(x)

# Generator function
def gen():
    yield 1
    yield 2
    yield 3
for x in gen():
    print(x)

# Generator with state
def countdown(n):
    while n > 0:
        yield n
        n -= 1
for i in countdown(5):
    print(i)

# Infinite generator
def infinite():
    n = 0
    while True:
        yield n
        n += 1
# Use with caution: next(infinite())

# Generator expression
genexp = (x**2 for x in range(4))
print(list(genexp))

# Using send() with generator
def echo():
    while True:
        val = (yield)
        print(f"Received: {val}")
e = echo()
next(e)
e.send('Hello')

Decorators

Learn to use and write function decorators in Python, including functools.wraps.

# Simple decorator
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before call")
        result = func(*args, **kwargs)
        print("After call")
        return result
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")
say_hello()

# Decorator with functools.wraps
from functools import wraps
def log(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log
def add(a, b):
    return a + b
print(add(2, 3))
print(add.__name__)

# Decorator with arguments
def repeat(n):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def greet():
    print("Hi!")
greet()

# Class as decorator
class Uppercase:
    def __init__(self, func):
        self.func = func
    def __call__(self, *args, **kwargs):
        result = self.func(*args, **kwargs)
        return str(result).upper()

@Uppercase
def say(text):
    return text
print(say("hello world"))

Context Managers

Use the with statement and create custom context managers with __enter__ and __exit__.

# Using with statement
with open("test.txt", "w") as f:
    f.write("Hello, file!\n")

# Reading file safely
with open("test.txt") as f:
    for line in f:
        print(line.strip())

# Custom context manager (class)
class MyContext:
    def __enter__(self):
        print("Entering context")
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Exiting context")
        if exc_type:
            print(f"Exception: {exc_type}")
        return False  # Propagate exception
with MyContext():
    print("Inside context")

# Custom context manager (function)
from contextlib import contextmanager
@contextmanager
def my_cm():
    print("Enter")
    yield
    print("Exit")
with my_cm():
    print("Doing stuff")

# Suppressing exceptions
from contextlib import suppress
with suppress(ZeroDivisionError):
    print(1/0)

# Nested context managers
from contextlib import ExitStack
with ExitStack() as stack:
    f1 = stack.enter_context(open("test.txt"))
    # Add more context managers as needed