Skip to content

Python Functions

This section presents python functions and some common use cases.

Definition

Functions are used for capturing logic that is intended to be used multiple times. In this sense they help avoiding copying and pasting code. Standard python function are defined using the def keyword followed by the name of the function.

def Name( args ):
    return result

Function may require parameters, also known as arguments, passed when calling them. They perform some computation and return a result in the mathematical sense of y = f( x ), where y is the result value and x is the supplied value or vector of values.

def Constant( ):
    return 1.0

def NumberSquared( value ):
    return value ** 2

def Addition( alpha, beta ):
    return alpha + beta

Functions may return a single, multiple or no values at all. Functions that do not return a value are also known as procedures. On the other hand, multiple values can be return by being wrapped in a list or tuple.

def PrintHelloWorld( ):
    print( 'Hello World' )

def InverseSquareRoot( value ):
    return 1.0 / math.sqrt( value )

def PointOnUnitCircle( radius, angle ):
    return ( radius * math.cos( angle ),
             radius * math.sin( angle ) )

Calling

Using a function, also known as calling or invoking, is performed by using its name followed by the list of parameters, if any, placed within parentheses in the exact order they are expected. The use of parentheses is what denotes that invocation. The return value can be stored in a variable or ignored.

""" No value returned
"""
PrintHelloWorld( )

""" Returned value ignored
"""
InverseSquareRoot( 1.0 )

""" Returned value stored
"""
point = PointOnUnitCircle( 1.0, 0.0 )

""" Returned values spread
"""
x, y = PointOnUnitCircle( 1.0, 0.0 )

It is also possible to use the parameters' names when calling function, which sometimes makes the code easier to read. Note that if the names are provided then the order of parameters can be rearranged.

point = PointOnUnitCircle( radius = 1.0, angle = 0.0 )
point = PointOnUnitCircle( angle = 0.0, radius = 1.0 )

Scopes

Functions have access to variables defined globally. In other words, values defined outside a function's body can be accessed within. In fact they can be also modified but this must be avoided because most often it leads to confusing logical errors.

constant = 1
def Compute( param ):
    return param * 2 + constant

def Avoid( param ):
    constant = 3 """ Don't modify """
    return param * 2 + constant

However, the opposite is not true: code outside a function cannot access variables defined within a function.

def Compute( param ):
    constant = 1
    return param + constant

""" No access! """
print( constant )

Defaults

Parameters may have default values which are used if the parameter is not passed when the function is called. Therefore, parameters with default values may be considered as optional.

def InverseSquareRoot( value = 1.0 ):
    return 1.0 / math.sqrt( value )

one  = InverseSquareRoot(   )
half = InverseSquareRoot( 4 )

There is a special rule about default parameter values: all parameter must have a default value after the first instance. Otherwise, a syntax error is raised. For example below, beta must also have a default value because alpha is optional.

""" Invalid syntax!
"""
def Addition( alpha = 1.0, beta ):
    return alpha + beta

A typical idiom for optional parameters where we want to know if the parameter was passed or it is missing is to set the default to None and check as follows.

def Operation( required, optional = None ):
    if( optional is None ): """ Is missing? """
        optional = 'assign value'
    # ...

As Values

Functions in python can be used as values. It is possible to store a function in a variable for instance. As long as the parentheses syntax for calling the function is not used, its name can be used as a value. Variables that contain function values are callable.

In the example below there are two functions with the same parameters defined L1 and L2. The first computes the Manhattan distance and the second the Euclidean distance. We can assign either function is a variable called norm based on some condition and invoke the function using the alias.

def L1( a, b ):
    return abs( a ) + abs( b )

def L2( a, b ):
    return math.sqrt( a ** 2 + b ** 2 )

norm = L2 if( metric == 'Euclidean' ) else L1
print( norm( 1, 2 ) )

It is possible to pass functions as arguments to another function or use it as a default value as seen in the example below.

def Norm( a, b, method = L2 ):
    return method( a, b )

l1 = Norm( 1, 2, method = L1 )
l2 = Norm( 1, 2 )

It is also possible to store functions in lists or dictionaries associated with a key.

""" List of functions
"""
def L0( a, b ):
    return 0.0

norms = [L0, L1, L2]
print( norms[2]( 1, 2 ) )

""" Dictionary of functions
"""
norms = {
    'Manhattan': L1,
    'Euclidean': L2 }

print( norms['Euclidean']( 1, 2 ) )

Recursion

Recursion is a pattern where a function calls itself. Some computations are more intuitive using this approach. However, it is also possible to create infinite calls which will drain memory and raise a stack overflow exception.

def Fibonacci( number ):
    if( number <= 1 ): return number
    return ( Fibonacci( number - 1 ) +
             Fibonacci( number - 2 ) )

def StackOverflow( value ):
    return StackOverflow( value + 1 )

Nesting

Functions can be nested by placing one definition inside another. This feature is not so commonly used. The intent is to contain functions that have only local use within a scope.

def GlobalUse( a, b ):
    def LocalUse( a, b ):
        return a ** 2 + b ** 2
    return math.sqrt( LocalUse( a, b ) )

Lambdas

Since functions are like values, it is possible to define them without names. Those are known as anonymous or lambda functions. The example below shows two functions with equivalent logic defined using the def and lambda syntax.

def Add( a, b ): return a + b

Add = lambda a, b: a + b

Note that lambdas do not allow defining local variables unlike regular functions. They are used for passing inline a function like a predicate to filter or sort a list for instance, see ordering lists.

def Select( items, predicate ):
    return [item
        for item in items
            if( predicate( item ) )]

evens = Select( range( 10 ),
    lambda a : ( a % 2 ) == 0 )

odds = Select( range( 10 ),
    lambda a : ( a & 1 ) == 1 )

Patching

If multiple definitions of a function with the same name are encountered, the latest one is overwrites the previous. In the example below, the Apply function perform subtraction because it overwrote the earlier definition that performs addition.

def Apply( a, b ):
    return a + b

def Apply( a, b ):
    return a - b

This can be used for overriding or patching functions. In the example below the builtin print( ) function is redefined to prefix each message with a status code. The original function is stored in a global variable builtin_print and the new definition will overwrite the default.

class Status:
    Message = 'Message'
    Failure = 'Failure'
    Warning = 'Warning'

builtin_print = print
def print( message, status = Status.Message ):
    builtin_print( '{}: {}'.format( status, message ) )
    return status

print( 'Success', Status.Message )
print( 'Possible issue', Status.Warning )

Generators

Python supports a special style of functions known as generators. Instead of returning a single value, generators can be used to produce sequences of values. Their main use is in for-loop enumerations, where they provide iterable / enumerable sequences of values. The range( ) function is the most commonly used generator in python extensively used in for index in range( count ) enumerations.

Note that the result of a generator function is a sequence of values created and returned one by one. It is not values that are first constructed, then packaged in a list and then enumerated. Because generators do not construct lists but return values on-the-fly they are more efficient in memory terms.

""" Even though these work the same,
    range does not construct a list!
"""
for index in [0, 1, 2, 3]:
    print( index )

for index in range( 4 ):
    print( index )

""" The result of range is not a list
    but an enumerator object
"""
enumeration = range( 10 )

""" Enumerator objects can be converted
    to lists using the expression below
"""
numbers = list( enumeration )

Generators can be identified easily because instead of using the return keyword they instead use yield. While return exits the function permanently passing back a result, yield does not exit the function but just places a value in a notional conveyor belt and suspends until the next value is requested or sequence ends whereby the function runs out of scope.

The example below demonstrates two usage-wise equivalent functions for generating a sequence of squared numbers. The key difference is that ListOfSquares constructs a list of numbers, while the EnumOfSquares returns one at a time without inserting them in a collection.

def ListOfSquares( count ):
    result = []
    for index in range( count ):
        result.append( index ** 2 )
    return result

def EnumOfSquares( count ):
    for index in range( count )
        yield index ** 2

""" Usage
"""
for square in ListOfSquares( 10 ):
    print( square )

for square in EnumOfSquares( 10 ):
    print( square )

The following example demonstrates a more useful application for generators. The objective of the PolySides( ) function is given a sequence of points, to produce all the consecutive pairs which represent the line segments of a polygon's or polyline's sides.

This is achieved using the patterning strategy presented earlier to yield-return each pair of points one by one. The wrap parameter is for controlling whether the function treats the sequence of points as an open polyline or a closed polygon, in which case the last-to-first side must also be returned.

""" Polyline's Sides Enumeration
"""
def PolySides( points, wrap = False ):
    for a, b in zip( points[:-1], points[1:] ):
        yield ( a, b )
    if( wrap ): yield points[-1], points[0]

""" Point list
"""
points = [Point3d( 0.0, 0.0, 0.0 ),
          Point3d( 1.0, 0.0, 0.0 ),
          #-- more points
          Point3d( 0.0, 1.0, 0.0 )]

""" Usage
"""
for a, b in PolySides( points ):
    print( a, b )