Simply put, functions are a block a code that is packaged up so as to be executed independently, how and when you choose.
Functions have parameters, and you provide arguments when you execute the function. That's generally referred to as calling the function, and the code that calls it is referred to as the caller.
The idea of functions in programming goes all the way back to early assembly languages and the concept of the subroutine. Most assembly languages have a CALL
operation used to transfer control to the subroutine, and a RET
operation to return control back to the calling code. The BASIC language also used the term subroutine and had a GOSUB
statement that was used to call them, and a RETURN
statement.
Most modern languages have dropped the explicit CALL
or GOSUB
statement in favor of implicit syntax to indicate a function call:
function_name(arg1, arg2, ..., argn)
In assembly and BASIC, a subroutine had to end with an explicit RET
or RETURN
. Python loosens that up somewhat. If there is no value that needs to be sent back as the result of the function, the return can be left implicit, as in the following:
def foo(x): print("X is {}".format(x))
>>> foo(5) x is 5
We can use an explicit return, but it gains nothing and takes an additional line of code.
def foo(x): print("X is {}".format(x)) return
There are two cases in Python where we do need to make the return explicit.
- When you need to return from the function before it naturally reaches the end.
- There is a value (or values) that needs to be sent back to the caller.
def foo(x): if x > 5: return print("X is {}".format(x))
>>> foo(4) X is 4 >>> foo(10)
Using an if/return
combination like this is often referred to as a guard clause. The idea being that it gaurds entry into the body of the function much like security at an airport: you have to get past inspection before being allowed in. If you fail any of the checks, you get tossed out immediately.
Preconditions
This is also something called a precondition, although that usually implies a stronger response than simply returning early. Raising an exception, for example. You use a precondition when the "bad" argument value should never be sent into the function, indicating a programming error somewhere. This can be done using assert
instead of the if/return
. This will raise an AssertionError
if the condition results in False
.
def foo(x): assert x <= 5, "x can not be greater than 5" print("X is {}".format(x))
>>> foo(10) Traceback (most recent call last): File "", line 1, in File "", line 2, in foo AssertionError: x can not be greater than 5
Since preconditions are meant to catch programming errors, they should only ever raise an exception while you are working on the code. By the time it's finished, you should have fixed all the problems. Preconditions provide a nice way of helping make sure you have.
Although assert
is supported, it's a little different in the context of CircuitPython. Your running project likely does not have a terminal connected. This means that you have no way to see that an assert triggered, or why. The CircuitPython runtime gets around this for runtime errors by flashing out a code on the onboard NeoPixel/DotStar. This lets you know there's a problem and you can connect to the board and see the error output. But as I said, by the time you get to that point, there should be no programming errors remaining to cause unexpected exceptions.
That's the key difference between things like guards and preconditions: your code should check for, and deal with, problems that legitimately could happen; things that could be expected. There's no way to handle situations that should never happen; they indicate that somewhere there's some incorrect code. All you can do is figure out what's wrong and fix it.
Returning a Result
The second case of requiring a return
statement is when the function has to return a value to the caller. For example:
def the_answer(): return 42
So what if you have a function that is expected to return a value and you want to use a guard? There is no universal answer to this, but there is often some value that can be used to indicate that it wasn't appropriate to execute the function.
For example, if a function returns a natural number (a positive integer, and sometimes zero depending on which definition you use), you could return a -1 to indicate that a guard caused an early return. A better solution is to return some sort of default value, maybe 0. This, also, is very situation dependant.
Multiple Return Values
Not only can functions return a value, they can return more than one. This is done by listing the values after the return keyword, separated by commas.
def double_and_square(x): return x+x, x*x
A function that returns multiple values actually returns a tuple containing those values:
>>> double_and_square(5) (10, 25) >>> type(double_and_square(5)) <class 'tuple'>
You can then use Python's parallel assignment to extract the values from the tuple into separate variables.
>>> d, s = double_and_square(5) >>> d 10 >>> s 25
Defining Functions
As you can gather from the examples above, you use the def
keyword to define a function. The general form is:
def function_name (parameter_name_1, ..., parameter_name_n): statement ... statement
You can then call the function by using the name and providing arguments:
function_name(argument_1, ..., argument_n)
This much we can see in the examples. Note that a function doesn't require parameters and arguments If it does have some, those names are available inside the function, but not beyond it. We say that the scope of the parameters is the body of the function. The scope of a name is the part of the program where it is usable.
If you use a name outside of its scope, CircuitPython will raise a NameError
.
>>> foo Traceback (most recent call last): File "", line 1, in NameError: name 'foo' is not defined
Choosing good names for your functions is very important. They should concisely communicate to someone reading your code exactly what the function does. There's an old programmer joke that the person trying to read and understand your code will quite likely be you in a couple months. This becomes even more crucial if you share your code and others will be reading it.
Default Arguments
When a function is called, it must be provided with an argument for each parameter. This is generally done as part of the function call, but Python provides a way to specify default arguments: follow the parameter name by an equals and the value to be used if the function call doesn't provide one.
def foo(x, msg=''): print("X is {}. {}".format(x, msg))
>>> foo(5, 'hi') X is 5. hi >>> foo(5) X is 5.
We can see that if we provide an argument for the msg
parameter, that is the value that will be used. If we don't provide an argument for it, the default value specified when the function was defined will be used.
Keyword Arguments
So far the examples have been using what's called positional arguments. That just means that arguments are matched to parameters by their positions: the first argument is used as the value of the first parameter, the second argument is used as the value of the second parameter, and so on. This is the standard and is used by pretty much every language that uses this style of function definition/call.
Python provides something else: keword arguments (sometimes called named arguments). These let you associate an argument with a specific parameter, regardless of it's position in the argument list. You name an argument by prefixing the parameter name and an equals sign. Using the previous function foo
:
>>> foo(5, msg="hi") X is 5. hi >>> foo(msg='hi', x=5) X is 5. hi
Notice that by naming arguments their order can be changed. This can be useful to draw attention to arguments that are usually later in the list. There's one limitation: any (and all) positional arguments have to be before any keyword arguments:
>>> foo(msg='hi', 5) File "", line 1 SyntaxError: positional argument follows keyword argument
Even without changing the order of arguments, keywords arguments lets us skip arguments that have default values and only provides the ones that are meaningful for the call. Finally, keyword arguments put labels on the arguments, and if the parameters are well named it's like attaching documentation to the arguments. Trey Hunner has a great write-up on the topic. To pull an example from there, consider this call:
GzipFile(None, 'wt', 9, output_file)
What's all this? It's dealing with a file so 'wt'
is probably the mode (write and truncate). The output_file
argument is clearly the file to write to, assuming it's been well named. Even then, it could be a string containing the name of the output file. None
and 9
, however, are pretty vague. Much clearer is a version using keyword arguments:
GzipFile(fileobj=output_file, mode='wt', compresslevel=9)
Here it's clear that output_file
is the file object, and not the name. 'wt'
is, indeed, the mode. And 9
is the compression level.
This is also a prime example of the problems using numeric constant. What is compression level 9? Is it 9/9, 9/10, or 9/100? Making things like this named constants removes a lot of ambiguity and misdirection.
Text editor powered by tinymce.