The fundamental idea is that we can use an array of integer as indices to select values from an array
import numpy as np
Let's create a one-dimensional array
src = np.arange(100)
print(src)
Let's create an array of integers as indices
dst_selector = np.array([[-1,-2,-3],[1,2,3]],dtype=np.int)
print(dst_selector)
... and let's apply it to the original array
print(src[dst_selector])
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.
src[[1000,1001,1002]]
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.
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)
src[ix1,ix2,ix3].shape
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 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.
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.
@do_twice
def helloWorld():
print("Hello, World!")
helloWorld()
This is equivalent to
def helloWorld():
print("Hello, World!")
helloWorld = do_twice(helloWorld)
helloWorld()
... but has higher readability!
A decorator defined like this will fail, if the function takes arguments
@do_twice
def greet(name):
print("Hello, "+name+"!")
greet("Nicola")
Here, the positional and keyword arguments come handy and we can also channel a potential return argument
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
@do_twice_better
def greet(name):
print("Hello, "+name+"!")
return "Bye bye!"
greet("Nicola")
We can also have parameters as part of the wrapper
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
@repeat(ntimes=3)
def greet(name):
print("Hello, "+name+"!")
return "Bye bye!"
greet("Nicola")
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
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 = []
v = Vector([1,2,3])
print(v.numbers)
v.numbers = [3,4]
print(v.numbers)
del v.numbers
print(v.numbers)
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.
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
)
v = Vector([1,2,3])
print(v.numbers)
v.numbers = [3,4]
print(v.numbers)
del v.numbers
print(v.numbers)
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.
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
@timeit
def myFunction(n):
s = 0
for i in range(n):
s += i
return s
myFunction(1000000)
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
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
@timeit
def myFunction(n):
s = 0
for i in range(n):
s += i
return s
%timeit myFunction(1000000)
%timeit myFunction(100)
def myFunction(n):
s = 0
for i in range(n):
s += i
return s
%timeit myFunction(1000000)
%timeit myFunction(100)
Hence it can easily be 20% for rather fast functions, but does not make a difference for slower ones