Introduction to functions
Functions are a set of instructions grouped under a name. They are defined with def
and have a name, input parameters, and an output.
def sum(a, b):
result = a + b
return result
There are other variants, such as functions that do not return any arguments or functions that do not accept any input arguments. Thanks to functions, we can:
- โป๏ธ Reuse: Eliminate repetitive code. Instead of writing the same 10 lines repeatedly, you can put them in a function and call it when needed in one line.
- ๐ฆ Organize: Group related code together, making it easier to understand. Instead of having a huge block of code doing multiple things, it is better to divide it into functions with specific functionality.
- โฌ Abstract: Abstract the complexity inside. A function can be very complex internally, but it is enough for the user to know what inputs it accepts and what outputs it returns. This is the well-known black box approach. It does not matter what is inside; it only matters to know what goes in and what comes out.
Letโs look at a function that converts degrees Celsius to Fahrenheit.
def celsius_to_fahrenheit(celsius):
fahrenheit = (celsius * 9/5) + 32
return fahrenheit
The function can be called with ()
, passing in the necessary arguments and capturing what it returns in a temperature_f
variable.
temperature_c = 25
temperature_f = celsius_to_fahrenheit(temperature_c)
print(temperature_f)
# 77.0
We can also simplify the function to one line.
def celsius_to_fahrenheit(celsius):
return (celsius * 9/5) + 32
Since Python is neither statically typed nor compiled, if we call our function with a str
, we will get the following error. This is because the divide operation is not defined for str
and int
. It makes sense, you cannot divide text
by 5
.
The interesting thing here is that the error is not detected until the function is called. It is a runtime error.
print(celsius_to_fahrenheit("text"))
# TypeError: unsupported operand type(s)
An important note is that it is not the same to use the function with and without ()
.
- With
()
: This calls the function with some input arguments. - Without
()
: This accesses the function itself, which is an object of thefunction
class.
# With ()
print(celsius_to_fahrenheit(30))
# 86.0
# Without ()
print(celsius_to_fahrenheit)
# <function celsius_to_fahrenheit at 0x1009341f0>
Functions also accept various input parameters.
import math
def hypotenuse(a, b):
return math.sqrt(a**2 + b**2)
print(hypotenuse(3, 4))
# 5.0
We can call the function by indicating the name of the argument, which is equivalent.
print(hypotenuse(a=3, b=4))
# 5.0
Since the arguments are named, we can also change the order.
print(hypotenuse(b=3, a=4))
As expected, if we pass the name of an argument that does not exist, we will get an error.
print(hypotenuse(c=10))
# TypeError: hypotenuse()
The arguments of a function can also have a default value. This will be used if none is passed. In this case, if b
is not passed, it will take 1
as the default value.
def multiply(a, b=1):
return a * b
print(multiply(10))
# 10
print(multiply(10, 2))
# 20
It is important that the arguments with default values go after those without, otherwise we will get an error. This definition is not correct.
def multiply(a=1, b):
return a * b
# SyntaxError
On the other hand, functions can have a variable number of arguments, using *args
.
def sum(*args):
return sum(args)
This sum
function can be called as follows.
print(sum())
# 0
print(sum(100, 200))
# 300
print(sum(5, 3, 2, 2, 2))
# 12
And if you want to name each argument, you can do it like this.
def sum(**kwargs):
total = 0
for key, value in kwargs.items():
print(key, value)
total += value
return total
print(sum(a=5, b=20, c=23))
# 48
print(sum(a=1, b=2))
# 3
Similarly, we can pass a dictionary as an input parameter. It is equivalent to the above.
d = {'a': 5, 'b': 20, 'c': 23}
print(sum(**d))
# 48
We can also have functions that accept a number of fixed parameters (arg1
and arg2
) and other variables (args
). The following function allows between 2
and n
arguments.
def variable_arguments(arg1, arg2, *args):
print(f"Fixed arguments: {arg1}, {arg2}")
print(f"Variable arguments: {args}")
With 0
arguments, we get an error, since arg1
and arg2
are mandatory.
variable_arguments()
# TypeError:
Using 2
arguments.
variable_arguments(1, 2)
# Fixed arguments: 1, 2
# Variable arguments: ()
Using 4
arguments.
variable_arguments(1, 2, 3, 4)
# Fixed arguments: 1, 2
# Variable arguments: (3, 4)
But it does not end there. We can also have a variable number of arguments to which we can give a name. The function below combines all types of arguments:
- Two positional fixed arguments:
arg1
andarg2
. These must always be present. - Positional variable arguments:
*args
. We can pass as many as we want. - Variable key/value arguments:
**kwargs
. We can also pass as many as we want, but they must be named.
def variable_arguments(arg1, arg2, *args, **kwargs):
print(f"Fixed arguments: {arg1}, {arg2}")
print(f"Variable arguments: {args}")
print(f"Key/value arguments: {kwargs}")
Letโs see an example of how to use it.
variable_arguments(1, 2, 3, 4, other_arg=10, more_arg=20)
# Fixed arguments: 1, 2
# Variable arguments: (3, 4)
# Key/value arguments: {'other_arg': 10, 'more_arg': 20}
Having seen the input arguments, letโs look at the output arguments. A function can have multiple output arguments. This function calculates the mean and variance of some data.
def mean_and_variance(data):
mean = sum(data) / len(data)
variance = sum((x - mean) ** 2 for x in data) / len(data)
return mean, variance
print(mean_and_variance([10, 20, 30, 40]))
# (25.0, 125.0)
As you can see, a tuple
is returned. If you want to assign the values to different variables, you can do the following.
mean, variance = mean_and_variance([10, 20, 30, 40])
print(mean) # 25.0
print(variance) # 125.0
Imagine that you do not want the variance. You can ignore it by using _
.
mean, _ = mean_and_variance([10, 20, 30, 40])
On the other hand, a function without an explicit return
will automatically return None
. The following function returns nothing, but Python automatically returns None
.
def my_function():
pass
output = my_function()
print(output)
# None
It is important to be aware of this because it can lead to hours of wasted time looking for where the None
is coming from. In some cases, it may be more difficult to notice.
def time_to_meal(hour):
match hour:
case 8:
return "Breakfast"
case 14:
return "Lunch"
case 21:
return "Dinner"
print(time_to_meal(7))
# None
Therefore, it is better to be explicit and take into account the None
. The result is the same, but it is more explicit.
def time_to_meal(hour):
match hour:
case 8:
return "Breakfast"
case 14:
return "Lunch"
case 21:
return "Dinner"
case _:
return None
print(time_to_meal(7))
# None
Functions can also be assigned to variables. In this case, say_hello
acts as an alias for greet
.
def greet(name):
return f"Hello, {name}!"
say_hello = greet
And we can call it.
print(say_hello("John"))
# Hello, John!
Functions can also be stored in a list. In this case, we store add
and subtract
in operations
.
def add(x, y):
return x + y
def subtract(x, y):
return x - y
operations = [add, subtract]
print(operations[0](10, 5)) # 15
print(operations[1](10, 5)) # 5
A function can also return another function by storing a context, in this case the factor
. This is known as a closure. It is just a function with another function inside plus some context.
def create_multiplier(factor):
def multiply(x):
return x * factor
return multiply
multiply_by_2 = create_multiplier(2)
multiply_by_3 = create_multiplier(3)
print(multiply_by_2(5)) # 10
print(multiply_by_3(5)) # 15
A function can also be an input argument to another. Inside the function, we can call the other one. You can see how execute
calls function
.
def execute(function, a, b):
return function(a, b)
def add(a, b):
return a + b
print(execute(add, 3, 4))
# 7
As you can see, you can do anything with functions. This is because Python treats functions as objects of the function
class.
def add(a, b):
return a + b
print(type(add))
# <class 'function'>
And as a last note, it is a good practice that your functions do not have side effects. That is, they do not modify external variables. This example is not ideal. The function modifies an external variable.
i = 0
def increment():
global i
i += 1
return i
print(increment()) # 1
print(increment()) # 2
print(increment()) # 3