1.3. Functions#
1.3.1. An Introduction to Functions#
In Python, the import
statement is used to bring functionality from other Python modules or packages into your current script or program. This allows you to use functions, classes, variables, and other resources defined in external modules to enhance the functionality of your code without having to rewrite everything from scratch.
The import
statement has different forms to import various components from modules [Downey, 2015, Python Software Foundation, 2023]:
1.3.1.1. Importing the whole module#
You can use the import
statement followed by the module name to import the entire module.
import math
result = math.sqrt(16)
print(result) # Output: 4.0
4.0
1.3.1.2. Importing specific items from a module#
You can import specific functions, classes, or variables from a module using the from
keyword.
from math import sqrt
result = sqrt(16)
print(result) # Output: 4.0
4.0
1.3.1.3. Importing with an alias:#
You can use the as
keyword to give a module or item an alias, making it easier to reference.
import math as m
result = m.sqrt(16)
print(result) # Output: 4.0
4.0
1.3.1.4. Importing all items from a module (not recommended):#
You can use the *
wildcard to import all items from a module. However, this approach is not recommended as it may lead to naming conflicts and make the code less readable.
from math import *
result = sqrt(16)
print(result) # Output: 4.0
4.0
The Python standard library comes with many built-in modules that provide a wide range of functionality, such as math operations, file handling, networking, etc. Additionally, you can create your own modules to organize your code and make it more reusable. To use external modules, ensure they are installed and accessible to your Python environment.
1.3.2. Adding new functions#
Adding new functions in Python is a fundamental way to extend the functionality of your code. To create a new function, you use the def
keyword, followed by the function name, a list of parameters (if any), and a colon. The function body is indented below the function definition, and it contains the code that defines what the function does [Downey, 2015, Python Software Foundation, 2023].
Here’s the basic syntax for defining a new function:
def function_name(parameter1, parameter2, ...):
# Function body
# Code to perform the task of the function
# Optional: return statement to return a value
Let’s see an example of defining and using a new function:
def greet(name):
"""
This function takes a 'name' as input and prints a greeting message.
"""
print("Hello", name,"!")
# Call the greet function with the argument "Alice"
greet("Alice")
# Output: "Hello, Alice!"
Hello Alice !
In the example above, we defined a new function named greet
that takes a single parameter name
. When called with an argument, the function prints a greeting message containing the provided name.
Functions can have multiple parameters, and they can also return values using the return
statement. Here’s an example of a function that calculates the sum of two numbers and returns the result:
def add_numbers(a, b):
"""
This function takes two numbers 'a' and 'b' as input
and returns their sum.
"""
return a + b
# Call the add_numbers function with arguments 5 and 10
result = add_numbers(5, 10)
print(result) # Output: 15
15
When you call a function with arguments, the values you pass are assigned to the function’s parameters. Inside the function, you can then perform any operations using those parameter values.
Remember to define functions before calling them in your code. Functions allow you to encapsulate logic, promote code reuse, and make your programs more organized and easier to understand.
1.3.3. Parameters and arguments#
In Python functions, the terms “parameters” and “arguments” are used to describe the input values that a function can accept. These terms are often used interchangeably, but they have distinct meanings in the context of functions:
1.3.3.1. Parameters:#
Parameters are the variables listed in the function definition, representing the input values that the function expects. They act as placeholders for the actual values that will be provided when the function is called. Parameters are defined inside the parentheses ()
following the function name [Downey, 2015, Python Software Foundation, 2023].
Example:
Example:
def greet(name):
# 'name' is the parameter of the function
print("Hello", name,"!")
In this example, name
is the parameter of the greet
function, representing the name of the person to greet.
1.3.3.2. Arguments:#
Arguments are the actual values passed to a function when it is called. They provide the values that correspond to the function’s parameters. When you call a function, the arguments are provided inside the parentheses ()
[Downey, 2015, Python Software Foundation, 2023].
Example:
# 'Alice' is the argument passed to the 'greet' function
greet("Alice")
Hello Alice !
In this example, "Alice"
is the argument passed to the greet
function, and it will be assigned to the name
parameter inside the function.
It’s important to note that the number of arguments provided during the function call must match the number of parameters defined in the function’s definition. If the function expects a certain number of parameters, you must provide the same number of arguments during the function call.
There are also different ways to pass arguments to functions:
Positional Arguments: These are arguments passed to the function based on their position. The order of arguments matters.
Example:
def add_numbers(a, b):
return a + b
result = add_numbers(5, 10)
print(result)
15
Keyword Arguments: These are arguments passed with their corresponding parameter names, regardless of their positions.
Example:
def greet(name, age):
print("My Name is", name, ", and I am", age ,"years old.")
greet(name="John Doe", age=35)
My Name is John Doe , and I am 35 years old.
Default Arguments: These are parameters that have a default value assigned in the function definition. If the corresponding argument is not provided during the function call, the default value is used.
Example:
def greet(name, greeting="Hello"):
print(greeting + ", " + name + "!")
greet("Alice")
greet("Bob", greeting="Hi")
Hello, Alice!
Hi, Bob!
Understanding parameters and arguments is crucial for writing functions that can accept input and perform operations based on that input. It also allows you to create flexible and reusable functions in Python.
1.3.4. Variables and parameters are local#
In Python, variables and parameters defined within a function are considered local to that function. This means that they have a local scope and are only accessible within the body of the function where they are defined. Variables and parameters with the local scope are created when the function is called and destroyed when the function execution is completed.
Let’s see an example to illustrate local variables and parameters:
def my_function(x, y):
# x and y are parameters (local variables) within the function
result = x + y
print("Inside the function:", result)
my_function(5, 10)
# Output: "Inside the function: 15"
# Trying to access the local variable 'result' outside the function will raise an error
# print("Outside the function:", result)
# Uncommenting the above line will raise a NameError: name 'result' is not defined
Inside the function: 15
In this example, x
and y
are parameters (local variables) of the my_function
. They are accessible only within the function, and trying to access them outside the function scope would raise an error.
It’s important to note that variables defined outside the function, in the global scope, are not directly accessible within the function unless explicitly passed as arguments. If you want to modify a global variable inside a function, you need to use the global
keyword to indicate that the variable is global and not local.
Example:
global_variable = 10
def modify_global():
# To modify a global variable inside a function, use the 'global' keyword
global global_variable
global_variable += 5
modify_global()
print(global_variable) # Output: 15
15
As seen in the example, the global_variable
is modified inside the function modify_global
using the global
keyword.
Summary
In summary, variables and parameters defined inside a function have local scope, meaning they are accessible only within that function. If you need to use a variable from the global scope inside a function or modify a global variable, you need to explicitly use the global
keyword.
1.3.4.1. The role of “__main__”#
In Python, __main__
is a special built-in variable or name that holds the name of the script that is currently being executed as the main program. It is used to differentiate between the script that is being run directly and the script that is being imported as a module into another program [Downey, 2015, Python Software Foundation, 2023].
When a Python script is executed directly, the Python interpreter assigns the value "__main__"
to the __name__
variable, indicating that this script is the main program. On the other hand, if the script is imported as a module into another script, the __name__
variable will be set to the name of the module (i.e., the name of the imported script).
Consider the following example:
Suppose you have two Python scripts: main_script.py
and module_script.py
.
main_script.py:
def hello():
print("Hello from the main script!")
if __name__ == "__main__":
hello()
Hello from the main script!
module_script.py:
def hello():
print("Hello from the module script!")
print("This is the module script.")
This is the module script.
Now, if you run main_script.py
directly from the command line (or a terminal):
python main_script.py
Output:
Hello from the main script!
Note
The “main” construct in Python is indeed specific to Python script files (“.py”) and not applicable to Jupyter Notebooks. In Python, when a script is executed, the special variable “name” is set to “main” if the script is the entry point of the program. This allows you to write code that can be both used as a standalone script and imported as a module into other scripts.
However, in Jupyter Notebooks, the execution model is different. Cells in a Jupyter Notebook are executed independently, and there is no concept of a main script. Instead, each cell is executed as it is run, and variables and functions defined in one cell can be used in subsequent cells within the same notebook.
Therefore, you won’t typically find the “main” construct in Jupyter Notebooks, as it’s not needed in this context. Instead, you can execute code directly within the cells of the notebook to achieve the desired functionality.
1.3.5. Stack diagrams#
A stacking diagram, also known as a call stack or function call stack, is a graphical representation of the execution flow of a program involving function calls. It helps us to visualize how functions are called, executed, and returned in a program [Downey, 2015, Python Software Foundation, 2023].
Here’s how a stack diagram works in Python:
Whenever a function is called, a new “frame” is created on top of the stack. The frame contains information about the function call, such as the local variables, parameters, and the return address.
The current function’s frame is always on top of the stack, representing the currently executing function.
When a function is done executing, its frame is removed from the stack, and the control returns to the previous function.
The process continues until the main program is completed, and there are no more function calls to be made.
Example:
def add(a, b):
return a + b
def multiply(a, b):
return a * b
def main():
x = 2
y = 3
sum_result = add(x, y)
product_result = multiply(x, sum_result)
print("Final Result:", product_result)
main()
Final Result: 10
In the diagram:
The main program starts executing, and variables
x
andy
are assigned values.The
add
function is called with argumentsx
andy
, resulting insum_result
being assigned the value5
.The
multiply
function is called with argumentsx
andsum_result
, resulting inproduct_result
being assigned the value10
.The final result,
"Final Result: 10"
, is printed.The call stack then starts unwinding as the functions complete their execution, eventually reaching back to the
main()
function.
1.3.6. Fruitful functions and void functions#
1.3.6.1. Fruitful Functions:#
Fruitful functions are functions that return a value after performing some computation or processing. They use the return
statement to send a value back to the caller. The returned value can be assigned to a variable or used directly in the calling code [Downey, 2015, Python Software Foundation, 2023].
Example of a fruitful function:
def add_numbers(a, b):
return a + b
result = add_numbers(3, 5)
print(result) # Output: 8
8
In the example above, add_numbers
is a fruitful function that takes two arguments a
and b
, performs addition, and returns the result.
1.3.6.2. Void Functions:#
Void functions, also known as non-fruitful functions or procedures, are functions that do not return a value. They perform some actions or side effects but do not send anything back to the caller. Instead of using the return
statement, they may have print statements, modify global variables, or perform other actions [Downey, 2015, Python Software Foundation, 2023].
Example of a void function:
def greet(name):
print("Hello,", name, "! Welcome to ENGG 680!")
greet("John")
Hello, John ! Welcome to ENGG 680!
In this example, greet
is a void function that takes a name
as an argument and prints a greeting message.
It’s important to note that all Python functions return a value, even if you don’t explicitly use the return
statement. If the return
statement is omitted, the function returns None
, which represents the absence of a value. So, even if a function does not have a return
statement, it is technically a fruitful function that implicitly returns None
.
Observe that:
def say_hello():
print("Hello, World!")
result = say_hello()
print(result) # Output: None
Hello, World!
None
In this case, the say_hello
function implicitly returns None
, which is then assigned to the variable result
.
Summary
To summarize, fruitful functions return a value using the return
statement, void functions do not return anything explicitly, and they may have side effects in the form of print statements or other actions. Both types of functions are useful in different scenarios depending on the task they need to perform.
1.3.7. Functions within other functions#
In Python, the concept of nested functions, also known as inner functions, involves the ability to define one function within another function. This nested structure is grounded in the principles of scope and encapsulation, making it a valuable feature for creating organized, modular, and efficient code. By defining functions within specific contexts, you can maintain a cleaner codebase, prevent global namespace pollution, and even leverage advanced techniques such as closures and decorators [Python Software Foundation, 2023].
Consider the following example to illustrate the idea of nested functions:
def main_function(x, y):
def square(a):
return a ** 2
def add(a, b):
return a + b
def multiply(a, b):
return a * b
result1 = square(x)
result2 = add(result1, y)
result3 = multiply(result1, result2)
return result3
a = 3
b = 4
final_result = main_function(a, b)
print("The final result is:", final_result)
The final result is: 117
This example demonstrates how you can define and utilize multiple functions within the body of a single function to create more complex behavior and calculations. Details:
The
square
,add
, andmultiply
functions are all defined within the body of themain_function
.The
square
function calculates the square of its input.The
add
function performs addition between two values.The
multiply
function calculates the product of two values.The
main_function
takes two arguments,x
andy
. Inside this function, thesquare
,add
, andmultiply
functions are called to perform various calculations.The
a
andb
variables are set to 3 and 4, respectively.The
main_function
is called witha
andb
as arguments, and the final result is stored in thefinal_result
variable.
1.3.7.1. Nested and closures functions#
Nested functions are particularly valuable in scenarios where you need to create utility functions or helpers that are closely related to the main function, but you want to encapsulate them to maintain code organization and prevent clashes in the global namespace.
Another powerful use case for nested functions is when you need to create closures. Closures are functions that retain access to the variables in their enclosing scope even after the outer function has completed execution. This property allows you to create specialized functions, such as decorators in Python.
Imagine you have a main function that defines another function inside it. This inner function can still remember and use variables from the main function, even after the main function has finished running. It’s like the inner function carries around a piece of the main function’s memory. This unique behavior allows you to create special functions that “remember” certain things from the past, and one common use of this is in creating decorators in Python. Decorators are functions that can modify the behavior of other functions in a flexible and convenient way. So, closures enable you to do some clever tricks with functions that remember their past context [Python Software Foundation, 2023].
def multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = multiplier(2)
triple = multiplier(3)
print(double(5)) # Output: 10
print(triple(5)) # Output: 15
10
15
This Python code snippet defines a function called multiplier
which takes a single argument, factor
. Within this multiplier
function, another nested function named multiply
is defined. The multiply
function, in turn, takes an argument x
and returns the result of multiplying x
by the factor
provided to the outer multiplier
function.
Here is a step-by-step breakdown of the code:
def multiplier(factor):
: This line defines themultiplier
function, which takes a single argumentfactor
.def multiply(x):
: Inside themultiplier
function, a nested functionmultiply
is defined. This inner function takes a single argumentx
.return x * factor
: Themultiply
function returns the result of multiplyingx
by thefactor
. This multiplication operation is performed using the value offactor
that was provided when calling the outermultiplier
function.return multiply
: The outermultiplier
function returns themultiply
function itself as its result. This means that when you callmultiplier(2)
, it returns themultiply
function with afactor
of 2, and when you callmultiplier(3)
, it returns themultiply
function with afactor
of 3.double = multiplier(2)
: This line assigns themultiply
function with afactor
of 2 to the variabledouble
. Essentially,double
now represents a function that will double any number passed to it.triple = multiplier(3)
: Similarly, this line assigns themultiply
function with afactor
of 3 to the variabletriple
.triple
now represents a function that will triple any number passed to it.print(double(5))
: Here, we call thedouble
function with an argument of 5. Sincedouble
was created with afactor
of 2, it multiplies 5 by 2 and prints the result, which is 10.print(triple(5))
: This line calls thetriple
function with an argument of 5. Sincetriple
was created with afactor
of 3, it multiplies 5 by 3 and prints the result, which is 15.
In summary, the code demonstrates the concept of closures in Python, where the multiply
function “remembers” the value of factor
from the outer multiplier
function, allowing you to create specialized multiplication functions (double
and triple
) based on different factors. When these specialized functions are called, they apply the corresponding multiplication factor to the input value.
1.3.8. Lambda functions#
Lambda functions, also known as anonymous functions or lambda expressions [McKinney, 2022], are a feature in Python that allows you to create small, unnamed functions without explicitly defining them using the def
keyword. Lambda functions are often used when you need a simple function for a short period and don’t want to create a named function using def
.
Here is the basic syntax of a lambda function:
lambda arguments: expression
lambda
: The keyword used to define a lambda function.arguments
: The input parameters or arguments for the function.expression
: The single expression or calculation that the function will return.
Lambda functions can take multiple arguments separated by commas, but they should consist of a single expression. The result of the expression is automatically returned by the lambda function.
Example: A lambda function that adds two numbers:
# Define a lambda function 'add' that takes two arguments x and y and returns their sum.
add = lambda x, y: x + y
# Call the lambda function with arguments 5 and 3 and store the result in 'result'.
result = add(5, 3)
# Print the result of the addition.
print(result) # Output: 8
8
Example: A lambda function to square a number:
# Define a lambda function 'square' that takes a single argument x and returns its square.
square = lambda x: x ** 2
# Call the lambda function with argument 4 and store the result in 'result'.
result = square(4)
# Print the result, which is the square of 4.
print(result) # Output: 16
16
Example: Sorting a list of tuples based on the second element using lambda:
# Create a list 'pairs' containing tuples with two elements each.
pairs = [(1, 5), (2, 2), (3, 8), (4, 1)]
# Sort the 'pairs' list based on the second element (index 1) of each tuple.
# The key argument specifies the sorting criterion using a lambda function.
sorted_pairs = sorted(pairs, key=lambda x: x[1])
# Print the sorted list of pairs.
print(sorted_pairs) # Output: [(4, 1), (2, 2), (1, 5), (3, 8)]
[(4, 1), (2, 2), (1, 5), (3, 8)]
Note
Both [(1, 5), (2, 2), (3, 8), (4, 1)]
and [(4, 1), (2, 2), (1, 5), (3, 8)]
are instances of lists containing tuples. We will delve deeper into the concepts of lists and tuples in Chapter 3.
1.3.9. The Benefits of Using Functions in Programming#
The utilization of functions within a program might not immediately reveal its advantages, but upon closer examination, there are several compelling reasons why incorporating functions into your codebase is highly beneficial [Downey, 2015, Python Software Foundation, 2023]:
Enhanced Readability and Simplified Debugging: The practice of creating distinct functions facilitates the organization of related statements under descriptive names. This structural approach significantly contributes to the clarity and comprehensibility of your program. Moreover, during the debugging phase, well-defined functions simplify the identification of errors, allowing for targeted fixes within specific segments of code.
Promotes Code Reusability: Functions serve as powerful tools for minimizing code repetition. By encapsulating repetitive code within function definitions, you effectively centralize and maintain such code in a singular location. Consequently, any modifications or updates applied to the function instantly propagate across all instances where the function is invoked. This strategy not only reduces redundancy but also ensures consistency throughout the program.
Modularity and Facilitated Testing: Dividing a complex program into smaller, self-contained functions introduces a modular framework that greatly facilitates the debugging process. By addressing the testing and debugging of individual functions independently, you adopt an incremental approach that makes the overall debugging endeavor more manageable and efficient. This systematic approach allows for comprehensive testing of each component before its integration into the complete program.
Facilitates Repurposing: Proficiently designed functions, once crafted and debugged, hold the potential to be seamlessly integrated into multiple programs. This practice of function reuse streamlines the development trajectory of subsequent projects. By harnessing proven and tested code, you circumvent the need for redundant implementations, thus accelerating the development process and promoting consistency.
In essence, the incorporation of functions into programming not only enhances the legibility and maintainability of code but also fosters efficient development practices through the facilitation of debugging, code reuse, and modular testing.