Virtual assistance

Python Functions

Functions are reusable blocks of code that perform specific tasks. They help organize code, reduce duplication, and improve maintainability.

Python Functions

What are Functions?

Functions are named blocks of code that perform a specific task. They take input (parameters), process it, and optionally return output. Functions promote code reusability and modularity.

"Functions break down complex problems into smaller, manageable pieces."

Defining Functions

Use the def keyword to define functions:

# 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):
    return a + b

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

# Function with multiple return values
def get_coordinates():
    return 10, 20

x, y = get_coordinates()
print(f"x: {x}, y: {y}")  # Output: x: 10, y: 20

Function Parameters

Functions can accept different types of parameters:

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

greet()          # Hello, World!
greet("Alice")   # Hello, Alice!

# Keyword arguments
def describe_person(name, age, city):
    print(f"{name} is {age} years old and lives in {city}")

describe_person(age=25, name="Bob", city="NYC")
# Output: Bob is 25 years old and lives in NYC

# Variable-length arguments (*args)
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

# Variable-length keyword arguments (**kwargs)
def print_info(**info):
    for key, value in info.items():
        print(f"{key}: {value}")

print_info(name="Alice", age=25, city="Boston")
# Output:
# name: Alice
# age: 25
# city: Boston

Scope and Lifetime

Variables have different scopes in Python:

# Local scope
def my_function():
    local_var = "I'm local"
    print(local_var)

my_function()  # I'm local
# print(local_var)  # NameError: name 'local_var' is not defined

# Global scope
global_var = "I'm global"

def access_global():
    print(global_var)  # Can access global variable

access_global()  # I'm global

# Modifying global variables
count = 0

def increment():
    global count
    count += 1

increment()
print(count)  # 1

# nonlocal (for nested functions)
def outer():
    x = "outer"
    
    def inner():
        nonlocal x
        x = "inner"
        print(f"inner: {x}")
    
    inner()
    print(f"outer: {x}")

outer()
# Output:
# inner: inner
# outer: inner

Lambda Functions

Lambda functions are anonymous, single-expression functions:

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

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

# Lambda in sorting
students = [("Alice", 85), ("Bob", 92), ("Charlie", 78)]
students.sort(key=lambda student: student[1])
print(students)  # [('Charlie', 78), ('Alice', 85), ('Bob', 92)]

# Lambda with conditional
max_value = lambda a, b: a if a > b else b
print(max_value(10, 20))  # 20

# Using lambda with map, filter, reduce
numbers = [1, 2, 3, 4, 5]

# map
squares = list(map(lambda x: x**2, numbers))
print(squares)  # [1, 4, 9, 16, 25]

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

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

Function Documentation

Document functions with docstrings:

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

print(calculate_area.__doc__)
help(calculate_area)

Function Annotations

Type hints help document expected types:

def greet_person(name: str, age: int) -> str:
    return f"Hello, {name}! You are {age} years old."

def add_numbers(a: float, b: float) -> float:
    return a + b

# Annotations don't enforce types at runtime
result = add_numbers("hello", "world")  # This works but may not be intended

# Using typing module for complex types
from typing import List, Dict, Optional

def process_data(data: List[Dict[str, int]], threshold: Optional[int] = None) -> List[int]:
    if threshold is None:
        threshold = 0
    return [item['value'] for item in data if item['value'] > threshold]

Recursion

Functions can call themselves:

# Factorial with recursion
def factorial(n):
    if n == 0 or n == 1:
        return 1
    return n * factorial(n - 1)

print(factorial(5))  # 120

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

print(fibonacci(6))  # 8

# Recursion with memoization
memo = {}
def fibonacci_memo(n):
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = fibonacci_memo(n - 1) + fibonacci_memo(n - 2)
    return memo[n]

print(fibonacci_memo(10))  # 55

Best Practices

  • Use descriptive names: Function names should clearly indicate their purpose
  • Keep functions small: Each function should do one thing well
  • Use docstrings: Document what the function does, its parameters, and return values
  • Use type hints: Add type annotations for better code clarity
  • Avoid global variables: Pass data as parameters instead
  • Handle edge cases: Consider what happens with invalid inputs
  • Use meaningful parameter names: Parameters should be self-documenting
  • Return early: Use guard clauses to handle error conditions

Common Function Patterns

# Factory function
def create_multiplier(factor):
    def multiplier(x):
        return x * factor
    return multiplier

double = create_multiplier(2)
triple = create_multiplier(3)
print(double(5))  # 10
print(triple(5))  # 15

# Callback function
def process_list(items, callback):
    result = []
    for item in items:
        result.append(callback(item))
    return result

def square(x):
    return x ** 2

numbers = [1, 2, 3, 4, 5]
squared = process_list(numbers, square)
print(squared)  # [1, 4, 9, 16, 25]

# Decorator pattern
def timer(func):
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.2f} seconds")
        return result
    return wrapper

@timer
def slow_function():
    import time
    time.sleep(1)
    return "Done"

print(slow_function())

Functions are the building blocks of Python programs. They allow you to organize code into reusable, testable units that make your programs more maintainable and easier to understand.