Notebook 6 - Functions, Namespaces, and Modules¶
You can make a copy of this notebook by selecting File->Save a copy in Drive from the menu bar above.
Things you'll learn in this lesson:
- what functions are and why they useful
- how to define and use functions
- the global and local namespaces
- what modules are and how to use them
- Integrated Developer Environments (IDEs) for Python
Functions¶
Functions are flexible software building blocks¶
- So far, we’ve been writing small programs.
- Things get much more complicated when we write large programs, especially with multiple authors.
- Ideally, we'd like to build software like snapping lego pieces together.
- What would that buy us?
- abstraction
- reuse
- modularity
- maintainability
Abstraction - You don't need to do everything¶
- When building a house, you don't do everything yourself.
- You hire an architect, a carpenter, an electrician, a roofer, a plumber, a mason.
- You might hire a contractor to hire and manage all those people.
- In our programs we delegate tasks to certain functions, like
print()
, so that we don't have to worry about all the details.- It's a bit like hiring an electrician so that we don't have to worry about the details of electrical wiring in our house.
Reuse - Don't Reinvent the Wheel¶
- it's ok to reuse other people's work
- it's not stealing
- it makes you more efficient and more productive
- Very few people build a house from scratch
- so don't try to build programs from scratch
Reuse Example¶
You can count the number of words in a string the hard way...
mystr = 'This is a test.'
cnt = 0
for i in mystr:
if i.isspace():
cnt += 1
cnt += 1
print('number of words =', cnt)
Or the easy way, by calling a string method...
mystr = 'This is a test.'
cnt = len(mystr.split())
print('number of words =', cnt)
Which would you rather use? Which is more reliable?
- The first approach is great for learning.
- The second approach is great for getting real work done.
Modularity - Divide and Conquer¶
- So far, our programs have been monoliths - one continous sequence of Python statements.
- Real programs are often much bigger than the ones we've written.
- Google's software repository has billions of lines of source code (Why Google Stores Billions of Lines of Code in a Single Repository)
- No one person can write a program that big.
- Large programs are built by teams.
- In order to build large, complex programs, we need the ability to divide program logic into manageable pieces.
- We call this modularity - dividing software into pieces or modules.
Maintainability - Keeping your code DRY (Don't Repeat Yourself)¶
- Imagine that you need to do roughly the same thing in ten different places so you copy the code to those ten locations.
- What happens when you find a bug or want to improve that piece of code?
- You need to make the change ten times.
- Will you remember to do that?
- If you do remember, will you catch all ten locations?
- Copying code is a bad thing - it leads to bugs.
Functions solve all of these Problems¶
- Functions give us the ability to:
- Hide low level details (abstraction)
- Share and reuse pieces of functionality (reuse)
- Split programs into manageable pieces (modularity)
- Write one copy of an algorithm and use it anywhere (maintainability)
- We've already used several functions
print()
,input()
,int()
,len()
,range()
, etc.
- Now let's see how to define our own functions.
Defining Functions¶
- example:
def function_name(arg1, arg2):
'''This is a docstring.''' # optional but a good idea
statement1
statement2
...
- Not surprisingly, we define the scope of the function body using indentation (just like how we define blocks for if statements, for loops, etc.).
- This is a bit like an assignment statement in that it assigns a block of code (the function body) to the function name.
- Function names have the same rules as variable names.
- This only defines a function - it doesn't execute it.
def speak():
'''
This function generates hello in cat language.
Don't try this near a dog.
'''
print('meow')
Notice how nothing seemed to happen in that last cell. But something did - we defined a function. However, that function doesn't get executed unless and until we call it. Let's do that now.
speak()
Docstrings¶
- a string defined immediately after the
def
line - usually triple quoted since it may be multi-line
- not required but a good way to document your functions
- IDEs use the docstring to make your life easier
- automates output of
help(function)
Example Function¶
# The factorial of N is defined as 1*2*...*N.
# Here's an example function definition which prints the factorial of 10.
def fact10():
'''
Print the factorial of 10.
Factorial of 10 is defined as 1*2*...*10.
'''
limit = 10
result = 1
for i in range(1, limit+1):
result *= i # result = result * i
print(result)
# Get help about this function...
help(fact10)
# And here's how we would call this function...
fact10()
Passing Values to a Function¶
fact10()
is limited.- It only prints the factorial for one value (10).
- It would be nice if we could define a more flexible version, that could print the factorial of any number, like this...
def fact(num):
'''Print the factorial of any number.'''
result = 1
for i in range(1, num+1):
result *= i
print(result)
fact(20)
Parameters and Arguments¶
- The variables we define in a function to take on the values passed by the caller are called parameters.
- In this code,
a
,b
andc
are parameters:
def sum(a, b, c):
return a + b + c
- The values supplied by the caller when calling a function are called arguments.
- In this code,
1
,2
, and3
are arguments:
sum(1, 2, 3)
Passing Arguments¶
- Functions can define any number of parameters, including zero.
- Multiple parameters are separated by commas, like this...
def product(a, b, c):
return a * b * c
- If you pass the wrong number of arguments, you'll hear about it:
product(1, 2)
...
TypeError: product() takes exactly 3 positional argument (2 given)
...
Return Values¶
Instead of printing the result, we can also have the function return a result to the caller so that the caller can print it or use it in a calculation.
def fact(num):
''' compute factorial of any number '''
fact = 1
for i in range(1, num+1):
fact *= i
return fact
Here's how we would call this function:
f = fact(5)
print('5! = ', f)
- Functions return a value to the caller via the
return
statement. - The
return
statement causes two things to happen...- the function ends and program execution resumes just after the place where the function was called
- the returned value is passed back to the caller
- You can have as many return statements as you like (including zero).
- If the caller wants to do something with a returned value, it needs to save it or use it in an expression...
resp = input('Yes or No (y/n)? ')
- If the caller ignores the return value, it's lost...
input('Yes or No (y/n)? ') # this loses the answer
Returning Multiple Values¶
- Functions can return multiple values.
- Simply specify more than one comma separated value in the
return
statement. - This function returns two values...
def divide(dividend, divisor):
quotient = int(dividend / divisor)
remainder = dividend % divisor
return quotient, remainder
- You could receive the results like this:
(q, r) = divide(100, 9)
print(f'quotient={q}, remainder={r}')
def divide(dividend, divisor):
quotient = int(dividend / divisor)
remainder = dividend % divisor
return quotient, remainder
(q, r) = divide(100, 9)
print(f“quotient={q}, remainder={r}")
Challenge¶
Can you think of a function we've used which...
- takes no arguments?
- takes only one argument?
- takes a variable number of arguments?
- returns no return values?
- returns one return value?
- returns a sequence of values?
#@title Double click here to reveal answers...
# takes no arguments?
print()
# takes only one argument?
input(‘Enter your name: ‘)
# takes a variable number of arguments?
print(‘hello’)
print(‘hello’, ‘world’)
# returns no return values?
print()
# returns one return value?
len(‘test’)
# returns a sequence of values?
range(5)
Namespaces¶
- A
namespace
is an abstract collection of variables that exist in a particular context.- Kind of like a company's ID numbers.
- My id is only valid within the scope of my company.
- We've seen an example of a namespace in this code...
import random
x = random.randrange(10)
- In this case, the random module has it's own namespace, which is completely separate from the rest of your program and is accessed via "dot" notation.
- If you neglect the "random." prefix, you're referring to a different namespace: your program's global namespace.
Global Namespace¶
- Every Python source file has its own global name space.
- The global namespace includes all names (variables and functions) defined outside of any functions, in the Python source file.
- In the following code,
count
,mystr
andreq
are all global variables, i.e. they all reside in the global namespace.
count = 0
while True:
mystr = input('? ')
if (mystr == '1'):
req = 'add'
else:
break
count += 1
Local Namespace¶
- When you define a function, Python creates a local namespace for that function, encompassing all parameters and variables created inside the function body.
- Variables in a local namespace are separate from, and independent of, variables in the global name space.
- Inside a function, local variables supersede global variables with the same name.
- Local variables are transient - they exist only during the lifetime of function execution.
Example illustrating local variable superseding a global variable¶
var = 1
def myfunc():
var = 2 # local scope
print('local var:', var)
myfunc()
print('global var:', var)
Example illustrating the transient nature of the local namespace¶
def func():
var2 = 2 # local scope
print('local var:', var2)
func()
print('global var:', var2)
Modules¶
- A module is a file of reusable Python code.
- The module's name is the file without the .py extension.
- We've already seen how modules are imported...
import random
- In order for Python to see one your modules, the module file (with .py extension) needs to be in the same folder as the program you are running or in a special, predefined system location.
sys.path
in PythonPYTHONPATH
in shell environment
Module Namespace¶
- Every module has its own namespace, which is independent of the main source file's global namespace
- You can access objects in a module's namespace using this general syntax:
module-name.variable
module-name.function(<args>)
- For example, as we've already seen...
rand_val = random.randrange(0, 10)
import random
print(random.random())
print(random.randint(1, 10))
The from
Statement¶
- You can also import code using this syntax...
from module-name import *
- This says loads all the names (*) from the designated module into the global namespace.
- With this kind of import, the module names get loaded into the global namespace, which means you don't need to qualify your accesses with the
module-name.
prefix. - For example, you could do this...
from random import *
rand_val = randrange(0, 10)
# I didn't need to use random.randrange(0, 10)
- You can also import selected names from a module
from random import randrange, randint
- This says load only those names explicitly listed (
randrange
andrandint
, in this case) from the designated module into the global namespace. - As in the previous example, after this import the names are loaded into the common global namespace so there is no need to qualify them...
from random import randrange, randint
rand_val = randrange(0, 10)
from random import random, randint
print(random())
print(randint(1, 10))
When to use import
vs. from
¶
- Generally, it's better to use
import
because...- less risk of name clashes and other surprises
- makes your code more explicit and clear
- Occasionally, you may find that you use a module’s functions so frequently that it pays to import it directly into the global namespace with
from
. - That’s fine but do so carefully and watch out for name conflicts.
Integrated Developer Environments (IDEs)¶
- Integrated Development Environments
- Programs to help you write programs
- Some recommended ones to check out...
- Microsoft Visual Studio Code - the dominant IDE nowadays
- IDX- cloud hosted VS Code from Google
- repl.it - cloud based multi-lingual IDE, great for learning
Homework¶
Question 1¶
Write a function called total
that returns the sum of a range of integers starting with the first passed value and ending at one less than the second passed value.
For example, total(4, 7)
returns 15
(4
+5
+6
).
def total(a, b):
# Add your code here
# Run this cell to test your code...
test_data = (
(4, 7),
(10, 12),
(1, 1),
(0, 0),
(0, 1),
(1, 0),
(-5, 2),
(-2, 2),
)
for i, (a, b) in enumerate(test_data):
computed = total(a, b)
actual = sum([i for i in range(a, b)])
assert computed == actual, f‘Test {i} failed: {computed} != {actual}’
print(‘All tests passed!’)
Question 2¶
Write a function called reverse
that returns a reversed copy of a passed string. For example, reverse('Obama')
returns amabO
.
# Add your code here
# Run this cell to test your code...
import random
import string
def reverse_test(a, b):
rev = reverse(a)
assert rev == b, f’{rev} != {b}’
# Some basic tests.
reverse_test(‘Obama’, ‘amabO’)
reverse_test(‘Marc’, ‘craM’)
reverse_test(’’, ’’)
reverse_test(‘aaa’, ‘aaa’)
reverse_test(‘A a’, ‘a A’)
# Test a very long string.
random_str = ’’
reverse_random_str = ’’
for i in range(1000):
ch = random.choice(string.ascii_lowercase)
random_str += ch
reverse_random_str = ch + reverse_random_str
reverse_test(random_str, reverse_random_str)
print(‘tests passed’)
Question 3¶
Import only the random()
function from the random
module. Write a function called rando
that returns, on average, True
half the time and False
half the time.
from random import random
def rando():
# Add your code here
return random() >= 0.5
# Run this cell to test your code...
import seaborn as sns
from random import randint
num_trials = 10000
width = 97
num_steps = 50
li = []
for i in range(num_trials):
position = width // 2
for j in range(num_steps):
if rando():
position += .5
else:
position -= .5
li.append(position)
#print(li)
sns.displot(li)
Question 4¶
Try some exercises on codingbat.com/python.