A function is a reusable block of code that performs a specific task. Functions help in:
- Organizing code into manageable chunks
- Avoiding code repetition (DRY - Don't Repeat Yourself)
- Making code more readable and maintainable
- Function Definition: Using the
def
keyword - Parameters: Input values the function accepts
- Return Values: Output the function provides
- Docstrings: Documentation strings explaining the function
- Scope: Where variables can be accessed
- Local Scope: Variables defined inside a function
- Global Scope: Variables defined outside functions
- LEGB Rule: Local → Enclosing → Global → Built-in
# Example demonstrating scope
global_var = "I'm global"
def scope_example():
local_var = "I'm local"
print(local_var) # Accessible
print(global_var) # Accessible
# print(local_var) # Would raise Error - local_var not accessible here
print(global_var) # Accessible
# Best Practices for Function Definitions:
def calculate_area(length: float, width: float) -> float:
"""
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
"""
return length * width
- Forgetting to return a value
- Modifying global variables (avoid this)
- Using mutable default arguments
- Not handling edge cases
- Required Parameters: Must be provided
- Default Parameters: Have preset values
- Keyword Arguments: Specified by name
- Positional Arguments: Specified by position
- Default parameters must come after required parameters
- Default values are evaluated only once at function definition
- Mutable default values can cause unexpected behavior
# Good Practice
def create_user(name, age, city="Unknown", active=True):
return {"name": name, "age": age, "city": city, "active": active}
# Bad Practice - Mutable Default Argument
def add_to_list(item, my_list=[]): # Don't do this!
my_list.append(item)
return my_list
# Better Practice
def add_to_list(item, my_list=None):
if my_list is None:
my_list = []
my_list.append(item)
return my_list
- args: Collects positional arguments into a tuple
- kwargs: Collects keyword arguments into a dictionary
- *args: When number of positional arguments is unknown
- **kwargs: When number of keyword arguments is unknown
- Both: For creating flexible function interfaces
def flexible_function(*args, **kwargs):
"""
Example of flexible function arguments.
Args:
*args: Variable positional arguments
**kwargs: Variable keyword arguments
"""
print("Positional arguments:", args)
print("Keyword arguments:", kwargs)
# Different ways to use
flexible_function(1, 2, 3, name="John", age=25)
flexible_function() # Both args and kwargs can be empty
- Use meaningful parameter names
- Document expected arguments
- Validate argument types when necessary
- Keep the number of parameters manageable
- Single-expression functions
- Anonymous (unnamed) functions
- Useful for short operations
- As arguments to higher-order functions
- For simple operations
- In list comprehensions and map/filter operations
- For complex operations
- When function needs documentation
- When function is reused multiple times
# Good Lambda Use
sorted_names = sorted(names, key=lambda x: x.lower())
# Bad Lambda Use (Should be a regular function)
complex_operation = lambda x, y: (x**2 + y**2) * sum([i for i in range(x)])
- map(): Transform each element
- filter(): Select elements based on condition
- reduce(): Aggregate elements to single value
- map() and filter() return iterators
- Performance considerations vs. list comprehensions
- Consider readability when choosing approach
# Map Examples with Explanation
numbers = [1, 2, 3, 4, 5]
# Using map
squared_map = map(lambda x: x**2, numbers)
# Equivalent list comprehension (often more readable)
squared_comp = [x**2 for x in numbers]
# Filter Examples
evens_filter = filter(lambda x: x % 2 == 0, numbers)
# Equivalent list comprehension
evens_comp = [x for x in numbers if x % 2 == 0]
- Iterator Protocol: iter and next methods
- Generator Functions: Using yield statement
- Generator Expressions: Inline generators
- Memory efficient
- Lazy evaluation
- Perfect for large datasets
# Generator Function Example
def number_generator(start, end):
"""
Generate numbers from start to end.
Memory efficient as it yields one value at a time.
"""
current = start
while current <= end:
yield current
current += 1
# Generator Expression
squares = (x**2 for x in range(1000000)) # Memory efficient
- Use generators for large sequences
- Consider memory usage
- Handle StopIteration appropriately
- Concise way to create lists
- More readable than loops
- Can include conditions
- Basic: [expression for item in iterable]
- Filtered: [expression for item in iterable if condition]
- Nested: [expression for x in iterable1 for y in iterable2]
# Simple List Comprehension
squares = [x**2 for x in range(10)]
# With Condition
even_squares = [x**2 for x in range(10) if x % 2 == 0]
# Nested Comprehension
matrix = [[i+j for j in range(3)] for i in range(3)]
- Simple transformations
- Filtering elements
- Creating new lists from existing ones
- Complex operations
- Multiple conditions
- Side effects needed
- Mutable Default Arguments
# Problem
def add_user(name, users=[]): # DON'T DO THIS
users.append(name)
return users
# Solution
def add_user(name, users=None):
if users is None:
users = []
users.append(name)
return users
- Variable Scope Issues
# Problem
total = 0
def add_to_total(value):
total += value # UnboundLocalError
# Solution
def add_to_total(value):
global total
total += value
- Generator Exhaustion
# Problem
gen = (x for x in range(3))
list(gen) # [0, 1, 2]
list(gen) # [] - Generator is exhausted
# Solution
def get_generator():
return (x for x in range(3))
gen1 = get_generator()
gen2 = get_generator()
-
Function Design
- Keep functions small and focused
- Use clear, descriptive names
- Document with docstrings
- Handle edge cases
-
Parameters
- Limit number of parameters
- Use type hints
- Validate inputs
- Use default values appropriately
-
Returns
- Be consistent with return types
- Document return values
- Consider using named tuples for multiple returns
-
Error Handling
- Use try/except appropriately
- Raise specific exceptions
- Document possible exceptions
-
General Tips
- Follow PEP 8 style guide
- Write testable functions
- Avoid side effects
- Use meaningful variable names
- Basic Function Practice
def calculate_statistics(numbers):
"""
Calculate basic statistics for a list of numbers.
Practice: Add median, mode, and range calculations.
"""
pass # Your implementation here
- Advanced Parameter Handling
def process_data(*args, **kwargs):
"""
Process different types of data based on input.
Practice: Add type checking and validation.
"""
pass # Your implementation here
- Generator Practice
def fibonacci_generator(n):
"""
Generate Fibonacci sequence.
Practice: Add error handling and validation.
"""
pass # Your implementation here