Chapter 4: Functions & Modules - Master Reusable Code and Code Organization
Welcome to the comprehensive guide to Python functions and modules! This chapter covers how to create reusable code blocks, organize your programs effectively, and build modular applications. Functions are the building blocks of Python programming, and modules help you organize and share code across projects.
Learning Objectives
By the end of this chapter, you will understand:
- Function definition and calling mechanisms
- Parameters and arguments including default values and keyword arguments
- Return values and multiple return statements
- Variable scope and the global/local distinction
- Lambda functions and anonymous functions
- Decorators and function enhancement
- Modules and packages for code organization
- Import statements and namespace management
- Best practices for function design and module organization
Function Basics
Defining and Calling Functions
Functions are reusable blocks of code that perform specific tasks:
# Basic function definition
def greet():
"""A simple function that prints a greeting."""
print("Hello, World!")
# Function call
greet() # Output: Hello, World!
# Function with parameters
def greet_person(name):
"""Greet a specific person."""
print(f"Hello, {name}!")
greet_person("Alice") # Output: Hello, Alice!
# Function with multiple parameters
def add_numbers(a, b):
"""Add two numbers and return the result."""
return a + b
result = add_numbers(5, 3)
print(result) # Output: 8
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
Raises:
ValueError: If length or width is negative
"""
if length < 0 or width < 0:
raise ValueError("Length and width must be positive")
return length * width
# Accessing function documentation
print(calculate_area.__doc__)
help(calculate_area)
Function Parameters and Arguments
Default Parameters
# Function with default parameters
def greet_person(name, greeting="Hello", punctuation="!"):
"""Greet a person with customizable greeting."""
return f"{greeting}, {name}{punctuation}"
# Using default values
print(greet_person("Alice")) # Hello, Alice!
print(greet_person("Bob", "Hi")) # Hi, Bob!
print(greet_person("Charlie", "Good morning", ".")) # Good morning, Charlie.
# Default parameters with mutable objects
def add_item(item, my_list=None):
"""Add item to list, creating new list if none provided."""
if my_list is None:
my_list = []
my_list.append(item)
return my_list
list1 = add_item("apple")
list2 = add_item("banana")
print(list1) # ['apple']
print(list2) # ['banana'] (not ['apple', 'banana'])
Keyword Arguments
# Function with keyword arguments
def create_profile(name, age, city, country="USA"):
"""Create a user profile."""
return {
"name": name,
"age": age,
"city": city,
"country": country
}
# Using keyword arguments
profile1 = create_profile("Alice", 25, "New York")
profile2 = create_profile("Bob", 30, "London", "UK")
profile3 = create_profile(age=35, name="Charlie", city="Tokyo")
print(profile1)
print(profile2)
print(profile3)
Variable-Length Arguments
# *args for variable number of positional arguments
def sum_numbers(*args):
"""Sum all provided numbers."""
total = 0
for num in args:
total += num
return total
print(sum_numbers(1, 2, 3)) # 6
print(sum_numbers(1, 2, 3, 4, 5)) # 15
print(sum_numbers()) # 0
# **kwargs for variable number of keyword arguments
def create_user(**kwargs):
"""Create a user with flexible attributes."""
user = {}
for key, value in kwargs.items():
user[key] = value
return user
user1 = create_user(name="Alice", age=25, city="New York")
user2 = create_user(name="Bob", age=30, city="London", country="UK")
print(user1)
print(user2)
# Combining *args and **kwargs
def flexible_function(*args, **kwargs):
"""Function that accepts both positional and keyword arguments."""
print("Positional arguments:", args)
print("Keyword arguments:", kwargs)
flexible_function(1, 2, 3, name="Alice", age=25)
Return Values
Single and Multiple Returns
# Single return value
def square(number):
"""Return the square of a number."""
return number ** 2
result = square(5)
print(result) # 25
# Multiple return values (as tuple)
def get_name_and_age():
"""Return name and age."""
return "Alice", 25
name, age = get_name_and_age()
print(f"Name: {name}, Age: {age}")
# Multiple return values with different types
def analyze_number(num):
"""Analyze a number and return multiple values."""
return {
"original": num,
"square": num ** 2,
"is_even": num % 2 == 0,
"is_positive": num > 0
}
analysis = analyze_number(4)
print(analysis)
Early Returns
def validate_email(email):
"""Validate email format."""
if not email:
return False, "Email cannot be empty"
if "@" not in email:
return False, "Email must contain @ symbol"
if "." not in email:
return False, "Email must contain domain"
return True, "Email is valid"
# Using early returns
is_valid, message = validate_email("")
print(f"Valid: {is_valid}, Message: {message}")
is_valid, message = validate_email("[email protected]")
print(f"Valid: {is_valid}, Message: {message}")
Variable Scope
Local and Global Scope
# Global variable
global_var = "I'm global"
def demonstrate_scope():
"""Demonstrate variable scope."""
# Local variable
local_var = "I'm local"
# Accessing global variable
print(f"Global: {global_var}")
print(f"Local: {local_var}")
# Modifying global variable
global global_var
global_var = "Modified global"
demonstrate_scope()
print(f"Global after function: {global_var}")
# Nonlocal for nested functions
def outer_function():
"""Outer function with nested function."""
outer_var = "I'm in outer function"
def inner_function():
nonlocal outer_var
outer_var = "Modified in inner function"
print(f"Inner: {outer_var}")
inner_function()
print(f"Outer: {outer_var}")
outer_function()
Scope Examples
# Scope demonstration
x = 10 # Global
def function1():
x = 20 # Local
print(f"Function1 local x: {x}")
def function2():
global x
x = 30 # Modifies global
print(f"Function2 global x: {x}")
def function3():
print(f"Function3 global x: {x}")
print(f"Global x: {x}") # 10
function1() # 20
print(f"Global x: {x}") # 10
function2() # 30
print(f"Global x: {x}") # 30
function3() # 30
Lambda Functions
Basic Lambda 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
# Lambda with conditional expression
is_even = lambda x: True if x % 2 == 0 else False
print(is_even(4)) # True
print(is_even(5)) # False
Lambda with Built-in Functions
# Lambda with map()
numbers = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x ** 2, numbers))
print(squares) # [1, 4, 9, 16, 25]
# Lambda with filter()
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers) # [2, 4]
# Lambda with sorted()
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 92},
{"name": "Charlie", "grade": 78}
]
# Sort by grade
sorted_by_grade = sorted(students, key=lambda x: x["grade"])
print(sorted_by_grade)
# Sort by name
sorted_by_name = sorted(students, key=lambda x: x["name"])
print(sorted_by_name)
Decorators
Basic Decorators
# Simple decorator
def my_decorator(func):
"""A simple decorator that adds functionality."""
def wrapper(*args, **kwargs):
print("Something is happening before the function is called.")
result = func(*args, **kwargs)
print("Something is happening after the function is called.")
return result
return wrapper
@my_decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice")
# Decorator with parameters
def repeat(times):
"""Decorator that repeats function execution."""
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Bob")
Practical Decorators
import time
import functools
# Timing decorator
def timing_decorator(func):
"""Decorator to measure function execution time."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
return result
return wrapper
@timing_decorator
def slow_function():
"""A function that takes time to execute."""
time.sleep(1)
return "Done"
result = slow_function()
# Caching decorator
def cache_decorator(func):
"""Simple caching decorator."""
cache = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key in cache:
print(f"Cache hit for {func.__name__}")
return cache[key]
result = func(*args, **kwargs)
cache[key] = result
print(f"Cache miss for {func.__name__}")
return result
return wrapper
@cache_decorator
def fibonacci(n):
"""Calculate Fibonacci number."""
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10))
print(fibonacci(10)) # This will use cache
Modules and Packages
Creating and Using Modules
# math_utils.py (module file)
"""Math utility functions."""
def add(a, b):
"""Add two numbers."""
return a + b
def multiply(a, b):
"""Multiply two numbers."""
return a * b
def power(base, exponent):
"""Calculate base raised to exponent."""
return base ** exponent
# Constants
PI = 3.14159
E = 2.71828
# Using the module
import math_utils
result = math_utils.add(5, 3)
print(result) # 8
# Import specific functions
from math_utils import add, multiply
result = add(5, 3)
print(result) # 8
# Import with alias
import math_utils as mu
result = mu.multiply(4, 5)
print(result) # 20
# Import all (not recommended)
from math_utils import *
result = power(2, 3)
print(result) # 8
Package Structure
my_package/
├── __init__.py
├── math_utils.py
├── string_utils.py
└── data_utils.py
# my_package/__init__.py
"""My package for utility functions."""
from .math_utils import add, multiply
from .string_utils import reverse_string
__version__ = "1.0.0"
__all__ = ["add", "multiply", "reverse_string"]
# my_package/math_utils.py
def add(a, b):
return a + b
def multiply(a, b):
return a * b
# my_package/string_utils.py
def reverse_string(text):
return text[::-1]
# Using the package
import my_package
result = my_package.add(5, 3)
print(result) # 8
# Import from package
from my_package import add, reverse_string
result = add(5, 3)
reversed_text = reverse_string("Hello")
print(result, reversed_text) # 8 olleH
Built-in Modules
# Using built-in modules
import math
import random
import datetime
import os
import sys
# Math module
print(math.pi) # 3.141592653589793
print(math.sqrt(16)) # 4.0
print(math.sin(math.pi/2)) # 1.0
# Random module
print(random.randint(1, 10)) # Random integer between 1 and 10
print(random.choice(['a', 'b', 'c'])) # Random choice from list
print(random.random()) # Random float between 0 and 1
# Datetime module
now = datetime.datetime.now()
print(now) # Current date and time
print(now.strftime("%Y-%m-%d")) # Formatted date
# OS module
print(os.getcwd()) # Current working directory
print(os.listdir('.')) # List files in current directory
# Sys module
print(sys.version) # Python version
print(sys.argv) # Command line arguments
Advanced Function Concepts
Function Annotations
# Type hints (Python 3.5+)
def greet(name: str, age: int) -> str:
"""Greet a person with type hints."""
return f"Hello, {name}! You are {age} years old."
# Complex type hints
from typing import List, Dict, Optional, Union
def process_data(
numbers: List[int],
config: Dict[str, Union[str, int]],
debug: Optional[bool] = None
) -> Dict[str, int]:
"""Process data with complex type hints."""
result = {}
for num in numbers:
result[str(num)] = num * 2
return result
# Using the function
data = process_data([1, 2, 3], {"mode": "fast"}, debug=True)
print(data)
Generator Functions
# Generator function
def fibonacci_generator(n):
"""Generate Fibonacci numbers up to n."""
a, b = 0, 1
while a < n:
yield a
a, b = b, a + b
# Using generator
for num in fibonacci_generator(100):
print(num, end=" ")
print()
# Generator expression
squares = (x**2 for x in range(10))
for square in squares:
print(square, end=" ")
print()
Closures
# Closure example
def create_multiplier(factor):
"""Create a function that multiplies by factor."""
def multiplier(number):
return number * factor
return multiplier
# Create specific multipliers
double = create_multiplier(2)
triple = create_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# Closure with mutable state
def create_counter():
"""Create a counter with closure."""
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
counter1 = create_counter()
counter2 = create_counter()
print(counter1()) # 1
print(counter1()) # 2
print(counter2()) # 1
print(counter1()) # 3
Practical Examples
Example 1: Calculator with Functions
def calculator():
"""A calculator using functions for each operation."""
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
def power(a, b):
return a ** b
def get_operation():
"""Get operation choice from user."""
operations = {
'1': ('Addition', add),
'2': ('Subtraction', subtract),
'3': ('Multiplication', multiply),
'4': ('Division', divide),
'5': ('Power', power)
}
print("Calculator Operations:")
for key, (name, _) in operations.items():
print(f"{key}. {name}")
while True:
choice = input("Enter operation (1-5): ")
if choice in operations:
return operations[choice][1]
print("Invalid choice. Please enter 1-5.")
def get_numbers():
"""Get two numbers from user."""
while True:
try:
a = float(input("Enter first number: "))
b = float(input("Enter second number: "))
return a, b
except ValueError:
print("Please enter valid numbers.")
# Main calculator loop
while True:
try:
operation = get_operation()
a, b = get_numbers()
result = operation(a, b)
print(f"Result: {result}")
continue_calc = input("Continue? (y/n): ").lower()
if continue_calc != 'y':
break
except ValueError as e:
print(f"Error: {e}")
except KeyboardInterrupt:
print("\nCalculator stopped.")
break
# Run the calculator
if __name__ == "__main__":
calculator()
Example 2: Text Processing Module
# text_processor.py
"""Text processing utilities."""
def count_words(text):
"""Count words in text."""
return len(text.split())
def count_characters(text):
"""Count characters in text."""
return len(text)
def count_sentences(text):
"""Count sentences in text."""
return text.count('.') + text.count('!') + text.count('?')
def reverse_text(text):
"""Reverse the text."""
return text[::-1]
def capitalize_words(text):
"""Capitalize each word."""
return ' '.join(word.capitalize() for word in text.split())
def remove_punctuation(text):
"""Remove punctuation from text."""
import string
return text.translate(str.maketrans('', '', string.punctuation))
def analyze_text(text):
"""Comprehensive text analysis."""
return {
'word_count': count_words(text),
'character_count': count_characters(text),
'sentence_count': count_sentences(text),
'average_words_per_sentence': count_words(text) / max(count_sentences(text), 1),
'text_without_punctuation': remove_punctuation(text)
}
# Using the module
if __name__ == "__main__":
sample_text = "Hello, world! This is a sample text. It has multiple sentences!"
print("Text Analysis:")
analysis = analyze_text(sample_text)
for key, value in analysis.items():
print(f"{key}: {value}")
print(f"\nReversed: {reverse_text(sample_text)}")
print(f"Capitalized: {capitalize_words(sample_text)}")
Example 3: Function Decorators for Logging
import functools
import time
from datetime import datetime
def log_function_calls(func):
"""Decorator to log function calls."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] Calling {func.__name__} with args={args}, kwargs={kwargs}")
try:
result = func(*args, **kwargs)
print(f"[{timestamp}] {func.__name__} returned: {result}")
return result
except Exception as e:
print(f"[{timestamp}] {func.__name__} raised exception: {e}")
raise
return wrapper
def retry_on_failure(max_retries=3, delay=1):
"""Decorator to retry function on failure."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_retries - 1:
print(f"Function {func.__name__} failed after {max_retries} attempts")
raise
print(f"Attempt {attempt + 1} failed: {e}. Retrying in {delay} seconds...")
time.sleep(delay)
return wrapper
return decorator
# Using the decorators
@log_function_calls
@retry_on_failure(max_retries=2, delay=0.5)
def unreliable_function():
"""A function that might fail."""
import random
if random.random() < 0.7: # 70% chance of failure
raise ValueError("Random failure")
return "Success!"
# Test the decorated function
for i in range(3):
try:
result = unreliable_function()
print(f"Attempt {i+1}: {result}")
except Exception as e:
print(f"Attempt {i+1}: Failed with {e}")
print()
Best Practices for Functions and Modules
Function Design Principles
# Good: Single responsibility
def calculate_tax(amount, rate):
"""Calculate tax for given amount and rate."""
return amount * rate
def format_currency(amount):
"""Format amount as currency."""
return f"${amount:.2f}"
# Good: Clear parameter names
def create_user(first_name, last_name, email, age):
"""Create a user with clear parameter names."""
return {
"first_name": first_name,
"last_name": last_name,
"email": email,
"age": age
}
# Good: Proper error handling
def safe_divide(a, b):
"""Safely divide two numbers."""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
Module Organization
# Good module structure
"""
Module docstring describing the module's purpose.
"""
# Imports at the top
import os
import sys
from typing import List, Dict
# Constants
DEFAULT_CONFIG = {
"timeout": 30,
"retries": 3
}
# Main functions
def main_function():
"""Main function of the module."""
pass
def helper_function():
"""Helper function."""
pass
# Module execution
if __name__ == "__main__":
main_function()
Summary
In this chapter, we've covered:
- Function basics: definition, calling, and documentation
- Parameters and arguments: default values, keyword arguments, and variable-length arguments
- Return values: single and multiple returns, early returns
- Variable scope: local, global, and nonlocal scope
- Lambda functions: anonymous functions and their uses
- Decorators: function enhancement and practical decorators
- Modules and packages: code organization and import statements
- Advanced concepts: type hints, generators, and closures
Functions and modules are essential for writing maintainable, reusable, and organized Python code. They allow you to break down complex problems into manageable pieces and create libraries of reusable functionality.
Next Steps
Now that you understand functions and modules, you're ready to explore:
- Object-Oriented Programming: Create classes and objects
- File Handling: Read from and write to files
- Error Handling: Manage exceptions and errors gracefully
- Advanced Topics: Explore metaclasses, context managers, and more
- Package Management: Learn about pip, virtual environments, and package distribution
Ready to build objects and classes? Continue with Chapter 5: Object-Oriented Programming to master Python's OOP concepts!