More on functions

Published

2023-08-02

A deeper dive into functions

Recall that we define a function using the Python keyword def. So, for example, if we wanted to implement the following mathematical function in Python:

f(x) = x^2 - 1

Our function would need to take a single argument and return the result of the calculation.

def f(x):
    return x ** 2 - 1

We refer to f as the name or identifier of the function. What follows the identifier, within parentheses, are the formal parameters of the function. A function may have zero or more parameters. The function above has one parameter x.1

Let’s take a look at a complete Python program in which this function is defined and then called twice: once with the argument 12 and once with the argument 5.

Remember: Writing a function definition does not execute the function. A function is executed only when it is called.

Calling a function

Once we have written our function, we may call or invoke the function by name, supplying the necessary arguments. To call the function f() above, we must supply one argument.

y = f(12)

This calls the function f(), with the argument 12 and assigns the result to a variable named y. Now what happens?

When we call a function with an argument, the argument is passed to the function. The formal parameter receives the argument—that is, the argument is assigned to the formal parameter. So when we pass the argument 12 to the function f(), then the first thing that happens is that the formal parameter x is assigned the argument. It’s almost as if we performed the assignment x = 12 as the first line within the body of the function.

Once the formal parameter has been assigned the value of the argument, the function does its work, executing the body of the function.

Then, the function returns the result. Flow of control is returned to the point at which the function was called.

For this example, we print the result, 143.

Let’s call the function again, this time with a different argument, 5.

What to pass to a function?

A function call must match the signature of the function. The signature of a function is its identifier and formal parameters. When we call a function, the number of arguments must agree with the number of formal parameters.2

A function should receive as arguments all of the information it needs to do its work. A function should not depend on variables that exist only in the outer scope. (In most cases, it’s OK for a function to depend on a constant defined in the outer scope.)

Here’s an example of how things can go wrong if we write a function which depends on some variable that exists in the outer scope.

y = 2

def square(x):
    return x ** y
    
print(square(3))  # prints "9"

y = 3

print(square(3))  # oops! prints "27"

This is a great way to introduce bugs and cause headaches. Better that the function should use a value passed in as an argument, or a value assigned within the body of the function (a local variable), or a literal. For example, this is OK:

def square(x):
    y = 2
    return x ** y

and this is even better:

def square(x):
    return x ** 2

Now, whenever we supply some particular argument, we’ll always get the same, correct return value.

Functions should be “black boxes”

In most cases functions should operate like black boxes which take some input (or inputs) and return some output.

We should write functions in such a way that, once written, we don’t need to keep track of what’s going on within the function in order to use it correctly. A cardinal rule of programming: functions should hide implementation details from the outside world. In fact, information hiding is considered a fundamental principle of software design.3

For example, let’s say I gave you a function which calculates the square root of any real number greater than zero. Should you be required to understand the internal workings of this function in order to use it correctly? Of course not! Imagine if you had to initialize variables used internally by this function in order for it to work correctly! That would make our job as programmers much more complicated and error-prone.

Instead, we write functions that take care of their implementation details internally, without having to rely on code or the existence of variables outside the function body.

Original author: Clayton Cafiero < [given name] DOT [surname] AT uvm DOT edu >

No generative AI was used in producing this material. This was written the old-fashioned way.

This material is for free use under either the GNU Free Documentation License or the Creative Commons Attribution-ShareAlike 3.0 United States License (take your pick).

Footnotes

  1. While Python does support optional parameters, we won’t present the syntax for this here.↩︎

  2. Again, we’re excluding from consideration functions with optional arguments or keyword arguments.↩︎

  3. If you’re curious, check out David Parnas’ seminal 1972 article: “On the Criteria To Be Used in Decomposing Systems into Modules”, Communications of the ACM, 15(12) (https://dl.acm.org/doi/pdf/10.1145/361598.361623).↩︎