Functions

What are Functions?

Functions are reusable blocks of code that perform specific tasks. They help organize code and avoid repetition.

# Basic function definition
def greet():
    print("Hello, World!")

# Calling the function
greet()  # Output: Hello, World!

# Function with parameters
def greet_person(name):
    print(f"Hello, {name}!")

greet_person("Alice")  # Output: Hello, Alice!

# Function with return value
def add_numbers(a, b):
    result = a + b
    return result

sum_result = add_numbers(5, 3)
print(sum_result)  # Output: 8

Function Parameters and Arguments

# Positional parameters
def introduce(name, age, city):
    return f"Hi, I'm {name}, {age} years old, from {city}"

print(introduce("Bob", 25, "Boston"))

# Default parameters
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(greet("Alice"))           # Hello, Alice!
print(greet("Bob", "Hi"))       # Hi, Bob!

# Keyword arguments
def create_profile(name, age, city="Unknown", occupation="Student"):
    return {
        "name": name,
        "age": age,
        "city": city,
        "occupation": occupation
    }

profile1 = create_profile("Alice", 22)
profile2 = create_profile("Bob", 30, occupation="Engineer", city="Seattle")
print(profile1)
print(profile2)

Variable-Length Arguments

# *args - variable number of positional arguments
def sum_all(*numbers):
    total = 0
    for num in numbers:
        total += num
    return total

print(sum_all(1, 2, 3))        # 6
print(sum_all(1, 2, 3, 4, 5))  # 15

# **kwargs - variable number of keyword arguments
def create_student(**info):
    student = {}
    for key, value in info.items():
        student[key] = value
    return student

student1 = create_student(name="Alice", age=20, major="Computer Science")
student2 = create_student(name="Bob", grade="A", subject="Math")
print(student1)
print(student2)

# Combining all parameter types
def flexible_function(required, *args, default="default", **kwargs):
    print(f"Required: {required}")
    print(f"Args: {args}")
    print(f"Default: {default}")
    print(f"Kwargs: {kwargs}")

flexible_function("must_have", 1, 2, 3, default="custom", extra="info")

Lambda Functions (Anonymous Functions)

# Basic lambda function
square = lambda x: x ** 2
print(square(5))  # 25

# Lambda with multiple parameters
add = lambda x, y: x + y
print(add(3, 4))  # 7

# Using lambda with built-in functions
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
print(squared)  # [1, 4, 9, 16, 25]

even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers)  # [2, 4]

# Sorting with lambda
students = [("Alice", 85), ("Bob", 90), ("Charlie", 78)]
students.sort(key=lambda student: student[1])  # Sort by grade
print(students)  # [('Charlie', 78), ('Alice', 85), ('Bob', 90)]

Scope and Global Variables

# Local vs Global scope
global_var = "I'm global"

def test_scope():
    local_var = "I'm local"
    print(global_var)  # Can access global variable
    print(local_var)   # Can access local variable

test_scope()
print(global_var)  # Can access global variable
# print(local_var)  # Error! local_var not accessible here

# Modifying global variables
counter = 0

def increment():
    global counter
    counter += 1

increment()
increment()
print(counter)  # 2

# Nested functions and nonlocal
def outer_function():
    x = 10
    
    def inner_function():
        nonlocal x
        x += 5
        print(f"Inner x: {x}")
    
    inner_function()
    print(f"Outer x: {x}")

outer_function()

Higher-Order Functions

# Functions that take other functions as arguments
def apply_operation(numbers, operation):
    result = []
    for num in numbers:
        result.append(operation(num))
    return result

def double(x):
    return x * 2

def square(x):
    return x ** 2

numbers = [1, 2, 3, 4, 5]
doubled = apply_operation(numbers, double)
squared = apply_operation(numbers, square)
print(doubled)  # [2, 4, 6, 8, 10]
print(squared)  # [1, 4, 9, 16, 25]

# Functions that return other functions
def create_multiplier(factor):
    def multiplier(x):
        return x * factor
    return multiplier

times_2 = create_multiplier(2)
times_3 = create_multiplier(3)

print(times_2(5))  # 10
print(times_3(4))  # 12

Recursion

# Factorial using recursion
def factorial(n):
    if n == 0 or n == 1:  # Base case
        return 1
    else:
        return n * factorial(n - 1)  # Recursive case

print(factorial(5))  # 120

# Fibonacci sequence
def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)

for i in range(10):
    print(fibonacci(i), end=" ")  # 0 1 1 2 3 5 8 13 21 34

# Binary search (recursive)
def binary_search(arr, target, left=0, right=None):
    if right is None:
        right = len(arr) - 1
    
    if left > right:
        return -1  # Not found
    
    mid = (left + right) // 2
    
    if arr[mid] == target:
        return mid
    elif arr[mid] > target:
        return binary_search(arr, target, left, mid - 1)
    else:
        return binary_search(arr, target, mid + 1, right)

sorted_list = [1, 3, 5, 7, 9, 11, 13, 15]
index = binary_search(sorted_list, 7)
print(f"Found at index: {index}")  # Found at index: 3

Function Documentation

# Docstrings for function documentation
def calculate_area(length, width):
    """
    Calculate the area of a rectangle.
    
    Args:
        length (float): The length of the rectangle
        width (float): The width of the rectangle
    
    Returns:
        float: The area of the rectangle
    
    Example:
        >>> calculate_area(5, 3)
        15
    """
    return length * width

# Accessing docstring
print(calculate_area.__doc__)

# Type hints (Python 3.5+)
def greet_user(name: str, age: int) -> str:
    """Greet a user with their name and age."""
    return f"Hello {name}, you are {age} years old!"

# Function annotations
def process_data(data: list, multiplier: float = 1.0) -> list:
    """Process data by multiplying each element."""
    return [x * multiplier for x in data]
💡 Best Practices:
← Data Structures Next: Classes & OOP →