Functions & Modules

Deep dive into Python functions - from basics to advanced concepts like decorators and closures

Overview

Functions are reusable blocks of code that perform specific tasks. They are fundamental to writing maintainable, modular Python code.

Function Basics

Defining Functions

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

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

# Function with return value
def add(a, b):
    return a + b

# Function with multiple return values
def get_min_max(numbers):
    return min(numbers), max(numbers)

# Function with docstring
def calculate_area(radius):
    """
    Calculate the area of a circle.
    
    Args:
        radius (float): The radius of the circle
    
    Returns:
        float: The area of the circle
    """
    import math
    return math.pi * radius ** 2

Parameters and Arguments

#### Default Parameters

PYTHON
def connect_to_database(host="localhost", port=5432, database="mydb"):
    return f"Connecting to {database} at {host}:{port}"

# Various ways to call
connect_to_database()
connect_to_database("remote.server.com")
connect_to_database(port=3306)
connect_to_database(database="production", host="prod.server.com")

#### Positional and Keyword Arguments

PYTHON
def create_user(name, age, email, is_active=True):
    return {
        'name': name,
        'age': age,
        'email': email,
        'is_active': is_active
    }

# Positional arguments
user1 = create_user("Alice", 25, "alice@example.com")

# Mix of positional and keyword
user2 = create_user("Bob", 30, email="bob@example.com")

# All keyword arguments
user3 = create_user(name="Charlie", age=35, email="charlie@example.com", is_active=False)

#### args and *kwargs

PYTHON
# Variable positional arguments
def sum_all(*args):
    return sum(args)

result = sum_all(1, 2, 3, 4, 5)  # 15

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

print_info(name="Alice", age=25, city="NYC")

# Combining both
def process_data(*args, **kwargs):
    print(f"Positional: {args}")
    print(f"Keyword: {kwargs}")

process_data(1, 2, 3, name="test", debug=True)

# Unpacking arguments
def multiply(a, b, c):
    return a * b * c

numbers = [2, 3, 4]
result = multiply(*numbers)  # Unpacking list

params = {'a': 2, 'b': 3, 'c': 4}
result = multiply(**params)  # Unpacking dictionary

Function Annotations (Type Hints)

PYTHON
from typing import List, Dict, Optional, Union, Tuple, Callable

def process_numbers(numbers: List[int]) -> float:
    """Calculate average of numbers"""
    return sum(numbers) / len(numbers) if numbers else 0.0

def find_user(user_id: int) -> Optional[Dict[str, any]]:
    """Find user by ID, returns None if not found"""
    # Database lookup logic
    return {"id": user_id, "name": "John"} if user_id > 0 else None

def parse_value(value: Union[str, int, float]) -> float:
    """Convert value to float"""
    return float(value)

def apply_operation(
    numbers: List[float],
    operation: Callable[[float], float]
) -> List[float]:
    """Apply operation to all numbers"""
    return [operation(n) for n in numbers]

# Complex type hints
def analyze_data(
    data: List[Dict[str, Union[str, int, float]]],
    filters: Optional[Dict[str, any]] = None
) -> Tuple[float, float, int]:
    """Analyze data and return statistics"""
    # Processing logic
    return 0.0, 100.0, len(data)

Advanced Function Concepts

Lambda Functions

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

# Lambda with multiple arguments
multiply = lambda x, y: x * y

# Lambda in sorting
students = [
    {'name': 'Alice', 'grade': 85},
    {'name': 'Bob', 'grade': 92},
    {'name': 'Charlie', 'grade': 78}
]
students.sort(key=lambda s: s['grade'], reverse=True)

# Lambda with conditional
is_even = lambda x: x % 2 == 0

# Lambda in map, filter, reduce
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))

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

Closures

PYTHON
def outer_function(x):
    """Outer function that returns a closure"""
    
    def inner_function(y):
        """Inner function has access to x from outer scope"""
        return x + y
    
    return inner_function

# Create closures
add_five = outer_function(5)
add_ten = outer_function(10)

print(add_five(3))   # 8
print(add_ten(3))    # 13

# Practical closure example
def create_multiplier(factor):
    """Factory function for multipliers"""
    def multiplier(number):
        return number * factor
    return multiplier

double = create_multiplier(2)
triple = create_multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15

# Closure with mutable state
def create_counter():
    count = 0
    
    def increment():
        nonlocal count
        count += 1
        return count
    
    def decrement():
        nonlocal count
        count -= 1
        return count
    
    def get_count():
        return count
    
    return increment, decrement, get_count

inc, dec, get = create_counter()
print(inc())  # 1
print(inc())  # 2
print(dec())  # 1
print(get())  # 1

Decorators

PYTHON
# Basic decorator
def timer_decorator(func):
    import time
    
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.4f} seconds")
        return result
    
    return wrapper

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

# Decorator with arguments
def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            results = []
            for _ in range(times):
                results.append(func(*args, **kwargs))
            return results
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    return f"Hello, {name}!"

# Class decorator
def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class DatabaseConnection:
    def __init__(self):
        print("Creating database connection")

# Property decorator
class Temperature:
    def __init__(self):
        self._celsius = 0
    
    @property
    def celsius(self):
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature below absolute zero is not possible")
        self._celsius = value
    
    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32

# Chaining decorators
def bold(func):
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

def italic(func):
    def wrapper(*args, **kwargs):
        return f"<i>{func(*args, **kwargs)}</i>"
    return wrapper

@bold
@italic
def get_text(text):
    return text

print(get_text("Hello"))  # <b><i>Hello</i></b>

Recursive Functions

PYTHON
# Factorial
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

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

# Tree traversal
class TreeNode:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

def inorder_traversal(node):
    if node:
        inorder_traversal(node.left)
        print(node.value, end=' ')
        inorder_traversal(node.right)

# Quick sort
def quicksort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quicksort(left) + middle + quicksort(right)

Modules and Packages

Creating Modules

PYTHON
# math_utils.py
"""Math utility functions"""

PI = 3.14159

def area_circle(radius):
    """Calculate area of a circle"""
    return PI * radius ** 2

def area_rectangle(width, height):
    """Calculate area of a rectangle"""
    return width * height

class Calculator:
    """Basic calculator class"""
    
    @staticmethod
    def add(a, b):
        return a + b
    
    @staticmethod
    def multiply(a, b):
        return a * b

# Using the module
import math_utils
area = math_utils.area_circle(5)

from math_utils import area_rectangle, Calculator
area = area_rectangle(10, 20)
result = Calculator.add(5, 3)

# Import with alias
import math_utils as mu
area = mu.area_circle(5)

# Import all (not recommended)
from math_utils import *

Package Structure

PYTHON
# Project structure
"""
my_package/
    __init__.py
    core/
        __init__.py
        models.py
        utils.py
    api/
        __init__.py
        endpoints.py
        auth.py
    tests/
        __init__.py
        test_models.py
        test_api.py
"""

# my_package/__init__.py
"""Main package initialization"""
from .core import models
from .api import endpoints

__version__ = "1.0.0"
__all__ = ['models', 'endpoints']

# my_package/core/models.py
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

# Using the package
from my_package.core.models import User
from my_package.api import endpoints

user = User("Alice", "alice@example.com")

Real-World Examples

Example 1: Data Processing Pipeline

PYTHON
from typing import List, Dict, Callable, Any
from functools import wraps
import json
import csv

class DataPipeline:
    """Flexible data processing pipeline"""
    
    def __init__(self):
        self.steps = []
        self.error_handler = None
    
    def add_step(self, func: Callable) -> 'DataPipeline':
        """Add processing step to pipeline"""
        self.steps.append(func)
        return self
    
    def set_error_handler(self, handler: Callable) -> 'DataPipeline':
        """Set error handling function"""
        self.error_handler = handler
        return self
    
    def process(self, data: Any) -> Any:
        """Process data through all steps"""
        result = data
        for step in self.steps:
            try:
                result = step(result)
            except Exception as e:
                if self.error_handler:
                    result = self.error_handler(e, result, step)
                else:
                    raise
        return result

# Define processing functions
def load_json(filename: str) -> Dict:
    """Load data from JSON file"""
    with open(filename, 'r') as f:
        return json.load(f)

def validate_data(data: Dict) -> Dict:
    """Validate required fields"""
    required = ['id', 'name', 'email']
    for field in required:
        if field not in data:
            raise ValueError(f"Missing required field: {field}")
    return data

def transform_data(data: Dict) -> Dict:
    """Transform data format"""
    return {
        'user_id': data['id'],
        'full_name': data['name'].upper(),
        'contact': data['email'].lower(),
        'active': data.get('active', True)
    }

def enrich_data(data: Dict) -> Dict:
    """Add additional information"""
    data['processed_at'] = '2024-01-01'
    data['version'] = '1.0'
    return data

# Create and use pipeline
pipeline = DataPipeline()
pipeline.add_step(validate_data).add_step(transform_data).add_step(enrich_data)

# Process single record
user_data = {'id': 1, 'name': 'Alice', 'email': 'ALICE@EXAMPLE.COM'}
result = pipeline.process(user_data)
print(result)

Example 2: Caching Decorator

PYTHON
import time
import functools
from typing import Dict, Any, Callable

class Cache:
    """Advanced caching decorator with TTL and LRU"""
    
    def __init__(self, ttl: int = 60, maxsize: int = 128):
        self.ttl = ttl
        self.maxsize = maxsize
        self.cache: Dict[str, tuple] = {}
        self.access_order = []
    
    def __call__(self, func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # Create cache key
            key = self._make_key(func.__name__, args, kwargs)
            
            # Check cache
            if key in self.cache:
                value, timestamp = self.cache[key]
                if time.time() - timestamp < self.ttl:
                    # Move to end (most recently used)
                    self.access_order.remove(key)
                    self.access_order.append(key)
                    return value
                else:
                    # Expired
                    del self.cache[key]
                    self.access_order.remove(key)
            
            # Compute value
            result = func(*args, **kwargs)
            
            # Store in cache
            self.cache[key] = (result, time.time())
            self.access_order.append(key)
            
            # Enforce max size (LRU eviction)
            if len(self.cache) > self.maxsize:
                oldest = self.access_order.pop(0)
                del self.cache[oldest]
            
            return result
        
        # Add cache control methods
        wrapper.cache_clear = lambda: self.cache.clear()
        wrapper.cache_info = lambda: {
            'size': len(self.cache),
            'maxsize': self.maxsize,
            'ttl': self.ttl
        }
        
        return wrapper
    
    def _make_key(self, func_name: str, args: tuple, kwargs: dict) -> str:
        """Create unique cache key"""
        return f"{func_name}:{args}:{sorted(kwargs.items())}"

# Usage
@Cache(ttl=30, maxsize=50)
def expensive_computation(n: int) -> int:
    """Simulate expensive operation"""
    print(f"Computing for {n}...")
    time.sleep(2)
    return n * n

# First call - computes
result1 = expensive_computation(5)

# Second call - from cache
result2 = expensive_computation(5)

# Check cache info
print(expensive_computation.cache_info())

Example 3: Plugin System

PYTHON
import importlib
import inspect
from typing import Dict, List, Type
from abc import ABC, abstractmethod

class Plugin(ABC):
    """Base plugin interface"""
    
    @abstractmethod
    def execute(self, data: Any) -> Any:
        """Execute plugin logic"""
        pass
    
    @property
    @abstractmethod
    def name(self) -> str:
        """Plugin name"""
        pass
    
    @property
    @abstractmethod
    def version(self) -> str:
        """Plugin version"""
        pass

class PluginManager:
    """Dynamic plugin loading and management"""
    
    def __init__(self):
        self.plugins: Dict[str, Plugin] = {}
        self.hooks: Dict[str, List[Callable]] = {}
    
    def load_plugin(self, module_path: str) -> None:
        """Load plugin from module"""
        module = importlib.import_module(module_path)
        
        for name, obj in inspect.getmembers(module):
            if inspect.isclass(obj) and issubclass(obj, Plugin) and obj != Plugin:
                plugin = obj()
                self.plugins[plugin.name] = plugin
                print(f"Loaded plugin: {plugin.name} v{plugin.version}")
    
    def register_hook(self, event: str, callback: Callable) -> None:
        """Register callback for event"""
        if event not in self.hooks:
            self.hooks[event] = []
        self.hooks[event].append(callback)
    
    def trigger_hook(self, event: str, *args, **kwargs) -> List[Any]:
        """Trigger all callbacks for event"""
        results = []
        if event in self.hooks:
            for callback in self.hooks[event]:
                try:
                    result = callback(*args, **kwargs)
                    results.append(result)
                except Exception as e:
                    print(f"Hook error: {e}")
        return results
    
    def execute_plugin(self, plugin_name: str, data: Any) -> Any:
        """Execute specific plugin"""
        if plugin_name in self.plugins:
            return self.plugins[plugin_name].execute(data)
        raise ValueError(f"Plugin not found: {plugin_name}")
    
    def list_plugins(self) -> List[Dict[str, str]]:
        """List all loaded plugins"""
        return [
            {'name': p.name, 'version': p.version}
            for p in self.plugins.values()
        ]

# Example plugin implementation
class DataTransformPlugin(Plugin):
    @property
    def name(self) -> str:
        return "DataTransform"
    
    @property
    def version(self) -> str:
        return "1.0.0"
    
    def execute(self, data: Any) -> Any:
        # Transform logic
        return {"transformed": data}

# Usage
manager = PluginManager()
# manager.load_plugin('plugins.transform')
# result = manager.execute_plugin('DataTransform', {'raw': 'data'})

Practice Exercises

Exercise 1: Function Factory

PYTHON
# TODO: Create a function factory for mathematical operations
def create_math_function(operation: str):
    """
    Create mathematical functions dynamically
    - Support: add, multiply, power, modulo
    - Return appropriate function
    - Handle invalid operations
    """
    # Your code here
    pass

# Test cases
add_five = create_math_function('add')(5)
square = create_math_function('power')(2)
# print(add_five(3))  # Should return 8
# print(square(4))    # Should return 16

Exercise 2: Advanced Decorator

PYTHON
# TODO: Create a decorator with validation
def validate_types(**expected_types):
    """
    Decorator that validates function argument types
    - Check types match expected
    - Raise TypeError with details if mismatch
    - Support optional type checking
    """
    # Your code here
    pass

# Usage example
@validate_types(name=str, age=int, email=str)
def create_user(name, age, email):
    return {'name': name, 'age': age, 'email': email}

Exercise 3: Module Manager

PYTHON
# TODO: Build a dynamic module manager
class ModuleManager:
    """
    Implement a module management system:
    - Dynamic import of modules
    - Lazy loading support
    - Dependency resolution
    - Module caching
    - Reload capability
    """
    def __init__(self):
        # Your initialization here
        pass
    
    def import_module(self, name: str, lazy: bool = False):
        # Your code here
        pass
    
    def reload_module(self, name: str):
        # Your code here
        pass

# Test your implementation
# manager = ModuleManager()
# math = manager.import_module('math')

Best Practices

1. Use descriptive function names that indicate action 2. Keep functions small and focused on single task 3. Document with docstrings using consistent format 4. Use type hints for better code clarity 5. Avoid mutable default arguments 6. Return consistent types from functions 7. Handle exceptions at appropriate level 8. Use functools.wraps for decorators 9. Prefer pure functions when possible 10. Test functions with various inputs including edge cases

Performance Considerations

  • Use generators for large sequences
  • Cache expensive computations with lru_cache
  • Profile functions to identify bottlenecks
  • Consider async functions for I/O operations
  • Minimize function call overhead in tight loops
  • Use built-in functions when available