Functions

In Python it's realy easy to define your own functions. Once defined, you can use them just like any standard Python function. By condensing functionality into a function, your code will get structured and is way more readable. Beside that this specific chunk of code can be used over and over again with way less effort. A good function is as general and abstract, that it cannot only be used in the project it was written for, but in any Python project.
Every Python function is composed by some main parts: the function name, signature and the body. Optionally, a return value and arguments can be defined. The arguments are also referred to as attributes.

Syntax

The function sytanc is as follows:

__def__ *name* __(__*attributes*__):__ *body* *return value*


In [ ]:
print('Hello, Wolrd!')
print('This is Python.')

group the two print statements into a print_all function


In [ ]:

Extend print_all. It should accept a user name as an attribute and should substitute the World in the first print call with that name. Name the new function welcome.


In [ ]:

Using the input function, we could first prompt for a name and them use it in welcome.
What does make more sense: extending welcome or defining a second function for prompting a name and then pass it to welcome?


In [ ]:

Execute this function in a endlessly until the phrase 'exit' or 'Exit' was passed by the user.


In [ ]:

Return Values

Up to now, no of the defined functions returned a value. In Python, there is no need to define a return value in the function signature (different from C++ or Java). It's also possible to return values only conditionally or to return a varying number of values.
Different to welcome the function greet will return the 'Hello World' Phares instead of printing it.


In [ ]:
def greet():
    return 'Hello, World!'

message = greet()
print(message)

Rewrite welcome in the style of greet. It should substitute the user name and return the result.


In [ ]:

Arguments

Using input in productive Pyhton code is very uncommon. It is quite complicated to reuse the code, as it will not always be executed in a python console. Beyond that, it's more readable to define possible inputs as attributes and function arguments as it makes debugging way easier. Then, it is up to a developer how the input value shall be obtained. The input function, a configuration file, a web formular, a text file or a global variable could be used to get the value for the argument.
The example below will take two number and return their sum. This is very clear and reuseable.


In [ ]:
def sum_a_and_b(a, b):
    return a + b

print(sum_a_and_b(5, 3))

A function argument can also be optional. Therefore a default value has to be defined in the signature. You woould call this attribute an optinal attribute or more pythonic: keyword argument. Without this default value, an argument is also called a positional argument as Python can only reference it by the passing order. Thus, one cannot mix positional and keyword arguments. Nevertheless, the keyword arguemnts can be mixed in their ordner as they can be identified by thier keyword. As a last option, you can set any keyword argument in a positional manner, but then the order matters again.


In [ ]:
def print_arguments(arg1, arg2, arg3='Foo', arg4='Bar', g=9.81):
    print('arg1:\t', arg1)
    print('arg2:\t', arg2)
    print('arg3:\t', arg3)
    print('arg4:\t', arg4)
    print('g:\t', g)
    print('-' * 14)

In [ ]:

locals / globals

Another important topic for functions is variable validity and life span. A variable is alife until you overwrite or explicitly delete it. In case you re-assign a variable the life span ends, even if the variable typse stays the same. That means, the new value will allocate another position in the memory.


In [ ]:
x = 5
print('x = 5\t\tmemory address:', hex(id(x)))
x = 6
print('x = 6\t\tmemory address:', hex(id(x)))

Additionally one can declare and assign a variable, but it may only be valid in specific sections of your code. These sections of validity are called namespace. Any function defines its own namespace (the function body) and any variable declared within that scope is not valid outside this namespace. Nevertheless, namespaces can be nested. Then any outer namespace is also valid inside the inner namespaces.


In [ ]:
a = 5
def f():
    b = 3
    print('a: ', a, 'b: ', b)

# call
f()
b

The running Python session also defines a namespace. This is called the global namespace. The built-in function globals can be used to return the global namespace content as a dict. The locals function does the same for the local (inner) namespaces.


In [ ]:
a = 5
print('Global a: ', globals()['a'], '\t\tmemory address: ', hex(id(a)))

def f(b):
    print('Local b:', locals()['b'], '\t\tmemory address: ', hex(id(b)))
f(a)

*args, **kwargs

Two very specific and important attributes are *args and **kwargs attributes. You can use any name for these two variables, but it is highly recommented to use default name to prevent confusions!. The star is a operator that is also called the asterisk operator. The single operator stacks or unstacks a varying number of positional arguments into a list, while the double operator does the same thing to keyword arguments and dictionaries. For a better understanding use the function below, just returning the content of args and kwargs.


In [ ]:
def get_attributes(*args, **kwargs):
    return args, kwargs

In [ ]:
a, b = get_attributes('foo', 'bar', g=9.81, version=2.7, idiot_president='Donald Trump')
print('args:', a)
print('kwargs:', b)

lambda

The lambda function is a special case, as it is an anonymous function. This is the only function without a function name. On the other side, lambdas does not accept keyword arguments. lambdas are helpful to write short auxiliary functions, or inline functions that are used as arguments themselves. The seconda field are list comprehensions.


In [ ]:
list(map(lambda x:x**2, [1,2,3,4,5,6,7,8,9]))

In [ ]:
list(map(lambda x:len(x), 'This is a sentence with rather with rather short and extraordinary long words.'.split()))

In [ ]:
list(map(lambda x:(x,len(x)), 'This is a sentence with rather with rather short and extraordinary long words.'.split()))

Returning functions

In Python it is also possible to define a function, that will return another function. There is even a built-in function called callable, taht tests python objects for being callable. Caution: This does still not guarantee, that the object is a function, because Python also defines callable class instances.
The example below could be used in a data management environment.


In [ ]:
def mean(data):
    return sum(data) / len(data)

def get_aggregator(variable_name):
    if variable_name.lower() == 'temperature':
        return mean
    elif variable_name.lower() == 'rainfall':
        return sum

In [ ]:
data = [2,4,7,9,2,3,5]

# Temperature data
agg = get_aggregator('Temperature')
print('Temperature: ', agg(data), '°C')
agg = get_aggregator('Rainfall')
print('Rainfall:    ', agg(data), 'mm')

Recursive functions

A recursive function calls itself in the body. This technique can dramatically decrease the code size and shifts the code often very close to mathematical algorithms, but there are also some downsides. If a 'break' condition is never met or implemented, the recursive calling will never stop. (In fact it will stop, as Python will stop it but it will also stop your code.). The other downside is the dramatical loss of performance for highly nested functions. As an example the faculty function is implemented below: $$ n! = n+ (n-1)!~~~~~~~~~ n > 1, 1! = 1 $$


In [ ]:
def factorial(n):
    return n*factorial(n-1) if n > 1 else 1

In [ ]:

Now define the Fibonacci series into fibonacci\_recursive below. $$F_n = F_{n - 1} + F_{n - 2} $$ using the start values $F_0 = 0$ and $F_1 = 1$.


In [ ]:
def fibonacci_iterative(n):
    a,b = 0, 1
    for i in range(n):
        a,b = b, a + b
    return a

def fibonacci_recursive(n):
    pass
    
print('iterative:', fibonacci_iterative(10))
print('recursive:', fibonacci_recursive(10))

In [ ]:
%time fibonacci_iterative(40)
%time fibonacci_recursive(40)

Nested functions

In Python it is also possible to define a function within the namespace of another function. Just like variables, this function also has a validity. You can use this concept to define a very specific auxiliary function, that cannot be used in another scope just in that namespace where it is valid. Then other developers cannot misuse your function.
The second use case is that an outer function wants to influence the definition of the inner function. The outer will be declared as your code is imported. The inner first as the outer is run. Therefore anything happening during runtime could still influence the declaration of the inner function. Here, the default values of a model are dependent on the boundary conditions.


In [ ]:
def get_model(arid=True):
    if arid:
        a = 1.2
    else:
        a = 1.4
    def _model(rain, evap, a=a):
        return (rain - evap)**a if rain > evap else 0
    return _model

In [ ]:
rain_values = [3, 0, 0, 16, 4]
evap_values = [1, 1, 5, 3, 2]

print(list(map(get_model(arid=True), rain_values, evap_values)))
print(list(map(get_model(arid=False), rain_values, evap_values)))