9. More on Functions¶
9.1. Recursion¶
Consider computing the Fibonacci number of order \(n\):
Fibonacci numbers have practical applications in generating pseudorandom numbers.
Can we define the function by calling the function itself?
%%mytutor -r -h 450
def fibonacci(n):
if n > 1:
return fibonacci(n - 1) + fibonacci(n - 2) # recursion
elif n == 1:
return 1
else:
return 0
fibonacci(2)
1
Recursion is a function that calls itself (recurs).
Exercise Write a function gcd
that implements the Euclidean algorithm for the greatest common divisor:
%%mytutor -r -h 550
def gcd(a, b):
### BEGIN SOLUTION
return gcd(b, a % b) if b else a
### END SOLUTION
gcd(3 * 5, 5 * 7)
5
Is recursion strictly necessary?
No. We can always convert a recursion to an iteration.
E.g., the following computes the Fibonnacci number of order using a while loop instead.
%%mytutor -r -h 550
def fibonacci_iteration(n):
if n > 1:
_, F = 0, 1 # next two Fibonacci numbers
while n > 1:
_, F, n = F, F + _, n - 1
return F
elif n == 1:
return 1
else:
return 0
fibonacci_iteration(3)
2
# more tests
for n in range(5):
assert fibonacci(n) == fibonacci_iteration(n)
Exercise Implement gcd_iteration
using a while loop instead of a recursion.
%%mytutor -r -h 550
def gcd_iteration(a, b):
### BEGIN SOLUTION
while b:
a, b = b, a % b
return a
### END SOLUTION
gcd_iteration(3 * 5, 5 * 7)
5
# test
for n in range(5):
assert fibonacci(n) == fibonacci_iteration(n)
What is the benefit of recursion?
Recursion is often shorter and easier to understand.
It is also easier to write code by wishful thinking or declarative programming.
Is recusion more efficient than iteration?
Exercise Find the smallest values of n
forfibonacci(n)
and fibonacci_iteration(n)
respectively to run for more than a second.
# Assign n
### BEGIN SOLUTION
n = 33
### END SOLUTION
fib_recursion = fibonacci(n)
# Assign n
### BEGIN SOLUTION
n = 300000
### END SOLUTION
fib_iteration = fibonacci_iteration(n)
To see why recursion is slow, we will modify fibonacci
to print each function call as follows.
def fibonacci(n):
'''Returns the Fibonacci number of order n.'''
print('fibonacci({!r})'.format(n))
return fibonacci(n - 1) + fibonacci(n - 2) if n > 1 else 1 if n == 1 else 0
fibonacci(5)
fibonacci(5)
fibonacci(4)
fibonacci(3)
fibonacci(2)
fibonacci(1)
fibonacci(0)
fibonacci(1)
fibonacci(2)
fibonacci(1)
fibonacci(0)
fibonacci(3)
fibonacci(2)
fibonacci(1)
fibonacci(0)
fibonacci(1)
5
fibonacci(5)
calls fibonacci(3)
and fibonacci(4)
, which in turn call fibonacci(2)
and fibonacci(3)
. fibonacci(3)
is called twice.
9.2. Global Variables¶
Consider the problem of generating a sequence of Fibonacci numbers.
for n in range(5):
print(fibonacci_iteration(n))
0
1
1
2
3
Is the above loop efficient?
No. Each call to fibonacci_iteration(n)
recomputes the last two Fibonacci numbers \(F_{n-1}\) and \(F_{n-2}\) for \(n\geq 2\).
How to avoid redundant computations?
One way is to store the last two computed Fibonacci numbers.
%%mytutor -h 600
def next_fibonacci():
'''Returns the next Fibonacci number.'''
global _Fn, _Fn1, _n # global declaration
value = _Fn
_Fn, _Fn1, _n = _Fn1, _Fn + _Fn1, _n + 1
return value
def print_fibonacci_state():
print('''States:
_Fn : Next Fibonacci number = {}
_Fn1 : Next next Fibonacci number = {}
_n : Next order = {}'''.format(_Fn,_Fn1,_n))
# global variables for next_fibonacci and print_fibonacci_state
_Fn, _Fn1, _n = 0, 1, 0
for n in range(5):
print(next_fibonacci())
print_fibonacci_state()
Rules for global/local variables:
A local variable must be defined within a function.
An assignment defines a local variable except in a
global
statement.
Why global
is NOT needed in print_fibonacci_state
?
Without ambiguity, _Fn, _Fn1, _n
in print_fibonacci_state
are not local variables by Rule 1 because they are not defined within the function.
Why global
is needed in next_fibonacci
?
What happens otherwise:
def next_fibonacci():
'''Returns the next Fibonacci number.'''
# global _Fn, _Fn1, _n
value = _Fn
_Fn, _Fn1, _n = _Fn1, _Fn + _Fn1, _n + 1
return value
next_fibonacci()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-13-44c8c496bfd8> in <module>
6 return value
7
----> 8 next_fibonacci()
<ipython-input-13-44c8c496bfd8> in next_fibonacci()
2 '''Returns the next Fibonacci number.'''
3 # global _Fn, _Fn1, _n
----> 4 value = _Fn
5 _Fn, _Fn1, _n = _Fn1, _Fn + _Fn1, _n + 1
6 return value
UnboundLocalError: local variable '_Fn' referenced before assignment
Why is there an UnboundLocalError
?
The assignment defines
_Fn
as a local variable by Rule 2.However, the assignment requires first evaluating
_Fn
, which is not yet defined.
Are global variables preferred over local ones?
Suppose for aesthetic reasons we remove the underscores in global variable names?
%%mytutor -h 600
def next_fibonacci():
'''Returns the next Fibonacci number.'''
global Fn, Fn1, n
value = Fn
Fn, Fn1, n = Fn1, Fn + Fn1, n + 1
return value
def print_fibonacci_state():
print('''States:
Fn : Next Fibonacci number = {}
Fn1 : Next next Fibonacci number = {}
n : Next order = {}'''.format(Fn,Fn1,n))
# global variables renamed without underscores
Fn, Fn1, n = 0, 1, 0
n = 0
while n < 5:
print(next_fibonacci())
n += 1
print_fibonacci_state()
Exercise Why does the while loop prints only 3 instead of 5 Fibonacci numbers?
There is a name collision. n
is also incremented by next_fibonacci()
, and so the while loop is only executed 3 times in total.
With global variables
codes are less predictable, more difficult to reuse/extend, and
tests cannot be isolated, making debugging difficult.
Is it possible to store the function states without using global variables?
Yes. We can use nested functions and nonlocal
variables.
def fibonacci_closure(Fn, Fn1):
def next_fibonacci():
'''Returns the next (generalized) Fibonacci number starting with
Fn and Fn1 as the first two numbers.'''
nonlocal Fn, Fn1, n # declare nonlocal variables
value = Fn
Fn, Fn1, n = Fn1, Fn + Fn1, n + 1
return value
def print_fibonacci_state():
print('''States:
Next Fibonacci number = {}
Next next Fibonacci number = {}
Next order = {}'''.format(Fn, Fn1, n))
n = 0 # Fn and Fn1 specified in the function arguments
return next_fibonacci, print_fibonacci_state
next_fibonacci, print_fibonacci_state = fibonacci_closure(0, 1)
n = 0
while n < 5:
print(next_fibonacci())
n += 1
print_fibonacci_state()
0
1
1
2
3
States:
Next Fibonacci number = 5
Next next Fibonacci number = 8
Next order = 5
The state variables Fn, Fn1, n
are now encapsulated, and so
the functions returned by fibonacci_closure
no longer depends on any global variables.
Another benefit of using nested functions is that we can also create different Fibonacci sequence with different base cases.
my_next_fibonacci, my_print_fibonacci_state = fibonacci_closure('cs', '1302')
for n in range(5):
print(my_next_fibonacci())
my_print_fibonacci_state()
cs
1302
cs1302
1302cs1302
cs13021302cs1302
States:
Next Fibonacci number = 1302cs1302cs13021302cs1302
Next next Fibonacci number = cs13021302cs13021302cs1302cs13021302cs1302
Next order = 5
next_fibonacci
and print_fibonacci_state
are local functions of fibonacci_closure
.
They can access (capture) the other local variables of
fibonacci_closure
by forming the so-called closures.Similar to the use of
global
statement, anon-local
statement is needed for assigning nonlocal variables.
Each local function has an attribute named __closure__
that stores the captured local variables.
def print_closure(f):
'''Print the closure of a function.'''
print('closure of ', f.__name__)
for cell in f.__closure__:
print(' {} content: {!r}'.format(cell, cell.cell_contents))
print_closure(next_fibonacci)
print_closure(print_fibonacci_state)
closure of next_fibonacci
<cell at 0x7f6814541050: int object at 0x56190ea45380> content: 5
<cell at 0x7f68145410d0: int object at 0x56190ea453e0> content: 8
<cell at 0x7f6814541150: int object at 0x56190ea45380> content: 5
closure of print_fibonacci_state
<cell at 0x7f6814541050: int object at 0x56190ea45380> content: 5
<cell at 0x7f68145410d0: int object at 0x56190ea453e0> content: 8
<cell at 0x7f6814541150: int object at 0x56190ea45380> content: 5
9.3. Generator¶
Another way to generate a sequence of objects one-by-one is to write a generator.
fibonacci_generator = (fibonacci_iteration(n) for n in range(3))
fibonacci_generator
<generator object <genexpr> at 0x7f6814534250>
The above uses a generator expression to define fibonacci_generator
.
How to obtain items from a generator?
We can use the next
function.
while True:
print(next(fibonacci_generator)) # raises StopIterationException eventually
0
1
1
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-19-e6f031ce2829> in <module>
1 while True:
----> 2 print(next(fibonacci_generator)) # raises StopIterationException eventually
StopIteration:
A generator object is iterable, i.e., it implements both __iter__
and __next__
methods that are automatically called in a for
loop as well as the next
function.
fibonacci_generator = (fibonacci_iteration(n) for n in range(5))
for fib in fibonacci_generator: # StopIterationException handled by for loop
print(fib)
0
1
1
2
3
Is fibonacci_generator
efficient?
No again due to redundant computations.
A better way to define the generator is to use the keyword yield
:
%%mytutor -h 450
def fibonacci_sequence(Fn, Fn1, stop):
'''Return a generator that generates Fibonacci numbers
starting from Fn and Fn1 until stop (exclusive).'''
while Fn < stop:
yield Fn # return Fn and pause execution
Fn, Fn1 = Fn1, Fn1 + Fn
for fib in fibonacci_sequence(0, 1, 5):
print(fib)
yield
causes the function to return a generator without executing the function body.Calling
__next__
resumes the execution, whichpauses at the next
yield
expression, orraises the
StopIterationException
at the end.
Exercise The yield expression yield ...
is mistaken in [Halterman17] to be a statement. It is actually an expression because
The value of a
yield
expression isNone
by default, butit can be set by the
generator.send
method.
Add the document string to the following function. In particular, explain the effect of calling the method send
on the returned generator.
%%mytutor -r -h 500
def fibonacci_sequence(Fn, Fn1, stop):
### BEGIN SOLUTION
'''Return a generator that generates Fibonacci numbers
starting from Fn and Fn1 to stop (exclusive).
generator.send(value) sets next number to value.'''
### END SOLUTION
while Fn < stop:
value = yield Fn
if value is not None:
Fn1 = value # set next number to the value of yield expression
Fn, Fn1 = Fn1, Fn1 + Fn
9.4. Optional Arguments¶
How to make function arguments optional?
def fibonacci_sequence(Fn=0, Fn1=1, stop=None):
while stop is None or Fn < stop:
value = yield Fn
Fn, Fn1 = Fn1, Fn1 + Fn
for fib in fibonacci_sequence(0,1,5):
print(fib) # with all arguments specified
0
1
1
2
3
for fib in fibonacci_sequence(stop=5):
print(fib) # with default Fn=0, Fn1=1
0
1
1
2
3
stop=5
is called a keyword argument. Unlike positional arguments
, it specifies the name of the argument explicitly.
Exercise stop
is an optional argument with the default value None
. What is the behavior of the following code?
for fib in fibonacci_sequence(5):
print(fib)
if fib > 10:
break # Will this be executed?
5
1
6
7
13
With the default value of None
, the while loop becomes an infinite loop. The generator will keep generating the next Fibonacci number without any bound on the order. In particular, fibonacci_sequence(5)
creates an unstoppable (default) generator with base case Fn=5
(specified) and Fn1=1
(default).
Rules for specifying arguments:
Keyword arguments must be after all positional arguments.
Duplicate assignments to an argument are not allowed.
E.g., the following results in error:
fibonacci_sequence(stop=10, 1)
File "<ipython-input-27-c4b4809b18c1>", line 1
fibonacci_sequence(stop=10, 1)
^
SyntaxError: positional argument follows keyword argument
fibonacci_sequence(1, Fn=1)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-28-2db5e024912c> in <module>
----> 1 fibonacci_sequence(1, Fn=1)
TypeError: fibonacci_sequence() got multiple values for argument 'Fn'
The following shows that the behavior of range
is different.
for count in range(1, 10, 2):
print(count, end=' ') # counts from 1 to 10 in steps of 2
print()
for count in range(1, 10):
print(count, end=' ') # default step=1
print()
for count in range(10):
print(count, end=' ') # default start=0, step=1
range(stop=10) # fails
1 3 5 7 9
1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-29-f3395132058b> in <module>
7 for count in range(10):
8 print(count, end=' ') # default start=0, step=1
----> 9 range(stop=10) # fails
TypeError: range() takes no keyword arguments
range
takes only positional arguments.
However, the first positional argument has different intepretations (start
or stop
) depending on the number of arguments (2 or 1).
range
is indeed NOT a generator.
print(type(range),type(range(10)))
<class 'type'> <class 'range'>
9.5. Variable number of arguments¶
We can simulate the behavior of range by having a variable number of arguments.
def print_arguments(*args, **kwargs):
'''Take any number of arguments and prints them'''
print('args ({}): {}'.format(type(args),args))
print('kwargs ({}): {}'.format(type(kwargs),kwargs))
print_arguments(0, 10, 2, start=1, stop=2)
print("{k}".format(greeting="Hello",k=8),"*" )
args (<class 'tuple'>): (0, 10, 2)
kwargs (<class 'dict'>): {'start': 1, 'stop': 2}
8 *
args
is a tuple of positional arguments.kwargs
is a dictionary of keyword arguments.
*
and **
are unpacking operators for tuple/list and dictionary respectively:
args = (0, 10, 2)
kwargs = {'start': 1, 'stop': 2}
print_arguments(*args, **kwargs)
args (<class 'tuple'>): (0, 10, 2)
kwargs (<class 'dict'>): {'start': 1, 'stop': 2}
The following function converts all the arguments to a string.
It will be useful later on.
def argument_string(*args, **kwargs):
'''Return the string representation of the list of arguments.'''
return '({})'.format(', '.join([
*['{!r}'.format(v) for v in args], # arguments
*['{}={!r}'.format(k, v)
for k, v in kwargs.items()] # keyword arguments
]))
argument_string(0, 10, 2, start=1, stop=2)
'(0, 10, 2, start=1, stop=2)'
Exercise Redefine fibonacci_sequence
so that the positional arguments depend on the number of arguments:
def fibonacci_sequence(*args):
'''Return a generator that generates Fibonacci numbers
starting from Fn and Fn1 to stop (exclusive).
generator.send(value) sets next number to value.
fibonacci_sequence(stop)
fibonacci_sequence(Fn,Fn1)
fibonacci_sequence(Fn,Fn1,stop)
'''
Fn, Fn1, stop = 0, 1, None # default values
# handle different number of arguments
if len(args) is 1:
### BEGIN SOLUTION
stop = args[0]
### END SOLUTION
elif len(args) is 2:
Fn, Fn1 = args[0], args[1]
elif len(args) > 2:
Fn, Fn1, stop = args[0], args[1], args[2]
while stop is None or Fn < stop:
value = yield Fn
if value is not None:
Fn1 = value # set next number to the value of yield expression
Fn, Fn1 = Fn1, Fn1 + Fn
for fib in fibonacci_sequence(5): # default Fn=0, Fn=1
print(fib)
0
1
1
2
3
for fib in fibonacci_sequence(1, 2): # default stop=None
print(fib)
if fib>5:
break
1
2
3
5
8
args = (1, 2, 5)
for fib in fibonacci_sequence(*args): # default stop=None
print(fib)
1
2
3
9.6. Decorator¶
What is function decoration?
Why decorate a function?
def fibonacci(n):
'''Returns the Fibonacci number of order n.'''
global count, depth
count += 1
depth += 1
print('{:>3}: {}fibonacci({!r})'.format(count, '|' * depth, n))
value = fibonacci(n - 1) + fibonacci(n - 2) if n > 1 else 1 if n == 1 else 0
depth -= 1
if depth is -1: # recursion done
print('Done')
count = 0 # reset count for subsequent recursions
return value
count, depth = 0, -1
for n in range(6):
print(fibonacci(n))
1: fibonacci(0)
Done
0
1: fibonacci(1)
Done
1
1: fibonacci(2)
2: |fibonacci(1)
3: |fibonacci(0)
Done
1
1: fibonacci(3)
2: |fibonacci(2)
3: ||fibonacci(1)
4: ||fibonacci(0)
5: |fibonacci(1)
Done
2
1: fibonacci(4)
2: |fibonacci(3)
3: ||fibonacci(2)
4: |||fibonacci(1)
5: |||fibonacci(0)
6: ||fibonacci(1)
7: |fibonacci(2)
8: ||fibonacci(1)
9: ||fibonacci(0)
Done
3
1: fibonacci(5)
2: |fibonacci(4)
3: ||fibonacci(3)
4: |||fibonacci(2)
5: ||||fibonacci(1)
6: ||||fibonacci(0)
7: |||fibonacci(1)
8: ||fibonacci(2)
9: |||fibonacci(1)
10: |||fibonacci(0)
11: |fibonacci(3)
12: ||fibonacci(2)
13: |||fibonacci(1)
14: |||fibonacci(0)
15: ||fibonacci(1)
Done
5
The code decorates the fibonacci
function by printing each recursive call and the depth of the call stack.
The decoration is useful in showing the efficiency of the function, but it rewrites the function definition.
How to decorate a function without changing its code?
What if the decorations are temporary and should be removed later?
Go through the source codes of all decorated functions to remove the decorations?
When updating a piece of code, switch back and forth between original and decorated codes?
What about defining a new function that calls and decorates the original function?
def fibonacci(n):
'''Returns the Fibonacci number of order n.'''
return fibonacci(n - 1) + fibonacci(n - 2) if n > 1 else 1 if n is 1 else 0
def fibonacci_decorated(n):
'''Returns the Fibonacci number of order n.'''
global count, depth
count += 1
depth += 1
print('{:>3}: {}fibonacci({!r})'.format(count, '|' * depth, n))
value = fibonacci(n)
depth -= 1
if depth is -1: # recursion done
print('Done')
count = 0 # reset count for subsequent recursions
return value
count, depth = 0, -1
for n in range(6):
print(fibonacci_decorated(n))
1: fibonacci(0)
Done
0
1: fibonacci(1)
Done
1
1: fibonacci(2)
Done
1
1: fibonacci(3)
Done
2
1: fibonacci(4)
Done
3
1: fibonacci(5)
Done
5
We want fibonacci
to call fibonacci_decorated
instead.
What about renaming fibonacci_decorated
to fibonacci
?
fibonacci = fibonacci_decorated
count, depth = 0, -1
fibonacci_decorated(10)
(If you are faint-hearted, don’t run the above code.)
We want fibonacci_decorated
to call the original fibonacci
.
The solution is to capture the original fibonacci
in a closure:
import functools
def print_function_call(f):
'''Return a decorator that prints function calls.'''
@functools.wraps(f) # give wrapper the identity of f and more
def wrapper(*args, **kwargs):
nonlocal count, depth
count += 1
depth += 1
call = '{}{}'.format(f.__name__, argument_string(*args, **kwargs))
print('{:>3}:{}{}'.format(count, '|' * depth, call))
value = f(*args, **kwargs) # wrapper calls f
depth -= 1
if depth is -1:
print('Done')
count = 0
return value
count, depth = 0, -1
return wrapper # return the decorated function
print_function_call
takes in f
and returns wrapper
, which captures and decorates f
:
wrapper
expects the same set of arguments forf
,returns the same value returned by
f
on the arguments, butcan execute additional codes before and after calling
f
to print the function call.
By redefining fibonacci
as the returned wrapper
, the original fibonacci
captured by wrapper
calls wrapper
as desired.
def fibonacci(n):
return fibonacci(n - 1) + fibonacci(n - 2) if n > 1 else 1 if n is 1 else 0
fibonacci = print_function_call(
fibonacci) # so original fibonnacci calls wrapper
fibonacci(5)
1:fibonacci(5)
2:|fibonacci(4)
3:||fibonacci(3)
4:|||fibonacci(2)
5:||||fibonacci(1)
6:||||fibonacci(0)
7:|||fibonacci(1)
8:||fibonacci(2)
9:|||fibonacci(1)
10:|||fibonacci(0)
11:|fibonacci(3)
12:||fibonacci(2)
13:|||fibonacci(1)
14:|||fibonacci(0)
15:||fibonacci(1)
Done
5
The redefinition does not change the original fibonacci
captured by wrapper
.
import inspect
for cell in fibonacci.__closure__:
if callable(cell.cell_contents):
print(inspect.getsource(cell.cell_contents))
def fibonacci(n):
return fibonacci(n - 1) + fibonacci(n - 2) if n > 1 else 1 if n is 1 else 0
Python provides the syntatic sugar below to simplify the redefinition.
@print_function_call
def fibonacci(n):
return fibonacci(n - 1) + fibonacci(n - 2) if n > 1 else 1 if n is 1 else 0
fibonacci(5)
1:fibonacci(5)
2:|fibonacci(4)
3:||fibonacci(3)
4:|||fibonacci(2)
5:||||fibonacci(1)
6:||||fibonacci(0)
7:|||fibonacci(1)
8:||fibonacci(2)
9:|||fibonacci(1)
10:|||fibonacci(0)
11:|fibonacci(3)
12:||fibonacci(2)
13:|||fibonacci(1)
14:|||fibonacci(0)
15:||fibonacci(1)
Done
5
There are many techniques used in the above decorator.
Why use a variable number of arguments in wrapper
To decorate any function with possibly different number of arguments.
Why decorate the wrapper with @functools.wraps(f)
?
Ensures some attributes (such as
__name__
) of the wrapper function is the same as those off
.Add useful attributes. E.g.,
__wrapped__
stores the original function so we can undo the decoration.
fibonacci, fibonacci_decorated = fibonacci.__wrapped__, fibonacci # recover
print('original fibonacci:')
print(fibonacci(5))
fibonacci = fibonacci_decorated # decorate
print('decorated fibonacci:')
print(fibonacci(5))
original fibonacci:
5
decorated fibonacci:
1:fibonacci(5)
2:|fibonacci(4)
3:||fibonacci(3)
4:|||fibonacci(2)
5:||||fibonacci(1)
6:||||fibonacci(0)
7:|||fibonacci(1)
8:||fibonacci(2)
9:|||fibonacci(1)
10:|||fibonacci(0)
11:|fibonacci(3)
12:||fibonacci(2)
13:|||fibonacci(1)
14:|||fibonacci(0)
15:||fibonacci(1)
Done
5
How to use decorator to improve recursion?
We can also use a decorator to make recursion more efficient by caching the return values.
cache
is a dictionary where cache[n]
stores the computed value of \(F_n\) to avoid redundant computations.
def caching(f):
'''Return a decorator that caches a function with a single argument.'''
@functools.wraps(f)
def wrapper(n):
if n not in cache:
cache[n] = f(n)
else:
print('read from cache')
return cache[n]
cache = {}
wrapper.clear_cache = lambda : cache.clear() # add method to clear cache
return wrapper
@print_function_call
@caching
def fibonacci(n):
return fibonacci(n - 1) + fibonacci(n - 2) if n > 1 else 1 if n == 1 else 0
fibonacci(5)
fibonacci(5)
fibonacci.clear_cache()
fibonacci(5)
1:fibonacci(5)
2:|fibonacci(4)
3:||fibonacci(3)
4:|||fibonacci(2)
5:||||fibonacci(1)
6:||||fibonacci(0)
7:|||fibonacci(1)
read from cache
8:||fibonacci(2)
read from cache
9:|fibonacci(3)
read from cache
Done
1:fibonacci(5)
read from cache
Done
1:fibonacci(5)
2:|fibonacci(4)
3:||fibonacci(3)
4:|||fibonacci(2)
5:||||fibonacci(1)
6:||||fibonacci(0)
7:|||fibonacci(1)
read from cache
8:||fibonacci(2)
read from cache
9:|fibonacci(3)
read from cache
Done
5
A method clear_cache
is added to the wrapper to clear the cache.
lambda <argument list> : <expression>
is called a lambda expression, which conveniently defines an anonymous function.
type(fibonacci.clear_cache), fibonacci.clear_cache.__name__
(function, '<lambda>')
9.7. Module¶
How to create a module?
To create a module, simply put the code in a python source file <module name>.py
in
the current directory, or
a python site-packages directory in system path.
import sys
print(sys.path)
['/home/lec/ccha23/CS1302ICP/Lecture6', '/opt/anaconda/lib/python37.zip', '/opt/anaconda/lib/python3.7', '/opt/anaconda/lib/python3.7/lib-dynload', '', '/home/lec/ccha23/.local/lib/python3.7/site-packages', '/opt/anaconda/lib/python3.7/site-packages', '/opt/anaconda/lib/python3.7/site-packages/IPython/extensions', '/home/lec/ccha23/.ipython']
For example, to create a module for generating Fibonacci numbers:
%more fibonacci.py
import fibonacci as fib # as statement shortens name
help(fib)
Help on module fibonacci:
NAME
fibonacci - Contain functions for generating fibonacci numbers.
FUNCTIONS
fibonacci(n)
Returns the Fibonacci number of order n.
fibonacci_iteration(n)
Returns the Fibonacci number of order n but without recursion.
FILE
/home/lec/ccha23/CS1302ICP/Lecture6/fibonacci.py
print(fib.fibonacci(5))
print(fib.fibonacci_iteration(5))
5
5