[0] Functions

1 [0] Defining


In [ ]:
def say_hello():
    print('Hello world!')

2 [0] Running (calling)


In [ ]:
say_hello()

3 [0] Using parameters


In [ ]:
def max(a, b):
    if a > b:
        return a
    else:
        return b

max(1,2)

[0] 3.1 Arguments are passed by assignment (by value, in C)

Arguments changed in functions are local.


In [1]:
def func(a):
    a = 2
x = 1
func(x)
print(x)


1

[0] 3.2 To modify an argument (call by reference, in C)

Arguments changed in functions never are global ... so, we need to return them (if we want to see that changes).


In [2]:
def func(a,b):
    a += 1
    b -= 1
    return a,b
x = 1
y = 2
x, y = func(x, y)
print(x,y)


2 1

Containers are passed to functions by reference.


In [4]:
def func(l):
    print(l)
    del l[0]

l = [1,2]
func(l)
print(l)


[1, 2]
[2]

3.3 [1] Parameters can be optional


In [ ]:
def optional_args(a=1, b=2):
    print(a, b)
    
optional_args()

In [ ]:
optional_args(3)

In [ ]:
optional_args(4,3)

In [ ]:
optional_args(b=3, a=4)

In [ ]:
optional_args(b=3)

In [4]:
# Positional parameters must go before than optinal parameters

def func(a, b=2):
    print(a,b)
    
func(1)
func(1,3)
func(b=2)


1 2
1 3
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-abb8987751ce> in <module>()
      6 func(1)
      7 func(1,3)
----> 8 func(b=2)

TypeError: func() missing 1 required positional argument: 'a'

3.4 [1] Working with an undefined number of unnamed parameters


In [ ]:
def variable_args(*vargs):
    print(vargs)
    print(type(vargs))
    for i in vargs:
        print(i)
    print('first argument =', vargs[0])
    
variable_args("hola", "caracola", ("hi", "folks"))

3.5 [1] Working with an undefined number of named parameters


In [ ]:
def keyworded_args(**kargs):
    print(kargs)
    print(type(kargs))
    for i in kargs:
        print(i, kargs[i])
    print("'a' argument =", kargs['a'])
    
keyworded_args(a=1, b='a')

3.6 [1] Functions can be arguments to functions


In [ ]:
def add(x, y):
    return x + y

def compute(function, x, y):
    return function(x, y)

compute(add, 1, 2)

3.7 [1] Of course, functions are objects!

Functions are objects which can, for example, be "copied" to other objects:


In [ ]:
print(type(say_hello))
a = say_hello
a()

In [ ]:
id(say_hello)

In [ ]:
id(a)

4 [0] Recursion

Call yourself!


In [ ]:
def factorial(x):
    if x == 0:
        return 1
    else:
        return x * factorial(x-1)
print(factorial(3))

In [ ]:
import time
now = time.time()
print(factorial(500))
print ("Time =", time.time() - now, "seconds")

5 [1] Nesting functions


In [ ]:
def outter():
    def inner():
        print('Hello world')
    inner()
    
outter()

6 [1] Decorating functions

Extending the behavior of functions that we don't want (or cannot) to modify:


In [ ]:
def divide(numerator, denominator):
    return numerator/denominator

def safe_division(function):
    def wrapper(numerator, denominator):
        if denominator != 0:
            return function(numerator, denominator)
    return wrapper

# Function "decoration".
divide = safe_division(divide)

print(divide(1,2))
print(divide(1,0))

In [ ]:
None == 0

The same example using a decorator:


In [ ]:
# This function is identical to the previous one
def safe_division(function):
    def wrapper(numerator, denominator):
        if denominator != 0:
            return function(numerator, denominator)
    return wrapper

@safe_division
def divide(numerator, denominator):
    return numerator/denominator

print(divide(1,2))
print(divide(1,0))

In [2]:
# http://www.bytemining.com/2010/02/be-careful-searching-python-dictionaries/

import time

def print_timing(func):
    def wrapper(*arg):
        t1 = time.clock()
        res = func(*arg)
        t2 = time.clock()
        print('%0.3fms' % ((t2-t1)*1000.0))
        return res
    return wrapper
 
@print_timing
def my_function():
     #stuff
     return

my_function()


0.009ms

7 [1] Lambda functions

$\lambda$-funcions are "anonymous" functions:


In [ ]:
# Standard function:
def power(x,y):
    return x**y

power(2,3)

In [ ]:
# Lambda function:
power = lambda x,y: x**y

power(2,3)

Lambda functions are useful because they can be defined inline and we don't need give a name to use them:


In [ ]:
(lambda x,y: x**y)(2,3)

7.1 Filtering data with $\lambda$-functions

... and filter() ...


In [ ]:
help(filter)

In other words ..., filter() creates a sequences of elements for which a function returns true.


In [ ]:
for i in filter(lambda x: x < 0, range(-3,3)):
    print (i)

In [ ]:
tuple(filter(lambda x: x%2, range(5*2)))

7.2 Mapping data with $\lambda$-functions

... and map() ...


In [ ]:
help(map)

In other words ..., map() applies a $\lambda$-function to all the items in an input sequence.


In [ ]:
list(map(lambda x: x%2, range(10)))

7.3 Reducing data with $\lambda$-functions


In [ ]:
from functools import reduce
help(reduce)

In other words ..., reduce() process iteratively a sequence of items. One of them is item of the sequence and the other, the previus output of the reduce().


In [ ]:
n=10; list(range(1, n+1))

In [ ]:
def factorial(n):
    return reduce(lambda x,y: x*y, list(range(1, n+1)))
#                 ---------------  ------------------
#                    function           sequence

print(factorial(3))

Only to see the performance:


In [ ]:
import math
print(math.factorial(3))

In [ ]:
now = time.time()
factorial(100000)
print ("Time =", time.time() - now)

In [ ]:
now = time.time()
math.factorial(100000)
print ("Time =", time.time() - now)

Another interesting example:

Now, lets compute prime numbers. First using a classical approach:


In [ ]:
# https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes
import math
N = 20
primes = [True]*N

print('[', end='')
for j in range(2, N):
    if primes[j]:
        print(j, end=', ')
print(']')

for i in range(2, int(math.sqrt(N))):
    if primes[i]:
        for j in [i**2+x*i for x in range(N) if i**2+x*i<N]:
            primes[j] = False

    print('[', end='')
    for j in range(2, N):
        if primes[j]:
            print(j, end=', ')
    print(']', i)
    
# Be aware of this code does not produce a list!

Now using $\lambda$-functions:


In [ ]:
# A different implementation of the Sieve of Eratosthenes
# (http://stackoverflow.com/questions/27990094/finding-primes-with-modulo-in-python)
primes = list(range(2, N))
print(primes)
for i in range(2, int(math.sqrt(N))):
    primes = list(filter(lambda x: x == i or x % i, primes))
    print(primes, i)

# This code produces a list

8 [0] Namespaces and scopes

8.1 [0] Names

Python objects are referenced by names:


In [ ]:
id(1) # The `id()` funtion returns the address of a object

In [ ]:
a = 1
id(a) # "a" is the same object than "1"

Different objects have different id's:


In [ ]:
id(2)

A namespace is a collection of names. All packages, modules, classes and functions define their own namespace (basically, the variables -- or names -- locally defined). The scope of a name is the region of code where that name can be referenced without using any prefix.

8.2 [0] Functions create their own namespace

(Please, restart this kernel) Do you think that the second print(a) of in the following code should work?


In [ ]:
def func():
    a = 1
    print(a)
func()
print(a)

[0] 8.3 Names of outter scopes are accessible in inner ones


In [ ]:
a = 1
def func():
    print(a) # This is the external 'a'
func()

8.4 [0] When there is a coincidence, local scopes create their own names


In [ ]:
a = 1
def func():
    a = 2
    print(a) # This is the internal 'a'
func()
print(a)

8.5 [1] To avoid creating a new name when there is a coincidence, use global

However, this is DISCOURAGED. It is confusing and isn’t thread-safe (among other things).


In [ ]:
a = 1
def func():
    global a
    a = 2 # This is the external 'a'
func()
print(a)

It's better to do:


In [ ]:
a = 1
def func(a):
    a = 2
    return a
a = func(a)
print(a)