# 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

# Call this function with the value 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_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)]