Learning Management Platform for Written Tutorial Series.

Python 3 Functions - The complete guide with source code

Python 3 Functions - The complete guide with source code

Python Functions

A function is simply a named reusable piece of code that we can call as many times as we need to. We have actually already worked with functions before in these tutorial series except we didn't write them ourselves. When we call print(...) we are actually using a function.

In this lesson, we will learn how to create our own functions and how we can work with them to promote code reusability.

Topics to be covered

We will cover the following topics in this lesson.

  • User defined functions
  • Function parameters
  • Function parameter scope
  • Functions that return values
  • Lambda (Anonymous) functions
  • Pure and impure functions
  • Recursive functions
  • Decorator functions
  • Generator functions

User defined functions

Let's start with a really basic example of a function. Let's say we want to be able to display the application version of our program. We can define a function that does that for us then we can call it from wherever whenever we need it.

We can define it like so.

def app_version():
    print('Version: 1.7')


app_version()

HERE,

  • The keyword def is used to define a function name app_version. The function name is immediately followed by open and close parenthesis () then a full colon :. That is the syntax of defining functions in python.
  • print('Version: 1.7') has been indented beneath the function definition. This tells python that this code belongs to the function and should only be executed if the function has been called
  • app_version() actually calls our function and notice that it is not indented under the function definition. Removing the indentation tells python that we have ended the function definition. As a good programming practice, it's recommended to leave two (2) blank spaces after the function definition.

Our code above when executed produces the following results

Version: 1.7

Let's now work on a more slightly advanced function.

Function parameters

Let's say we are working on a content management system that will have a section for users to create content such as pages, blog posts etc and we want to be turning the title of the post into a SEO friendly URL. We can create a simple function that accepts a string value, turns it into lower case letters, then replaces the spaces with dashes.

Let's create our function like so

def seo_url(title):
    url = title.lower().replace(' ','-')
    print (url)


seo_url('The life of brian')

HERE,

  • def seo_url(title): defines a function named seo_url that accepts a parameter called title.
  • url = title.lower().replace(' ','-') calls the lower and replace methods of the string object.
  • print(url) prints the SEO friendly url in the console.

Let's now look at an example that accepts multiple parameters. We will simply create a function that accepts two numbers x and y then multiplies them.

def multiply(x,y):
    print(x * y)


multiply(3,3)
multiply(3,4)
multiply(3,5)

HERE,

  • def multiply(x,y): defines a function named multiply that accepts two parameters named x and y.
  • print(x * y) multiplies the parameters then prints the value in the console.

Function parameter scope

The scope of variables defined in a function are only accessible within the function and not outside the function. Let's revisit the seo_url function that declares a url variable then we will try to access the variable url outside the function

def seo_url(title):
    url = title.lower().replace(' ','-')
    print (url)


print (url)

HERE,

  • We defined a function seo_url(...) that defines a variable url within the function definition.
  • print (url) attempts to access the function variable outside the function.

executing the above code produces the following error

Traceback (most recent call last):
  File "funcs.py", line 12, in <module>
    print (url)
NameError: name 'url' is not defined

Functions that return values

So far we have only created functions that print out the value in the console. In this section, we will modify the seo_url function so that instead of printing a value in the terminal, we will return a value.

For us to do that, we will have to use the return keyword like so

def seo_url(title):
    url = title.lower().replace(' ','-')
    return url


title = 'Blessed are the cheese makers'

print(f'The SEO title is {seo_url(title)}')

HERE,

  • return url returns the value of url when the function seo_url is called.

An important thing to note about the return statement is that any code written after it will not be executed. Execution ends at the return statement.

Let's look at the code below

def the_stoning():
    return 'no one is to stone anybody'
    print ('even if they say jehovah')


print(the_stoning())

HERE,

  • print ('even if they say jehovah') is placed after the return statement so python won't reach it during execution

The above code produces the following results

no one is to stone anybody

Note the line even if they say jehovah has not been printed.

Lambda (Anonymous) functions

Lambda functions are simply functions that do not have a name and are usually disposed of after use.

Consider the following code.

squared = (lambda x: x * x) (3)

print(squared)

HERE,

  • squared = (lambda x: x * x) (3) uses the keyword lambda to define an anonymous function that accepts a parameter of x then multiples x by itself then returns the value. (3) is the parameter value that is passed to our lambda function.

In short, a lambda function is defined and executed on the fly and after the value has been returned it is disposed of so u can't use it again.

The above code produces the following results

9

For the sake of understanding, let's translate the lambda function into a defined function so that you can easily comprehend its syntax

def square(x):
    return x * x

squared = square(3)

print(squared)

and here is the lambda function equivalent

print ((lambda x: x * x) (3))

HERE,

  • In the named function, we have def square(x):... ; square(3) which in the lambda function are equivalent to lambda x:.... By using the keyword lambda, we both define and call our function. While the defined function has an explicit return statement, the lambda function has return implicitly.

At first the syntax might be confusion but it's really easy to understand once you practice writing named functions then translating them into lambda functions. When you need a one-off function, lambda are real-time savers.

Let's now look at a more practical example. Suppose you have a list of numbers and you would like to square each number. You can easily do it with just 2 lines of code like so

for number in range(5):
    print((lambda x: x * x) (number))

HERE,

  • for number in range(5): the for loop iterates through a list of numbers from 0 to 4 that we generated using the range function.
  • print((lambda x: x * x) (number)) uses the lambda function to square the number then display the results in the console.

In future lessons, we will look at more advanced examples of lambda functions and their real-world practical uses.

Pure and impure functions

A pure function is a function that returns the same results always given the same values for the arguments and it does not modify the system state.

Let's look at a practical example. Consider the following function.

def square(x):
    return x * x

squared = square(3)

HERE,

  • The above function is considered pure because 1. If we give it a value of 3 then the answer will always be 9 and **2. It does not modify anything outside the scope of the function.

Let's now look at its polar opposite an impure function. It is basically the opposite of a pure function. It does not always return the same result given the same parameter value and it does modify the state of the system.

Let's look at a practical example. Consider the following function.

ladies = ['Jane','Janet']

def add_lady(lady):
    ladies.append(lady)
    return 


print(f'the original state of ladies is {ladies}\n')

lady = 'Judith'
add_lady(lady)

print(f'The result of add_lady function with param lady and a value of {lady} is \n{ladies}\n')

add_lady(lady)
print(f'The result of add_lady function with param lady and a value of {lady} is \n{ladies}\n')

print(f'the modified state of ladies after calling add_lady is {ladies}')

HERE,

  • The above function add_lady(lady) accesses a global variable ladies and modifies its state by add an item to it and the result returned given the same parameter will not always be the same therefore, it is an impure function.

The above code produces the following results

The original state of ladies is 
['Jane', 'Janet']

The result of add_lady function with param lady and a value of Judith is 
['Jane', 'Janet', 'Judith']

The result of add_lady function with param lady and a value of Judith is 
['Jane', 'Janet', 'Judith', 'Judith']

the modified state of ladies after calling add_lady is 
['Jane', 'Janet', 'Judith', 'Judith']

As you can see from the above results, calling add_lady(lady) with a value of Judith returned a list with three (3) items. Recalling the function with the same parameter value returned a list with four (4) items.

In summary, pure functions given same input value always return the same result and they don't modify the system state while impure functions given the same input value do not always return the same result and they can modify the state of the system.

Recursive functions

A recursive function is a function that calls itself repeatedly until a certain condition is met.

Let's look at a very simple example. Suppose you want to count from a starting number all the way to an ending point. You can create a recursive function to do that like so.

def count_func(n,m):
    if n > m:
        return 'Counting Completed'
    else:
        print(f'The count is {n}')
        return count_func(n + 1,m)

print(count_func(3,9))

HERE,

  • def count_func(n,m): defines a function called count_func(...) that accepts two parameters n and m.
  • if n > m: uses an if statement to check if the starting point is greater than the ending point. If it is true then the function returns a message that says Counting Completed and execution of the function ends.
  • else: ...;return count_func(n + 1,m) if the count is less than the ending number, then the function calls itself using return count_func(n + 1,m)). Notice how the parameter n is incremented with the value1`.

Our above code returns the following results.

The count is 3
The count is 4
The count is 5
The count is 6
The count is 7
The count is 8
The count is 9
Counting Completed

Advantages of recursive functions

The following are some of the advantages of recursive functions.

  • easy to write and read
  • they solve complex problems using very minimal code

Disadvantages of recursive functions

The following are some of the disadvantages of recursive functions

  • They can take up a lot of memory on the computer if not properly written
  • If the logic is not properly written they can execute for all of eternity until the whole program or system hangs.
  • following the logic can sometime be a bit difficult especially if it is solving a very complex problem. In such cases, a simple comment explaining what the function does helps.

Decorator functions

Decorators allow us to extend existing functions without really modifying the function.

Let's create a simple function that simulates reading log files and pauses for a second then proceeds.

def read_logs():
    print('Application started')
    time.sleep(1)
    print('Bills calculated')
    time.sleep(1)
    print('Bills sent')


read_logs()

HERE,

  • The above code simply prints a line pauses for a second then continues. We then call the function.

Let's say we want to know the time that the execution of the function starts and ends without modifying the function. We can create a decorator function that does that for us like so.

def execution_time(func):
    def wrapper():
        start_time = time.asctime(time.localtime(time.time()))
        print(f'Function {func.__name__} execution started at {start_time}')
        func()
        end_time = time.asctime(time.localtime(time.time()))
        print(f'Function {func.__name__} execution finished at {end_time}')

    return wrapper

HERE,

  • def execution_time(func): defines a function execution_time that takes in a function as a parameter.
  • def wrapper(): defines an internal function called wrapper within the execution_time function. This is how we write code for the decorator function.
  • start_time = time.asctime(time.localtime(time.time())) defines a variable start_time and assigns the current system time to it in a human readable form.
  • print(f'Function {func.__name__} execution started at {start_time}') prints a message in the console with the function name and human readable start time.
  • func() executes the function that is passed in as a parameter.
  • end_time = time.asctime(time.localtime(time.time())) gets the time immediately after execution of the func is done
  • print(f'Function {func.__name__} execution finished at {end_time}') prints the end time in the console.
  • return wrapper is actually defined in the body of the main function execution_time and not within the nested function wrapper. Also note that we just return the function name without the parenthesis.

Let's now decorate our function read_logs like so

@execution_time
def read_logs():
    print('Application started')
    time.sleep(1)
    print('Bills calculated')
    time.sleep(1)
    print('Bills sent')
    
read_logs()

HERE,

  • the @ symbol followed by the function name execution_time is used to decorate the function read_logs.

Running the above code produces results similar to the following.

Function read_logs execution started at Sat Aug 17 18:38:19 2019
Application started
Bills calculated
Bills sent
Function read_logs execution finished at Sat Aug 17 18:38:21 2019

As you can see from the above results, our function took 2 seconds to execute. We slowed it down on purpose so that we can see different values for the start and end times.

Let's look at another example. Will create a decorator function that executes the same function twice.

def execute_twice(func):
    def wrapper():
        func()
        func()

    return wrapper


@execute_twice
def homer():
    print("D'oh!")

homer()

HERE,

  • def execute_twice(func): defines a decorator function that accepts a function parameter called func.
  • def wrapper(): defines a nested function called wrapper that calls the passed in function twice.
  • return wrapper returns the nested function in the body of the decorator function execute_twice.
  • func(); func() calls the passed in function twice.
  • @execute_twice; def homer():... defines a function homer and the decorator function execute_twice is applied to it.

Executing the above code produces the following results.

D'oh!
D'oh!

Decorator functions with parameters

In the above 2 examples, we decorated functions that did not accept any parameters. In this section, we will create a function that greets friends and will accept a name parameter.

def awesome_dude(func):
    def wrapper():
        func()
        print('I appreciate you buddy!')
    return wrapper


@awesome_dude
def greet_buddy(name):
    print(f'Hello {name}')


greet_buddy('Fernie')

Running the above code produces the following error message.

Traceback (most recent call last):
  File "g.py", line 70, in <module>
    greet_buddy('Fernie')
TypeError: wrapper() takes 0 positional arguments but 1 was given

We are getting the above error because our function greet_buddy accepts a parameter but we did not supply it in the decorated nested function.

We can fix the above error like so

def awesome_dude(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        print('I appreciate you buddy!')
    return wrapper


@awesome_dude
def greet_buddy(name):
    print(f'Hello {name}')


greet_buddy('Fernie')

HERE,

  • wrapper(*args, **kwargs) enables our decorated nested function to accept any number of positional or keyword parameters.
  • func(*args, **kwargs) calls the function with parameters supplied.

Executing the above code produces the following results.

Hello Fernie
I appreciate you buddy!

Generator functions

Just as the name suggests, generator functions are used to generate items and they return an iterator object. They are similar to functions that return values except they use the yield keyword. Let's look at a practical example then discuss generators in more details.

def generate_numbers():
    yield 2
    yield 4
    yield 6

print(generate_numbers())
  • yield x returns the number x immediately when the next method of the iterator object is called.

Our above code produces the following result.

<generator object generate_numbers at 0x10e848150>

Because the generator object is an iterator, we can use the for loop to print out its values like so.

def generate_numbers():
    yield 2
    yield 4
    yield 6

for n in generate_numbers():
    print(n)

the above code produces the following results.

2
4
6

The major similarity between functions that return values and generators is that they both return a value(s).

The major difference is generators use the yield keyword that returns a value but does not terminate the execution instead proceeds to the next line unlike the return statement.

Summary

Functions allow us to reuse the same code over and over. For example, you can create a function called total that you can with arguments like 3 and 7 and it returns 10 as the total. Functions are defined using the def keyword. The statement return is used to return a value after a function call. Functions can be pure or impure. Decorators allow us to add more functionality to functions without modifying the underlying function.

What next?

If you enjoyed this lesson then show us your appreciation by creating a free accounts on our site. As always we appreciate your comments down below.


...