Introduction to Python for sciences 2020

Acquire strong basis in Python to use it efficiently

Pierre Augier (LEGI), Cyrille Bonamy (LEGI), Eric Maldonado (Irstea), Franck Thollard (ISTerre), Christophe Picard (LJK), Loïc Huder (ISTerre)

Functions

A function is a block of organized, reusable code that is used to perform a single, related action. Functions provide better modularity for your application and a high degree of code reusing.

Simple function definitions and calls

Function blocks begin with the keyword def followed by the function name and parentheses (()).

  • The code block within every function starts with a colon (:) and is indented.
  • Any input parameters or arguments should be placed within these parentheses.
In [2]:
def print_hello():
    "hello printer"
    print('hello')

def myprint(s):
    "my hello printer"
    print('I print', s)
    
# function calls
print_hello()
print_hello()
myprint('First call of myprint')
myprint('Second call of myprint')
hello
hello
I print First call of myprint
I print Second call of myprint

Simple function definitions and calls

  • The first statement of a function can be the documentation string of the function, also called "docstring".
  • The statement return [expression] exits a function, optionally passing back an expression to the caller. No return statement or a return statement with no arguments is the same as return None.

(Note: Wikipedia about duck typing: https://fr.wikipedia.org/wiki/Duck_typing)

In [8]:
import unittest

def add(arg0, arg1):
    """Print and return the sum of the two arguments (duck typing)."""
    result = arg0 + arg1
    print('result = ', result)
    return result


class TestMyStuff(unittest.TestCase):    
    def test_add(self):
        """ test max_temp computes correctly"""
        self.assertEqual(8, add(3, 5))
        self.assertEqual("aabb", add("aa", "bb"))

_res = unittest.TextTestRunner(verbosity=2).run(unittest.TestLoader().loadTestsFromTestCase(TestMyStuff0))
test_add (__main__.TestMyStuff0)
test max_temp computes correctly ... 
result =  5
result =  aabb
ok

----------------------------------------------------------------------
Ran 1 test in 0.004s

OK
In [3]:
add(2, 3)
result =  5
Out[3]:
5
In [4]:
add('a', 'b')
result =  ab
Out[4]:
'ab'

Do it yourself: simple function definition

Write a function that returns the sum of the first argument with twice the second argument.

In [3]:
import unittest

def add_second_twice(arg0, arg1):
    """Return the sum of the first argument with twice the second one.
        Arguments should be of type that support sum and product by 
        an integer (e.g. numerical, string, list, ...)
        :param arg0: first argument
        :param arg1: second argument 
        :return: arg0 + 2 * arg1
    """
    pass


class TestMyStuff0(unittest.TestCase):    
    def test_add_second_twice(self):
        """ test max_temp computes correctly"""
        self.assertEqual(13, add_second_twice(3, 5))
        self.assertEqual("aabbbb", add_second_twice("aa", "bb"))
        self.assertListEqual([1, 2, 3, 4, 3, 4], add_second_twice([1, 2], [3, 4])) 
        
_res = unittest.TextTestRunner(verbosity=2).run(unittest.TestLoader().loadTestsFromTestCase(TestMyStuff0))
test_add_second_twice (__main__.TestMyStuff0)
test max_temp computes correctly ... 
arg0 + 2*arg1 = 3 + 2*5 = 13
arg0 + 2*arg1 = aa + 2*bb = aabbbb
arg0 + 2*arg1 = [1, 2] + 2*[3, 4] = [1, 2, 3, 4, 3, 4]
ok

----------------------------------------------------------------------
Ran 1 test in 0.003s

OK

Do it yourself: simple function definition

A solution:

In [5]:
def add_second_twice(arg0, arg1):
    """Return the sum of the first argument with twice the second one.
        Arguments should be of type that support sum and product by 
        an integer (e.g. numerical, string, list, ...)
        :param arg0: first argument
        :param arg1: second argument 
        :return: arg0 + 2 * arg1
    """
    result = arg0 + 2*arg1
    print(f'arg0 + 2*arg1 = {arg0} + 2*{arg1} = {result}')
    return result

myfunc(4, 6)
arg0 + 2*arg1 = 4 + 2*6 = 16
Out[5]:
16
In [6]:
myfunc('a', 'b')
arg0 + 2*arg1 = a + 2*b = abb
Out[6]:
'abb'

All Python functions return exactly one object but... None and tuple

In [7]:
type(print())

Out[7]:
NoneType
In [8]:
def return_a_tuple():
    return 1, 'hello', 3  # a tuple, same as (1, 'hello', 3)

my_tuple = return_a_tuple()
print(my_tuple)
(1, 'hello', 3)
In [9]:
a, b, c = return_a_tuple()
print(b)
hello

Function call: namespaces and objects "passed by references"

For each function call:

  • a new namespace is created (as at the beginning of a module)
  • the objects are "passed by references": new names of the arguments are created in the function namespace and they point towards the objects given as arguments to the function (so it is possible to modify the mutable objects).

Function call: objects "passed by references"

Exercice: use 2 schemes "namespaces-objects" to understand these 2 pieces of code.

In [7]:
number = 2
mylist = []

def my_strange_append_square(l, n):
    # new function namespace with names "l" and "n"
    n = n**2
    l.append(n)

my_strange_append_square(mylist, number)
print(mylist, number)
[4] 2
In [8]:
number = 2
mylist = []

def my_strange_append_square(mylist, number):
    # new function namespace with names "mylist" and "number"
    number = number**2
    mylist.append( number)
    
my_strange_append_square(mylist, number)
print(mylist, number)
[4] 2

Global vs Local variables

Variables that are defined inside a function body have a local scope (i.e. are defined in the function namespace), and those defined outside have a global scope.

This means that local variables can be accessed only inside the function in which they are declared, whereas global variables can be accessed throughout the module by all functions.

In [12]:
# global variables
result = 0
multiplicator = 2

def multiply(arg0):
    # here we create a new name `result` in the function namespace
    # `result` is a local variable
    # we can use the global variable `multiplicator`
    result = multiplicator * arg0
    print('Inside the function local result:\t', result)
    return result

multiply(10)
print('Outside the function global result:\t', result)
Inside the function local result:	 20
Outside the function global result:	 0
  • Global variables can be used in a function.
  • Global variables can not be modified in a function (except with the global keyword. Discourage!).

Global vs Local variables: global keyword

There is a keyword global to define inside a function a global variable and to modify a global variable in the function. It is often a bad idea to use it :-)

In [13]:
def func():
    global me
    # Defined locally but declared as global
    me = 'global variable locally defined'
    print(me)

func()
# Ask for a global variable
print(me)
global variable locally defined
global variable locally defined
In [14]:
delta = 0

def add_1_to_delta():
    global delta
    # global variable modified in a function
    delta += 1

for i in range(4):
    add_1_to_delta()
    print(delta, end=', ')
1, 2, 3, 4, 

Function Arguments

You can call a function by using the following types of formal arguments:

  • Required arguments
  • Keyword arguments
  • Default arguments
  • Variable-length arguments

Required arguments

Required arguments are the arguments passed to a function in correct positional order. Here, the number of arguments in the function call should match exactly with the function definition.

In [7]:
def myfunc(arg0, arg1):
    "Return the sum of the first argument with twice the second one."
    result = arg0 + 2*arg1
    print(f'arg0 + 2*arg1 = {arg0} + 2*{arg1} = {result}')
    return result

myfunc(4, 6)
arg0 + 2*arg1 = 4 + 2*6 = 16
Out[7]:
16

To call the function myfunc, you definitely need to pass two arguments, otherwise it gives a syntax error.

Keyword arguments

Keyword arguments are related to the function calls. When you use keyword arguments in a function call, Python identifies the arguments by the parameter name.

In [16]:
myfunc(arg1=3, arg0=2)
arg0 + 2*arg1 = 2 + 2*3 = 8
Out[16]:
8

Default arguments

A default argument is an argument that assumes a default value if a value is not provided in the function call for that argument.

In [12]:
def myfunc1(arg0, arg1=None):
    "Return the sum of the first argument with twice the second one."
    if arg1 is None:
        if type(arg0) == int:
            arg1 = 0
        else:
            arg1 = ""
    myfunc(arg0, arg1)

myfunc1("a", "n")
arg0 + 2*arg1 = a + 2*n = ann
In [18]:
myfunc1(1, 3)
arg0 + 2*arg1 = 1 + 2*3 = 7

Default arguments

Warning: the default arguments are created only once, at the function definition! They are stored in a tuple associated with the function object.

In [9]:
def do_not_use_mutable_object_for_default_arg(l=[]):
    l.append(1)
    print(l)

Exercice: what will be the result of 3 calls of this function? Use a namespaces-objects diagram!

Default arguments

Warning: the default arguments are created only once, at the function definition! They are stored in a tuple associated with the function object.

In [20]:
def do_not_use_mutable_object_for_default_arg(l=[]):
    l.append(1)
    print(l)
In [21]:
do_not_use_mutable_object_for_default_arg()
do_not_use_mutable_object_for_default_arg()
do_not_use_mutable_object_for_default_arg()
[1]
[1, 1]
[1, 1, 1]
In [16]:
def how_to_use_list_as_default_arg(l=None):
    if l is None:
        l = []
    l.append(1)
    print(l)

how_to_use_list_as_default_arg()
how_to_use_list_as_default_arg()
how_to_use_list_as_default_arg()
l1 = [1,2,3]
how_to_use_list_as_default_arg(l1)
l1
[1]
[1]
[1]
[1, 2, 3, 1]
Out[16]:
[1, 2, 3, 1]

Variable-length arguments

You may need to process a function for more arguments than you specified while defining the function. These arguments are called variable-length arguments and are not named in the function definition, unlike required and default arguments.

An asterisk (*) is placed before the variable name that holds the values of all nonkeyword variable arguments.

In [23]:
def sum_args(*args):
    """Return the sum of numbers."""
    totalsum = 0
    print('args =', args)
    for var in args:
        totalsum += var
    print('totalsum =', totalsum)
    return totalsum

sum_args()
sum_args(4)
sum_args(4, 3, 4, 7)
args = ()
totalsum = 0
args = (4,)
totalsum = 4
args = (4, 3, 4, 7)
totalsum = 18
Out[23]:
18

Variable-length arguments

There is also a (very useful) syntax with two asterisks **, which works like this:

In [19]:
def func(a, b, *args, **kwargs):
    print(f'call:\n\ta = {a}\n\targs = {args}\n\tkwargs = {kwargs}')
    
func(1, 2, 3, toto=3, titi=3)
func('a', 'b', bob=3)
call:
	a = 1
	args = (3,)
	kwargs = {'toto': 3, 'titi': 3}
call:
	a = a
	args = ()
	kwargs = {'bob': 3}

Do it yourself

Write a function that takes as input a list l and a number a and that multiplies all the elements of the list by the number. If not set, number is defaulted to 2.

A possible solution:

In [22]:
def func(l, a=2):
    for i, val in enumerate(l):
        l[i] = a * val
        
l = list(range(3))
print(l)
func(l)
print(l)
func(l, 4)
print(l)
l = ['a', 'b']
func(l, 4)
print(l)
[0, 1, 2]
[0, 2, 4]
[0, 8, 16]
['aaaa', 'bbbb']

lambda keyword and anonymous functions

These functions are called anonymous because they are not declared by using the def keyword but with the lambda keyword so they have not name.

  • Lambda forms can take any number of arguments but return just one value in the form of an expression. They cannot contain commands or multiple expressions.
  • Lambda form is used in functional programming (but it is usually better to use list comprehension) and for callbacks in GUI.
In [26]:
f = lambda x, y: x + y
f(1, 2)
Out[26]:
3

Example of the builtin function print

In [1]: print?
Docstring:
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

Prints the values to a stream, or to sys.stdout by default.
Optional keyword arguments:
file:  a file-like object (stream); defaults to the current sys.stdout.
sep:   string inserted between values, default a space.
end:   string appended after the last value, default a newline.
flush: whether to forcibly flush the stream.
Type:      builtin_function_or_method
In [27]:
print('hello', 'Eric')
hello Eric
In [28]:
print('hello', 'Eric', sep='_', end='')
print('.')
hello_Eric.

Input from Keyboard (builtin function input)

In [29]:
answer = input("what's your name ?")
print("your name is ", answer)
your name is  eric