HyperLearning AI - Introduction to Python

An introductory course to the Python 3 programming language, with a curriculum aligned to the Certified Associate in Python Programming (PCAP) examination syllabus (PCAP-31-02).
https://knowledgebase.hyperlearning.ai/courses/introduction-to-python

06. Functions and Modules Part 1

https://knowledgebase.hyperlearning.ai/courses/introduction-to-python/modules/6/functions-and-modules-part-1

In this module we will formally introduce functions, generator functions and lambda functions in Python, starting with formal definitions before exploring their structures and core components, including:

  • Functions - bespoke functions, parameters, arguments, default parameters, name scope, and important keywords such as return, None and global
  • Generator Functions - bespoke generators, lazy evaluation and lazy iterators, generator termination and generator expressions
  • Lambda Functions - functional programming, anonymous functions, and lambda functions within filter, map, reduce, sorted and reversed functions

1. Functions

In [1]:
# A simple example of a user-defined function
def product(number_1, number_2):
    return number_1 * number_2
In [2]:
# Call the user-defined function
a = 12
b = 20
c = product(a, b)
print(c)
240

1.1. Defining Functions

In [1]:
# Define a function to test whether a given number is a prime number or not
def is_prime(num):
    """ Test whether a given number is a prime number or not.
    
    Tests whether a given number is a prime number or not,
    by first testing whether it is 0, 1, negative or not a
    whole number. If neither of these conditions are met, 
    then the function proceeds to test whether the given
    number can be divided by the numbers from 2 to the
    floor division of the given number by 2 without a
    remainder. If not, then the given number is indeed a 
    prime number.
    
    Args:
        num (int): The number to test
        
    Returns:
        True if the number is a prime number,
        False otherwise.
    """
    
    if num <= 1 or num % 1 > 0:
        return False
    
    for i in range(2, num//2):
        if num % i == 0:
            return False
        
    return True

1.2. Calling Functions

In [2]:
# Call the is_prime() function on a given integer
a = 8
print(f'Is the number {a} a prime number? {is_prime(a)}')
b = 13
print(f'Is the number {b} a prime number? {is_prime(b)}')
c = 277
print(f'Is the number {c} a prime number? {is_prime(c)}')
d = -23
print(f'Is the number {d} a prime number? {is_prime(d)}')
e = 7.181
print(f'Is the number {e} a prime number? {is_prime(e)}')
f = 0
print(f'Is the number {f} a prime number? {is_prime(f)}')
Is the number 8 a prime number? False
Is the number 13 a prime number? True
Is the number 277 a prime number? True
Is the number -23 a prime number? False
Is the number 7.181 a prime number? False
Is the number 0 a prime number? False
In [3]:
# Call a function with an incorrect number of positional arguments
print(is_prime())
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-38e10062a072> in <module>
      1 # Call a function with an incorrect number of positional arguments
----> 2 print(is_prime())

TypeError: is_prime() missing 1 required positional argument: 'num'

1.3. Arbitrary Arguments

In [8]:
# Define a function to expect an arbitrary number of arguments
def product(*nums):
    x = 1
    for num in nums:
        x *= num
    return x
In [18]:
# Call this function with a tuple of arguments
print(product(2, 10, 5))

my_numbers = (9, 11, 2)
print(f'\nThe product of the numbers {my_numbers} is {product(*my_numbers)}')
100

The product of the numbers (9, 11, 2) is 198

1.4. Unpacking Argument Lists

In [4]:
# Define a function expecting separate positional arguments
def findPrimesBetween(start_num, end_num):
    return [num for num in range(start_num, end_num) if is_prime(num)]
In [5]:
# Call this function using argument unpacking
args = [1, 100]
print(findPrimesBetween(*args))
[2, 3, 4, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

1.5. Keyword Arguments

In [23]:
# Call a function using keyword arguments
print(findPrimesBetween(start_num = 100, end_num = 200))
[101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199]

1.6. Arbitrary Keyword Arguments

In [24]:
# Define a function to expect an arbitrary number of keyword arguments
def concatenate_words(**words):
    return ' '.join(words.values())
In [28]:
# Call this function with a dictionary of arguments
print(concatenate_words(word_1="Wonderful", word_2="World", word_3="of", word_4="Python"))
Wonderful World of Python

1.7. Default Parameter Values

In [6]:
# Define a function with default parameter values
def findPrimesBetween(start_num = 2, end_num = 100):
    return [num for num in range(start_num, end_num) if is_prime(num)]
In [7]:
# Call this function without sending selected arguments
print(findPrimesBetween())
print(findPrimesBetween(end_num = 50))
print(findPrimesBetween(start_num = 25))
print(findPrimesBetween(start_num = 1000, end_num = 1250))
print(findPrimesBetween(100, 200))
[2, 3, 4, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
[2, 3, 4, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
[29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
[1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249]
[101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199]

1.8. Passing Mixed Arguments

In [49]:
# Define a function with parameters of mixed types
def calculate_bmi(fname, lname, /, dob, *, weight_kg, height_m):
    bmi = round((weight_kg / (height_m * height_m)), 2)
    print(f'{fname} {lname} (DOB: {dob}) has a BMI of: {bmi}')
In [50]:
# Call this function with mixed arguments
calculate_bmi("Barack", "Obama", height_m = 1.85, weight_kg = 81.6, dob = '04/08/1961')
Barack Obama (DOB: 04/08/1961) has a BMI of: 23.84

1.9. Pass Statement

In [34]:
# Try to define a function without a body
def my_empty_function():
    
  File "<ipython-input-34-5b3e28f1020f>", line 3
    
    ^
SyntaxError: unexpected EOF while parsing
In [35]:
# Define an empty function using the pass keyword
def my_empty_function():
    pass

1.10. None Keyword and Returning None

In [52]:
# Define a function that does not return a value
def my_logging_function(message):
    print(message)
    
# Call this function and examine the type that it returns
log = my_logging_function("01/09/2020 00:01 - Audit Log 1")
print(type(log))
Log Record 1
<class 'NoneType'>

1.11. Function Recursion

In [55]:
# Calculate the Nth element in Fibonacci sequence using function recursion
def fibonacci_sequence(n):
    if n <= 1:
        return n
    else:
        return(fibonacci_sequence(n-1) + fibonacci_sequence(n-2))
In [58]:
# Calculate the Nth element in the Fibonacci sequence
print(fibonacci_sequence(10))

# Print the first N elements in the Fibonacci sequence
for num in range(0, 10):
    print(fibonacci_sequence(num), end = ' ')
55
0 1 1 2 3 5 8 13 21 34 

1.12. Name Scope and Global Keyword

In [63]:
# Define a function that contains variables with local scope
def calculate_bmi(weight_kg, height_m):
    weight_lb = weight_kg * 2.205
    height_inches = height_m * 39.37
    bmi = round((weight_lb / (height_inches * height_inches)) * 703, 2)
    return bmi
In [64]:
# Try to access a variable with local scope
print(weight_lb)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-64-36404fd71278> in <module>
      1 # Try to access those variables with local scope
----> 2 print(weight_lb)

NameError: name 'weight_lb' is not defined
In [65]:
# Define the kg to lb ratio
kg_lb = 2.205

# Define the metres to inches ratio
m_inches = 39.37

# Define a function that uses variables created outside of the function
def calculate_bmi(weight_kg, height_m):
    weight_lb = weight_kg * kg_lb
    height_inches = height_m * m_inches
    bmi = round((weight_lb / (height_inches * height_inches)) * 703, 2)
    return bmi

# Call this function
print(calculate_bmi(weight_kg = 81.6, height_m = 1.85))
23.84
In [74]:
# Define variables with global scope
x = 1
y = 2

# Define a simple function
def sum():
    x = 10
    y = 20
    return x + y

# Print the value of x
print(sum())
print(x)
30
1
In [75]:
# Define variables with global scope
x = 1
y = 2

# Define a simple function
def sum():
    global x
    x = 10
    y = 20
    return x + y

# Print the value of x
print(sum())
print(x)
30
10
In [76]:
# Define a function that creates new variables with global scope
def product():
    global alpha, beta
    alpha = 1000
    beta = 1_000_000
    return alpha * beta

# Print the value of alpha and beta
print(product())
print(alpha)
print(beta)
1000000000
1000
1000000

2. Generator Functions

2.1. Defining Generator Functions

In [2]:
# Define a generator function to generate an infinite sequence
def infinite_sequence_generator():
    counter = 0
    while True:
        yield counter
        counter += 1
In [3]:
# Call this generator function and lazily evaluate the next element in the iterable object
infinite_generator = infinite_sequence_generator()
print(next(infinite_generator))
print(next(infinite_generator))
print(next(infinite_generator))
print(next(infinite_generator))
print(next(infinite_generator))
0
1
2
3
4

2.2. Generator Termination

In [4]:
# Define a generator function to lazily return an ordered sequence of letters given a starting letter and an ending letter
def letter_sequence_generator(start, stop, step=1):
    for ord_unicode_int in range(ord(start.lower()), ord(stop.lower()), step):
        yield chr(ord_unicode_int)
In [5]:
# Call this generator function until there are no further elements in the sequence to be evaluated
alphabet = letter_sequence_generator("a", "e")
print(next(alphabet))
print(next(alphabet))
print(next(alphabet))
print(next(alphabet))
a
b
c
d
In [6]:
# Attempt to call the next() function again on the terminated generator function
print(next(alphabet))
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-6-a775c4f3c4ea> in <module>
      1 # Attempt to call the next() function again on the terminated generator function
----> 2 print(next(alphabet))

StopIteration: 

2.3. Generators and For Loops

In [7]:
# Use a for loop to iterate over the iterable object returned by our letter sequence generator function
alphabet = letter_sequence_generator("a", "z")
for letter in alphabet:
    print(letter)
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y

2.4. Generator Expressions

In [8]:
# Use a generator expression to create a lazily evaluated iterable object
first_million_numbers = (num for num in range(1, 1_000_000))
print(next(first_million_numbers))
print(next(first_million_numbers))
print(next(first_million_numbers))
1
2
3

2.5. Generator Objects as Lists

In [9]:
# Create a lazily evaluated generator object
alphabet = letter_sequence_generator("a", "z")

# Convert the generator object into a list
alphabet_list = list(alphabet)
print(alphabet_list)
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y']

3. Lambda Functions

3.1. Defining Lambda Functions

In [10]:
# Create a simple lambda function to square a given number
square = lambda n: n * n

# Call this lambda function
print(square(8))
64
In [11]:
# Create a simple lambda function with three arguments
product = lambda x, y, z: x * y * z

# Call this lambda function
print(product(3, 10, 5))
150

3.2. Using Lambda Functions

In [13]:
# Create a list of numbers
my_numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Use the imperative paradigm to filter the list of integers, keeping only the even numbers
def filter_evens_imperative(number_list):
    even_number_list = []
    for number in number_list:
        if number % 2 == 0:
            even_number_list.append(number)
    return even_number_list

# Filter the list of numbers, keeping only the even numbers
even_numbers = filter_evens_imperative(my_numbers)
print(even_numbers)
[2, 4, 6, 8, 10]
In [19]:
# Use a functional approach to filter the same list of integers
even_numbers = list(filter(lambda num: num % 2 == 0, my_numbers))
print(even_numbers)
[2, 4, 6, 8, 10]

3.2.1. User Defined Functions

In [22]:
# Create a Python function containing a lambda anonymous function
def multiply(multiplier):
    return lambda x: x * multiplier

# Instantiate a function that always doubles a given number
doubler = multiply(2)

# Call this function with the value 10
print(doubler(10))

# Instantiate a function that always quadruples a given number
quadrupler = multiply(4)

# Call this function with the value 100
print(quadrupler(100))
20
400

3.2.2. Filter Function

In [26]:
# Create a list of numbers
my_numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Create a function that will test whether a given number is even or not
def test_even(num):
    if num % 2 == 0:
        return True
    return False

# Apply this function to the list of numbers using the filter() function
my_even_numbers = list(filter(test_even, my_numbers))
print(my_even_numbers)
[2, 4, 6, 8, 10]
In [28]:
# Use a lambda function within filter() to perform the same task
my_even_numbers = list(filter(lambda num: num % 2 == 0, my_numbers))
print(my_even_numbers)
[2, 4, 6, 8, 10]

3.2.3. Map Function

In [32]:
# Create a function that will square a given number but only if that number is odd
def square_odd_number(num):
    if num % 2 == 0:
        return num
    return num * num

# Apply this function to the list of numbers using the map() function
my_squared_odd_numbers = list(map(square_odd_number, my_numbers))
print(my_squared_odd_numbers)
[1, 2, 9, 4, 25, 6, 49, 8, 81, 10]
In [36]:
# Use a lambda function within map() to perform the same task
my_squared_odd_numbers = list(map(lambda num: num if num % 2 == 0 else num * num, my_numbers))
print(my_squared_odd_numbers)
[1, 2, 9, 4, 25, 6, 49, 8, 81, 10]

3.2.4. Reduce Function

In [42]:
from functools import reduce 

# Create a function that will calculate the product of two given numbers
def product(a, b):
    return a * b

# Apply this function to the list of numbers using the reduce() function
my_product_of_numbers = reduce(product, my_numbers)
print(my_product_of_numbers)

# Apply this function to the list of numbers using the reduce() function and an initializer value of 2
my_product_of_numbers = reduce(product, my_numbers, 2)
print(my_product_of_numbers)

# Apply this function to the list of numbers using the reduce() function and an initializer value of 10
my_product_of_numbers = reduce(product, my_numbers, 10)
print(my_product_of_numbers)
3628800
7257600
36288000
In [44]:
# Use a lambda function within reduce() to perform the same task
my_product_of_numbers = reduce(lambda a, b: a * b, my_numbers)
print(my_product_of_numbers)
3628800

3.2.5. Sorted Function

In [46]:
# Create a list of unordered numbers
my_unordered_numbers = [2, 100, 99, 3, 7, 8, 13, 48, 88, 38]

# Sort the list of numbers
sorted_numbers = sorted(my_unordered_numbers)
print(sorted_numbers)

# Sort the list of numbers in descending order
sorted_numbers_desc = sorted(my_unordered_numbers, reverse=True)
print(sorted_numbers_desc)
[2, 3, 7, 8, 13, 38, 48, 88, 99, 100]
[100, 99, 88, 48, 38, 13, 8, 7, 3, 2]
In [47]:
# Create a list of tuples modelling a football league (team name, total points, total goals scored, total goals conceded)
completed_football_league = [
    ('Manchester United', 75, 64, 35), 
    ('Aston Villa', 56, 48, 44), 
    ('Arsenal', 90, 73, 26), 
    ('Newcastle United', 56, 52, 40), 
    ('Liverpool', 60, 55, 37), 
    ('Chelsea', 79, 67, 30)
]

# Create a function to create a compound sorting key
def sorting_key(item):
    total_points = item[1]
    goal_difference = item[2] - item[3]
    return (total_points, goal_difference)

# Use this sorting function with the sorted() function to sort the football league by multiple keys i.e. total points and goal difference
sorted_football_league = sorted(completed_football_league, key=sorting_key, reverse=True)
print(sorted_football_league)
[('Arsenal', 90, 73, 26), ('Chelsea', 79, 67, 30), ('Manchester United', 75, 64, 35), ('Liverpool', 60, 55, 37), ('Newcastle United', 56, 52, 40), ('Aston Villa', 56, 48, 44)]
In [49]:
# Use a lambda function within sorted() as the key function to perform the same task
sorted_football_league = sorted(completed_football_league, key=lambda item: (item[1], item[2] - item[3]), reverse=True)
print(sorted_football_league)
[('Arsenal', 90, 73, 26), ('Chelsea', 79, 67, 30), ('Manchester United', 75, 64, 35), ('Liverpool', 60, 55, 37), ('Newcastle United', 56, 52, 40), ('Aston Villa', 56, 48, 44)]

3.2.6. Reversed Function

In [55]:
# Use the reversed() function to return a reversed iterable object
my_reversed_numbers = list(reversed(my_numbers))
print(my_reversed_numbers)

# Reverse a range of numbers
my_range = range(11, 21)
my_reversed_range = list(reversed(my_range))
print(my_reversed_range)

# Reverse the characters in a string
my_string = 'abracadabra'
my_reversed_string = list(reversed(my_string))
print(my_reversed_string)
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
[20, 19, 18, 17, 16, 15, 14, 13, 12, 11]
['a', 'r', 'b', 'a', 'd', 'a', 'c', 'a', 'r', 'b', 'a']

3.2.7. Sort Method

In [56]:
import copy

# Create a deepcopy of the previous list of tuples representing a football league
completed_football_league_deep_copy_1 = copy.deepcopy(completed_football_league)

# Sort the previous list of tuples representing a football league using the list sort() method
completed_football_league_deep_copy_1.sort(reverse=True, key=sorting_key)
print(completed_football_league_deep_copy_1)
[('Arsenal', 90, 73, 26), ('Chelsea', 79, 67, 30), ('Manchester United', 75, 64, 35), ('Liverpool', 60, 55, 37), ('Newcastle United', 56, 52, 40), ('Aston Villa', 56, 48, 44)]
In [57]:
# Use a lambda function within the list sort() method as the key function to perform the same task
completed_football_league_deep_copy_2 = copy.deepcopy(completed_football_league)
completed_football_league_deep_copy_2.sort(reverse=True, key=lambda item: (item[1], item[2] - item[3]))
print(completed_football_league_deep_copy_2)
[('Arsenal', 90, 73, 26), ('Chelsea', 79, 67, 30), ('Manchester United', 75, 64, 35), ('Liverpool', 60, 55, 37), ('Newcastle United', 56, 52, 40), ('Aston Villa', 56, 48, 44)]