Introduction
This chapter covers advanced topics that go beyond the basics of Python usage, aiming to maximize productivity and write more efficient code. The code developed through this chapter will be more robust, scalable, and easier to maintain.
1. Context Managers
Context managers are a Python feature that automates resource allocation and release in scenarios such as file opening, database connections, and using locks. They enhance code readability and reduce the likelihood of bugs.
1.1 Basic Usage of Context Managers
The most common example of a context manager in Python is opening a file using the with
statement.
with open('example.txt', 'r') as file:
data = file.read()
# The file is automatically closed when the block ends.
1.2 Custom Context Managers
To implement a context manager, you just need to define a class with __enter__
and __exit__
methods.
class CustomContext:
def __enter__(self):
# Resource allocation or setup
print("Resource allocated")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# Resource release
print("Resource released")
with CustomContext() as context:
print("Inside the block")
This example manages resources by recognizing the start and end of a block.
2. Generators
Generators are a simplified version of iterators and can save memory when processing large datasets. Generators yield one value at a time and wait until the next value is needed. This property allows generators to efficiently handle large datasets.
2.1 Generator Functions
Generator functions are defined like regular functions but use yield
instead of return
to provide values.
def simple_generator():
yield 1
yield 2
yield 3
The above function creates a generator object that returns 1, 2, and 3 sequentially each time it is called.
2.2 Infinite Generators
Generators can easily create infinite loops, making them useful for processes that repeat periodically.
def infinite_sequence():
num = 0
while True:
yield num
num += 1
This function returns an increasing number starting from 0 indefinitely until stopped.
3. Decorators
Decorators are powerful tools that dynamically alter or extend the behavior of functions or methods. They greatly enhance code reusability and are primarily used for logging, access control, and metrics.
3.1 Definition and Use of Decorators
Decorators can wrap another function to add specific logic or modify the input and output of an existing function.
def my_decorator(func):
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
In this example, the functionality added by the decorator runs when the say_hello
function is called.
3.2 Combining Multiple Decorators
Multiple decorators can be applied to a single function, and their behavior can vary based on the order of the decorators.
def decorator_one(func):
def wrapper():
print("Decorator 1 applied")
func()
return wrapper
def decorator_two(func):
def wrapper():
print("Decorator 2 applied")
func()
return wrapper
@decorator_two
@decorator_one
def display():
print("Display function")
display()
4. Parallelism and Multithreading
To improve program performance, you can use parallelism or multithreading. This allows the code to utilize multiple CPU cores to perform tasks simultaneously.
4.1 Multithreading
Multithreading is useful in cases where there are many I/O bound tasks. You can create threads using Python’s threading
module.
import threading
import time
def thread_function(name):
print(f"Thread {name} started")
time.sleep(2)
print(f"Thread {name} ended")
threads = []
for i in range(3):
thread = threading.Thread(target=thread_function, args=(i,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
This code creates three threads, each performing a task for 2 seconds.
4.2 Multiprocessing
For CPU bound tasks, the multiprocessing
module is more efficient. It creates processes to fully utilize CPU cores.
from multiprocessing import Process
def process_function(name):
print(f"Process {name} started")
time.sleep(2)
print(f"Process {name} ended")
processes = []
for i in range(3):
process = Process(target=process_function, args=(i,))
processes.append(process)
process.start()
for process in processes:
process.join()
5. Advanced Exception Handling
Exception handling is essential for enhancing the reliability of programs. In this section, we will explore advanced exception handling techniques.
5.1 Creating Custom Exceptions
You can create custom exceptions to explicitly express exceptions that may occur in specific situations.
class CustomError(Exception):
pass
try:
raise CustomError("This is a custom exception")
except CustomError as e:
print(e)
5.2 Exception Chaining
One exception may be the result of another exception. In Python, you can create exception chains using the raise ... from ...
syntax.
try:
raise ValueError("First exception")
except ValueError as ve:
raise KeyError("Second exception") from ve
6. Conclusion
This chapter has delved deeper into Python’s advanced features. By leveraging these techniques, you can write more robust and scalable code. In the next chapter, we will explore how to use Python for data analysis.