Explain the concept of generators in Python and provide an example of how they can be used to optimize memory usage and performance.
Generators in Python are a way to create iterators, allowing you to iterate through a potentially large set of data efficiently. They are memory-efficient as they generate values on-the-fly rather than storing them in memory. Here's a simple example:
def square_numbers(n):
for i in range(n):
yield i ** 2
# Usage
squares = square_numbers(5)
for square in squares:
print(square)