Closures are a powerful concept in programming that allow a function to remember and access the variables defined in its enclosing scope, even after the enclosing function has finished executing.

## Creating a Closure

To create a closure in Python, you define a nested function inside another function, and use the nested function after the enclosing function has returned. The nested function has access to the variables defined in the enclosing function, even if the enclosing function is no longer in memory.

For example:

```
def outer_function(x):
def inner_function():
print(x)
return inner_function
closure = outer_function(10)
closure() # Output: 10
```

In this example, the `inner_function()`

is a closure that remembers the value of `x`

defined in the `outer_function()`

. When the `closure()`

function is called, it prints the value of `x`

.

Closures can be used to create functions with different behavior, without having to define a separate function for each behavior. For example:

```
def make_multiplier(x):
def multiplier(y):
return x * y
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # Output: 10
print(triple(5)) # Output: 15
```

In this example, the `make_multiplier()`

function creates a closure that multiplies its argument by a specified value. The `double()`

and `triple()`

functions are created by calling `make_multiplier()`

with different values for `x`

.

Closures can also be used to preserve the state of a function between calls. For example:

```
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
counter = make_counter()
print(counter()) # Output: 1
print(counter()) # Output: 2
print(counter()) # Output: 3
```

In this example, the `make_counter()`

function defines a closure that increments a counter variable each time it is called. The counter variable is preserved between calls to the `counter()`

function, allowing it to keep track of the number of times it has been called.

## Exercises

To review these concepts, we will go through a series of exercises designed to test your understanding and apply what you have learned.

#### Use a closure to create a function that returns the next value in the Fibonacci sequence each time it is called.

```
def make_fibonacci():
a, b = 0, 1
def fibonacci():
nonlocal a, b
a, b = b, a + b
return a
return fibonacci
fib = make_fibonacci()
print(fib()) # Output: 1
print(fib()) # Output: 1
print(fib()) # Output: 2
print(fib()) # Output: 3
print(fib()) # Output: 5
```

#### Use a closure to create a function that returns the nth power of a number.

```
def make_power(n):
def power(x):
return x ** n
return power
square = make_power(2)
print(square(3)) # Output: 9
cube = make_power(3)
print(cube(3)) # Output: 27
quadruple = make_power(4)
print(quadruple(3)) # Output: 81
```

#### Use a closure to create a function that returns the next value in an arithmetic progression each time it is called.

```
def make_arithmetic_progression(start, step):
def arithmetic_progression():
nonlocal start
result = start
start += step
return result
return arithmetic_progression
progression = make_arithmetic_progression(0, 2)
print(progression()) # Output: 0
print(progression()) # Output: 2
print(progression()) # Output: 4
print(progression()) # Output: 6
```

#### Use a closure to create a function that returns the next value in a geometric progression each time it is called.

```
def make_geometric_progression(start, factor):
def geometric_progression():
nonlocal start
result = start
start *= factor
return result
return geometric_progression
progression = make_geometric_progression(2, 3)
print(progression()) # Output: 2
print(progression()) # Output: 6
print(progression()) # Output: 18
print(progression()) # Output: 54
```

#### Use a closure to create a function that returns the next value in a geometric progression, with a maximum value.

```
def make_bounded_geometric_progression(start, factor, maximum):
def bounded_geometric_progression():
nonlocal start
result = start
start *= factor
if start > maximum:
start = maximum
return result
return bounded_geometric_progression
progression = make_bounded_geometric_progression(2, 3, 50)
print(progression()) # Output: 2
print(progression()) # Output: 6
print
```