Functions

Functions are a mechanism of programing that makes it possible to organize your code better. It also makes it possible to re-use code without having to duplicate it. Let's take an example that you want to take a string and enclose that string inside of another string for formatting.

Doing this by hand would be:

my_string = "interface ethernet 1/1"
new_string = "<pre>" + my_string + "</pre>"

And you would have to repeat that everytime you would want to do the same string manipulation. That would make the code bloated and confusing. Functions make it possible for you to change that to something like:

Doing this by hand would be:

my_string = "interface ethernet 1/1"
new_string = insert_in_pre( my_string )

For this reason functions are a very important concept to understand in any language including Python.

In Python functions are created with the following structure:

def function_name( parameter, parameter, ... ):
    command
    command

Python uses indentation to delimit the body of the function, just like for loops we discussed previously use indentation to define what to loop into. Also functions always return a value back to the calling code. If no return is defined in the function, then None is returned.

Using the previous example, let's create the function insert_in_pre

def insert_in_pre( text ):
    new text = "<pre>" + text + "</pre>"
    return text

Parameters

A very important part of Python to understand are parameters. You saw a brief example of this previously with our insert_in_pre function. Python provides three mechanism arounds parameters.

Let's start with positional arguments that are the most common.

Positional Arguments

When using positional arguments in python, the value that you call is assigned into a variable in the function based on the position it has in the sequence of parameters.

For this reason you have to make sure that the order is correct and that specific parameters are left blank when attempting to pass value over a parameter that you will not use.

Console 1

                    
                

Arguments by name

Parameters by name has the advantage that it removes the requirement of positional order. This provides more flexibility when calling functions as you reference each specific variable of the function individually by name and order becomes irrelevant.

In the example to the right we have switched things around by sending the value parameters specified by name. Now the order as to how we call doesn't matter.

Going back to a previous example we had worked on, let's utilize a function to perform a check on a MAC address to validate that it is a Cisco OID Mac. We will call this function is_cisco_mac and it will receive the MAC address and return TRUE or FALSE based on the value passed.


def is_cisco_mac( mac ):
    it_is = False
    ethaddr = mac.split(":")
    if ethaddr[0]=="00" and ethaddr[1]=="01" and ethaddr[2]=="0c":
        it_is = True
    return it_is
        

To invoke the function we can utilize two distinct methods as we have discussed. The first by positional which we would just send the MAC as:

is_cisco_mac( "00:01:0c:15:15:15" )
The second way we can invoke this is to use argument by name.
is_cisco_mac( mac = "00:01:0c:15:15:15" )

As you can see the second method proves to be a cleaner choice when reading the code. Looking at the following console code we now wrap the function around a logical operator utilizing the return clause of the function if it passes the logical conditional.

Console 2

                    
                

Variable Arguments

Consider the scenario that you don't have a specific list of arguments you want to receive but would like to process these. This is common in scenarios that a variable amount of numbers would need to be processed or analyzed.

It is the opinion of this writter that variable arguments tend to be confusing. I believe that a better approach is to pass a LIST or a DICTIONARY to the function. This makes the code easier to read and understand

For an example of variable arguments.

def largest_latency( *latencies ):
    maxlatency = latencies[0]
    for x in latencies[1:]:
        if x > maxlatency:
            maxlatency = x
    return maxlatency

maxlatency( 5, 10, 45, 32, 34, 10, 17, 34, 18, 24 )
45

Variable Scopes

One very important concept to udnerstand is the concept of variable scopes. Variables defined inside of a function, exist only when the function is being executed. Once the function completes execution these variables are removed from memory and the value is lost.

Also important to understand is that variables outside of the function don't have visibility inside of the function unless specified. This means that you could have the variable x defined inside of a function and also outside of the function and they will not collide.

Let's observe an example of this.


x = 1
print "The value of x is: " + str(x)
def test_var():
    x = 200
    print "Inside the function:" + str(x)

test_var()
print "The value of x after running the function is: " + str(x)

$ python test.py
The value of x is: 1
inside the function
The value of x after running the function is: 1

There is a mechanism available in Python to reference variables outside of the scope of the function. This is called global ( and is popular in many languages ). Using the same example we used before and making x global will have completely different results.


x = 1
print "The value of x is: " + str(x)
def test_var():
    global x
    x = 200
    print "Inside the function:" + str(x)

test_var()
print "The value of x after running the function is: " + str(x)

$ python test.py
The value of x is: 1
Inside the function:200
The value of x after running the function is: 200

As you can observe the value of x is changed in the global scope of the variable and after the function is executed the value of x changes outside of the function.

Python behaviour with globals
One important thing to note with global variables in Python is that if you don't specify a local variable in a function then Python will attempt to reference the global representation of that variable. While this is possible it is highly recomended for code readability to not do this.

Following the same example we can observe the effect of Python using a global reference not defined in a function.


x = 1
print "The value of x is: " + str(x)
def test_var():
    # Removed definition of x in the function
    print "Inside the function:" + str(x)

test_var()
print "The value of x after running the function is: " + str(x)

$ python test.py
The value of x is: 1
Inside the function:1
The value of x after running the function is: 1

Using lambdas to create short functions

Python utilizes the functionality of lambda's to create simple functions. One way to show the value of this, is to create simple hexadecimal to decimal converter. A traditional function for doing this conversion would look as:


def hex2dec(value):
    return int(value,16)

print hex2dec("0x10")

$ python test.py
16

Another way to accomplish this is with lambda's. These are very simple functions that don't have defined a name in itself. They are executed by passing arguments into an expression

lambda parameter, parameter, ... : expression

And the always return back whatever the expression evaluates to ( a default return ).


import sys

conv = {
    "hex2dec" : lambda h2d: int(h2d, 16),
    "dec2hex" : lambda d2h: int(d2h, 10),
}

print conv["hex2dec"](sys.argv[1])

$ python test.py 0x10
16

Functions as variables

In Python functions are objects also ( everything in Python is an object ). Because functions are also objects they can have associated methods to them ( this will become more clear when we discuss classes). In addition because they are objects they can be passed as values. This makes it possible to pass a function as an argument.

Using the same example of converting Hex to Decimal and vice versa we can create a better way to manipulate user input. You first start by defining the functions that will do the bulk of the work for our hex/dec converters.


def hex2dec( value ):
    return int(value,16)

def dec2hex( value ):
    return int(value,10)

Then we can look at invoking these two functions in a special way with a intermediary function that we will call convhex and this function will invoke the other two functions based on the argument parameter specified to convhex


def hex2dec( value ):
    return int(value,16)

def dec2hex( value ):
    return int(value,10)

def convhex(func, value):
    function_map={ "hex2dec" : hex2dec, "dec2hex": dec2hex }
    print "Calling operation:" + func + " with:" + value
    return function_map[func](value)

The reason for the function_map is because we want to take user input to execute some code. A function is an object of type function and string is an object of type string. The difference makes it impossible to pass that string object straight from CLI input to call the function. To get around this we define a dictionary for the valid inputs and use the proper value ( as a function object ) to the call.


import sys

def hex2dec(value):
    return int(value, 16)

def dec2hex(value):
    return int(value, 10)

def convhex(func, value):
    function_map={ "hex2dec" : hex2dec, "dec2hex": dec2hex }
    print "Calling operation:" + func + " with:" + value
    return function_map[func](value)

print convhex(sys.argv[1], sys.argv[2])

$ python test.py hex2dec 0xc1612cf
Calling operation:hex2dec with:0xc1612cf
202773199

Function decorators

Decorators make is possible to wrap functions inside of other functions. They are built to add features to an original function. Think of it as doing roughly what the original function did and then add new functionality.

This needs to be completed!

Decorator Library

There is a large decorator library available on the python WIKI.