Introduction to Python Programming - Function Basics
Table of Contents
- Introduction
- Defining Functions
- Calling a Function
- Function Parameters and Arguments
- Return Statement
- Built-In Functions
- Lambda Functions
- An Example: Validate a Full Name
Introduction
At the end of this article, you will have learned the following:
- how to define and use your functions
- how to use function parameters and return statements
- built-in functions
- lambda functions for one-time-only tasks
- how to create a function to meet the requirements of a "real-life" scenario.
I assume that you've read the previous articles in this series. Also, you are either a beginner programmer or an experienced programmer learning. Let's get started :)
Let's say you want to calculate the body mass index (BMI) of 20 people. Maybe under different conditions or in other parts of your program. Typically you might be tempted to go straight to the calculation.
Let's assume the height and weight of the first person were 1.71m and 68kg, respectively.
# calculate BMI
# height in meters
height = 1.71
# weight in kilograms
weight = 68
# bmi = weight/height square
bmi = weight / height**2
print(bmi)
How about the second person? You copy the code to where you need it.
# calculate BMI
# height in meters
height = 1.89
# weight in kilograms
weight = 92
# bmi = weight/height square
bmi = weight / height**2
print(bmi)
The third person? Or in a different file? You copy again.
That is way too much repetition that you want to avoid. Luckily, Python functions allow you to group or organize your codes.
An inherent benefit of using functions, apart from logic grouping, is the expression of your intention. Function names are a great way to express your intent to yourself and others who will read your code. For example, let's rewrite the above code that calculates BMI.
# define the function
def calculate_bmi(height, weight):
bmi = weight / height**2
return bmi
# call the function and save its output in
# the result variable
result = calculate_bmi(1.71, 68)
print(result)
You don't have to look at the codes line by line to know what it is doing. It calculates BMI.
In Python, two categories of functions exist - Built-In functions provided by the standard library and user-defined functions (the ones you define yourself). Let's start with user-defined functions since they will enable us to understand Built-In functions.
Defining Functions
To define a function, start by using the def
keyword. Followed by the name of the function, a parenthesis, a colon (:
), and then the function body. The format is:
def function_name():
# Do something here in the function body
def second_function(parameter_1, parameter_2, ..., parameter_N):
# do something here
return something # to get the result
The function name must follow the rules on variable naming. The parenthesis can contain 0 or more implicitly-defined variables called parameters or arguments. The function body can contain a return
statement depending on whether the function generates an output.
Let's examine the calculate_bmi
function we created above. It has a name (stating the obvious) and has two parameters (height
and weight
). It computes the BMI within its body (bmi = weight / height**2
) and uses the return keyword to give back the result of the calculation (return bmi
). Here's the function again for reference.
def calculate_bmi(height, weight):
bmi = weight / height**2
return bmi
Calling a Function
To use (call) a function, write its name and include a parenthesis with or without arguments (or parameters). For example, call the calculate_bmi
function like so:
calculate_bmi(1.91, 80)
If the function returns a value, we can (optionally) save the value in a variable.
my_bmi = calculate_bmi(1.91, 80)
print(my_pmi)
Note: Any function that does not explicitly return a value will return
None
by default.
Function Parameters and Arguments
As earlier stated, parameters/arguments are variables implicitly created based on function definition. They act as the function's inputs.
A function can take multiple parameters as in the calculate_bmi
function. Also, a function can have no parameter. For example:
# define a function that takes no argument/parameter
def print_hello_world():
print("Hello World!")
# call the function
print_hello_world()
One or more arguments can have default values. A function uses the default values if you call it without specifying the argument. For example, sum_num
is a function that adds two numbers. By default, those numbers are 5 and 10.
def sum_num(num1 = 5, num2 = 10):
result = num1 + num2
return result
We can use sum_num
in several ways.
# although no arguments are passed in
# num1 is 5 and num2 is 10. The defaults.
fifteen = sum_num()
# returns 25 since num1 is 13 and num2 is 12
twenty_five = sum_num(13, 12)
# specify num1 (20) and use num2's default value (10)
thirty = sum_num(20)
But wait! How do I specify num2
alone? This question brings us to the concept of positional and named arguments.
Positional and Named (Keyword) Arguments
Let's start by defining a different version of sum_num
say sum_num_v2
.
# function with 1 positional required argument.
# num1 and num2 are optional named/keyword arguments
def sum_num_v2(num1, num2 = 5, num3 = 10):
return num1 + num2 + num3
You can use the return keyword in the same line as the calculation.
We can pass arguments to a function based on the position of the arguments or based on the names (keywords) of the arguments. The function sum_num_v2
has one positional argument num1
and two named arguments num2
and num2
. The required position argument num1
must always be in the first position during the function call while the named arguments can either take position 2 or 3. For example
# num1, num2, num3 take their positions - 1,2 and 3 respectively
sum_num_v2(10, 20, 30)
# This is the same as using the positions above
sum_num_v2(10, num2 = 20, num3 = 30)
# The order of num2 and num3 have been switched
# We get the same result as above since
sum_num_v2(10, num3 = 30, num2 = 20)
# we could omit num2 and num3 since they
# have default values.
sum_num_v2(10)
Calling a function without its required arguments throws an error.
# sum_num_v2() # fails :(
In recent Python versions, you can be strict about the number and placement of positional and keyword arguments. For example, consider the describe_me
function below:
def describe_me(name, age, city=None, zipcode=None):
# do nothing
pass
You can call the function in two ways:
- with four positional arguments
describe_me("Johnny Depp", 1020, "Caribbean", "123456")
- with two positional and two keyword arguments
describe_me("Johnny Depp", 1020, city="Caribbean", zipcode="123456")
What if you wanted to be very specific? You can use \
(positional) and/or *
(keyword) to restrict the function's use to a single way like so:
def describe_me(name, age, /, *, city=None, zipcode=None):
pass
# Fails
# describe_me("Peter Griffin", 48, "Quahog", "00093")
# TypeError: describe_me() takes 2 positional arguments but 4 were given
# works!
describe_me("Peter Griffin", 48, city="Quahog", zipcode="00093")
The first two arguments must be positional arguments while the last two must be keyword arguments. You can't call the function with four positional arguments.
Likewise, you can also restrict a function to use only keyword arguments like so:
def I_dont_take_positions(*, city=None, zipcode=None):
pass
# Fails
# I_dont_take_positions() takes 0 positional arguments but 2 were given
# I_dont_take_positions("Quahog", "00093")
# works!
I_dont_take_positions(city="Quahog", zipcode="00093")
# works! the order does not matter remember?
I_dont_take_positions(zipcode="00093", city="Quahog")
Return Statement
Earlier, you saw the return
keyword in the calculate_bmi
, sum_num
, and sum_num_v2
. These functions did some calculations and used the return
statement to give back the results.
# Store the calculation in the result variable
result = sum_num_v2(10, 20, 30)
print(result)
In other cases, you can return nothing (None
) from a function if you use the return
keyword alone. For example, the calculate_bmi
function requires the weight and height parameters. What if they're absent? What if you give strings instead?
One way to defend your function is to return early if the parameters are not valid:
# helper function: check if parameter n is a number
def is_number(n):
if isinstance(n, int) or isinstance(n, float):
return True
return False
# A safer, better version of calculate_bmi
def calculate_bmi_v2(weight, height):
# Do not calculate BMI if weight or height is not a number
# return early
if not is_number(weight) or not is_number(height):
return
bmi = weight / height**2
return bmi
# works well
print(calculate_bmi_v2(68, 1.61))
# returns None
print(calculate_bmi_v2("Hello", 1))
print(calculate_bmi_v2(1, "Hello"))
# returns None
print(calculate_bmi_v2("Hello", "World"))
Note that in the absence of a return statement, a function returns
None
implicitly.
You can return a single value or multiple values from a function. To do so, separate the return values with a comma. For example, the generate_123
function below returns three different values-1,2,3.
def generate_123():
return 1, 2, 3
The result of the function call is a tuple containing the values.
one_two_three = generate_123()
print(one_two_three)
You could also unpack the return values individually.
# Use tuple unpacking
one, two, three = generate_123()
print(one, two, three)
You can use the return statement within an if-statement, a for loop, or a while loop. The function will exit. For example:
def contains_only_even_numbers(numbers):
for num in numbers:
# If it's an odd number return False
if num % 2 != 0:
return False
# If we get here, the numbers parameter contains only even numbers
return True
outcome = contains_only_even_numbers([2, 4, 6])
print(outcome) # True
outcome = contains_only_even_numbers([1, 2, 4]) # False
print(outcome) # False
Built-In Functions
Python provides many functions in its standard library to make development easier.
These functions allow you to perform common operations without writing them yourself. For example, you can replace the functions sum_num
and sum_num_v2
with the sum
function provided by Python.
numbers = [1, 2, 3, 4, 5]
total = sum(numbers)
print(total) # 15
The table below shows some common Built-In functions:
Function Name | Description | Example |
---|---|---|
int |
Convert a string/float to an integer | int(4.8) |
float |
Convert a string/integer to a float | float("11.82") |
str |
Convert any type to a string | str(1) |
type |
Check the type of a variable/literal | type(5) |
map |
Apply a function to every element of a sequence/collection | map(your_function, collection) |
filter |
Apply a predicate to a collection | filter(your_function, collection) |
any |
Check if any item in a collection is True . Use with map |
any(map(predicate, collection)) |
all |
Check if all item in a collection are True . Use with map |
all(map(predicate, collection)) |
enumerate |
Add indices to a sequence/collection | enumerate(numbers) |
zip |
Combine collections | zip(numbers, letters) |
min |
Get the minimum value in a sequence/collection | min([1,2,3]) |
max |
Get the maximum value in a sequence/collection | max("xab") |
sorted |
Sort a sequence/collection in a customized way | sorted([2, 1, 5], reverse=True) |
reversed |
Reverse the content of a sequence | list(reversed([2, 4, 5, 1])) |
open |
Open a file for reading/writing/appending. | open("my_file.txt", mode="r", encoding="utf8") |
help |
Provide helpful message on variables, functions, etc. | help(print) |
A
predicate
function returns a boolean-True
orFalse
Let's use some of the functions. Start by creating some collections names
and numbers
# some names
names = ["John Doe", "Peter Griffin", "Ghost Busters", "Super Mario"]
# numbers 1 - 100
numbers = list(range(1, 101))
map: apply a function to every element of a sequence/collection/iterable (list, tuple, etc.)
Create two functions square
and length
. square
raises a number to power 2. length
gives the length of a string.
def square(num):
return num**2
def length(name):
return len(name)
Square all the numbers within the numbers
list.
squared_numbers = map(square, numbers)
# squared_number is wrapped (a map object)
print(squared_numbers)
# Convert to a list/tuple to view the actual content
squared_numbers = list(squared_numbers)
print(squared_numbers)
Get the length of each name in the names
list.
# length names are wrapped (a map object)
length_names = map(length, names)
# Convert to a tuple and view the content
print(tuple(length_names))
# CAREFUL HERE: # tuple is empty because we already used the content
# To avoid this scenario, save the content of the map object.
print(tuple(length_names))
The
map
function returns a map object, a collection that's not evaluated instantly. The operation runs when you uselist
ortuple
on the map object. Note You shouldn't uselist
ortuple
on the same map object twice. Else, the content of the second call will be empty.
Instead of storing and converting a map object, you can loop through its content. For example:
for length_of_name in map(length, names):
print(length_of_name)
filter: apply a predicate function to each element of an iterable/sequence
Let's say you only want some names. For example, names that have 13 letters or less. Or names that start with the letter P. Use the filter
function here.
def less_or_eq_13(name):
return len(name) <= 13
def starts_with_p(name):
return name.lower().startswith("p")
names_lte_13 = filter(less_or_eq_13, names)
pnames = filter(starts_with_p, names)
for name in names_lte_13:
print(name)
# Just like `map`, `filter` returns a filter object that is used only once
# For multiple uses, save the content in a variable
print(list(names_lte_13))
any and all
def is_multiple_of_5(n): return n % 5 == 0
def is_odd(n): return n % 2 != 0
# check if any of the elements is a multiple of 5
mult_5 = any(map(is_multiple_of_5, numbers))
print(mult_5)
# Check if numbers contain only odd numbers
all_odds = all(map(is_odd, numbers))
print(all_odds)
enumerate
enumerate
associates an index with each element of collection/sequence. enumerate
returns an enumerate object that can be converted to a list of tuples.
indices_and_names = enumerate(names)
# enumerate object
print(indices_and_names)
# Convert to a list and print
print(list(indices_and_names))
Like map objects and filter objects, you can loop through an enumerate object. For example:
for index, name in enumerate(names):
print(f"{name} is at position {index}")
zip
Using zip
, you can combine sequences to create a zip object. Consequently, the zip object can be converted into a list of tuples or used in a for loop.
For example, you can combine the names
list with the length_names
to have both a name and its length side by side.
# Create a zipped object
names_length_zipped = zip(names, length_names)
# loop through
for name, length in names_length_zipped:
print(f"{name} has {length} letters")
You can combine the zip step and the for loop.
# Do the above in a single step
for name, length in zip(names, length_names):
print(f"{name} has {length} letters")
Detour: Implementing the Map and Filter Functions Yourself
The map
and filter
functions are similar to the map_
and filter_
defined below.
# many people prefer def map_(f, items)
# I chose to write mine in full for demo purposes
def map_(function, items):
result = []
for item in items:
# Call the function and append its output to result
result.append(function(item))
return result
# many people prefer def filter_(f, items)
# I chose to write mine in full for demo purposes
def filter_(function, items):
result = []
for item in items:
if function(item):
result.append(item)
return result
Note that these functions
map_
andfilter_
are evaluated immediately. Don't use in real code.
Lambda Functions
If the question "Do I have to define a function each time I use map
and filter
?" comes to your mind, the answer is NO!
With lambdas
you can define a small function where you need it and forget it never existed. Lambdas also provide the following advantages:
- Lambda functions are nameless. No need to put thought into naming such functions.
- You won't have to litter your Python script with functions you'll only use once. For example,
square
,length
,is_multiple_of_5
,is_odd
,less_or_eq_13
, andstarts_with_p
ought to be lambdas.
Excited? How do I create a lambda function?
A lambda function has the following structure: lambda *0_or_more_parameters* : *function body*
. No parenthesis or explicit return statement is required. Implicitly, Python returns the result of the expression in the function body.
Let's rewrite some of the lines above using lambdas. First, ditch the square
and length
functions.
# Initially, you used a square function.
# Replace with a lambda that does exactly the same thing
squared_numbers = map(lambda x: x**2, numbers)
# Convert to a list/tuple to view the actual content
squared_numbers = list(squared_numbers)
print(squared_numbers)
# No need for the length function you created above
length_names = map(lambda name: len(name), names)
# Convert to a tuple and view the content
print(tuple(length_names))
The filter function is next. Let's select only names where the length of the name is less than or equal to 13 using a lambda function.
names_lte_13 = filter(lambda name: len(name) <= 13, names)
pnames = filter(lambda name: name.lower().startswith('p'), names)
print(list(names_lte_13))
print(list(pnames))
How about the any
and all
?
# check if any of the elements is a multiple of 5
mult_5 = any(map(lambda n: n % 5 == 0, numbers))
print(mult_5)
# Check if numbers contain only odd numbers
all_odds = all(map(lambda n: n % 2 != 0, numbers))
print(all_odds)
An Example: Validate a Full Name
Imagine that your boss says that the name of a user of your application must meet the following requirements:
- A space should separate first name and last name
- First name and last name must be an alphabet
- First name and last name must have at least three letters
- The function should return
True
orFalse
- The function must have two versions-one that uses a for loop and another that uses
map
To get you started, your colleague creates two functions that must print OK! if your code is correct.
def assert_true(result):
if result is True:
print("OK!")
return
raise ValueError("incorrect!")
def assert_false(result):
if result is False:
print("OK!")
return
raise ValueError("incorrect!")
Next, your colleague adds the following calls/tests to prove your function works.
# Test for valid names using both versions 1 and 2
assert_true(valid_name("Mark Edosa"))
assert_true(valid_name("Jet Lis"))
assert_true(valid_name_v2("Mark Edosa"))
assert_true(valid_name_v2("Jet Lis"))
# Test for invalid names using both versions
assert_false(valid_name("a b"))
assert_false(valid_name("Mark"))
assert_false(valid_name("abc"))
assert_false(valid_name_v2("a b"))
assert_false(valid_name_v2("Mark"))
assert_false(valid_name_v2("abc"))
Solution
def valid_name(name):
name_list = name.split()
if len(name_list) != 2:
return False
for name in name_list:
test = name.isalpha() and len(name) >= 3
if not test:
return False
return True
def valid_name_v2(name):
name_list = name.split()
if len(list_names) != 2:
return False
tests = map(lambda name: name.isalpha() and len(name) >= 3, list_names)
return all(tests)
Conclusion
Functions help you organize your code. They also help you express your intentions through appropriate names.
In this article, you learned how to define and use your functions. You learned about function parameters and return statements. You also saw some Built-In functions in action. And you saw how to use lambda functions for one-time-only tasks. Finally, you (hopefully) created a function to meet the requirements of a "real-life" scenario.
Thank you for reading.