Functions#

  • Consider a mathematical function like \(z = 3x + y\) evaluated when \(x = 3\) and \(y = 6\)

    • \(x\) and \(y\) are input into the function as 3 and 6 and \(z\) is output as 15

_images/mathfunction.png
  • Functions in computer programming can be thought of as a generalization of mathematical functions

    • Finite number of inputs (or parameters or arguments) are provided

      • Functions can have 0 inputs too

    • Result is an output

      • E.g., int, float, list objects

      • The output can also be None (e.g., print() function)

    • Some functions modify an input object if it is mutable (e.g., list, dict)

_images/progfunction.png
  • Functions are a control flow tool that:

    • Encapsulate a block of code that can be called on to perform some task repeatedly

    • Modularize the program

    • Help in dividing and conquering a problem to be solved

    • Aid in identifying and fixing bugs

  • Functions are objects too in Python

    • Function name is the identifier

    • They are of type function for user-defined functions or builtin_function_or_method for built-in functions

  • A function is a compound statement

def function_name(parameter1, parameter2, ..., parameterN):
    '''
    Descriptive string which is accessed by help function
    '''
    # body of function
    return output_parameters # optional
  • def is the required Python keyword

  • function_name is the name/identifier of the function

  • Function parameters/arguments follow the function name within ()

  • The def line is ended with a :

  • Function body - indented code that will be executed on calling the function

    • An optional help string can be included in the beginning

https://docs.python.org/3/tutorial/controlflow.html#defining-functions https://docs.python.org/3/reference/compound_stmts.html?highlight=function#function-definitions

  • Functions must be called to do anything

    • To call a function, simply use its name and pass in necessary arguments

    • E.g len("Hello") calls the len function passing in the argument "Hello"

_images/len.png
  • Functions must be defined before they are called

Examples#

def mathFun(x, y):
    z = 3 * x + y
    return z

a, b = 3, 6
c = mathFun(a, b)
print(f"{c} = 3*{a} + {b}")
15 = 3*3 + 6
def math_function(x, y):
    z = 3*x + y
    return z

x, y = 3, 6
z = math_function(x, y)
print(z)
15
  • The variable names x and y can be reused outside the function definition

  • Write a function that defines the determinant and use it in a program

    • Include help text

\[\begin{split} A = \begin{bmatrix} a & b \\ c & d \end{bmatrix} \\ \end{split}\]
\[ \vert A \vert = ad - bc \]
def determinant(a, b, c, d):
    '''
    Function to find the determinant of a 2x2 matrix
        [a, b
         c, d]
    '''
    det = a*d - b*c
    print(det)

determinant(1, 2, 3, 4)
-2
help(determinant)
Help on function determinant in module __main__:

determinant(a, b, c, d)
    Function to find the determinant of a 2x2 matrix
        [a, b
         c, d]
type(determinant)
function
  • The output of a function is captured by a variable with an assignment operation with the function call on the RHS

d = determinant(1, 3, 5, 7)
type(d)
-8
NoneType
  • The output of this function is None

    • This is to say the function provides no output

    • Functions with no output are referred to as void functions in C/C++

  • A function can be redefined with a different set of parameters and body (just like any other variable)

def determinant():
    pass # Do nothing

determinant() # No output
  • This function now has neither an input nor an output

The return Statement#

  • An output specifically requires the return statement

    • Text displayed by print functions are not ouput

  • The return statement:

    • Signifies the end of the function body

    • The variable(s)/expression next to the return keyword will be the output of the function

    • If not used or no variable follows the return keyword, the output is None and the function is a void function

    • Expressions and statements following this in the function’s body will be ignored

def determinant(a, b, c, d):
    det = a*d - b*c
    return det

determinant(1, 3, 4, 2)
-10
  • To capture the returned output, use the assignment operator

d = determinant(1, 3, 4, 2)
d
-10
def determinant(a, b, c, d):
    det = a*d - b*c
    return det

    det *= 2

determinant(1, 3, 4, 2)
-10
  • The code following the return statement is ignored, even if it is syntactically correct

Function Objects#

  • Functions can be copied

    • Allows for creating aliases for function names

    • Helpful when you wish to have a shorthand for a commonly used function

show = print
a = (1 + 5**0.5)/2
show(f"Golden Ratio = {a:0.6f}")
Golden Ratio = 1.618034
  • The function show will behave exactly like print

show("x", "y", sep=' --> ')
x --> y
  • The assignment operator creates a reference to the print function object

# Both function objects have the same id
show is print
True

Recursive Functions#

  • A recursive function is a function that calls itself

    • Word of caution: recursion, while elegant, may not be the most effective solution in terms of computational time and memory usage

  • Ex: factorial of a number using recursion

\[ n! = \prod_{i=1}^n i = n \times \prod_{i=1}^{n-1} i = n\times (n-1)! \]
def fact(n):
    if n == 1:
        return 1
    
    # Recursion
    return n*fact(n-1)    
fact(6)   
720
%%html
<iframe width="1200" height="600" frameborder="0" src="https://pythontutor.com/iframe-embed.html#code=def%20fact%28n%29%3A%0A%20%20%20%20if%20n%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20%0A%20%20%20%20%23%20Recursion%0A%20%20%20%20return%20n*fact%28n-1%29%20%20%20%20%0A%20%20%20%20%0Afact%285%29&codeDivHeight=400&codeDivWidth=350&cumulative=true&curInstr=0&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe>

Argument Passing#

  • Function arguments can be passed using:

    • Positional arguments

    • Keyword arguments

    • Mix of positional and keyword arguments

      • Keyword arguments must follow positional arguments

    • Keyword-only or positional-only arguments

    • Variable length arguments

      • Argument tuple packing

      • Argument dictionary packing

  • Default parameters

    • Created only once per function

    • Care should be taken when the datatype is mutable

Positional Arguments#

  • Arguments are passed in the same order as the parameter list

  • The number of arguments should match the number of parameters

  • This is the common way of passing arguments

# Function that emulates the range object
def myRangeFun(start, stop, step):
    '''My range implementation'''
    # Accumulator
    seq = []
    
    # Loop
    nextVal = start
    while True:
        if nextVal >= stop: 
            break
        seq.append(nextVal)
        nextVal += step
    return seq
print(myRangeFun(4, 10, 1))
print(list(range(4, 10)))
[4, 5, 6, 7, 8, 9]
[4, 5, 6, 7, 8, 9]
  • What is the expected and actual output?

myRangeFun(10, 0, -2)
[]
  • This implementation of range is not handling decreasing sequences

Keyword Arguments#

  • The keywords (parameter names) are used to pass the arguments

  • Arguments can be passed in any order, but their number should still match the number of parameters

# Function that emulates the range object
def myRangeFun(start, stop, step):
    '''My range implementation'''
    # Accumulator
    seq = []
    
    # Loop
    nextVal = start
    while True:
        if nextVal >= stop: 
            break
        seq.append(nextVal)
        nextVal += step
    return seq

myRangeFun(step = 2, stop = 10, start = 0)
[0, 2, 4, 6, 8]

Positional and Keyword Arguments#

  • Positional and keyword arguments can be combined

    • e.g. print("Hello", end = ' ')

  • However, positional arguments cannot follow keyword arguments

    • Will result in an exception

myRangeFun(start = 2, 10, step = 2)
  Cell In[21], line 1
    myRangeFun(start = 2, 10, step = 2)
                                      ^
SyntaxError: positional argument follows keyword argument
# Correct
myRangeFun(2,  step = 2, stop = 10)

Default Parameters#

  • Functions can have default parameters

    • The default is used when no input is specified

# Function that emulates the range object
def myRangeFun(start = 0, stop = 10, step = 2):
    '''My range implementation'''
    # Accumulator
    seq = []
    
    # Loop
    nextVal = start
    while True:
        if nextVal >= stop: 
            break
        seq.append(nextVal)
        nextVal += step
    return seq

myRangeFun()
  • Default parameters can be overridden when the function is called

    • e.g. print("Hello", sep = '/n') will overwrite default for sep but still use default for end

myRangeFun(2)
myRangeFun(2, stop = 4)
  • Non-default parameters cannot follow default parameters

    • E.g., this is not valid:

def myRangeFun(start=0, stop, step=2):
    pass

Example#

  • Function to average three numbers

def average(a, b, c):
    return (a+b+c)/  3
  • Consider using this function with positional only, keyword only, or positional + keyword arguments

average(1.2, 0, -1.2)               # positional
average(c = -1.2, b = 0, a = 1.2)     # keyword
average(1.2, c = -1.2, b = 0)           # mixed
  • How can you modify the function to include default parameters?

# With default parameters
def average(a=0, b=0, c=0):
    return (a+b+c)/3
average()         # Positional
average(c = 3)    # Keyword
average(1, c = 3)   # Mixed

Variable Length Arguments#

  • What if you do not know a priori how many parameters the function needs?

    • One option is to use a tuple or list

    • Another option is to use the built-in functionality of:

      • Argument tuple packing

      • Argument dictionary packing

  • Argument tuple packing packs arbitrary number of inputs into a single tuple object

def average(*args, scale=1):
    n = len(args)
    tot = sum(args)
    return tot/n*scale
  • The asterisk * tells Python that the arguments for this function should be packed into a tuple with specified parameter name args

  • scale becomes a keyword-only argument with a default value of 1

    • Must be passed with the keyword

average(1, 2, 3, 4, 5)
  • The built in function print uses tuple packing and keyword-only default parameters

help(print)
  • Argument dictionary packing packs arbitrary number of inputs into a single dictionary object

def average(**kwargs):
    for key, val in kwargs.items():
        print(f"{key}->{val}", end=', ')
    print()

    n = len(kwargs)
    total =sum(kwargs.values()) 
    return total/n
  • Use of two asterisks ** indicates that the arguments to this functions should be packed into a single dict object with the specified parameter name kwargs

  • All inputs should be keyword arguments

average(a = 1 , b = 3, c = 2.4, d = 5.3)
average(1, 2, c = 3, d = 5) # Error - no keywords for first two inputs

More Argument Passing#

  • Other possibilities not discussed:

    • Keyword-only arguments

    • Positional-only arguments

    • Positional arguments with tuple and/or dictionary packing

    • Tuple and dictionary unpacking

Lambda Functions#

  • An anonymous inline function with a single expression

  • Syntax:

lambda arguments: expression
adder = lambda x, y: x + y
adder(1, 2)
(lambda x, y, z: x*y*z)(1, 2, 3)
  • Sorting a list of tuples alphabetically with a lambda expression

pairs = [(1, 'January'), (2, 'February'), (3, 'March'), (4, 'April')]
pairs.sort(key = lambda pair: pair[1])
pairs
  • Sorting a list of tuples by string size with a lambda expression

pairs = [(1, 'January'), (2, 'February'), (3, 'March'), (4, 'April')]
pairs.sort(key = lambda pair: len(pair[1]))
pairs

Local vs. Global Variables#

  • A function has its own memory

  • Variables defined within a function are local, only existing within the function

    • Once a function completes, the local variables are forgotten

    • They cannot be accessed by the general program

  • Variables can be declared as global which makes them available outside of the function

def average(*args, scale=1):
    n = len(args)
    tot = sum(args)
    return tot/n*scale

a = average(1, 3, 5, 7)
print(tot)
  • The variables tot and n are local to the function average

tot = 0
def average(*args, scale=1):
    n = len(args)
    tot = sum(args)
    print(f'Inside the function - {tot}')
    return tot/n*scale

a = average(1, 3, 5, 7)
print(f'Outside the function - {tot}')
  • tot inside and outside the function will be treated differently and have different values

    • The tot outside the function is global

    • The tot inside the function is local

  • To use a local value outside the function, declare it global

    • If a variable with the same name exists outside the function, it is modified inside the function

    • Otherwise, the variable will be made available outside the function

def average(*args, scale=1):
    global tot
    n = len(args)
    tot = sum(args)
    print(f'Inside the function - {tot}')
    return tot/n*scale

a = average(1, 3, 5, 7)
print(f'Outside the function - {tot}')

Examples#

  • Conditional return

    • We can return values conditionally inside of our function

    • Note that only one return statement can be executed

      • Function immediately terminates once the first return is executed

    • Write a function that determines if a word or a sentence is a palindrome

def is_palindrome(word):
    word = word.lower().replace(' ', '')
    if word == word[::-1]:
        return True
    return False
  • replace method here removes spaces by replacing them with empty strings ''

test_word = 'tenet'
is_palindrome(test_word)
is_palindrome('never odd or even')
is_palindrome('3043')
  • Multiple outputs

    • Write a function that outputs both the sum and product of a set of numbers

      • Use variable length for the inputs

    • You can save the outputs to multiple individual variables or one tuple

def sum_prod(*nums):
    s, p = 0, 1
    for i in nums:
        s += i
        p *= i
    return s, p
output = sum_prod(1, 2, 3, 4)
print(output)
output1, output2 = sum_prod(1, 2, 3, 4)
print(output1)
print(output2)
  • Using return for error handling

    • Write a function to find the factors of a positive integer

    • Use an early return to avoid exceptions

      • return with no output is similar to break inside of a loop

def factors(num):
    if not(type(num) == int and num > 0):
        print("That wasn't a positive integer!")
        return
    
    factors = []
    for i in range(2, num//2 + 1):
        if num%i == 0:
            factors.append(i)

    return factors
factors(2450)
  • Using return for error handling

  • Write a function to find the factors of a positive integer

  • Use an early return to avoid exceptions

    • return with no output is similar to break inside of a loop

def factors(num):
    if not(type(num) == int and num > 0):
        print("That wasn't a positive integer!")
        return
    
    factors = []
    for i in range(2, num//2 + 1):
        if num%i == 0:
            factors.append(i)

    return factors
factors(2450)
  • Finding primes

    • Write a function to determine whether or not a number is prime. The function should take an integer input and return either True or False.

    • Use this function inside of a loop to create a list of all prime numbers from 1 to 100

def is_prime(num):
    if not(type(num) == int and num > 0):
        print("That wasn't a positive integer!")
        return
    
    if num == 1:
        return False

    for i in range(2, int(num**0.5) + 1):
        if num%i == 0:
            return False

    return True
 
is_prime(13)
is_prime(13139480598340259)
primes = []
for i in range(1, 101):
    if is_prime(i):
        primes.append(i)
        
print(primes)