Python Tutorial¶
Variables and Types¶
Python is dynamically typed:
Control Flow¶
If Statements¶
Loops¶
Functions¶
Functions are defined using the def keyword:
Decorators¶
Decorators provide a way to modify the behavior of functions or classes without permanently modifying their source code. They are frequently used for logging, access control, timing, and caching.
def my_decorator(func):
def wrapper():
print("Before function execution")
func()
print("After function execution")
return wrapper
@my_decorator
def say_hello():
print("Hello, world!")
say_hello()
# Output:
# Before function execution
# Hello, world!
# After function execution
Magic Methods (Dunder Methods)¶
Magic methods, often called "dunder" (double underscore) methods, are special methods that define how objects of a class behave for built-in operations (like addition, comparison, or string representation).
class Book:
def __init__(self, title, author):
# The constructor is a magic method
self.title = title
self.author = author
def __str__(self):
# Used by print() and str()
return f"'{self.title}' by {self.author}"
def __eq__(self, other):
# Used by the == operator
if not isinstance(other, Book):
return False
return self.title == other.title and self.author == other.author
def __len__(self):
# Used by len()
return len(self.title)
b1 = Book("1984", "George Orwell")
b2 = Book("1984", "George Orwell")
print(b1) # Output: '1984' by George Orwell
print(b1 == b2) # Output: True
print(len(b1)) # Output: 4
Concurrency and Parallelism¶
Python provides several ways to execute code concurrently or in parallel: asyncio, threading (Multithreading), and multiprocessing (Multiprocessing).
Asyncio¶
asyncio is a library to write concurrent code using the async/await syntax. It's often a perfect fit for IO-bound and high-level structured network code. It operates on a single thread using an event loop.
import asyncio
import time
async def fetch_data(id):
print(f"Task {id}: Fetching data...")
await asyncio.sleep(1) # Simulating I/O operation
print(f"Task {id}: Data fetched!")
return {"id": id, "data": "Sample"}
async def main():
start_time = time.time()
# Run tasks concurrently
results = await asyncio.gather(
fetch_data(1),
fetch_data(2),
fetch_data(3)
)
end_time = time.time()
print(f"Results: {results}")
print(f"Time taken: {end_time - start_time:.2f} seconds")
# asyncio.run(main())
Multithreading¶
The threading module is used for running multiple threads (tasks, function calls) in a single process. Due to Python's Global Interpreter Lock (GIL), multiple threads cannot execute Python bytecodes at once. Therefore, multithreading is primarily useful for I/O-bound tasks (like reading files, network requests) rather than CPU-bound tasks.
import threading
import time
def fetch_data_sync(id):
print(f"Thread {id}: Fetching data...")
time.sleep(1) # Simulating I/O operation
print(f"Thread {id}: Data fetched!")
def main_threading():
start_time = time.time()
threads = []
# Create and start threads
for i in range(1, 4):
thread = threading.Thread(target=fetch_data_sync, args=(i,))
threads.append(thread)
thread.start()
# Wait for all threads to complete
for thread in threads:
thread.join()
end_time = time.time()
print(f"Time taken: {end_time - start_time:.2f} seconds")
# main_threading()
Multiprocessing¶
The multiprocessing module allows the programmer to fully leverage multiple processors on a given machine. It creates separate processes, each with its own Python interpreter and memory space, completely bypassing the GIL. This is the recommended approach for CPU-bound tasks (like heavy mathematical computations).
import multiprocessing
import time
def cpu_bound_task(n):
print(f"Process {n}: Starting heavy computation...")
result = sum(i * i for i in range(10**7))
print(f"Process {n}: Computation finished!")
return result
def main_multiprocessing():
start_time = time.time()
processes = []
# Create and start processes
for i in range(1, 4):
process = multiprocessing.Process(target=cpu_bound_task, args=(i,))
processes.append(process)
process.start()
# Wait for all processes to complete
for process in processes:
process.join()
end_time = time.time()
print(f"Time taken: {end_time - start_time:.2f} seconds")
# if __name__ == '__main__':
# main_multiprocessing()