Functions & Namespaces

Much earlier in this course I taught you guys how to call functions, a little bit later on I taught you a bit about indentation. In this lecture I'll build on that knowledge and show you how to build your own functions. The Syntax:

def {function_name} ({argument1 (if any)}, {argument2 (if any)}, {...} ):
    {code block}
    return {something}

Take care to note that just like for-loops all code within the scope of the function is intended. The 'return' keyword acts as an 'exit point' for a function. As a minor aside, its not necessary to have one, but most functions will return something.

Alright, lets focus on talking about the very first line of the function first. We can create function with no arguments, one, two, or several arguments. Any number of which can be optional arguments. For example...


In [ ]:
def zero_args():
    # code goes here
    pass

def one_arg(a):
    # code goes here
    pass

def two_args(a, b):
    # code goes here
    pass

def optional_arg(a, b=0):     # <--- please note, optional arguments are listed LAST
    # code goes here
    pass

def two_options(a=True, b=False):
    # code goes here
    pass

# To refreash your memory, calling these functions look something like:
zero_args()
one_arg(10)
two_args(10, 20)

optional_arg(10)
optional_arg(10, b=20)

two_options()
two_options(a=True)

Simple stuff right? Okay lets move on and look at the body and return statement. I’m going to create a function that calculates the hypotenuse of a triangle when given sides 'a' and 'b' (i.e. the Pythagorean Theorem).


In [3]:
from math import sqrt # <-- importing the square root function from the math module.

def pythagoras (a, b):
    c2 = a** 2 + b ** 2
    return sqrt(c2) # note, this line would be math.sqrt(c2) if we had written 'import math' instead of 'from math...' 

# Lets call it!
round(pythagoras(10, 23), 3)


Out[3]:
6.381

So our pythagoras function takes the arguments 'a' and 'b'. The second line calculates 'c2' and once we have that we return the square root of 'c2'.

Okay, lets look at a more complex bit of code, lets try to calcuate whether n is prime or not.


In [1]:
def is_prime(num):
    """Returns True if number is prime, False otherwise"""
    if num <= 1: 
        return False    # numbers <= 1 are not prime
    
    # check for factors
    for i in range(2,num): # for loop that iterates 2-to-num. Each number in the iteration is called "i"
        if (num % i) == 0: # modular arithmetic; this asks if num is divisible by i (with no remainder).
            return False
    return True

To give you a quick run down this code works out if a number, 'n', is prime by seeing if there is a 'q' that is a divisor of n. where: 2 < q < n.

Lets walk through a example, is 10 prime?

  1. Our first check is to see if the number is greater than 1. In our case it is, but if it wasn’t we would return False and that would be all she wrote.
  2. Next up we ask if 2 is a divisor of 10. It is, so we return False, 10 is not prime.

Okay lets try again, this time with 5 as input.

  1. Is 5 > 1? Continue...
  2. is 2 divisor of 5? No. We Continue...
  3. is 3 divisor of 5? No. We Continue...
  4. is 4 divisor of 5? No. We Continue...
  5. End of the range function, we therefore return True, 5 is prime.

Namespaces and Scope

The last thing I want to talk about today is something called variable scope, or namespaces. Basically, everything within a function is, in some sense, 'separate' from the rest of your code. let me show you.


In [1]:
a = 10
def change_a():
    a = 20

change_a()
print(a)


10

We print A, and number we get is 10. Why isn't it 20? We called the 'change a' function and the function quite clearly assigned A to 20. what gives?

Well, the answer is basically that 'A' is actually two separate objects, one is defined in the 'main program' whereas the other 'A' only exists within the context ('scope') of the function.

I can prove that to you by calling "id" on both objects. the id function returns a unique number for an object. Its literally and id number.


In [4]:
a = 10
print(id(a))

def change_a():
    a = 20
    print(id(a))

change_a()


10914656
10914976

Basically, the "A" inside the function is separate from the "A" outside of it. Here's another example:


In [7]:
def variables():
    x = 10
    y = 10

print(x + y) # <--- note the error message; NameError


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-7-b7879293296b> in <module>()
      3     y = 10
      4 
----> 5 print(x + y) # <--- note the error message; we get a NameError

NameError: name 'x' is not defined

So Python is telling us X is not defined and that’s not a bug, X and Y are defined within the scope of the function, but not defined within the main program (e.g the place where we are calling it). There are a few fixes; we could move the print statement into the function by indenting it. Or, we could 'save' the function variables for use in the main program. For example:


In [2]:
def variables():
    x = 10
    y = 10
    print(x + y)
    return x, y

a, b = variables()  # this line maps x,y to a,b . 
print(a + b)


20
20

Why does Python do this?

The main advantage to variable scope and namespaces is that as your code becomes progressively larger you don't have to keep on coming up with new names for objects. It is easy to imagine how constantly coming up with new names would, after a while adversely affect readability and make coding a lot slower (because with so many names you are going to continually forget which one you need to use).

The other advantage is that if functions cannot change the content of another function (without your express blessing) then the chance for ugly bugs and/or nasty side-effects to occur is reduced.

Finally, I'd like to point out that its not just functions that their own scope. In the code snippet below I have defined three list-comprehensions (not covered in this course) and printed them out. What I want you to focus on is how I used and reused the letter 'i' as a variable name several times, and that is okay because 'i' is defined in context ('scope') of each individual list comprehension. These means that all these 'i's' are completely separate from each other, and moreover, 'i' is not defined in the 'main program', which explains the NameError.


In [15]:
a = [i for i in range(10)]
b = [i for i in range(20)]
c = [i for i in "abcde"]

print(a, b, c, sep="\n")
print(i)


[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
['a', 'b', 'c', 'd', 'e']
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-15-da2acc4759a1> in <module>()
      5 print(a, b, c, sep="\n")
      6 
----> 7 print(i)

NameError: name 'i' is not defined

In short, self-contained blocks of code (e.g. functions) have their own 'namespace', and that means if a variable is defined inside that block then that variable is said to be defined locally.

Homework Assignment

Your homework assignment this week is to write a function called 'average'. it takes a list as input and returns the arithmetic average. You should assume the list only contains numbers.

It should have a docstring explaining what it does (hint: """text""").

Hints:

  • len(input) will give you the length of the list.
  • sum(input) will add everything up.
  • If the list is empty, return the string "EMPTY LIST"

FOR BONUS POINTS

  • You are ONLY allowed to use addition and for-loops (The use of LEN and SUM is not allowed!).

The bonus problem is a bit tricky but I believe in you guys. Good luck!


In [ ]:
# YOUR CODE HERE!