<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Array views and slicing
A NumPy array is an object of numpy.ndarray type
Step1:  All ndarrays have a .base attribute.
If this attribute is not None, then the array is a view of some other object's memory, typically another ndarray.
This is a very powerful tool, because allocating memory and copying memory contents are expensive operations, but updating metadata on how to interpret some already allocated memory is cheap!
The simplest way of creating an array's view is by slicing it
Step2:  Let's look more closely at what an array's metadata looks like. NumPy provides the np.info function, which can list for us some low level attributes of an array
Step5:  By the end of the workshop you will understand what most of these mean.
But rather than listen through a lesson, you get to try and figure what they mean yourself.
To help you with that, here's a function that prints the information from two arrays side by side
Step6:  Exercise 1.
Create a one dimensional NumPy array with a few items (consider using np.arange).
Compare the printout of np.info on your array and on slices of it (use the [start
Step7:  Exercise 1 debrief
Every array has an underlying block of memory assigned to it.
When we slice an array, rather than making a copy of it, NumPy makes a view, reusing the memory block, but interpreting it differently.
Lets take a look at what NumPy did for us in the above examples, and make sense of some of the changes to info.
shape
Step8:  A look at data types
Similarly to how we can change the shape, strides and data pointer of an array through slicing, we can change how it's items are interpreted by changing it's data type.
This is done by calling the array's .view() method, and passing it the new data type.
But before we go there, lets look a little closer at dtypes. You are hopefully familiar with the basic NumPy numerical data types
Step9:  
Step11:  The Constructor They Don't Want You To Know About.
You typically construct your NumPy arrays using one of the many factory fuctions provided, np.array() being the most popular.
But it is also possible to call the np.ndarray object constructor directly.
You will typically not want to do this, because there are probably simpler alternatives.
But it is a great way of putting your understanding of views of arrays to the test!
You can check the full documentation, but the np.ndarray constructor takes the following arguments that we care about
Step12:  Reshaping Into Higher Dimensions
So far we have sticked to one dimensional arrays. Things get substantially more interesting when we move into higher dimensions.
One way of getting views with a different number of dimensions is by using the .reshape() method of NumPy arrays, or the equivalent np.reshape() function.
The first argument to any of the reshape functions is the new shape of the array. When providing it, keep in mind
Step13:  Exercise 5 debrief
As the examples show, an n-dimensional array will have an n item tuple .shape and .strides. The number of dimensions can be directly queried from the .ndim attribute.
The shape tells us how large the array is along each dimension, the strides tell us how many bytes to skip in memory to get to the next item along each dimension.
When we reshape an array using C order, a.k.a. row major order, items along higher dimensions are closer in memory. When we use Fortran orser, a.k.a. column major order, it is items along smaller dimensions that are closer.
Reshaping with a purpose
One typical use of reshaping is to apply some aggregation function to equal subdivision of an array.
Say you have, e.g. a 12 item 1D array, and would like to compute the sum of every three items. This is how this is typically accomplished
Step14:  You can apply fancier functions than .sum(), e.g. let's compute the variance of each group
Step15:  Exercise 6
Your turn to do a fancier reshaping
Step17:  Rearranging dimensions
Once we have a multidimensional array, rearranging the order of its dimensions is as simple as rearranging its .shape and .tuple attributes. You could do this with np.ndarray, but it would be a pain. NumPy has a bunch of functions for doing that, but they are all watered down versions of np.transpose, which takes a tuple with the desired permutation of the array dimensions.
Exercise 7
Write a function roll_axis_to_end that takes an array and an axis, and makes that axis the last dimension of the array.
For extra credit, rewrite your function using np.ndarray.
Step19:  Playing with strides
For the rest of the workshop we are going to dome some fancy tricks with strides, to create interesting views of an existing array.
Exercise 8
Create a function to extract the diagonal of a 2-D array, using the np.ndarray constructor.
Step22:  Exercise 9
Something very interesting happens when we set a stride to zero. Give that idea some thought and then
Step24:  Exercise 10
In the last exercise we used zero strides to reuse an item more than once in the resulting view. Let's try to build on that idea
Step25:  Parting pro tip
NumPy's worst kept secret is the existence of a mostly undocumented, mostly hidden, as_strided function, that makes creating views with funny strides much easier (and also much more dangerous!) than using np.ndarray. Here's the available documentation
Step26:  Note that this function will not protect you, the way np.ndarray does, from accessing memory that is not indexed by the array the view is taken for. You may want to do that, but be wary of the world of segmentation faults you are getting yourself into!
<ASSISTANT_TASK:>
Python Code:
a = np.arange(3)
type(a)
Explanation: Array views and slicing
A NumPy array is an object of numpy.ndarray type:
End of explanation
a = np.arange(3)
a.base is None
b = a
b.base is None
a[:].base is None
a[:].base is a
Explanation: All ndarrays have a .base attribute.
If this attribute is not None, then the array is a view of some other object's memory, typically another ndarray.
This is a very powerful tool, because allocating memory and copying memory contents are expensive operations, but updating metadata on how to interpret some already allocated memory is cheap!
The simplest way of creating an array's view is by slicing it:
End of explanation
np.info(a)
Explanation: Let's look more closely at what an array's metadata looks like. NumPy provides the np.info function, which can list for us some low level attributes of an array:
End of explanation
def info_for_two(one_array, another_array):
    Prints side-by-side results of running np.info on its inputs.
    def info_as_ordered_dict(array):
        Converts return of np.infor into an ordered dict.
        import collections
        import io
        buffer = io.StringIO()
        np.info(array, output=buffer)
        data = (
            item.split(':') for item in buffer.getvalue().strip().split('\n'))
        return collections.OrderedDict(
            ((key, value.strip()) for key, value in data))
    one_dict = info_as_ordered_dict(one_array)
    another_dict = info_as_ordered_dict(another_array)
    name_w = max(len(name) for name in one_dict.keys())
    one_w = max(len(name) for name in one_dict.values())
    another_w = max(len(name) for name in another_dict.values())
    output =  (
        f'{name:<{name_w}} : {one:>{one_w}} : {another:>{another_w}}'
        for name, one, another in zip(
            one_dict.keys(), one_dict.values(), another_dict.values()))
    print('\n'.join(output))
a = np.arange(5)
b = a[:]
info_for_two(a, b)
Explanation: By the end of the workshop you will understand what most of these mean.
But rather than listen through a lesson, you get to try and figure what they mean yourself.
To help you with that, here's a function that prints the information from two arrays side by side:
End of explanation
a = np.arange(4, dtype=np.uint16)
a
a[::2]
info_for_two(a, a[::2])
a[2::-1]
info_for_two(a, a[2::-1])
Explanation: Exercise 1.
Create a one dimensional NumPy array with a few items (consider using np.arange).
Compare the printout of np.info on your array and on slices of it (use the [start:stop:step] indexing syntax, and make sure to try steps other than one).
Do you see any patterns?
End of explanation
a = np.arange(10, dtype=np.float)
a
a.shape
a.strides
dt = a.dtype
dt.itemsize
flags = a.flags
flags.contiguous
a.data
Explanation: Exercise 1 debrief
Every array has an underlying block of memory assigned to it.
When we slice an array, rather than making a copy of it, NumPy makes a view, reusing the memory block, but interpreting it differently.
Lets take a look at what NumPy did for us in the above examples, and make sense of some of the changes to info.
shape: for a one dimensional array shape is a single item tuple, equal to the total number of items in the array. You can get the shape of an array as its .shape attribute.
strides: is also a single item tuple for one-dimensional arrays, its value being the number of bytes to skip in memory to get to the next item. And yes, strides can be negative. You can get this as the .strides attribute of any array.
data pointer: this is the address in memory of the first byte of the first item of the array. Note that this doesn't have to be the same as the first byte of the underlying memory block! You rarely need to know the exact address of the data pointer, but it's part of the string representation of the arrays .data attribute. 
itemsize: this isn't properly an attribute of the array, but of it's data type. It is the number of bytes that an array item takes up in memory. You can get this value from an array as the .itemsize attribute of its .dtype attribute, i.e. array.dtype.itemsize.
type: this lets us know how each array item should be interpreted e.g. for calculations. We'll talk more about this later, but you can get an array's type object through its .dtype attribute.
contiguous: this is one of several boolean flags of an array. Its meaning is a little more specific, but for now lets say it tells us whether the array items use the memory block efficiently, without leaving unused spaces between items. It's value can be checked as the .contiguous attribute of the arrays .flags attribute
Exercise 2
Take a couple or minutes to familiarize yourself with the NumPy array's attributes discussed above:
Create a small one dimensional array of your choosing.
Look at its .shape, .strides, .dtype, .flags and .data attributes.
For .dtype and .flags, store them into a separate variable, and use tab completion on those to explore their subattributes.
End of explanation
a = np.arange(4, dtype=np.uint16)
a
a.view(np.uint8)
# A np.uint32 is like adding to each item the next multiplied by 65536.
a.view(np.uint32)
# A weird one, see below for details!
a.view(np.uint8)[1:-1].view('>u2')
Explanation: A look at data types
Similarly to how we can change the shape, strides and data pointer of an array through slicing, we can change how it's items are interpreted by changing it's data type.
This is done by calling the array's .view() method, and passing it the new data type.
But before we go there, lets look a little closer at dtypes. You are hopefully familiar with the basic NumPy numerical data types:
| Type Family | NumPy Defined Types | Character Codes |
| :---: |
| boolean | np.bool | '?' |
| unsigned integers | np.uint8 - np.uint64 | 'u1', 'u2', 'u4', 'u8' |
| signed integers | np.int8 - np.int64 | 'i1', 'i2', 'i4', 'i8' |
| floating point | np.float16 - np.float128 | 'f2', 'f4', 'f8', 'f16' |
| complex | np.complex64, np.complex128 | 'c8', 'c16' |
You can create a new data type by calling its constructor, np.dtype(), with either a NumPy defined type, or the character code.
Character codes can have '&lt;' or '&gt;' prepended, to indicate whether the type is little or big endian. If unspecified, native encoding is used, which for all practical purposes is going to be little endian.
Exercise 3
Let's play a little with dtype views:
Create a simple array of a type you feel comfortable you understand, e.g. np.arange(4, dtype=np.uint16).
Take a view of type np.uint8 of your array. This will give you the raw byte contents of your array. Is this what you were expecting?
Take a few views of your array, with dtypes of larger itemsize, or changing the endianess of the data type. Try to predict what the output will be before running the examples.
Take a look at the wikipedia page on single precision floating point numbers, more specifically its examples of encodings. Create arrays of four np.uint8 values which, when viewed as a np.float32 give the values 1, -2, and 1/3.
End of explanation
# 1.0 = 0x3f800000
np.array([0x0, 0x00, 0x80, 0x3f], dtype=np.uint8).view(np.float32)
# 2.0 = 0xc0000000
np.array([0x0, 0x00, 0x00, 0xc0], dtype=np.uint8).view(np.float32)
# 1 / 3 = 0x3eaaaaab
np.array([0xab, 0xaa, 0xaa, 0x3e], dtype=np.uint8).view(np.float32)
Explanation: 
End of explanation
def reversed_array(array):
    Returns a reversed view of an array.
    assert isinstance(array, np.ndarray)  # Must be a numpy array.
    assert array.ndim == 1  # Must be one-dimensional
    return np.ndarray(
        shape=array.shape,
        dtype=array.dtype,
        buffer=array,
        offset=(len(array) - 1) * array.strides[0],
        strides=(-array.strides[0],))
a = np.arange(10, dtype=np.float32)
reversed_array(a)
Explanation: The Constructor They Don't Want You To Know About.
You typically construct your NumPy arrays using one of the many factory fuctions provided, np.array() being the most popular.
But it is also possible to call the np.ndarray object constructor directly.
You will typically not want to do this, because there are probably simpler alternatives.
But it is a great way of putting your understanding of views of arrays to the test!
You can check the full documentation, but the np.ndarray constructor takes the following arguments that we care about:
shape: the shape of the returned array,
dtype: the data type of the returned array,
buffer: an object to reuse the underlying memory from, e.g. an existing array or its .data attribute,
offset: by how many bytes to move the starting data pointer of the returned array relative to the passed buffer,
strides: the strides of the returned array.
Exercise 4
Write a function, using the np.ndarray constructor, that takes a one dimensional array and returns a reversed view of it.
End of explanation
a = np.arange(2 * 3, dtype=np.uint16)
a
a.reshape((2, 3))
a.reshape((3, 2))
info_for_two(a.reshape((2, 3)), a.reshape((3, 2)))
a.reshape((2, 3), order='F')
a.reshape((3, 2), order='F')
info_for_two(a.reshape((2, 3), order='F'), a.reshape((3, 2), order='F'))
Explanation: Reshaping Into Higher Dimensions
So far we have sticked to one dimensional arrays. Things get substantially more interesting when we move into higher dimensions.
One way of getting views with a different number of dimensions is by using the .reshape() method of NumPy arrays, or the equivalent np.reshape() function.
The first argument to any of the reshape functions is the new shape of the array. When providing it, keep in mind: 
the total size of the array must stay unchanged, i.e. the product of the values of the new shape tuple must be equal to the product of the values of the old shape tuple.
by entering -1 for one of the new dimensions, you can have NumPy compute its value for you, but the other dimensions must be compatible with the calculated one being an integer.
.reshape() can also take an order= kwarg, which can be set to 'C' (as the programming language) or 'F' (for the Fortran programming language). This correspond to row and column major orders, respectively.
Exercise 5
Let's look at how multidimensional arrays are represented in NumPy with an exercise.
Create a small linear array with a total length that is a multiple of two different small primes, e.g. 6 = 2 * 3.
Reshape the array into a two dimensional one, starting with the default order='C'. Try both possible combinations of rows and columns, e.g. (2, 3) and (3, 2). Look at the resulting arrays, and compare their metadata. Do you understand what's going on?
Try the same reshaping with order='F'. Can you see what the differences are?
If you feel confident with these, give a higher dimensional array a try.
End of explanation
a = np.arange(12, dtype=float)
a
a.reshape(4, 3).sum(axis=-1)
Explanation: Exercise 5 debrief
As the examples show, an n-dimensional array will have an n item tuple .shape and .strides. The number of dimensions can be directly queried from the .ndim attribute.
The shape tells us how large the array is along each dimension, the strides tell us how many bytes to skip in memory to get to the next item along each dimension.
When we reshape an array using C order, a.k.a. row major order, items along higher dimensions are closer in memory. When we use Fortran orser, a.k.a. column major order, it is items along smaller dimensions that are closer.
Reshaping with a purpose
One typical use of reshaping is to apply some aggregation function to equal subdivision of an array.
Say you have, e.g. a 12 item 1D array, and would like to compute the sum of every three items. This is how this is typically accomplished:
End of explanation
a.reshape(4, 3).var(axis=-1)
Explanation: You can apply fancier functions than .sum(), e.g. let's compute the variance of each group:
End of explanation
m, n = 3, 4
row_tiles, col_tiles = 5, 6
a = np.arange(
    row_tiles * m * col_tiles * n,
    dtype=float).reshape((row_tiles*m, col_tiles*n))
a.reshape(row_tiles, m, col_tiles, n).sum(axis=(1, 3))
a.reshape(row_tiles, m, -1).sum(axis=1).reshape(-1, col_tiles, n).sum(axis=-1)
a = np.arange(6).reshape(2, 3)
a
np.transpose(a, (1, 0))
Explanation: Exercise 6
Your turn to do a fancier reshaping: we will compute the average of a 2D array over non-overlapping rectangular patches:
Choose to small numbers m and n, e.g. 3 and 4.
Create a 2D array, with number of rows a multiple of one of those numbers, and number of columns a multiple of the other, e.g. 15 x 24.
Reshape and aggregate to create a 2D array holding the sums over non overlapping m x n tiles, e.g. a 5 x 6 array.
Hint: .sum() can take a tuple of integers as axis=, so you can do the whole thing in a single reshape from 2D to 4D, then aggregate back to 2D. If tyou find this confusing, doing two aggregations will also work.
End of explanation
def roll_axis_to_end(array, axis):
    Makess an axis of an array its last dimension.
    assert isinstance(array, np.ndarray)
    if not -array.ndim <= axis < array.ndim:
        raise ValueError('Axis out of bounds')
    if axis < 0:
        axis += array.ndim
    axes = list(range(array.ndim))
    permutation = axes[:axis] + axes[axis+1:] + [axis]
    return array.transpose(permutation)
a = np.zeros((4, 5 ,6))
info_for_two(a, roll_axis_to_end(a, 1))
Explanation: Rearranging dimensions
Once we have a multidimensional array, rearranging the order of its dimensions is as simple as rearranging its .shape and .tuple attributes. You could do this with np.ndarray, but it would be a pain. NumPy has a bunch of functions for doing that, but they are all watered down versions of np.transpose, which takes a tuple with the desired permutation of the array dimensions.
Exercise 7
Write a function roll_axis_to_end that takes an array and an axis, and makes that axis the last dimension of the array.
For extra credit, rewrite your function using np.ndarray.
End of explanation
def diagonal(array, k=0):
    Extracts the k-th diagonal of a 2D array.
    assert isinstance(array, np.ndarray)
    assert array.ndim == 2
    rows, cols = array.shape
    row_stride, col_stride = array.strides
    if k < 0:
        k = -k
        assert k <= rows
        offset = k * row_stride
        size = min(rows - k, cols)
    else:
        assert k <= cols
        offset = k * col_stride
        size = min(rows, cols - k)
    return np.ndarray(
        shape=(size,),
        dtype=array.dtype,
        buffer=array,
        offset=offset,
        strides=(row_stride + col_stride,))
a = np.arange(3 * 5).reshape(3, 5)
a
diagonal(a)
diagonal(a, k=3)
diagonal(a, -2)
Explanation: Playing with strides
For the rest of the workshop we are going to dome some fancy tricks with strides, to create interesting views of an existing array.
Exercise 8
Create a function to extract the diagonal of a 2-D array, using the np.ndarray constructor.
End of explanation
def stacked_column_view(vector, n=1):
    Creates a view stacking vector n times as columns.
    assert isinstance(vector, np.ndarray)
    assert vector.ndim == 1
    size, = vector.shape
    stride, = vector.strides
    return np.ndarray(
        shape=(size, n),  # (n, size) for row
        dtype=vector.dtype,
        buffer=vector,
        offset=0,
        strides=(stride, 0))  # (0, stride) for row
stacked_column_view(np.arange(5), 6)
def stacked_row_view(vector, n=1):
    Creates a view stacking vector n times as rows.
    assert isinstance(vector, np.ndarray)
    assert vector.ndim == 1
    size, = vector.shape
    stride, = vector.strides
    return np.ndarray(
        shape=(n, size),
        dtype=vector.dtype,
        buffer=vector,
        offset=0,
        strides=(0, stride))
stacked_row_view(np.arange(6), 5)
def outer_product(one_vector, another_vector):
    assert isinstance(one_vector, np.ndarray) and one_vector.ndim == 1
    assert isinstance(another_vector, np.ndarray) and another_vector.ndim == 1
    return (stacked_column_view(one_vector, len(another_vector)) *
            stacked_row_view(another_vector, len(one_vector)))
outer_product(np.arange(3), np.arange(4))
a = np.arange(3)
b = np.arange(4)
a.reshape(3, 1) * b.reshape(1, 4)
a[:, None] * b
Explanation: Exercise 9
Something very interesting happens when we set a stride to zero. Give that idea some thought and then:
Create two functions, stacked_column_vector and stacked_row_vector, that take a 1D array (the vector), and an integer n, and create a 2D view of the array that stack n copies of the vector, either as columns or rows of the view.
Use this functions to create an outer_product function that takes two 1D vectors and computes their outer product.
End of explanation
def sliding_window(array, window):
    Creates a sliding window view of an array.
    assert isinstance(array, np.ndarray)
    assert array.ndim == 1
    assert 0 < window <= len(array)
    return np.ndarray(
        shape=(len(array) - window + 1, window),
        dtype=array.dtype,
        buffer=array,
        strides=array.strides * 2)
    
a = np.arange(4)
sliding_window(a, 2)
Explanation: Exercise 10
In the last exercise we used zero strides to reuse an item more than once in the resulting view. Let's try to build on that idea:
Write a function that takes a 1D array and a window integer value, and creates a 2D view of the array, each row a view through a sliding window of size window into the original array.
Hint: There are len(array) - window + 1 such "views through a window".
Another hint: Here's a small example expected run:
&gt;&gt;&gt; sliding_window(np.arange(4), 2)
[[0, 1],
 [1, 2],
 [2, 3]]
End of explanation
from numpy.lib.stride_tricks import as_strided
np.info(as_strided)
Explanation: Parting pro tip
NumPy's worst kept secret is the existence of a mostly undocumented, mostly hidden, as_strided function, that makes creating views with funny strides much easier (and also much more dangerous!) than using np.ndarray. Here's the available documentation:
End of explanation
a = np.arange(6)
a
np.info(a)
as_strided(a, shape=a.shape, strides=(16,))
Explanation: Note that this function will not protect you, the way np.ndarray does, from accessing memory that is not indexed by the array the view is taken for. You may want to do that, but be wary of the world of segmentation faults you are getting yourself into!
End of explanation
<END_TASK>