2.2 Defining and Using Functions¶
This note explains why we use functions, as well as how to create a function.
Built-In Functions¶
In our examples, we've used two functions that we can import into Python, such as add and mul. There are also built-in functions that don't require any import, such as print. Let's see what we can learn from these functions before we try to implement our own.
A function, just like in math, takes a couple of variables, and spits out the result. If you've taken multivariable calculus, you know that functions have take in more than one variable, and also spit out more than one result. It is just like this with Python: I can put three parameters in to a function foo and it can spit out more than one result.
add and mul are two functions that can accept any arbitrary amount of parameters, but will always result in one value to be returned. Things like add(1, 2, 3, 4, 5, 6, 7, 8, 9) takes in 9 values, but will alway return one integer value, namely 45.
This brings me to emphasize the following point:
In our example above, the add takes in ints and it returns an int. Although this point seems very trivial, it is super, super important in analyzing more complicated functions that take in different types.
Return Types¶
Functions may return values, in the case of our add and mul functions, it returns an integer value. Other functions might not return anything at all. If that is the case, we say it returns None. None is another type, just like ints and strs. Specifically the word None is a NoneType. In Python, None is really nothing. You can try and input None into a Python session, and nothing will be spit out. It will just ask you for the next line to read.
>>> None
>>>
Python print¶
print is an odd function you encounter. Just type in
>>> print(3)
3
and you might assume print takes in a type and spits out what was passed into it. This may seem like the case, and all the following examples seem to support this assumption:
>>> print("Hello World")
Hello World
>>> print(3 + 3, 4)
6 4
>>> print(None)
None
One quirk is that printing None actually will spit out None in the terminal (like the physical word None), while just typing in None without any print over it spits out literally nothing. Also, note how printing strings removes the quotes that delimit it. Finally, print can take in as many parameters as needed like add and mul. It will separate the parameters with a space.
But we never really know what a value returns unless we try variable assignment; take a guess at what happens with the code segment
>>> x = print(3)
3
>>> print(x)
What is the result of print(x)?
If you try this into a Python session, you'll be surprised that the line print(x) prints out None, just like the example above. Weird, but let's analyze this situation based on what we know so far.
With any complicated segment of code, we should go through it line by line. Let's evaluate the right side first. Somehow it assigns the value None to the value x, since that is what appears in the next input line. Then why does it display 3 in the following line?
Another clue is that variable assignment should never output something, unless something is up with the print function. I'll just straight up tell you that print does two things.
- Displays the value passed in to the user
- Returns
None
So then let's now analyze this line-by-line.
Line 1: Evaluate the right hand side first. print(3) will display 3 in the terminal, then return None. Therefore, x is bound to the value None. We see 3 displayed in the terminal.
Line 2: Lookup the vlaue of x. It is None, so we are really evaluating print(None), which we have seen before. It prints None.
print Nesting¶
Let's go one step further. Figure out what is outputted with the following code segment:
>>> print(print(1), print(print(2, 3)))
You type it into a Python session, and you get the following result:
>>> print(print(1), print(print(2, 3)))
1
2 3
None
None None
Now that was confusing! But let's again take it with what we know using our methods in 1.2 Evaluation Procedure.
- Evaluate operator:
printis a built-in function, so we move on to step 2 -
Evaluate the operands. The first operand is another expression, so we have to evaluate it first.
- Evaluate operator:
printis a built-in function - Evaluate the operands:
1evaluates to1. - Apply the operator to the operands: we display
1, then we returnNone, so that whole expressionprint(1)evaluates toNone
Now we evaluate the second argument. It's another procedure. 1. Evaluate operator:
printis a built-in function 2. Evaluate operand: It's another procedure, so apply the evaluation procedure 1. Evaluate operator:printis a built-in function 2. Evaluate operands: each evaluate to2and3, respectively 3. Apply operator to operands: We display2 3to the output, but then returnNone.print(2, 3)evaluates toNone3. Apply operator to operands: We display the result of our computation, which isNone, so we are really doingprint(None). That is what you see in the next line of the output. 3. Apply operator to operands: Both operands evaluate toNone, so we really are doingprint(None, None), and that is what appears in the last line you see. The whole expression evaluates toNone, but since we aren't printing that, it should just literally show nothing and ask for the next line. - Evaluate operator:
That was a difficult one! I'd recommend to try an pretend you are explaining this to an imaginary person to make sure you really understand what is going on. But the point of this exercise is to realize the importance of return types and evaluation procedure.
As an extra practice, try to figure out the result of
>>> from operator import add, mul
>>> print(add(2, 3), print(2, mul(2, 3), print(1)))
You can see if you are correct by testing it in a Python session.
Anatomy of a Function¶
Now it is time to create your own functions that do what you want. A function is composed of 3 main parts:
- A define statement followed by a name. Unlike variables, functions require special syntax and a name to go by.
- A set of parameters, each given a name to be known by.
- A body, which can contain one or more lines of code that are indented.
If you've programmed in other languages, like Java, you've noticed that spacing doesn't really matter, as long as you separate segments of code with a semicolon and delimit multi-line code with curly braces. However, in Python, spacing matters. In our case, indenting your code is important. Here is an example function that follows the proper syntax:
def square(x):
return x * x
y = square(3)
print(y)
And running the code in a .py file will print out 9. Notice the body of the function is indented. We can put more than one line if we want to separate the steps.
def square(x)
number = x * x
return number
So you see we can define new variables inside the body of the function. Does this do anything different? Can I reference number outside of the function?The answer is no, but the explanation will be covered in [[4. Higher Order Functions]] and [[5. Environment Diagrams]]. For now, just assume that variables defined inside a function can only be accessed inside the function, but variables declared outside a function can be accessed inside a function. Again, once we get to the later chapters, you'll know a systematic way of knowing if you are allowed to reference variables inside or outside functions.
The general form of a function definition is
def <name>(<param_1>, <param_2>, ..., <param_n>):
<body>
You also don't have to have any parameters at all. Let's say you just want a function that returns a hardcoded value. You simply don't need to put anything in the parameter list.
def hardcode_hello_world():
return "Hello World"
print(hardcode_hello_world())
The last line will display Hello World, and you can try it yourself in a .py file.
Functions that Return Nothing or More than One Value¶
Let's say you want a function that returns nothing, like the print function. You can do this two ways: (1) don't include return, or (2) explicitly write return None.
def do_nothing():
return None
def do_nothing2():
x = "hi"
Now, I wish I could really write nothing in the body of the function of do_nothing2. but Python expects at least something in the body of any function. I just added a random line that does nothing since we never return that value.
To return more than one value, simply separate them with a comma in the return statement. For example, if I wanted to return the positive and negative values of a number x that is passed in, I might write
def pos_neg(x):
return x, -x
A new question arises: how do I store both these values at the same time? It is just like parallel assignment, as discussed in 2.1 Expressions and Variables. So I might try and write
a, b = pos_neg(2)
print(a) # Will print 2
print(b) # Will print -2
Summary¶
Now you know how functions should work. They are just like multivariable functions in math: you can take in zero or more parameters, do some operations to them, and then return zero or more items. In the next chapter, we will make our functions more useful by introducing iteration and selection.
Next chapter 3. Control.