# What does the "yield" keyword do in Python?

The "yield" keyword in Python is used in the context of creating iterators and generators. It allows a function to return a generator instead of a regular value. When a function contains the "yield" keyword, it becomes a generator function and can be used to create generator objects.

## Understanding Generators

In order to understand the purpose and use of the "yield" keyword, it is important to understand generators. A generator is a special type of iterator that generates values on the fly, instead of storing them in memory. They are used when you want to iterate over a large set of values without the need to store them all at once.

Generator functions are defined just like regular functions in Python, using the "def" keyword. However, instead of using the "return" keyword to return a value, they use the "yield" keyword. This allows the generator function to produce a series of values over time, each time it is called.

Here's an example of a basic generator function that generates a sequence of numbers:

```
def count_up_to(n):
i = 0
while i <= n:
yield i
i += 1
# Create a generator object
generator = count_up_to(5)
# Iterate over the generator
for num in generator:
print(num)
```

This code will output the numbers 0, 1, 2, 3, 4, 5. On each iteration, the generator function "count_up_to" generates the next number in the sequence using the "yield" keyword. Since the generator is an iterator, we can use it in a for loop to iterate over the generated values.

## Understanding the Code Example

Let's analyze the code example provided:

```
def _get_child_candidates(self, distance, min_dist, max_dist):
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
result, candidates = [], [self]
while candidates:
node = candidates.pop()
distance = node._get_dist(obj)
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result
```

The method "_get_child_candidates" is defined within a class. It takes three parameters: "distance", "min_dist", and "max_dist".

The first "if" statement within the method checks if the "_leftchild" exists and if the distance minus the max distance is less than the "_median" value. If this condition is true, it yields the "_leftchild". Similarly, the second "if" statement yields the "_rightchild" if it exists and if the distance plus the max distance is greater than or equal to the "_median" value.

In the caller code, the variables "result" and "candidates" are initialized as an empty list and a list containing "self" respectively. The while loop runs as long as there are items in the "candidates" list. On each iteration, a node is popped from the "candidates" list and its distance from the target object is calculated using the "_get_dist" method.

If the distance is within the range of "min_dist" and "max_dist", the values of the node are appended to the "result" list. The method "_get_child_candidates" is then called on the node, passing in the distance, min_dist, and max_dist as parameters. The yielded child candidates are then added to the "candidates" list using the extend() method.

This process continues until there are no more candidates in the "candidates" list. Finally, the "result" list is returned.

## Using "yield" to Optimize Memory Usage

One of the main advantages of using the "yield" keyword is its ability to optimize memory usage. Since generators produce values on the fly and do not store all the values in memory, they are particularly useful when dealing with large datasets or when memory is constrained.

For example, imagine you have a function that generates a large list of prime numbers:

```
def generate_primes(n):
primes = []
for i in range(2, n + 1):
# Check if i is prime
is_prime = True
for j in range(2, i):
if i % j == 0:
is_prime = False
break
if is_prime:
primes.append(i)
return primes
```

With this function, you would need to store all the prime numbers in memory before returning them. However, if you use the "yield" keyword instead, you can create a generator that produces the prime numbers on the fly:

```
def generate_primes(n):
for i in range(2, n + 1):
# Check if i is prime
is_prime = True
for j in range(2, i):
if i % j == 0:
is_prime = False
break
if is_prime:
yield i
```

This way, you can iterate over the generator and obtain the prime numbers without needing to store them all in memory. This can be especially useful when dealing with very large numbers or when the range of values is unknown.

## Stopping Subsequent Calls

The use of the "yield" keyword allows for the suspension and resumption of generator function execution. When a generator function is called, it returns a generator object that can be used to iterate over the values it generates. Each time the generator object is iterated, the function's execution resumes from where it left off, continuing until the next "yield" statement is encountered.

In the case of the provided code example, subsequent calls to "_get_child_candidates" will continue generating child candidates as long as the specified conditions are met. If there are no more child candidates to yield, the execution of the generator function will stop automatically.

## Conclusion

In summary, the "yield" keyword in Python is used to create generator functions and return generator objects. It allows for the on-demand generation of values, optimizing memory usage and enabling the creation of efficient iterators. By suspending and resuming function execution, subsequent calls to generator functions can continue generating values until the generator is exhausted. Understanding the use of "yield" is essential for leveraging the power of Python's iterator and generator capabilities.