Overview
Functions are reusable blocks of code that perform specific tasks. They are fundamental to writing maintainable, modular Python code.
Function Basics
Defining Functions
# 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
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
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
# 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)
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
# 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
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
# 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
# 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
# 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
# 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
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
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
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
# 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
# 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
# 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