Understand concurrency with threading and multiprocessing
1. Decorators
Decorators are a powerful feature in Python that allow you to modify or enhance functions and classes without changing their source code.
💡 What are Decorators?
Decorators are functions that take another function as an argument and return a new function that usually extends the behavior of the original function.
Basic Decorator
# Simple decorator exampledefmy_decorator(func):
defwrapper():
print("Before function call")
func()
print("After function call")
return wrapper
@my_decoratordefsay_hello():
print("Hello!")
say_hello()
# Output:# Before function call# Hello!# After function call
Decorator with Arguments
# Decorator that accepts argumentsdefrepeat(times):
defdecorator(func):
defwrapper(*args, **kwargs):
for _ inrange(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)defgreet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output: Hello, Alice! (printed 3 times)
Practical Decorators
# Timing decoratorimport time
from functools import wraps
deftimer(func):
@wraps(func)defwrapper(*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
@timerdefslow_function():
time.sleep(2)
return"Done"
slow_function() # Output: slow_function took 2.0001 seconds# Caching decorator (memoization)defmemoize(func):
cache = {}
@wraps(func)defwrapper(*args):
if args in cache:
return cache[args]
result = func(*args)
cache[args] = result
return result
return wrapper
@memoizedeffibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # Fast with memoization!
2. Generators and Iterators
Generators are a simple way to create iterators. They allow you to generate values on-the-fly, saving memory.
💡 Why Use Generators?
Generators are memory-efficient because they generate values one at a time instead of storing them all in memory. Perfect for working with large datasets or infinite sequences.
Generator Functions
# Simple generatordefcount_up_to(n):
count = 1while count <= n:
yield count
count += 1# Using the generatorfor num in count_up_to(5):
print(num) # Prints 1, 2, 3, 4, 5# Generator expression (like list comprehension)
squares = (x**2for x inrange(10))
print(next(squares)) # 0print(next(squares)) # 1
Practical Generator Examples
# Reading large files efficientlydefread_large_file(file_path):
withopen(file_path, 'r') as file:
for line in file:
yield line.strip()
# Process one line at a time without loading entire filefor line in read_large_file('huge_file.txt'):
process(line)
# Infinite sequence generatordeffibonacci_generator():
a, b = 0, 1whileTrue:
yield a
a, b = b, a + b
# Get first 10 Fibonacci numbers
fib = fibonacci_generator()
for _ inrange(10):
print(next(fib))
Context managers allow you to allocate and release resources precisely when you want to. The most common use is with the with statement.
Built-in Context Managers
# File handling with context managerwithopen('file.txt', 'r') as file:
content = file.read()
print(content)
# File is automatically closed here# Multiple context managerswithopen('input.txt', 'r') as infile, \
open('output.txt', 'w') as outfile:
for line in infile:
outfile.write(line.upper())
Creating Custom Context Managers
# Using contextlib decoratorfrom contextlib import contextmanager
@contextmanagerdeftimer_context(name):
import time
start = time.time()
print(f"Starting {name}...")
try:
yieldfinally:
end = time.time()
print(f"{name} completed in {end - start:.2f}s")
with timer_context("Data Processing"):
# Your code hereimport time
time.sleep(1)
# Class-based context managerclassDatabaseConnection:
def__init__(self, db_name):
self.db_name = db_name
self.connection = Nonedef__enter__(self):
print(f"Opening connection to {self.db_name}")
self.connection = "Connected"# Actual DB connection herereturnself.connection
def__exit__(self, exc_type, exc_val, exc_tb):
print(f"Closing connection to {self.db_name}")
self.connection = NonereturnFalse# Don't suppress exceptionswith DatabaseConnection("my_database") as conn:
print(f"Using {conn}")
4. Advanced OOP
Dive deeper into object-oriented programming with metaclasses, abstract base classes, and advanced inheritance patterns.
classDate:
def__init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethoddeffrom_string(cls, date_string):
"""Create Date from string 'YYYY-MM-DD'"""
year, month, day = map(int, date_string.split('-'))
returncls(year, month, day)
@staticmethoddefis_leap_year(year):
"""Check if year is leap year"""return year % 4 == 0and (year % 100 != 0or year % 400 == 0)
# Using class method as alternative constructor
date = Date.from_string("2024-03-15")
# Using static methodprint(Date.is_leap_year(2024)) # True
5. Exception Handling
Proper exception handling makes your code robust and user-friendly.
Basic Exception Handling
# Try-except-else-finallytry:
result = 10 / 2exceptZeroDivisionError:
print("Cannot divide by zero!")
exceptTypeErroras e:
print(f"Type error: {e}")
else:
print(f"Result: {result}") # Runs if no exceptionfinally:
print("Cleanup code") # Always runs
import csv
# Writing CSVwithopen('employees.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['Name', 'Age', 'Department'])
writer.writerow(['Alice', 30, 'IT'])
writer.writerow(['Bob', 25, 'HR'])
# Reading CSVwithopen('employees.csv', 'r') as f:
reader = csv.reader(f)
for row in reader:
print(row)
# Using DictReader/DictWriterwithopen('employees.csv', 'r') as f:
reader = csv.DictReader(f)
for row in reader:
print(f"{row['Name']} works in {row['Department']}")
7. Regular Expressions
Regular expressions (regex) are powerful tools for pattern matching and text processing.
import re
# Pattern matching
text = "Contact us at support@example.com or sales@example.com"# Find all email addresses
emails = re.findall(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', text)
print(emails) # ['support@example.com', 'sales@example.com']# Search and match
pattern = r'(\d{3})-(\d{3})-(\d{4})'
phone = "Call me at 555-123-4567"
match = re.search(pattern, phone)
if match:
area, prefix, line = match.groups()
print(f"Area: {area}, Prefix: {prefix}, Line: {line}")
# Substitution
text = "The price is $100"
result = re.sub(r'\$(\d+)', r'€\1', text)
print(result) # "The price is €100"# Validationdefis_valid_email(email):
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'return re.match(pattern, email) isnotNoneprint(is_valid_email("user@example.com")) # Trueprint(is_valid_email("invalid.email")) # False
8. Modules and Packages
Organize your code into reusable modules and packages.
📦 Module vs Package
Module: A single .py file Package: A directory containing multiple modules and an __init__.py file
Creating and Using Modules
# mymodule.pydefgreet(name):
returnf"Hello, {name}!"defadd(a, b):
return a + b
PI = 3.14159# main.pyimport mymodule
print(mymodule.greet("Alice"))
print(mymodule.add(5, 3))
print(mymodule.PI)
# Alternative importsfrom mymodule import greet, PI
from mymodule import * # Import everything (not recommended)import mymodule as mm # Alias
Creating a Package
# Package structure:# mypackage/# __init__.py# module1.py# module2.py# subpackage/# __init__.py# module3.py# mypackage/__init__.pyfrom .module1 import func1
from .module2 import func2
__version__ = "1.0.0"
__all__ = ["func1", "func2"]
# Using the packageimport mypackage
print(mypackage.__version__)
mypackage.func1()
from mypackage.subpackage import module3
9. GUI Development with Tkinter
Create desktop applications with graphical user interfaces using Python's built-in Tkinter library.
Basic Tkinter Application
import tkinter as tk
from tkinter import messagebox
defsay_hello():
name = entry.get()
messagebox.showinfo("Greeting", f"Hello, {name}!")
# Create main window
root = tk.Tk()
root.title("Hello App")
root.geometry("300x150")
# Widgets
label = tk.Label(root, text="Enter your name:", font=("Arial", 12))
label.pack(pady=10)
entry = tk.Entry(root, width=30)
entry.pack(pady=5)
button = tk.Button(root, text="Greet", command=say_hello, bg="#4CAF50", fg="white")
button.pack(pady=10)
root.mainloop()
Work with databases using Python's built-in SQLite and SQLAlchemy ORM.
SQLite Basics
import sqlite3
# Create connection and cursor
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
# Create table
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
age INTEGER
)
''')
# Insert data
cursor.execute("INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
("Alice", "alice@example.com", 30))
# Insert multiple rows
users = [
("Bob", "bob@example.com", 25),
("Charlie", "charlie@example.com", 35)
]
cursor.executemany("INSERT INTO users (name, email, age) VALUES (?, ?, ?)", users)
# Query data
cursor.execute("SELECT * FROM users WHERE age > ?", (25,))
results = cursor.fetchall()
for row in results:
print(row)
# Update data
cursor.execute("UPDATE users SET age = ? WHERE name = ?", (31, "Alice"))
# Delete data
cursor.execute("DELETE FROM users WHERE age < ?", (18,))
# Commit and close
conn.commit()
conn.close()
Using Context Manager with SQLite
import sqlite3
with sqlite3.connect('database.db') as conn:
cursor = conn.cursor()
# Create and populate table
cursor.execute('''
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY,
name TEXT,
price REAL
)
''')
cursor.execute("INSERT INTO products (name, price) VALUES (?, ?)",
("Laptop", 999.99))
# Query with row_factory for dictionary access
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT * FROM products")
for row in cursor.fetchall():
print(f"{row['name']}: ${row['price']}")
# Connection auto-commits and closes
11. Web Scraping
Extract data from websites using BeautifulSoup and requests.
Basic Web Scraping
import requests
from bs4 import BeautifulSoup
# Fetch webpage
url = "https://example.com"
response = requests.get(url)
if response.status_code == 200:
# Parse HTML
soup = BeautifulSoup(response.content, 'html.parser')
# Find elements
title = soup.find('title').text
print(f"Page title: {title}")
# Find all links
links = soup.find_all('a')
for link in links:
href = link.get('href')
text = link.text
print(f"{text}: {href}")
# Find by class
articles = soup.find_all('div', class_='article')
# Find by ID
main_content = soup.find(id='content')
# CSS selectors
headers = soup.select('h1, h2, h3')
Advanced Scraping Example
import requests
from bs4 import BeautifulSoup
import csv
defscrape_products(url):
response = requests.get(url)
soup = BeautifulSoup(response.content, 'html.parser')
products = []
for item in soup.find_all('div', class_='product'):
name = item.find('h3').text.strip()
price = item.find('span', class_='price').text.strip()
rating = item.find('div', class_='rating').text.strip()
products.append({
'name': name,
'price': price,
'rating': rating
})
return products
# Save to CSV
products = scrape_products("https://example.com/products")
withopen('products.csv', 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['name', 'price', 'rating'])
writer.writeheader()
writer.writerows(products)
⚠️ Web Scraping Ethics
Always check robots.txt
Respect rate limits and add delays
Check website's Terms of Service
Don't overload servers with requests
Consider using official APIs when available
12. NumPy for Numerical Computing
NumPy is the foundation for numerical computing in Python, providing powerful array operations.
Speed up your code with threading and multiprocessing.
Threading
import threading
import time
defworker(name, delay):
print(f"{name} starting")
time.sleep(delay)
print(f"{name} finished")
# Create threads
threads = []
for i inrange(5):
t = threading.Thread(target=worker, args=(f"Thread-{i}", 1))
threads.append(t)
t.start()
# Wait for all threadsfor t in threads:
t.join()
print("All threads completed")
Multiprocessing
from multiprocessing import Pool
import time
defsquare(n):
time.sleep(1)
return n * n
if __name__ == '__main__':
# Create process poolwith Pool(processes=4) as pool:
numbers = range(10)
results = pool.map(square, numbers)
print(results)
💡 Threading vs Multiprocessing
Threading: Good for I/O-bound tasks (network, file operations) Multiprocessing: Good for CPU-bound tasks (calculations, data processing)