Final session

Fancy indexing

The fundamental idea is that we can use an array of integer as indices to select values from an array

Minimal example

In [1]:
import numpy as np

Let's create a one-dimensional array

In [2]:
src = np.arange(100)
print(src)
[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
 96 97 98 99]

Let's create an array of integers as indices

In [3]:
dst_selector = np.array([[-1,-2,-3],[1,2,3]],dtype=np.int)
print(dst_selector)
[[-1 -2 -3]
 [ 1  2  3]]

... and let's apply it to the original array

In [4]:
print(src[dst_selector])
[[99 98 97]
 [ 1  2  3]]

The resulting array has the same shape as the index array and picks the values corresponding to the individual entries.
Of course the integers need to be within index range of the original array.

In [5]:
src[[1000,1001,1002]]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-5-da93227eaa81> in <module>
----> 1 src[[1000,1001,1002]]

IndexError: index 1000 is out of bounds for axis 0 with size 100

Multidimensional

We can also use multiple arrays to define the index of a multi-dimensional array. Here the arrays need to have either equal shape or need to be broadcastable, e.g. a 9x2x1, a 2x3 and a 9x1x3 array can be broadcasted to 9x2x3 arrays.

In [6]:
src = np.arange(1000).reshape(10,10,10)
ix1 = np.zeros((9,2,1),dtype=np.int)
ix2 = np.zeros((2,3),dtype=np.int)
ix3 = np.zeros((9,1,3),dtype=np.int)
In [7]:
src[ix1,ix2,ix3].shape
Out[7]:
(9, 2, 3)

The above example is rather trivial since we only have 0's as indices, hence only selecting the very first element 9x2x3 times.
Nota bene: The type of the index array has to be integer and not float

Function decorators

Function decorators are an easy way to modify the functionality of a method or function. They share with the OOP decorator pattern the commonality that the take a structure and return the same structure with the modified functionality.
A function that can act as a function decorator takes a function and returns the modified one.

Minimal example

In [8]:
def do_twice(func):
    # Create the modified version of the function
    def wrapper_do_twice():
        func()
        func()
    return wrapper_do_twice # Return the new function

Nota bene: the last line does not say return wrapper_do_twice() which would lead to an execution of the new function and return the non existing return value of wrapper_do_twice leading to a failed decoration.

In [9]:
@do_twice
def helloWorld():
    print("Hello, World!")
In [10]:
helloWorld()
Hello, World!
Hello, World!

This is equivalent to

In [11]:
def helloWorld():
    print("Hello, World!")
helloWorld = do_twice(helloWorld)
helloWorld()
Hello, World!
Hello, World!

... but has higher readability!

A decorator defined like this will fail, if the function takes arguments

In [12]:
@do_twice
def greet(name):
    print("Hello, "+name+"!")
In [13]:
greet("Nicola")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-13-f9d232806254> in <module>
----> 1 greet("Nicola")

TypeError: wrapper_do_twice() takes 0 positional arguments but 1 was given

Here, the positional and keyword arguments come handy and we can also channel a potential return argument

In [14]:
def do_twice_better(func):
    # Create the modified version of the function
    def wrapper_do_twice_better(*args,**kwargs):
        func(*args,**kwargs)
        return func(*args,**kwargs)
    return wrapper_do_twice_better # Return the new function
In [15]:
@do_twice_better
def greet(name):
    print("Hello, "+name+"!")
    return "Bye bye!"
In [16]:
greet("Nicola")
Hello, Nicola!
Hello, Nicola!
Out[16]:
'Bye bye!'

We can also have parameters as part of the wrapper

In [17]:
def repeat(ntimes):
    def repeating(func):
        def wrapper_repeat(*args,**kwargs):
            if ntimes<1:
                return
            for i in range(ntimes-1):
                func(*args,**kwargs)
            return func(*args,**kwargs)
        return wrapper_repeat
    return repeating
In [18]:
@repeat(ntimes=3)
def greet(name):
    print("Hello, "+name+"!")
    return "Bye bye!"
In [19]:
greet("Nicola")
Hello, Nicola!
Hello, Nicola!
Hello, Nicola!
Out[19]:
'Bye bye!'

Property decorator in classes

The property decorator in classes does exaclty this with the property function.
The three methods need to have the same name and they need to be in this order since the decorated method from the @property definition of the getter method is later expanded by the setter and deleter

In [20]:
class Vector:
    def __init__(self,numbers=[]):
        self._numbers = numbers
    @property
    def numbers(self):
        return self._numbers
    @numbers.setter
    def numbers(self,numbers):
        self._numbers = numbers
    @numbers.deleter
    def numbers(self):
        self._numbers = []
In [21]:
v = Vector([1,2,3])
print(v.numbers)
v.numbers = [3,4]
print(v.numbers)
del v.numbers
print(v.numbers)
[1, 2, 3]
[3, 4]
[]

The same could be achieved via the property function, but is a) less readable and b) requires the creation of additional function that are exposed to the user.

In [22]:
class Vector:
    def __init__(self,numbers=[]):
        self._numbers = numbers
    def _get_numbers(self):
        return self._numbers
    def _set_numbers(self,numbers):
        self._numbers = numbers
    def _del_numbers(self):
        self._numbers = []
    numbers = property(
        _get_numbers,
        _set_numbers,
        _del_numbers
    )
In [23]:
v = Vector([1,2,3])
print(v.numbers)
v.numbers = [3,4]
print(v.numbers)
del v.numbers
print(v.numbers)
[1, 2, 3]
[3, 4]
[]

Function decorators do not need to return functions

This contradicts what has been written in the beginning and actually it was indeed not 100% true: The function that decorates does not need to return a function, but just a structure that can be called. Hence also an object that can be called via the __call__ method is fine.

In [24]:
import time
def timeit(func):
    class Timer:
        def __call__(self,*args,**kwargs):
            self.t = time.time()
            value = func(*args,**kwargs)
            print("It took {0:.3f} sec!".format(time.time()-self.t))
            return value
    return Timer() # An object, not the class
In [25]:
@timeit
def myFunction(n):
    s = 0
    for i in range(n):
        s += i
    return s
In [26]:
myFunction(1000000)
It took 0.066 sec!
Out[26]:
499999500000

Performance considerations

In general decorated functions are slower since there is a chain of function calls. Of course the relative speed loss is more dominant, the less time the function that is decorated is using.
We use again the timeit function to show case it, but removed the print part since this would definitely slow down the decorated version

In [27]:
import time
def timeit(func):
    class Timer:
        def __call__(self,*args,**kwargs):
            self.t = time.time()
            value = func(*args,**kwargs)
            return value
    return Timer() # An object, not the class
In [39]:
@timeit
def myFunction(n):
    s = 0
    for i in range(n):
        s += i
    return s
In [40]:
%timeit myFunction(1000000)
50.3 ms ± 462 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [41]:
%timeit myFunction(100)
4.27 µs ± 109 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [42]:
def myFunction(n):
    s = 0
    for i in range(n):
        s += i
    return s
In [43]:
%timeit myFunction(1000000)
50.7 ms ± 461 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [44]:
%timeit myFunction(100)
3.76 µs ± 105 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Hence it can easily be 20% for rather fast functions, but does not make a difference for slower ones