Motivating Example for Automation with Python Functions
Suppose you want test out the Collatz Conjecture over the integers in the range [1,10]. In short, the Collatz conjecture states that if we start at any positive integer n
, and iteratively take n = n/2
if the number is even or n = 3*n + 1
if the number is odd, then eventually n
will become 1. We can generate the first iteration by creating a list of integers using the range
function, and apply the Collatz Conjecture to each element using a loop like we saw in our previous tutorial:
i_1 = [x for x in range(1, 11)]
i_2 = []
for i in i_1:
if i == 1:
i_2.append(i) # Skip if we've reached 1
elif i % 2 == 0: # % is the modulo operator
i_2.append(int(i/2)) # int converts float type to integer type
else:
i_2.append(int(3*i + 1))
print(i_2)
> [1, 1, 10, 2, 16, 3, 22, 4, 28, 5]
It looks like two numbers have reached one, so we’ll have to repeat:
i_3 = []
for i in i_2:
if i == 1:
i_3.append(i) # Skip if we've reached 1
elif i % 2 == 0: # % is the modulo operator
i_3.append(int(i/2)) # int converts float type to integer type
else:
i_3.append(int(3*i + 1))
print(i_3)
> [1, 1, 5, 1, 8, 10, 11, 2, 14, 16]
Now we have three numbers that have reached 1. It looks like we’ll have to repeat this process, so we’ll copy and paste the loop several times:
i_4 = []
for i in i_3:
if i == 1:
i_4.append(i) # Skip if we've reached 1
elif i % 2 == 0: # % is the modulo operator
i_4.append(int(i/2)) # int converts float type to integer type
else:
i_4.append(int(3*i + 1))
i_5 = []
for i in i_4:
if i == 1:
i_5.append(i) # Skip if we've reached 1
elif i % 2 == 0: # % is the modulo operator
i_5.append(int(i/2)) # int converts float type to integer type
else:
i_5.append(int(3*i + 1))
i_6 = []
for i in i_6:
if i == 1:
i_6.append(i) # Skip if we've reached 1
elif i % 2 == 0: # % is the modulo operator
i_6.append(int(i/2)) # int converts float type to integer type
else:
i_6.append(int(3*i + 1))
i_7 = []
for i in i_6:
if i == 1:
i_7.append(i) # Skip if we've reached 1
elif i % 2 == 0: # % is the modulo operator
i_7.append(int(i/2)) # int converts float type to integer type
else:
i_7.append(int(3*i + 1))
print(i_7) # Final answer?
> []
Oh dear. The Collatz Conjecture makes no prediction that numbers will disappear after 7 iterations. We have either disproved the conjecture and are worthy of a Nobel Prize, or there is an error in our code. After some tedious searching, we can see that in the sixth iteration loop, we accidentally changed the for
loop to iterate over the empty set i_6
instead of the previous iteration set i_5
.
Our problem is that we have fallen into an error trap. Error traps are situations in which the likelihood of making a mistake is high. The error trap in this situation was the repeated copying of code, each of which had several minor edits. The repetitive nature of this task can lead to mental fatigue and a higher probability of making a typo. How can we prevent falling into this trap and speed up the coding process? The answer is in functions. With a function, we can define the Collatz Conjecture formula only once, and loop over all of our iterations using only a few lines of code!
Code More, Distract Less: Support Our Ad-Free Site
You might have noticed we removed ads from our site - we hope this enhances your learning experience. To help sustain this, please take a look at our Python Developer Kit and our comprehensive cheat sheets. Each purchase directly supports this site, ensuring we can continue to offer you quality, distraction-free tutorials.
Introduction to Automation with Python Functions
Python functions are a set of objects that ease performance of repetitive tasks. This tutorial will introduce Python’s function format, uses, and how to build your own functions. The topics covered in the tutorial will be the following:
- Function Structure
- User-Defined Functions
- Function Objects
- Generator Functions
- Functions vs. Methods
- Anonymous Functions
- Functions Example
Function Structure
A Python function has the following format:
[output] = [function]([input])
The [function]([input])
side is the function is the “call”, and after evaluation within the body of the function will “return” an output. The above format assumes that the output is assigned to a variable, but this doesn’t have to be the case. Functions can take various Python objects as input and produce various Python objects as output. Some functions take no input, and some functions produce no output object.
We have used functions regularly in the last few tutorials. For example, in Data Types we used several type conversion functions like float
:
x = float(10)
Here the float
function took the 10
integer object, returned a float object 10.0
, and assigned it to x
.
We can use the type
function again to check the type of the output.
type(x)
> float
Which confirmed that the float
function did indeed produce a float object. But what did the type
function produce?
y = type(x)
type(y)
> type
It produced a type
object. Now let’s look at the print
function:
x = float(10)
print(x)
> 10.0
y = print(x)
> 10.0
print(y)
> None
Notice that when we assigned the print
function output to the variable y
it still printed 10.0
to the terminal, and the function output was in fact a None
object.
When a function performs an action not associated with an output object, it is termed a “side effect” of the function. Side effects can include printing a line to the terminal, modifying a file, or plotting a graph. We will focus more on side effect generating functions such as plotting in future tutorials.
Python has a multitude of built-in functions that we won’t cover in this tutorial, but select functions will be addressed in detail throughout these lessons.
User-Defined Functions
We’ve looked at the basic structure of functions as well as a few of Python’s built-in functions, but how do we create our own functions? A user-defined function has the following format:
def [name]([input]=[default]):
[actions]
return([output])
Like loops and if
statements, the actions associated with functions can be extended over multiple lines using indentation (tab or 4 spaces). The return
statement will tell Python to exit the function and output whatever object is included in [output]
. Make note that once the return
statement is executed, no further lines in the function will be executed (i.e. like a break
statement). For example, lets make a function that doubles its numeric input:
def double(n):
x = 2*n
return(x)
double(1)
> 2
double(8)
> 16
In the above function, n
is called a “dummy variable” representing the input object. Multiple objects in can be represented as a tuple. For example, the following function will divide x
by y
:
def divide(x, y):
o = x/y
return(o)
divide(6, 3)
> 3.0
divide(3, 6)
> 0.5
Notice that the order of the inputs determined the order of the dummy variables associated with them. We can override this ordering by using dummy variable names directly within the function call:
divide(x=6, y=3)
> 3.0
divide(y=3, x=6)
> 3.0
Calling a function without the required input will raise a TypeError.
divide(x=1)
> TypeError: divide() missing 1 required positional argument: 'y'
Likewise, including too many variables will also raise a TypeError.
divide(x=1, y=1, z=1)
> TypeError: divide() got an unexpected keyword argument 'z'
If we want a function to use a default value for a variable not passed to it, we can define it within the function definition by using an =
.
def divide(x, y=2):
o = x/y
return(o)
divide(x=1) # Use default y=2 value
> 0.5
divide(x=1, y=4) # Input values override defaults
> 0.25
One reason function input variables are called “dummy variables” instead of “variables” is because they can only be found within the namespace of the function. Think of a function’s namespace as a sandbox in which the variable can be referenced. For example:
y = 15
def example(): # We can define a function without arguments
y = 0
print(y)
return() # We can also return without an output
print(y)
example()
print(y)
> 15
> 0
> 15
Lets break down the above example. In the first line we define y
as 15. We then define the example
function in which y
is assigned 0. Printing y
after the function definition will return 15 as we defined prior to the definition. Calling the example
function will define y
as zero, and the internal print
function will print y
as 0. Using the print
function again after the example
function call will again print 15 as we first defined. This functionality is based on how Python looks up variable names within namespaces. When we set the value of y
at the start, we define it for the main program’s namespace, and printing y
from the main program will return the variables defined within the main program. When we set the value of y
within the function, we assign it to the function’s namespace, which was only reached by the variables within the function.
Note that functions themselves do not follow this rule. Functions created within the main program can be called within a function.
Namespaces can be confusing to Python beginners, therefore we won’t go into detail on them in this tutorial. The main takeaway here is that with some exceptions, assume that only variables passed into the input of a function can be used within the function, and assume that only variables returned by the function can be used in the main program.
Function Objects
In some of the previous sections, we’ve referred to functions as function objects. This is because the functions are actually objects defined within a namespace in Python, much like variables or data types. Like other objects, we can pass functions to variables:
def double(n):
x = 2*n
return(x)
y = double
print(y)
> <function double at 0x0000029149A6BC80>
type(y)
> function
Likewise we can pass objects to data structures:
l = [double, double]
print(l)
> [<function double at 0x0000029149A6BC80>, <function double at 0x0000029149A6BC80>]
l[1](2)
> 4
Functions objects can also be passed to other functions. We can see the usefulness of this property by using the map
function. The map
function will apply a given function to a given list, and return the sequence of output as a generator object:
l = [1, 2, 3, 4, 5]
[i for i in map(double, l)]
> [2, 4, 6, 8, 10]
Code More, Distract Less: Support Our Ad-Free Site
You might have noticed we removed ads from our site - we hope this enhances your learning experience. To help sustain this, please take a look at our Python Developer Kit and our comprehensive cheat sheets. Each purchase directly supports this site, ensuring we can continue to offer you quality, distraction-free tutorials.
Generator Functions
You may recall in our previous discussion on Python control flows a description of the range
function as a generator. Generators are useful for creating sequences of output without having to store the output or recalculate already computed values.
We can create our own generator functions. A generator function is distinguished from a regular function through the use of a yield
expression instead of a return
statement with the following format:
def [name]([input]=[default]):
[actions]
yield([output])
The first time the generator function is called as an iterator, the code executes to the yield
expression and returns a value. On each subsequent execution the code returns to the previous yield
expression, and continues until the next yield
expression is encountered.
For example, we can create our own version of the range
function:
def myRange(n):
i = 0
while i < n:
yield(i)
i += 1
y = myRange(11)
print([x for x in y])
> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
For a given generator function object, the next
function can be used to generate successive values of the generator:
y = myRange(2)
next(y)
> 0
next(y)
> 1
next(y)
> StopIteration
The next
function was useful for calculating successive values manually, however when the function can no longer reach the next yield
expression, a StopIteration exception is raised.
It is important to remember that functions are objects, otherwise generator functions can produce confusing results.
next(myRange(11))
> 0
next(myRange(11))
> 0
Despite using the next
function twice, we received the same results. This is because we called the generator function rather than a stored object of the generator function. Essentially, calling next
on the original generator created a “fresh” version of the generator, and could not “remember” previous executions. This is why it’s important to store generators before use, and call next
on the variable in which its stored.
x = myRange(11)
next(x)
> 0
y = myRange(11)
next(y)
> 0
next(x) # Each variable "remembers" where the generator was
> 1
Caution: When building a generator function that produces numbers over an infinite set, include a maximum iteration counter to prevent infinite loops. For example, the following generator will count by one on each iteration:
def counter():
i = 0
while True: # Do not do this
yield(i)
i += 1
y = counter()
next(y)
> 0
next(y)
> 1
The counter function seems to be working well, however if we used the generator in a list comprehension we’ll see a major flaw.
[x for x in y] # This operation will crash the program
The list comprehension will crash the program because the generator will never stop producing additional numbers to add to the list. This forms an infinite loop within the while
loop in the generating function. Always include a maximum number of iterations for a loop like so:
def safeCounter(max=1000):
i = 0
while i < max: # Allow the loop to eventually exit
yield(i)
i += 1
Functions vs. Methods
In the previous sections and tutorials, we’ve used the terms “method” and “function”, but what the the differences between the two? Put simply:
- Functions: are standalone objects
- Methods: are functions attached to, and defined by, objects.
For example, range
is a function because it isn’t associated with or dependent upon any other object, and [string].append()
is a method because it is defined by the [string]
object.
Code More, Distract Less: Support Our Ad-Free Site
You might have noticed we removed ads from our site - we hope this enhances your learning experience. To help sustain this, please take a look at our Python Developer Kit and our comprehensive cheat sheets. Each purchase directly supports this site, ensuring we can continue to offer you quality, distraction-free tutorials.
Anonymous Functions: `lambda` functions
Anonymous functions are just as the title implies: functions without names. These are function objects that cannot be called outside of their initial definition. Anonymous functions are useful for short, one-line functions where a full function definition would be unnecessary. Python defines anonymous functions using the lambda
expression, which are commonly referred to as lambda functions.
Lambda functions have the format lambda [input]: [output]
. Notice how the function is compact, and the output is evaluated and returned within the same line.
For example, a lambda function can be used to quickly check if numbers in a list are even:
n = [1, 2, 3, 4, 5]
[i for i in map(lambda x: x % 2 == 0, n)]
> [False, True, False, True, False]
Functions Example
Let’s return to our motivating example from the start of this tutorial. How can we use functions to automate an evaluation of the Collatz Conjecture? Let’s first see a simple use of formulating the conjecture as a function:
def Collatz(i):
if i == 1:
return(1) # Skip if we've reached 1
elif i % 2 == 0: # % is the modulo operator
return(int(i/2)) # int converts float type to integer type
else:
return(int(3*i + 1))
i_1 = Collatz(10)
print(i_1)
> 5
i_2 = Collatz(i_1)
print(i_2)
> 16
This certainly makes copying and pasting each iteration easier, but how can we perform all iterations without copying and pasting anything? We can use a generator:
def CollatzIterate(i, n):
k = 1
while k < n:
if i == 1:
i = 1
elif i % 2 == 0:
i = int(i/2)
else:
i = int(3*i + 1)
yield(i)
k += 1
all = [x for x in CollatzIterate(20, 10)]
print(all)
> [10, 5, 16, 8, 4, 2, 1, 1, 1]
Automation with Python Functions Exercises
Create a generator function that produces a consecutive Fibonacci Number with each next
call.
Solution
Use a lambda function to print out all the values of the following dictionary:
ing = {"Flour":"Self-Rising",
"Sugar":"White Granulated",
"Fat":"Unsalted Butter",
"Milk":"Whole",
"Fruit":"Peaches"}
What will the following code produce? Why?
v = [0.2, 0.5, 8.0, 0.5]
def norm_squared():
sum = 0
for i in v:
sum += v[i]**2
return(sum)
norm_squared()
Given a number n
, write a Python function that determines the total number of Collatz Conjecture iterations are required on n
to reach a value of 1.
Solution
Use the map
function,range
function, and a lambda function to calculate the squares of the numbers in the range [1,10] on a single line.
Solution
Solutions
The following generator function will produce the Fibonacci Numbers with each iteration. Note that a maximum counter was added to prevent infinite loops.
def Fibonacci(max=10^5):
a = 0
b = 1
i = 1
while i < max:
yield(a + b)
(a, b) = (b, a + b)
ing = {"Flour":"Self-Rising",
"Sugar":"White Granulated",
"Fat":"Unsalted Butter",
"Milk":"Whole",
"Fruit":"Peaches"}
x = lambda item: ing[item]
[x(item) for item in ing]
> ['Self-Rising', 'White Granulated', 'Unsalted Butter', 'Whole', 'Peaches']
It will return an error because v
was not defined within the namespace of the function, but only in the namespace of the main program. Remember the rule of thumb: generally functions will only see variables passed into them, and the main program will generally only see variables passed from functions. Exceptions to this rule include global variables, which will be discussed in later tutorials.
4.
def CollatzIterate(i, n): # Same as above
k = 1
while k < n:
if i == 1:
i = 1
elif i % 2 == 0:
i = int(i/2)
else:
i = int(3*i + 1)
yield(i)
k += 1
def CountCollatz(n, max=200):
count = 1
v = CollatzIterate(n, max)
i = next(v)
while i > 1:
i = next(v)
count += 1
return(count)
5.
[y for y in map(lambda x: x**2, [x for x in range(1,11)])]
Did you find this free tutorial helpful? Share this article with your friends, classmates, and coworkers on Facebook and Twitter! When you spread the word on social media, you’re helping us grow so we can continue to provide free tutorials like this one for years to come.