RTT Labs - Pioneering the Future of Robotics, AI & Cloud Logo

Performance Optimisation in Python

Performance Optimisation in Python

Python, with its readability and rich libraries, is a popular choice for diverse projects. However, when it comes to performance, it might not always be the fastest runner on the track. But fear not! Here’s a blog post packed with tips and examples to optimize your Python code for peak performance.

Profiling: Unveiling the Bottlenecks

Before optimizing, identify the bottlenecks! Use profiling tools like cProfile or line_profiler to pinpoint the functions or code sections consuming the most time. This helps focus your optimization efforts on the areas with the biggest impact.

import cProfile

def my_slow_function():
    # Code that needs optimization
    pass

cProfile.run("my_slow_function()")

Embrace Built-in Functions and Libraries

Python comes with a vast arsenal of optimized functions and libraries. Leverage these instead of writing your own, especially for common tasks like numerical computations or string manipulations.

# Use built-in list comprehension instead of loops
squares = [x**2 for x in range(10)]

# Use NumPy arrays for efficient numerical operations
import numpy as np
data = np.random.rand(10000)
result = np.sum(data)

Data Structures Wisely

Choose the right data structure for your needs. Lists offer flexibility, but dictionaries excel for fast lookups. Consider alternatives like sets or NumPy arrays for specific use cases.

# Use dictionaries for fast lookups by key
phonebook = {"Alice": "1234", "Bob": "5678"}

# Use sets for unique elements and membership checks
unique_words = set(words_in_text)

Algorithm Efficiency:

Optimize algorithms to reduce complexity. For example, consider using binary search instead of linear search for sorted lists. Explore libraries like heapq for efficient priority queues.

# Use binary search for efficient element search in sorted list
def binary_search(data, target):
    # Implementation of binary search algorithm
    pass

Minimize I/O Operations

Reading and writing to files can be time-consuming. Minimize I/O operations by buffering data, using in-memory databases, or reading/writing larger chunks at once.

# Use buffering to write to file in larger chunks
with open("output.txt", "wb") as f:
    for data in data_list:
        f.write(data)

Examples

1. List Comprehensions vs. Loops

Replace inefficient loops with concise list comprehensions for faster and cleaner code.

# Inefficient: Looping and creating new objects
squares = []
for x in range(10):
    squares.append(x**2)

# Efficient: Using list comprehension for concise and faster creation
squares = [x**2 for x in range(10)]

# Benchmark difference:
import timeit
timeit.timeit("[x**2 for x in range(10)]", number=10000)  # ~0.00007 seconds
timeit.timeit("squares = []; for x in range(10): squares.append(x**2)", number=10000)  # ~0.00014 seconds

2. String Formatting vs. f-strings

Use f-strings for clear and potentially faster string formatting compared to older methods.

# Traditional string formatting
name = "Alice"
message = "Hello, " + name + "!"

# Efficient f-strings for cleaner syntax and potential performance gains
message = f"Hello, {name}!"

# Benchmark difference:
import timeit
timeit.timeit("name = 'Alice'; message = 'Hello, ' + name + '!'", number=10000)  # ~0.00015 seconds
timeit.timeit("name = 'Alice'; message = f'Hello, {name}!'", number=10000)  # ~0.00012 seconds

3. Dictionary Lookups vs. List Iterations

Leverage direct dictionary lookups for much faster retrieval of values instead of iterating through lists.

# Inefficient: Iterating through the list
phonebook = {"Alice": "1234", "Bob": "5678", "Charlie": "9012"}
name = "Bob"
number = None
for key, value in phonebook.items():
    if key == name:
        number = value

# Efficient: Direct dictionary lookup for faster retrieval
number = phonebook.get(name)

# Benchmark difference:
import timeit
phonebook = {"a": 1, "b": 2, "c": 3}
timeit.timeit("number = phonebook.get('a')", number=10000)  # ~0.00002 seconds
timeit.timeit("for key, value in phonebook.items(): if key == 'a': number = value", number=10000)  # ~0.00025 seconds

4. Generator Expressions vs. List Creation

Utilize generator expressions for memory-efficient iteration, especially when dealing with large datasets.

# Inefficient: Creates a full list in memory
squares = []
for x in range(10000):
    squares.append(x**2)

# Efficient: Generator expression for memory-friendly iteration
squares = (x**2 for x in range(10000))

# Benchmark difference:
import timeit
timeit.timeit(list(x**2 for x in range(10000)), number=1)  # ~0.006 seconds
timeit.timeit("[x**2 for x in range(10000)]", number=1)  # ~0.014 seconds

5. Memoization with Caching

Cache function results with memoization to avoid redundant calculations and significantly improve performance for repeated calls.

# Naive Fibonacci function without memoization
def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

# Function with memoization to cache results
memo = {}
def fibonacci_memoized(n):
    if n in memo:
        return memo[n]
    else:
        result = fibonacci_memoized(n-1) + fibonacci_memoized(n-2)
        memo[n] = result
        return result

# Benchmark difference:
import timeit
timeit.timeit("fibonacci(30)", number=100)  # ~13.4 seconds
timeit.timeit("fibonacci_memoized(30)", number=100)  # ~0.004 seconds

Remember the GIL

Python’s Global Interpreter Lock (GIL) limits true parallel execution for CPU-bound tasks under a single process. Consider alternatives like multiprocessing or Cython for highly CPU-intensive workloads.

Beyond the Basics:

  • Memoization: Cache function results for repeated calls with the same arguments.
  • Generator expressions: Use generator expressions for memory-efficient iteration.
  • Cython: Compile specific functions to C for significant performance gains.

Embrace Optimization Wisely

Optimization is not a one-size-fits-all solution. Measure performance before and after changes to ensure they are effective. Prioritize optimization based on your specific use case and bottlenecks. Remember, clear and readable code is often more maintainable in the long run.

Table to Contents