Advanced Techniques and Patterns
The moment I discovered list comprehensions, my Python code became dramatically more concise and readable. What used to take 5 lines could now be done in 1. But list comprehensions were just the beginning - Python has many elegant features that separate beginner code from professional-quality code.
These advanced techniques aren’t just about showing off - they make your code more readable, efficient, and maintainable. Let’s explore the patterns that will elevate your Python skills.
List Comprehensions and Beyond
List comprehensions are Python’s way of creating lists concisely. Once you master them, you’ll use them everywhere:
# Basic list comprehension
squares = [x**2 for x in range(10)]
# With conditions
even_squares = [x**2 for x in range(10) if x % 2 == 0]
# Processing existing data
names = ['alice', 'bob', 'charlie']
capitalized = [name.capitalize() for name in names]
# Nested comprehensions (use sparingly)
matrix = [[i*j for j in range(3)] for i in range(3)]
# Dictionary comprehensions
word_lengths = {word: len(word) for word in names}
# Set comprehensions
unique_lengths = {len(word) for word in names}
Generators - Memory-Efficient Iteration
Generators are one of Python’s most powerful features. They create iterators that generate values on-demand, saving memory:
# 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
# Generator expression
squares_gen = (x**2 for x in range(1000000)) # Uses minimal memory
# Using generators
for fib in fibonacci_generator(100):
print(fib)
# Generators are memory efficient
import sys
list_comp = [x**2 for x in range(1000)] # Creates full list in memory
gen_exp = (x**2 for x in range(1000)) # Creates generator object
print(f"List size: {sys.getsizeof(list_comp)} bytes")
print(f"Generator size: {sys.getsizeof(gen_exp)} bytes")
Decorators - Enhancing Functions
Decorators let you modify or enhance functions without changing their code:
import time
from functools import wraps
def timing_decorator(func):
"""Decorator to measure function execution time"""
@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
def retry_decorator(max_attempts=3):
"""Decorator to retry function on failure"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise e
print(f"Attempt {attempt + 1} failed: {e}")
return None
return wrapper
return decorator
# Using decorators
@timing_decorator
@retry_decorator(max_attempts=3)
def unreliable_function():
"""Function that sometimes fails"""
import random
if random.random() < 0.7:
raise Exception("Random failure")
return "Success!"
Context Managers - Resource Management
Context managers ensure proper resource cleanup using the with
statement:
# Built-in context managers
with open('file.txt', 'r') as file:
content = file.read()
# File automatically closed
# Custom context manager using class
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.connection = None
def __enter__(self):
print(f"Connecting to {self.db_name}")
self.connection = f"Connection to {self.db_name}"
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Closing connection to {self.db_name}")
self.connection = None
# Custom context manager using contextlib
from contextlib import contextmanager
@contextmanager
def temporary_file(filename):
"""Context manager for temporary files"""
import os
try:
# Setup
with open(filename, 'w') as f:
f.write("Temporary content")
yield filename
finally:
# Cleanup
if os.path.exists(filename):
os.remove(filename)
# Using context managers
with DatabaseConnection("mydb") as conn:
print(f"Using {conn}")
with temporary_file("temp.txt") as temp_file:
print(f"Working with {temp_file}")
Advanced Function Techniques
Python functions have many advanced features:
# Default arguments and keyword arguments
def create_user(name, email, age=None, **kwargs):
"""Create user with flexible arguments"""
user = {
'name': name,
'email': email,
'age': age
}
user.update(kwargs) # Add any additional keyword arguments
return user
# Variable arguments
def calculate_average(*numbers):
"""Calculate average of any number of arguments"""
if not numbers:
return 0
return sum(numbers) / len(numbers)
# Lambda functions (anonymous functions)
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))
# Higher-order functions
def apply_operation(numbers, operation):
"""Apply operation to all numbers"""
return [operation(x) for x in numbers]
def double(x):
return x * 2
def square(x):
return x ** 2
# Usage
user = create_user("Alice", "[email protected]", role="admin", active=True)
avg = calculate_average(1, 2, 3, 4, 5)
doubled = apply_operation([1, 2, 3], double)
Object-Oriented Programming Basics
Classes help organize code and model real-world concepts:
class BankAccount:
"""Simple bank account class"""
def __init__(self, account_number, initial_balance=0):
self.account_number = account_number
self._balance = initial_balance # Protected attribute
self._transactions = []
@property
def balance(self):
"""Get current balance"""
return self._balance
def deposit(self, amount):
"""Deposit money"""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self._balance += amount
self._transactions.append(f"Deposit: +${amount}")
return self._balance
def withdraw(self, amount):
"""Withdraw money"""
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
self._transactions.append(f"Withdrawal: -${amount}")
return self._balance
def get_statement(self):
"""Get account statement"""
statement = f"Account: {self.account_number}\n"
statement += f"Balance: ${self._balance}\n"
statement += "Recent transactions:\n"
for transaction in self._transactions[-5:]: # Last 5 transactions
statement += f" {transaction}\n"
return statement
def __str__(self):
return f"BankAccount({self.account_number}, ${self._balance})"
# Usage
account = BankAccount("12345", 1000)
account.deposit(500)
account.withdraw(200)
print(account.get_statement())
Error Handling Patterns
Proper error handling makes your code robust:
class ValidationError(Exception):
"""Custom exception for validation errors"""
pass
def validate_email(email):
"""Validate email format"""
if '@' not in email:
raise ValidationError("Email must contain @ symbol")
if '.' not in email.split('@')[1]:
raise ValidationError("Email domain must contain a dot")
return True
def safe_divide(a, b):
"""Safely divide two numbers"""
try:
result = a / b
return result, None
except ZeroDivisionError:
return None, "Cannot divide by zero"
except TypeError:
return None, "Arguments must be numbers"
def process_user_data(data):
"""Process user data with comprehensive error handling"""
try:
# Validate required fields
if 'email' not in data:
raise ValidationError("Email is required")
validate_email(data['email'])
# Process data
processed = {
'email': data['email'].lower().strip(),
'name': data.get('name', '').strip(),
'age': int(data.get('age', 0))
}
return processed, None
except ValidationError as e:
return None, f"Validation error: {e}"
except ValueError as e:
return None, f"Value error: {e}"
except Exception as e:
return None, f"Unexpected error: {e}"
# Usage with error handling
user_data = {'email': '[email protected]', 'name': 'Alice', 'age': '30'}
result, error = process_user_data(user_data)
if error:
print(f"Error: {error}")
else:
print(f"Processed: {result}")
Working with Files and Data
Advanced file handling patterns:
import json
import csv
from pathlib import Path
class DataManager:
"""Manage different data formats"""
def __init__(self, data_dir):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(exist_ok=True)
def save_json(self, filename, data):
"""Save data as JSON"""
file_path = self.data_dir / f"{filename}.json"
with open(file_path, 'w') as f:
json.dump(data, f, indent=2)
def load_json(self, filename):
"""Load data from JSON"""
file_path = self.data_dir / f"{filename}.json"
try:
with open(file_path, 'r') as f:
return json.load(f)
except FileNotFoundError:
return None
def save_csv(self, filename, data, headers):
"""Save data as CSV"""
file_path = self.data_dir / f"{filename}.csv"
with open(file_path, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=headers)
writer.writeheader()
writer.writerows(data)
def load_csv(self, filename):
"""Load data from CSV"""
file_path = self.data_dir / f"{filename}.csv"
try:
with open(file_path, 'r') as f:
return list(csv.DictReader(f))
except FileNotFoundError:
return []
# Usage
dm = DataManager("data")
# Save and load JSON
user_data = {'name': 'Alice', 'age': 30, 'email': '[email protected]'}
dm.save_json('user', user_data)
loaded_user = dm.load_json('user')
# Save and load CSV
users = [
{'name': 'Alice', 'age': 30, 'email': '[email protected]'},
{'name': 'Bob', 'age': 25, 'email': '[email protected]'}
]
dm.save_csv('users', users, ['name', 'age', 'email'])
loaded_users = dm.load_csv('users')
Functional Programming Concepts
Python supports functional programming patterns:
from functools import reduce, partial
# Map, filter, reduce
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Map - transform each element
squared = list(map(lambda x: x**2, numbers))
# Filter - select elements that meet criteria
evens = list(filter(lambda x: x % 2 == 0, numbers))
# Reduce - combine elements into single value
sum_all = reduce(lambda x, y: x + y, numbers)
product = reduce(lambda x, y: x * y, numbers)
# Partial functions
def multiply(x, y):
return x * y
double = partial(multiply, 2) # Fix first argument to 2
triple = partial(multiply, 3) # Fix first argument to 3
# Usage
print(double(5)) # 10
print(triple(4)) # 12
# Function composition
def compose(f, g):
"""Compose two functions"""
return lambda x: f(g(x))
def add_one(x):
return x + 1
def square(x):
return x ** 2
# Compose functions
add_then_square = compose(square, add_one)
print(add_then_square(3)) # (3 + 1)^2 = 16
These advanced techniques transform how you write Python code. They’re not just syntactic sugar - they represent different ways of thinking about problems and solutions. Master these patterns, and your code will become more elegant, efficient, and maintainable.
Next, we’ll explore best practices and optimization techniques that ensure your Python code is not just functional, but professional-quality.