More on Functions

See also

Text: Chapter 3 (section 3.5-3.6)

Read time: 44 minutes (11179 words)

We introduced the basic concept of a function earlier. Now we want to look a bit deeper at what goes on inside a function and talk about another important topic: Scope (no, not the mouthwash!)

As we talked about before, a function is just a container holding a bit of code and local variables it can use to do its work. Most functions have parameters which are just more variables, but these variable are special, they are give values by the caller of the function. The idea is simple. The function is designed to work on whatever parameter values the caller wants to hand to it at any point in their processing. Think about that square root function we used earlier. That function would not be very useful if all it could do is figure out the square root of five. If we generalize it so it can work on any number, we can make that number a parameter and decide what number we want the square root of whenever we like.

Callers and Callees

Notice that I used the term caller in the discussion above. Formally, we say that our program calls a function to make it do work. The callee is the function we call. (Remember our idea about kicking the box to wake up the troll). The call just refers to the name of the function. The use of the function name causes Python to stop what it is doing at the moment, and activate the function. It waits until the function is done, then it presses on with what it was doing previously. For example, suppose we had a magic function we set up in our program that looks like this:

def magic(number):
    ...
    return magic_number

Somewhere inside this magic function, we expect the function to take the parameter known as number and produce a new variable (known only inside the function) called magic_number. That final number is returned to the caller.

The caller of this function can use this function wherever they need to. Since it returns a value, the actual call to the function can happen in the middle of an expression, in an assignment statement, or even in a call to another function.

x = magic(5)
x = magic(5) + magic(7)
x = sqrt(magic(5))
sqrt(6.0)

The first two examples should be easy to figure out. But the last two are a bit odd. In the third one, the caller wants the square root of a magic number. We could have done this in two steps:

x1 = magic(5)
x = sqrt(x1)

That is correct, but it makes us invent a temporary variable (one we really only need for a short time) to hold the intermediate value. By placing the call to magic inside the call to sqrt, we avoid needing another variable. The code works like you would expect.

We process the assignment statement, but notice that there is a call to sqrt on the right side. The parameter must be available to activate this function. Unfortunately, the parameter is another call, this one to magic. The call to magic also needs a parameter, and that parameter is in the code as a literal number. So, we call magic with its parameter and wait for it to return a magic number. Once we have that, we can hand it to the sqrt function and wait for it to give us back an answer. That final answer is placed in the container named x. Phew!

Back to the final example above. What if we set up the function to return a value, but we used it in a way that does not seem to want the value. Python actvates the function, fetches the result, and throws it away. In this case, nothing really happens, except a bit of time was used up while the troll did his/her/it’s thing!

How parameters work

When the caller of a function hands the function a value in the parameter list, that value is passed into the function. The function sees it in a container named whatever the def line had on it. The names on the def line are only really known inside the function. That is fine, since the caller has no real need to know what is going on inside the box! This is a key idea in programming. We separate the two worlds: caller’s world, and callee’s world. Neither needs to know what is going on in the other world to do their jobs.

How about multiple parameters

Many functions need more than one parameter to do their job. We simple list the individual parameter names inside the parentheses, separating each with a comma. When we call this kind of function, we list the parameter values, again separating them with commas:

def silly_multiply(a,b):
    return a * b

...

x = silly_multiply(4,5)

Keyword parameters

Python allows calling a function in a kind of odd way:

def loan_rate(principle, interest, term):
    ...

Here we defined names for the parameter in a normal way. We can call this function in different ways:

payment = loan_calcualtor(16000.00, 2.5, 60)
payment = load_calculator(term=60, interest = 2.5, principle=16000.00)

In that last example, Python figures out what is what by the names you provide on the call. Notice that the order of the parameters is not important when we do this.

Scope

In programming, scope refers to where physically within the code of your program certain names can be used. Each language has its own rules for setting this up, but most are similar enough that we can explain how it works in a way that will be useful in several languages (including C++, which you will take later).

File scope

At the highest level of programming, you write code and save it in a file. The tool that processes your code has no idea what other files might be part of a larger program, so any names you use in the file must be completely defined inside the file. This is called file scope.

In languages like C++, which is called a stongly typed language, meaning the compiler must know exactly what kind of thing the name refers to, using a name that has not been defined in the file will generate an error. Python is not so picky. If you refer to a name you have never mentioned before in your code, Python will try to create a new container with that name. If the name is being used as though it was a function, Python will complain, since it needs to know where the function can be found before you can use it.

What this means is that Python may create brand new variable containers whenever you invent a new container name, but you cannot create a new function name unless that function is defined in the current file.

The import statement

In our early examples, we used an import statement to introduce a function name to Python, so we could call that function when we wanted:

from math import sqrt

This line tells Python that it should look in another file (named math) and find a name called sqrt inside of that file. If that lookup succeeds, we can use that function later in our code:

x = sqrt(5.0)

Let’s see Python getting annoyed. Without including the import line, try using the sqrt function:

>>> x = sqrt(5.0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'sqrt' is not defined

Python had no idea what that name was all about. You have something to fix!

Finding files to import

By default, Python knows to look for files in a set of directories on your system. Most of these directories are set up when you install Python. To see the full list of places where it will look, do this:

>>> import sys
>>> print(sys.path)
['', 'C:\\Windows\\system32\\python32.zip', 'C:\\Python32\\DLLs', 'C:\\Python32\
\lib', 'C:\\Python32', 'C:\\Python32\\lib\\site-packages']

Note

The double back-slash stuff is Python dealing with Windows! Windows decided to use the back-slash to separate folder names in a path to a location on the drive. Everyone else uses forward slashes. Ask Bill why!

All of the directories in this list are on the drive where Python was installed. You can add to this list if you create your own useful files, something we will look at later.

Finding out what names are available when importing

Once you import something, you can list the names available using this:

>>> import math
>>> dir(math)
['__doc__', '__name__', '__package__', 'acos', 'acosh', 'asin', 'asinh', 'atan',
 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'e', 'erf', 'er
fc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gam
ma', 'hypot', 'isfinite', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', '
log1p', 'modf', 'pi', 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 't
runc']

It appears like we have a bunch of useful “math” finctions available in that file.

Note

We shall see later that we can import either a file, or a specially prepared folder contining a number of files. This later setup Python calls a package.

We can ask what kind of things these available names are by doing this:

>>> type(math.sin)
<class 'builtin_function_or_method'>
>>>

There is that funky notation again. The name math refers to a file (or directory) containing a bunch of names we can use. The math.sin notation tells Python which of those names we are interested in. If we use another form of import, we get simpler notation:

>>> from math import sin
>>> type(sin)
<class 'builtin_function_or_method'>
>>>

Global variables

If you define variables outside of any function, those variables are called global. We can use them whenever we like in our code. These variables are not available to code in other files unless we tell Python we want to do this explicitly.

Global constants

What is a constant anyway? Well variables can vary and constants cannot. Many programming languages do not really let you create containers with values that do not vary while a program runs. Python is one of those. (C++ does let you set up such beasts). A common tactic in Python to set up something you should not modify is to yell at yourself. Name the container in all capital letters. (In email, all caps is how you yell!) The capital letters are to remind you this is a special container you should not mess with!

Here is a common example:

PI = 3.1415926

The capital letters warn you that you should not modify this.

Note

Did you know that a king once got mad at PI, declaring it as an obnoxious number. So he ordered it to be given a value of 3! It really messed up the world of math!

Local variables

If you are writing the code inside of a function, any variables you create are called local variables. The parameters are also local variables, except they are given initial values by the caller. Those local variables are only visible to the code inside the function. The outside world cannot use those local names for their own purposes (except in that one special way where we can refer to the names as keywords).

This is as it should be. As you write a good function, one you hope will be used in many programs, you do not want to worry that the names you are using might be in use in another part of the program.

Testing scope

Here is an example demonstrating how this works:

>>> global_variable = 'test'
>>> def func1():
...     local_variable = 'batfeathers'
...     print(global_variable)
...     print(local_variable)
...
>>> func1()
test
batfeathers
>>> print(global_variable)
test
>>> print(local_variable)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'local_variable' is not defined

The local_variable is not known at this point since it is not visible to the outer code.

Here is an example of a setup that hides a global variable:

>>> global_variable = 'test'
>>> def func2():
...     global_variable = 'junk'
...     print(global_variable)
...
func2()
junk

The function used the local variable (wrongly named global_variable. That effectively hides the real global variable!

Suppose we really wanted to access the global variable and give it a new value:

>>> global_variable = 'test'
>>> def func2():
...     global global_variable
...     global_variable = 'junk'
...     print(global_variable)
...
func2()
junk
print(global_variable)
junk

This time, we told Python that when we use the name global_variable inside the function, we really mean for it to use the original, global, name.

Explaining scope another way

Long ago and far away, I used a simple analogy to explain how all this scope stuff works. For Python, let’s lay it out this way:

Pretend that your file is laid out top to bottom and surrounded by a one way glass mirrored wall. The mirror is set up so that no one outside the file can see in. Now, surround each function inside your file by other one-way mirrored walls, set up so you can see out from inside the function, but not in from outside the function. Paint the name of the function on the outside of the wall.

With this setup, scope rules are simple. From any point in your code, inside or outside of a function, you can use any name you see by looking toward the top of your code. (You cannot look below where you are at the moment.) By the way, you can see around all the functions to code above them. You can see the names of the functions above you, but cannot see inside those functions.

From inside a function, you can see all names defined inside that function (local variables) and can see names defined outside the function above the function definition. If a name is defined in two places, you use the first one you find as you scan your code upward. That means if you use a name that was created inside the function, and that same name exists outside the function, you will use the local version, unless you force things as shown earlier.

Phew.

After a bit of writing code, you get into the pattern, and do not think about this much. Python can byte you, though, since it creates new variables whenever you assign a value to a new name.