Data Structures

Introduction

So far we've covered a handful of data structures in Python

We covered Primitive Data Structures (data types), which are predefined ways of storing data

Some examples would be ints, booleans and chars

Since these are predefined by the system, they are acted upon by machine-level instructions and identical across various programming languages

We have also covered some non-primitive Data Structures, like lists and tuples

Strings are also non-primitive Data Structures. Think of them as arrays of characters

This week, we'll learn of other complex Data Structures as well as how to implement them in Python

Mutability

What Is It?

Before we dive into these data structures we must first understand the concept of mutability

Simply put - mutability is the liability or tendency to change 

In object-oriented and functional programming a mutable object is one whose state can be modified after creation

It follows, then, that an immutable object is one whose state cannot be changed

 

# A String is an example of an immutable object

# A String is an example of an immutable object
# Once created it cannot be modified

# A String is an example of an immutable object
# Once created it cannot be modified

# But, wait! What about String concatenation?
>>> hero = 'bat'
>>> hero += 'man'

# A String is an example of an immutable object
# Once created it cannot be modified

# But, wait! What about String concatenation?
>>> hero = 'bat'
>>> hero += 'man'
>>> hero
'batman'

# A String is an example of an immutable object
# Once created it cannot be modified

# But, wait! What about String concatenation?
>>> hero = 'bat'
>>> hero += 'man'
>>> hero
'batman'

# Or String-substitution?
>>> movie = 'Bee Movie'
>>> desc = 'cultural epitome'

# A String is an example of an immutable object
# Once created it cannot be modified

# But, wait! What about String concatenation?
>>> hero = 'bat'
>>> hero += 'man'
>>> hero
'batman'

# Or String-substitution?
>>> movie = 'Bee Movie'
>>> desc = 'cultural epitome'
>>> 'The %s is the %s of the cinematic form' % (movie, desc)

# A String is an example of an immutable object
# Once created it cannot be modified

# But, wait! What about String concatenation?
>>> hero = 'bat'
>>> hero += 'man'
>>> hero
'batman'

# Or String-substitution?
>>> movie = 'Bee Movie'
>>> desc = 'cultural epitome'
>>> 'The %s is the %s of the cinematic form' % (movie, desc)
'The Bee Movie is the cultural epitome of the cinematic form'

# A String is an example of an immutable object
# Once created it cannot be modified

# But, wait! What about String concatenation?
>>> hero = 'bat'
>>> hero += 'man'
>>> hero
'batman'

# Or String-substitution?
>>> movie = 'Bee Movie'
>>> desc = 'cultural epitome'
>>> 'The %s is the %s of the cinematic form' % (movie, desc)
'The Bee Movie is the cultural epitome of the cinematic form'

# Sure it looks like Strings can be modified, but the above 
# examples only show the output of "modification"

# Let's take a look at what's going on under the hood

# Let's take a look at what's going on under the hood
# In the first line we have our variable hero pointing at the 
# String 'bat' in memory
>>> hero = 'bat'

# Let's take a look at what's going on under the hood
# In the first line we have our variable hero pointing at the 
# String 'bat' in memory
>>> hero = 'bat'

# We can check its memory address using the id() function
>>> id(hero)

# Let's take a look at what's going on under the hood
# In the first line we have our variable hero pointing at the 
# String 'bat' in memory
>>> hero = 'bat'

# We can check its memory address using the id() function
>>> id(hero)
4467157176

# Let's take a look at what's going on under the hood
# In the first line we have our variable hero pointing at the 
# String 'bat' in memory
>>> hero = 'bat'

# We can check its memory address using the id() function
>>> id(hero)
4467157176

# In the following line we modify our variable 
>>> hero += 'man' 	    # value of hero is now 'batman'

# Let's take a look at what's going on under the hood
# In the first line we have our variable hero pointing at the 
# String 'bat' in memory
>>> hero = 'bat'

# We can check its memory address using the id() function
>>> id(hero)
4467157176

# In the following line we modify our variable 
>>> hero += 'man' 	    # value of hero is now 'batman'

# But is this the same hero we had originally?
>>> id(hero)

# Let's take a look at what's going on under the hood
# In the first line we have our variable hero pointing at the 
# String 'bat' in memory
>>> hero = 'bat'

# We can check its memory address using the id() function
>>> id(hero)
4467157176

# In the following line we modify our variable 
>>> hero += 'man' 	    # value of hero is now 'batman'

# But is this the same hero we had originally?
>>> id(hero)
4467156896

# Let's take a look at what's going on under the hood
# In the first line we have our variable hero pointing at the 
# String 'bat' in memory
>>> hero = 'bat'

# We can check its memory address using the id() function
>>> id(hero)
4467157176

# In the following line we modify our variable 
>>> hero += 'man' 	    # value of hero is now 'batman'

# But is this the same hero we had originally?
>>> id(hero)
4467156896

4467157176 != 4467156896    # No!

# Though 'batman' may be the hero we deserve, he certainly 
# isn't the hero we started with!

# Though 'batman' may be the hero we deserve, he certainly 
# isn't the hero we started with!

# So what gives? What's really happening here?

# Though 'batman' may be the hero we deserve, he certainly 
# isn't the hero we started with!

# So what gives? What's really happening here?
# Originally, we have our variable hero pointing to 'bat'  
>>> hero = 'bat'

# Though 'batman' may be the hero we deserve, he certainly 
# isn't the hero we started with!

# So what gives? What's really happening here?
# Originally, we have our variable hero pointing to 'bat'  
>>> hero = 'bat'

# When we concatenate 'man' a String with the combined length 
# of 'bat' and 'man' is created in memory and filled in with 
# the appropriate characters
>>> hero += 'man'

# Though 'batman' may be the hero we deserve, he certainly 
# isn't the hero we started with!

# So what gives? What's really happening here?
# Originally, we have our variable hero pointing to 'bat'  
>>> hero = 'bat'

# When we concatenate 'man' a String with the combined length 
# of 'bat' and 'man' is created in memory and filled in with 
# the appropriate characters
>>> hero += 'man'

# Our variable, then, is reassigned to the new String, and any
# further "modification" would require a similar process

# Though 'batman' may be the hero we deserve, he certainly 
# isn't the hero we started with!

# So what gives? What's really happening here?
# Originally, we have our variable hero pointing to 'bat'  
>>> hero = 'bat'

# When we concatenate 'man' a String with the combined length 
# of 'bat' and 'man' is created in memory and filled in with 
# the appropriate characters
>>> hero += 'man'

# Our variable, then, is reassigned to the new String, and any
# further "modification" would require a similar process

# With no variables pointing to either 'bat' or 'man' the data
# at those addresses are scrapped when the addresses are reused
 

# Below is a list of Mutable and Immutable Python data types

# Below is a list of Mutable and Immutable Python data types

# Mutable
list, dict, set, bytearray

# Below is a list of Mutable and Immutable Python data types

# Mutable
list, dict, set, bytearray

# Immutable
int, float, bool, string, tuple, range, bytes

# Below is a list of Mutable and Immutable Python data types

# Mutable
list, dict, set, bytearray

# Immutable
int, float, bool, string, tuple, range, bytes

# As a rule of thumb, all primitive types are immutable

# Below is a list of Mutable and Immutable Python data types

# Mutable
list, dict, set, bytearray

# Immutable
int, float, bool, string, tuple, range, bytes

# As a rule of thumb, all primitive types are immutable

# Side Note - Readdressing During Computation

# Below is a list of Mutable and Immutable Python data types

# Mutable
list, dict, set, bytearray

# Immutable
int, float, bool, string, tuple, range, bytes

# As a rule of thumb, all primitive types are immutable

# Side Note - Readdressing During Computation
# Any time a number value is modified, even with simple 
# addition, the variable storing that value is reassigned to 
# a memory address that contains the new value.

# Below is a list of Mutable and Immutable Python data types

# Mutable
list, dict, set, bytearray

# Immutable
int, float, bool, string, tuple, range, bytes

# As a rule of thumb, all primitive types are immutable

# Side Note - Readdressing During Computation
# Any time a number value is modified, even with simple 
# addition, the variable storing that value is reassigned to 
# a memory address that contains the new value. The old value 
# sits in the original address until it is needed by another 
# variable.

# Below is a list of Mutable and Immutable Python data types

# Mutable
list, dict, set, bytearray

# Immutable
int, float, bool, string, tuple, range, bytes

# As a rule of thumb, all primitive types are immutable

# Side Note - Readdressing During Computation
# Any time a number value is modified, even with simple 
# addition, the variable storing that value is reassigned to 
# a memory address that contains the new value. The old value 
# sits in the original address until it is needed by another 
# variable. Sometimes that variable never comes..

# Below is a list of Mutable and Immutable Python data types

# Mutable
list, dict, set, bytearray

# Immutable
int, float, bool, string, tuple, range, bytes

# As a rule of thumb, all primitive types are immutable

# Side Note - Readdressing During Computation
# Any time a number value is modified, even with simple 
# addition, the variable storing that value is reassigned to 
# a memory address that contains the new value. The old value 
# sits in the original address until it is needed by another 
# variable. Sometimes that variable never comes..
# This kills the old value :( 

Mutability in Python

Consequence

So why do we care about mutability?

 

# Well, one reason is efficiency 

# Well, one reason is efficiency
my_string = 'Aloha'
for i in range(100):
    my_string += 'a' 

# Well, one reason is efficiency
my_string = 'Aloha'
for i in range(100):
    my_string += 'a'

# At the end of the above code segment, the value of my_string
# is 'Aloha' followed by 100 'a's 

# Well, one reason is efficiency
my_string = 'Aloha'
for i in range(100):
    my_string += 'a'

# At the end of the above code segment, the value of my_string
# is 'Aloha' followed by 100 'a's

# In the process, however, we create 101 unique Strings of 
# increasing length from 5 to 105 

# Well, one reason is efficiency
my_string = 'Aloha'
for i in range(100):
    my_string += 'a'

# At the end of the above code segment, the value of my_string
# is 'Aloha' followed by 100 'a's

# In the process, however, we create 101 unique Strings of 
# increasing length from 5 to 105, a total memory requirement 
# of at least 5000 bytes
 

# Well, one reason is efficiency
my_string = 'Aloha'
for i in range(100):
    my_string += 'a'

# At the end of the above code segment, the value of my_string
# is 'Aloha' followed by 100 'a's

# In the process, however, we create 101 unique Strings of 
# increasing length from 5 to 105, a total memory requirement 
# of at least 5000 bytes

# Rather than updating our String on each iteration we could
# concatenate our suffix all at once for over a magnitude
# improvement on both storage and runtime complexity

# Well, one reason is efficiency
my_string = 'Aloha'
for i in range(100):
    my_string += 'a'

# At the end of the above code segment, the value of my_string
# is 'Aloha' followed by 100 'a's

# In the process, however, we create 101 unique Strings of 
# increasing length from 5 to 105, a total memory requirement 
# of at least 5000 bytes

# Rather than updating our String on each iteration we could
# concatenate our suffix all at once for over a magnitude
# improvement on both storage and runtime complexity
my_string = 'Aloha'
my_string += 'a' * 100
 

# Another alternative would be to store our String as a list 
# of characters
my_string = ['A', 'l', 'o', 'h', 'a'] 

# Another alternative would be to store our String as a list 
# of characters
my_string = ['A', 'l', 'o', 'h', 'a']
for i in range(100):
    my_string.append('a') 

# Another alternative would be to store our String as a list 
# of characters
my_string = ['A', 'l', 'o', 'h', 'a']
for i in range(100):
    my_string.append('a')

# Once we are done we can construct our final String using the
# join() method. This will concatenate all characters of our
# list 

# Another alternative would be to store our String as a list 
# of characters
my_string = ['A', 'l', 'o', 'h', 'a']
for i in range(100):
    my_string.append('a')

# Once we are done we can construct our final String using the
# join() method. This will concatenate all characters of our
# list, separated by an empty String '' in our case
''.join(my_string)
 

# Another alternative would be to store our String as a list 
# of characters
my_string = ['A', 'l', 'o', 'h', 'a']
for i in range(100):
    my_string.append('a')

# Once we are done we can construct our final String using the
# join() method. This will concatenate all characters of our
# list, separated by an empty String '' in our case
''.join(my_string)

# Since lists are mutable, we do not have to create a new data
# structure each time we append a character 

# Another alternative would be to store our String as a list 
# of characters
my_string = ['A', 'l', 'o', 'h', 'a']
for i in range(100):
    my_string.append('a')

# Once we are done we can construct our final String using the
# join() method. This will concatenate all characters of our
# list, separated by an empty String '' in our case
''.join(my_string)

# Since lists are mutable, we do not have to create a new data
# structure each time we append a character. We must, however,
# allocate a block of memory each time we extend our list 

# Another alternative would be to store our String as a list 
# of characters
my_string = ['A', 'l', 'o', 'h', 'a']
for i in range(100):
    my_string.append('a')

# Once we are done we can construct our final String using the
# join() method. This will concatenate all characters of our
# list, separated by an empty String '' in our case
''.join(my_string)

# Since lists are mutable, we do not have to create a new data
# structure each time we append a character. We must, however,
# allocate a block of memory each time we extend our list, so
# we may further improve our time-efficiency by predefining the 
# size of our list prior to filling in any indices

Pass by Reference

Efficiency isn't the only reason we care about mutability

In Python, we pass in all function parameters by reference (aka pointers)

Attempts to modify parameters may result in some unexpected behavior if one fails to account for mutability

 

# Given the following function..
def modify( n ):
    n += 5 

# Given the following function..
def modify( n ):
    n += 5

# ..what is the value of foo after execution?
>>> foo = 5
>>> modify(foo) 

# Given the following function..
def modify( n ):
    n += 5

# ..what is the value of foo after execution?
>>> foo = 5
>>> modify(foo)
>>> foo
5 

# Given the following function..
def modify( n ):
    n += 5

# ..what is the value of foo after execution?
>>> foo = 5
>>> modify(foo)
>>> foo
5

# Is it what you expected? 

# Given the following function..
def modify( n ):
    n += 5

# ..what is the value of foo after execution?
>>> foo = 5
>>> modify(foo)
>>> foo
5

# Is it what you expected?
# Although the value stored in foo is passed in by reference,
# an int is immutable 

# Given the following function..
def modify( n ):
    n += 5

# ..what is the value of foo after execution?
>>> foo = 5
>>> modify(foo)
>>> foo
5

# Is it what you expected?
# Although the value stored in foo is passed in by reference,
# an int is immutable. Therefore, attempting to modifying the 
# value of parameter n within modify() changes the memory
# address referenced by n rather than changing the value at 
# the original address
 

# This time we'll try modifying a list
def modify( A ):
    A[0] = 5
 

# This time we'll try modifying a list
def modify( A ):
    A[0] = 5

# Can you guess the value of bar after execution?
>>> bar = [1, 2, 3, 4]
>>> modify(bar) 

# This time we'll try modifying a list
def modify( A ):
    A[0] = 5

# Can you guess the value of bar after execution?
>>> bar = [1, 2, 3, 4]
>>> modify(bar)
>>> bar
[5, 2, 3, 4]
 

# This time we'll try modifying a list
def modify( A ):
    A[0] = 5

# Can you guess the value of bar after execution?
>>> bar = [1, 2, 3, 4]
>>> modify(bar)
>>> bar
[5, 2, 3, 4]

# Is it what you expected? 

# This time we'll try modifying a list
def modify( A ):
    A[0] = 5

# Can you guess the value of bar after execution?
>>> bar = [1, 2, 3, 4]
>>> modify(bar)
>>> bar
[5, 2, 3, 4]

# Is it what you expected?
# Since bar is passed in by reference, parameter A points to
# the same list in memory as bar 

# This time we'll try modifying a list
def modify( A ):
    A[0] = 5

# Can you guess the value of bar after execution?
>>> bar = [1, 2, 3, 4]
>>> modify(bar)
>>> bar
[5, 2, 3, 4]

# Is it what you expected?
# Since bar is passed in by reference, parameter A points to
# the same list in memory as bar. When we change the value of
# the first element of A, the list at the memory address
# referenced by bar is directly modified 

# This time we'll try modifying a list
def modify( A ):
    A[0] = 5

# Can you guess the value of bar after execution?
>>> bar = [1, 2, 3, 4]
>>> modify(bar)
>>> bar
[5, 2, 3, 4]

# Is it what you expected?
# Since bar is passed in by reference, parameter A points to
# the same list in memory as bar. When we change the value of
# the first element of A, the list at the memory address
# referenced by bar is directly modified. This is possible
# since lists are mutable

Multidimensional Arrays

2D-Arrays

So far we've seen implementations of Arrays in 1-Dimension as lists in Python

What if we wanted to create a 2-Dimensional Array (a grid)?

 

# There are several reasons we might want to implement a grid 

# There are several reasons we might want to implement a grid
# One application could be to simulate a game of tic-tac-toe
[ [ O  X  X ],
  [ -  O  - ],
  [ -  X  - ] ]
 

# There are several reasons we might want to implement a grid
# One application could be to simulate a game of tic-tac-toe
[ [ O  X  X ],
  [ -  O  - ],
  [ -  X  - ] ]

# ..or maybe to implement a game of checkers with indices 
# representing locations on the board 

# There are several reasons we might want to implement a grid
# One application could be to simulate a game of tic-tac-toe
[ [ O  X  X ],
  [ -  O  - ],
  [ -  X  - ] ]

# ..or maybe to implement a game of checkers with indices 
# representing locations on the board
# 2D-Arrays are often used to represent tables of entries that 
# have multiple attributes per entry 

# There are several reasons we might want to implement a grid
# One application could be to simulate a game of tic-tac-toe
[ [ O  X  X ],
  [ -  O  - ],
  [ -  X  - ] ]

# ..or maybe to implement a game of checkers with indices 
# representing locations on the board
# 2D-Arrays are often used to represent tables of entries that 
# have multiple attributes per entry

# We can implement multi-dimensional arrays as lists of lists

# There are several reasons we might want to implement a grid
# One application could be to simulate a game of tic-tac-toe
[ [ O  X  X ],
  [ -  O  - ],
  [ -  X  - ] ]

# ..or maybe to implement a game of checkers with indices 
# representing locations on the board
# 2D-Arrays are often used to represent tables of entries that 
# have multiple attributes per entry

# We can implement multi-dimensional arrays as lists of lists
# As seen in the tic-tac-toe example we have a 3x3 grid,
# represented as a 3-element list 

# There are several reasons we might want to implement a grid
# One application could be to simulate a game of tic-tac-toe
[ [ O  X  X ],
  [ -  O  - ],
  [ -  X  - ] ]

# ..or maybe to implement a game of checkers with indices 
# representing locations on the board
# 2D-Arrays are often used to represent tables of entries that 
# have multiple attributes per entry

# We can implement multi-dimensional arrays as lists of lists
# As seen in the tic-tac-toe example we have a 3x3 grid,
# represented as a 3-element list, where each of those elements
# is also a list of length 3
[ [ O  X  X ], [ -  O  - ], [ -  X  - ] ]
 

# So how do we instantiate a structure like this?
[ [ -  -  - ], [ -  -  - ], [ -  -  - ] ]
 

# So how do we instantiate a structure like this?
[ [ -  -  - ], [ -  -  - ], [ -  -  - ] ]

# The easiest way is to take the single element and copy it 

# So how do we instantiate a structure like this?
[ [ -  -  - ], [ -  -  - ], [ -  -  - ] ]

# The easiest way is to take the single element and copy it
# First let's try generating a single row
>>> ['-'] * 3 

# So how do we instantiate a structure like this?
[ [ -  -  - ], [ -  -  - ], [ -  -  - ] ]

# The easiest way is to take the single element and copy it
# First let's try generating a single row
>>> ['-'] * 3
[ '-', '-', '-' ] 

# So how do we instantiate a structure like this?
[ [ -  -  - ], [ -  -  - ], [ -  -  - ] ]

# The easiest way is to take the single element and copy it
# First let's try generating a single row
>>> ['-'] * 3
[ '-', '-', '-' ]

# Then we can just copy that row to create our grid
>>> a = ['-'] * 3 

# So how do we instantiate a structure like this?
[ [ -  -  - ], [ -  -  - ], [ -  -  - ] ]

# The easiest way is to take the single element and copy it
# First let's try generating a single row
>>> ['-'] * 3
[ '-', '-', '-' ]

# Then we can just copy that row to create our grid
>>> a = ['-'] * 3
>>> [a] * 3 

# So how do we instantiate a structure like this?
[ [ -  -  - ], [ -  -  - ], [ -  -  - ] ]

# The easiest way is to take the single element and copy it
# First let's try generating a single row
>>> ['-'] * 3
[ '-', '-', '-' ]

# Then we can just copy that row to create our grid
>>> a = ['-'] * 3
>>> [a] * 3
[ ['-', '-', '-'], ['-', '-', '-'], ['-', '-', '-'] ]
 

# So how do we instantiate a structure like this?
[ [ -  -  - ], [ -  -  - ], [ -  -  - ] ]

# The easiest way is to take the single element and copy it
# First let's try generating a single row
>>> ['-'] * 3
[ '-', '-', '-' ]

# Then we can just copy that row to create our grid
>>> a = ['-'] * 3
>>> [a] * 3
[ ['-', '-', '-'], ['-', '-', '-'], ['-', '-', '-'] ]

# Where '-' can be replaced with whatever data we choose to 
# initialize our list

A = [ [ 1  2  3 ], [ 4  5  6 ], [ 7  8  9 ] ]
 

A = [ [ 1  2  3 ], [ 4  5  6 ], [ 7  8  9 ] ]

# Now that we've instantiated our list we can access individual
# elements like so:
>>> A[0][2] 	# row 0, column 2 

A = [ [ 1  2  3 ], [ 4  5  6 ], [ 7  8  9 ] ]

# Now that we've instantiated our list we can access individual
# elements like so:
>>> A[0][2] 	# row 0, column 2
3 

A = [ [ 1  2  3 ], [ 4  5  6 ], [ 7  8  9 ] ]

# Now that we've instantiated our list we can access individual
# elements like so:
>>> A[0][2] 	# row 0, column 2
3
>>> A[1][1] 	# row 1, column 1 

A = [ [ 1  2  3 ], [ 4  5  6 ], [ 7  8  9 ] ]

# Now that we've instantiated our list we can access individual
# elements like so:
>>> A[0][2] 	# row 0, column 2
3
>>> A[1][1] 	# row 1, column 1
5 

A = [ [ 1  2  3 ], [ 4  5  6 ], [ 7  8  9 ] ]

# Now that we've instantiated our list we can access individual
# elements like so:
>>> A[0][2] 	# row 0, column 2
3
>>> A[1][1] 	# row 1, column 1
5

# And modify existing indices like so
>>> A[0][0] = 14 

A = [ [ 1  2  3 ], [ 4  5  6 ], [ 7  8  9 ] ]

# Now that we've instantiated our list we can access individual
# elements like so:
>>> A[0][2] 	# row 0, column 2
3
>>> A[1][1] 	# row 1, column 1
5

# And modify existing indices like so
>>> A[0][0] = 14
>>> A
[ [ 14  2  3 ], [ 4  5  6 ], [ 7  8  9 ] ] 

A = [ [ 1  2  3 ], [ 4  5  6 ], [ 7  8  9 ] ]

# Now that we've instantiated our list we can access individual
# elements like so:
>>> A[0][2] 	# row 0, column 2
3
>>> A[1][1] 	# row 1, column 1
5

# And modify existing indices like so
>>> A[0][0] = 14
>>> A
[ [ 14  2  3 ], [ 4  5  6 ], [ 7  8  9 ] ]
>>> A[2][1] = 0 

A = [ [ 1  2  3 ], [ 4  5  6 ], [ 7  8  9 ] ]

# Now that we've instantiated our list we can access individual
# elements like so:
>>> A[0][2] 	# row 0, column 2
3
>>> A[1][1] 	# row 1, column 1
5

# And modify existing indices like so
>>> A[0][0] = 14
>>> A
[ [ 14  2  3 ], [ 4  5  6 ], [ 7  8  9 ] ]
>>> A[2][1] = 0
>>> A
[ [ 14  2  3 ], [ 4  5  6 ], [ 7  0  9 ] ]

A = [ [14  2  3 ], [ 4  5  6 ], [ 7  0  9 ] ] 

A = [ [14  2  3 ], [ 4  5  6 ], [ 7  0  9 ] ]

# Since lists are mutable we can even add rows
>>> A.append([1, 4, 9]) 

A = [ [14  2  3 ], [ 4  5  6 ], [ 7  0  9 ] ]

# Since lists are mutable we can even add rows
>>> A.append([1, 4, 9])
>>> A 

A = [ [14  2  3 ], [ 4  5  6 ], [ 7  0  9 ] ]

# Since lists are mutable we can even add rows
>>> A.append([1, 4, 9])
>>> A
[ [ 14  2  3 ], [ 4  5  6 ], [ 7  0  9 ], [ 1  4  9 ] ]
 

A = [ [14  2  3 ], [ 4  5  6 ], [ 7  0  9 ] ]

# Since lists are mutable we can even add rows
>>> A.append([1, 4, 9])
>>> A
[ [ 14  2  3 ], [ 4  5  6 ], [ 7  0  9 ], [ 1  4  9 ] ]

# or add elements to existing rows
>>> A[0].append(5) 

A = [ [14  2  3 ], [ 4  5  6 ], [ 7  0  9 ] ]

# Since lists are mutable we can even add rows
>>> A.append([1, 4, 9])
>>> A
[ [ 14  2  3 ], [ 4  5  6 ], [ 7  0  9 ], [ 1  4  9 ] ]

# or add elements to existing rows
>>> A[0].append(5)

[ [14  2  3  5 ], 
  [ 4  5  6 ], 
  [ 7  0  9 ], 
  [ 1  4  9 ] ]
 

A = [ [14  2  3 ], [ 4  5  6 ], [ 7  0  9 ] ]

# Since lists are mutable we can even add rows
>>> A.append([1, 4, 9])
>>> A
[ [ 14  2  3 ], [ 4  5  6 ], [ 7  0  9 ], [ 1  4  9 ] ]

# or add elements to existing rows
>>> A[0].append(5)

[ [14  2  3  5 ], 
  [ 4  5  6 ], 
  [ 7  0  9 ], 
  [ 1  4  9 ] ]

# More exciting than your typical game of tic-tac-toe, right? 

A = [ [14  2  3 ], [ 4  5  6 ], [ 7  0  9 ] ]

# Since lists are mutable we can even add rows
>>> A.append([1, 4, 9])
>>> A
[ [ 14  2  3 ], [ 4  5  6 ], [ 7  0  9 ], [ 1  4  9 ] ]

# or add elements to existing rows
>>> A[0].append(5)

[ [14  2  3  5 ], 
  [ 4  5  6 ], 
  [ 7  0  9 ], 
  [ 1  4  9 ] ]

# More exciting than your typical game of tic-tac-toe, right?
# What?

A = [ [14  2  3 ], [ 4  5  6 ], [ 7  0  9 ] ]

# Since lists are mutable we can even add rows
>>> A.append([1, 4, 9])
>>> A
[ [ 14  2  3 ], [ 4  5  6 ], [ 7  0  9 ], [ 1  4  9 ] ]

# or add elements to existing rows
>>> A[0].append(5)

[ [14  2  3  5 ], 
  [ 4  5  6 ], 
  [ 7  0  9 ], 
  [ 1  4  9 ] ]

# More exciting than your typical game of tic-tac-toe, right?
# What? Not even a little?

3D-Arrays

Depending on the task at hand we may require more than 2 dimensions to represent our data

Colored images, for example, require a 3rd dimension to represent multiple color values at each pixel location of an image

The most popular and intuitive representation is the RGB color-space

in which the 3rd dimension has 3 indices to represent saturation values for Red, Green and Blue

We can also use 3D Arrays to represent the physical domains of planes and quadcopters

Since they can travel at various altitudes the extra dimension is necessary to fully specify their location in space

 
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays 
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices
 
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same 
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same
>>>           [3 * [0]]
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same
>>>      [4 * [3 * [0]]]
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same
>>> [2 * [4 * [3 * [0]]]]
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same
>>> [2 * [4 * [3 * [0]]]]

# The above would give us a 2 x 4 x 3 Array
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same
>>> [2 * [4 * [3 * [0]]]]

# The above would give us a 2 x 4 x 3 Array
    [0, 0, 0]
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same
>>> [2 * [4 * [3 * [0]]]]

# The above would give us a 2 x 4 x 3 Array
  [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ]
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same
>>> [2 * [4 * [3 * [0]]]]

# The above would give us a 2 x 4 x 3 Array
[ [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ],
  [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ] ]
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same
>>> [2 * [4 * [3 * [0]]]]

# The above would give us a 2 x 4 x 3 Array
[ [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ],
  [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ] ]

# Note that the dimensions are not necessarily read from left 
# to right during instantiation 
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same
>>> [2 * [4 * [3 * [0]]]]

# The above would give us a 2 x 4 x 3 Array
[ [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ],
  [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ] ]

# Note that the dimensions are not necessarily read from left 
# to right during instantiation. They are actually read from
# outer to inner 
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same
>>> [2 * [4 * [3 * [0]]]]

# The above would give us a 2 x 4 x 3 Array
[ [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ],
  [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ] ]

# Note that the dimensions are not necessarily read from left 
# to right during instantiation. They are actually read from
# outer to inner, so the following would return an equivalent
# array to the one above 
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same
>>> [2 * [4 * [3 * [0]]]]

# The above would give us a 2 x 4 x 3 Array
[ [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ],
  [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ] ]

# Note that the dimensions are not necessarily read from left 
# to right during instantiation. They are actually read from
# outer to inner, so the following would return an equivalent
# array to the one above
>>>   [[0] * 3]
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same
>>> [2 * [4 * [3 * [0]]]]

# The above would give us a 2 x 4 x 3 Array
[ [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ],
  [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ] ]

# Note that the dimensions are not necessarily read from left 
# to right during instantiation. They are actually read from
# outer to inner, so the following would return an equivalent
# array to the one above
>>>  [[[0] * 3] * 4]
 
# We can implement 3D arrays in Python following the same 
# syntax we had with 2D arrays. Imagine you're storing items 
# within a cube where the items are accessed by their row, 
# column and depth indices

# You can instantiate, access and modify them all the same
>>> [2 * [4 * [3 * [0]]]]

# The above would give us a 2 x 4 x 3 Array
[ [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ],
  [ [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0] ] ]

# Note that the dimensions are not necessarily read from left 
# to right during instantiation. They are actually read from
# outer to inner, so the following would return an equivalent
# array to the one above
>>> [[[[0] * 3] * 4] * 2]

# What do you think the following array would look like?
>>> A = [ 4 * [[0, 0]], 4 * [[1, 1]], 4 * [[2, 2]] ]

# What do you think the following array would look like?
>>> A = [ 4 * [[0, 0]], 4 * [[1, 1]], 4 * [[2, 2]] ]

# Hint - the following would produce an identical array
>>> A = [ [ 4 * [[i, i]] ] for i in range(3) ]

# What do you think the following array would look like?
>>> A = [ 4 * [[0, 0]], 4 * [[1, 1]], 4 * [[2, 2]] ]

# Hint - the following would produce an identical array
>>> A = [ [ 4 * [[i, i]] ] for i in range(3) ]

# Well, the loop iterates from 0 to 2 for i

# What do you think the following array would look like?
>>> A = [ 4 * [[0, 0]], 4 * [[1, 1]], 4 * [[2, 2]] ]

# Hint - the following would produce an identical array
>>> A = [ [ 4 * [[i, i]] ] for i in range(3) ]

# Well, the loop iterates from 0 to 2 for i, creating a 
# [ 4 * [[i, i]] ] at each index of the outer list

# What do you think the following array would look like?
>>> A = [ 4 * [[0, 0]], 4 * [[1, 1]], 4 * [[2, 2]] ]

# Hint - the following would produce an identical array
>>> A = [ [ 4 * [[i, i]] ] for i in range(3) ]

# Well, the loop iterates from 0 to 2 for i, creating a 
# [ 4 * [[i, i]] ] at each index of the outer list

# Now let's break down that inner statement 

# What do you think the following array would look like?
>>> A = [ 4 * [[0, 0]], 4 * [[1, 1]], 4 * [[2, 2]] ]

# Hint - the following would produce an identical array
>>> A = [ [ 4 * [[i, i]] ] for i in range(3) ]

# Well, the loop iterates from 0 to 2 for i, creating a 
# [ 4 * [[i, i]] ] at each index of the outer list

# Now let's break down that inner statement
# Well it looks like we just have a list of length 2 ([i, i]) 
# being repeated 4 times..  

# What do you think the following array would look like?
>>> A = [ 4 * [[0, 0]], 4 * [[1, 1]], 4 * [[2, 2]] ]

# Hint - the following would produce an identical array
>>> A = [ [ 4 * [[i, i]] ] for i in range(3) ]

# Well, the loop iterates from 0 to 2 for i, creating a 
# [ 4 * [[i, i]] ] at each index of the outer list

# Now let's break down that inner statement
# Well it looks like we just have a list of length 2 ([i, i]) 
# being repeated 4 times.. what would that look like?
 

# What do you think the following array would look like?
>>> A = [ 4 * [[0, 0]], 4 * [[1, 1]], 4 * [[2, 2]] ]

# Hint - the following would produce an identical array
>>> A = [ [ 4 * [[i, i]] ] for i in range(3) ]

# Well, the loop iterates from 0 to 2 for i, creating a 
# [ 4 * [[i, i]] ] at each index of the outer list

# Now let's break down that inner statement
# Well it looks like we just have a list of length 2 ([i, i]) 
# being repeated 4 times.. what would that look like?

# Something like this, right?
[[i, i], [i, i], [i, i], [i, i]]	 

# What do you think the following array would look like?
>>> A = [ 4 * [[0, 0]], 4 * [[1, 1]], 4 * [[2, 2]] ]

# Hint - the following would produce an identical array
>>> A = [ [ 4 * [[i, i]] ] for i in range(3) ]

# Well, the loop iterates from 0 to 2 for i, creating a 
# [ 4 * [[i, i]] ] at each index of the outer list

# Now let's break down that inner statement
# Well it looks like we just have a list of length 2 ([i, i]) 
# being repeated 4 times.. what would that look like?

# Something like this, right?
[[i, i], [i, i], [i, i], [i, i]]	
# where i ranges from 0 to 2 depending on the outer row

# Have an idea yet?
A = [ [ 4 * [[i, i]] ] for i in range(3) ] 

# Have an idea yet?
A = [ [ 4 * [[i, i]] ] for i in range(3) ]

[ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
  [ [1, 1], [1, 1], [1, 1], [1, 1] ],
  [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]
 

# Have an idea yet?
A = [ [ 4 * [[i, i]] ] for i in range(3) ]

[ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
  [ [1, 1], [1, 1], [1, 1], [1, 1] ],
  [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# Is that the same answer you got? 

# Have an idea yet?
A = [ [ 4 * [[i, i]] ] for i in range(3) ]

[ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
  [ [1, 1], [1, 1], [1, 1], [1, 1] ],
  [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# Is that the same answer you got?
# If not you should try playing around with your own lists on 
# the python shell 

# Have an idea yet?
A = [ [ 4 * [[i, i]] ] for i in range(3) ]

[ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
  [ [1, 1], [1, 1], [1, 1], [1, 1] ],
  [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# Is that the same answer you got?
# If not you should try playing around with your own lists on 
# the python shell. You can access it using 'python3' on the
# terminal

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]
 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before   

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first 
A[0]

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first, then the second 
A[0][0]

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first, then the second, and so on 
A[0][0][0] =>

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first, then the second, and so on
A[0][0][0] => 0 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first, then the second, and so on
A[0][0][0] => 0
A[2][1][0] => 	 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first, then the second, and so on
A[0][0][0] => 0
A[2][1][0] => 2		
 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first, then the second, and so on
A[0][0][0] => 0
A[2][1][0] => 2		

# Not that interesting.. but how about this?
A[0][0] => 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first, then the second, and so on
A[0][0][0] => 0
A[2][1][0] => 2		

# Not that interesting.. but how about this?
A[0][0] => [0, 0]
 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first, then the second, and so on
A[0][0][0] => 0
A[2][1][0] => 2		

# Not that interesting.. but how about this?
A[0][0] => [0, 0]
A[1]    => 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first, then the second, and so on
A[0][0][0] => 0
A[2][1][0] => 2		

# Not that interesting.. but how about this?
A[0][0] => [0, 0]
A[1]    => [ [1, 1], [1, 1], [1, 1], [1, 1] ]
 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first, then the second, and so on
A[0][0][0] => 0
A[2][1][0] => 2		

# Not that interesting.. but how about this?
A[0][0] => [0, 0]
A[1]    => [ [1, 1], [1, 1], [1, 1], [1, 1] ]

# We can even slice our list like before
A[1][1:] =>  

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first, then the second, and so on
A[0][0][0] => 0
A[2][1][0] => 2		

# Not that interesting.. but how about this?
A[0][0] => [0, 0]
A[1]    => [ [1, 1], [1, 1], [1, 1], [1, 1] ]

# We can even slice our list like before
A[1][1:] => [ [1, 1], [1, 1], [1, 1] ] 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first, then the second, and so on
A[0][0][0] => 0
A[2][1][0] => 2		

# Not that interesting.. but how about this?
A[0][0] => [0, 0]
A[1]    => [ [1, 1], [1, 1], [1, 1], [1, 1] ]

# We can even slice our list like before
A[1][1:] => [ [1, 1], [1, 1], [1, 1] ]
A[1:]    =>  

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# We can access arrays just as before where the first 
# dimension is specified first, then the second, and so on
A[0][0][0] => 0
A[2][1][0] => 2		

# Not that interesting.. but how about this?
A[0][0] => [0, 0]
A[1]    => [ [1, 1], [1, 1], [1, 1], [1, 1] ]

# We can even slice our list like before
A[1][1:] => [ [1, 1], [1, 1], [1, 1] ]
A[1:]    => [ [ [1, 1], [1, 1], [1, 1], [1, 1] ],
	      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ] 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# You can check the size of dimensions using len()
len(A) 	     => 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# You can check the size of dimensions using len()
len(A) 	     =>  3 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# You can check the size of dimensions using len()
len(A) 	     =>  3
len(A[0])    =>  

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# You can check the size of dimensions using len()
len(A) 	     =>  3
len(A[0])    =>  4 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# You can check the size of dimensions using len()
len(A) 	     =>  3
len(A[0])    =>  4
len(A[0][0]) =>  

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# You can check the size of dimensions using len()
len(A) 	     =>  3
len(A[0])    =>  4
len(A[0][0]) =>  2
 

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# You can check the size of dimensions using len()
len(A) 	     =>  3
len(A[0])    =>  4
len(A[0][0]) =>  2

# Assignment works the same way too



[ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
  [ [1, 1], [1, 1], [1, 1], [1, 1] ],
  [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# You can check the size of dimensions using len()
len(A) 	     =>  3
len(A[0])    =>  4
len(A[0][0]) =>  2

# Assignment works the same way too
A[1][3][0] = 7
 

[ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
  [ [1, 1], [1, 1], [1, 1], [7, 1] ],
  [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

A = [ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
      [ [1, 1], [1, 1], [1, 1], [1, 1] ],
      [ [2, 2], [2, 2], [2, 2], [2, 2] ]  ]

# You can check the size of dimensions using len()
len(A) 	     =>  3
len(A[0])    =>  4
len(A[0][0]) =>  2

# Assignment works the same way too
A[1][3][0] = 7
A[2][1][1] = 9

[ [ [0, 0], [0, 0], [0, 0], [0, 0] ],
  [ [1, 1], [1, 1], [1, 1], [7, 1] ],
  [ [2, 2], [2, 9], [2, 2], [2, 2] ]  ]

Higher Dimensions

For arrays of greater than 3 dimensions, spatial conceptualization is rather difficult 

Just think of the indices as partial identifiers

where a single member of the multidimensional array is determined by its complete set of identifiers

Let's go back to our picture example to see what a 4D array might look like

We have our first 3 dimensions specifying a pixel's row, column, and color-category

Therefore a single element of our 3D-array will describe the value of a specific color at a specific pixel location on the image grid

i.e. the value of red at upper-left corner of our image will be different than the value of blue at that same location

Now let's add a 4th dimension - time

A single element now will be separated from other elements of the same color and location by the variable time

Sound familiar?

It should

This is essentially what a video is - a layer of distinct images that we iterate through over time

Given a specific frame rate, a colored video can be fully represented by a 4D array

In a similar fashion, we can also add color and time to the 3D spatial domain mentioned earlier

So how many dimensions would we need to represent this colored, 3D space over time?

That's right, 5

(row, column, depth, color, time)

In this 5D-array, a single element would represent the value of a color at a specific location in space at a specific time

Can you imagine what this would look like?

It would be a full colored rendering of a 3 dimensional environment over time

Classes

Definition

On top of using structures provided by Python's standard library we have custom object types, called Classes

Classes are widely used by developers for object-oriented programming and can be found in virtually all Python libraries

Existing classes come with predefined attributes such as variables and methods

And we can define our own classes with specific attributes

 

# We can define a simple class as follows
class ClassName:
    static_declarations
    method_declarations
 

# We can define a simple class as follows
class ClassName:
    static_declarations
    method_declarations

# static_declarations include any variables whose values are
# intended to stay consistent between instances of the class 

# We can define a simple class as follows
class ClassName:
    static_declarations
    method_declarations

# static_declarations include any variables whose values are
# intended to stay consistent between instances of the class 
# as well as the Class' constructor, which is the method that 
# executes whenever we create an instance of the class
 

# We can define a simple class as follows
class ClassName:
    static_declarations
    method_declarations

# static_declarations include any variables whose values are
# intended to stay consistent between instances of the class 
# as well as the Class' constructor, which is the method that 
# executes whenever we create an instance of the class

# method_declarations include any methods of the class 

# We can define a simple class as follows
class ClassName:
    static_declarations
    method_declarations

# static_declarations include any variables whose values are
# intended to stay consistent between instances of the class 
# as well as the Class' constructor, which is the method that 
# executes whenever we create an instance of the class

# method_declarations include any methods of the class
# These are called using ClassName.methodName() if it is a
# static method (a method that has the same behavior across all
# instances of the class) 

# We can define a simple class as follows
class ClassName:
    static_declarations
    method_declarations

# static_declarations include any variables whose values are
# intended to stay consistent between instances of the class 
# as well as the Class' constructor, which is the method that 
# executes whenever we create an instance of the class

# method_declarations include any methods of the class
# These are called using ClassName.methodName() if it is a
# static method (a method that has the same behavior across all
# instances of the class), or using instanceName.methodName()
# if the method behavior depends on the instance of the class
 

# We can define a simple class as follows
class ClassName:
    static_declarations
    method_declarations

# static_declarations include any variables whose values are
# intended to stay consistent between instances of the class 
# as well as the Class' constructor, which is the method that 
# executes whenever we create an instance of the class

# method_declarations include any methods of the class
# These are called using ClassName.methodName() if it is a
# static method (a method that has the same behavior across all
# instances of the class), or using instanceName.methodName()
# if the method behavior depends on the instance of the class

# Note that the first letter of a class' name is capitalized

# Let's now define our own class  

# Let's now define our own class 
# We'll start by adding a static variable
class MyClass:
	releaseDate = 1997
 

# Let's now define our own class 
# We'll start by adding a static variable
class MyClass:
	releaseDate = 1997

# As mentioned before this variable should stay consistent 
# between instances of the class 

# Let's now define our own class 
# We'll start by adding a static variable
class MyClass:
	releaseDate = 1997

# As mentioned before this variable should stay consistent 
# between instances of the class, so we should be able to 
# access them from any instantiation of our class 

# Let's now define our own class 
# We'll start by adding a static variable
class MyClass:
	releaseDate = 1997

# As mentioned before this variable should stay consistent 
# between instances of the class, so we should be able to 
# access them from any instantiation of our class
>>> A = MyClass()
>>> B = MyClass() 

# Let's now define our own class 
# We'll start by adding a static variable
class MyClass:
	releaseDate = 1997

# As mentioned before this variable should stay consistent 
# between instances of the class, so we should be able to 
# access them from any instantiation of our class
>>> A = MyClass()
>>> B = MyClass()
>>> A.releaseDate 

# Let's now define our own class 
# We'll start by adding a static variable
class MyClass:
	releaseDate = 1997

# As mentioned before this variable should stay consistent 
# between instances of the class, so we should be able to 
# access them from any instantiation of our class
>>> A = MyClass()
>>> B = MyClass()
>>> A.releaseDate
1997 

# Let's now define our own class 
# We'll start by adding a static variable
class MyClass:
	releaseDate = 1997

# As mentioned before this variable should stay consistent 
# between instances of the class, so we should be able to 
# access them from any instantiation of our class
>>> A = MyClass()
>>> B = MyClass()
>>> A.releaseDate
1997
>>> B.releaseDate 

# Let's now define our own class 
# We'll start by adding a static variable
class MyClass:
	releaseDate = 1997

# As mentioned before this variable should stay consistent 
# between instances of the class, so we should be able to 
# access them from any instantiation of our class
>>> A = MyClass()
>>> B = MyClass()
>>> A.releaseDate
1997
>>> B.releaseDate
1997

# Now let's add a constructor to our class
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.addition = param1 + param2 

# Now let's add a constructor to our class
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.addition = param1 + param2


# All constructors are defined as __init__ in Python 

# Now let's add a constructor to our class
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.addition = param1 + param2


# All constructors are defined as __init__ in Python
# Note the 'self' keyword as well 

# Now let's add a constructor to our class
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.addition = param1 + param2


# All constructors are defined as __init__ in Python
# Note the 'self' keyword as well. When an object's method is 
# called in Python, the object itself is passed in as the first
# parameter by default 

# Now let's add a constructor to our class
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.addition = param1 + param2


# All constructors are defined as __init__ in Python
# Note the 'self' keyword as well. When an object's method is 
# called in Python, the object itself is passed in as the first
# parameter by default. Therefore, we always define class 
# methods with the parameter 'self' to represent the object
 

# Now let's add a constructor to our class
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.addition = param1 + param2


# All constructors are defined as __init__ in Python
# Note the 'self' keyword as well. When an object's method is 
# called in Python, the object itself is passed in as the first
# parameter by default. Therefore, we always define class 
# methods with the parameter 'self' to represent the object

# Since we want to save 'addition' as a class-variable rather
# than a local one we specify this using the 'self' keyword 

# Now let's add a constructor to our class
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.addition = param1 + param2


# All constructors are defined as __init__ in Python
# Note the 'self' keyword as well. When an object's method is 
# called in Python, the object itself is passed in as the first
# parameter by default. Therefore, we always define class 
# methods with the parameter 'self' to represent the object

# Since we want to save 'addition' as a class-variable rather
# than a local one we specify this using the 'self' keyword. 
# We must do this any time we access a class-variable inside 
# the scope of a method, whether it be for reading or writing

# So let's add the following line and see which value survives
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.myNum = param1 + param2
	myNum = param1 * param2  

# So let's add the following line and see which value survives
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.myNum = param1 + param2
	myNum = param1 * param2 

>>> A = MyClass(1, 2) 

# So let's add the following line and see which value survives
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.myNum = param1 + param2
	myNum = param1 * param2 

>>> A = MyClass(1, 2)
>>> A.myNum 

# So let's add the following line and see which value survives
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.myNum = param1 + param2
	myNum = param1 * param2 

>>> A = MyClass(1, 2)
>>> A.myNum
3 

# So let's add the following line and see which value survives
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.myNum = param1 + param2
	myNum = param1 * param2 

>>> A = MyClass(1, 2)
>>> A.myNum
3

# As expected, it's the first one 

# So let's add the following line and see which value survives
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.myNum = param1 + param2
	myNum = param1 * param2 

>>> A = MyClass(1, 2)
>>> A.myNum
3

# As expected, it's the first one. The second definition of
# 'myNum' was only saved locally so was lost after the
# execution of the constructor 

# So let's add the following line and see which value survives
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
        self.myNum = param1 + param2
	myNum = param1 * param2 

>>> A = MyClass(1, 2)
>>> A.myNum
3

# As expected, it's the first one. The second definition of
# 'myNum' was only saved locally so was lost after the
# execution of the constructor

# Note - when we don't explicitly define a constructor,
#        Python defaults to one that does nothing

# The method_declarations we mentioned earlier include any 
# other methods defined within our class 


class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
	self.myNum = param1 + param2


 

# The method_declarations we mentioned earlier include any 
# other methods defined within our class. Let's implement a
# product method for our class that returns the product of the
# input number and our class' myNum variable
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
	self.myNum = param1 + param2 

# The method_declarations we mentioned earlier include any 
# other methods defined within our class. Let's implement a
# product method for our class that returns the product of the
# input number and our class' myNum variable
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
	self.myNum = param1 + param2
    def product( self, n ):
	return self.myNum * n 

# The method_declarations we mentioned earlier include any 
# other methods defined within our class. Let's implement a
# product method for our class that returns the product of the
# input number and our class' myNum variable
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
	self.myNum = param1 + param2
    def product( self, n ):
	return self.myNum * n

>>> A = MyClass(1, 2) 

# The method_declarations we mentioned earlier include any 
# other methods defined within our class. Let's implement a
# product method for our class that returns the product of the
# input number and our class' myNum variable
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
	self.myNum = param1 + param2
    def product( self, n ):
	return self.myNum * n

>>> A = MyClass(1, 2)
>>> A.product(5) 

# The method_declarations we mentioned earlier include any 
# other methods defined within our class. Let's implement a
# product method for our class that returns the product of the
# input number and our class' myNum variable
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
	self.myNum = param1 + param2
    def product( self, n ):
	return self.myNum * n

>>> A = MyClass(1, 2)
>>> A.product(5)
15 

# The method_declarations we mentioned earlier include any 
# other methods defined within our class. Let's implement a
# product method for our class that returns the product of the
# input number and our class' myNum variable
class MyClass:
    releaseDate = 1997
    def __init__( self, param1, param2 ):
	self.myNum = param1 + param2
    def product( self, n ):
	return self.myNum * n

>>> A = MyClass(1, 2)
>>> A.product(5)
15
# Notice once again that the A itself is passed in as 'self',
# the first parameter of the method

Das It

And that covers everything you need to know for now!

We'll practice implementing our own classes on this week's assignment

References

Mutability

Multidimensional Arrays

Classes

Lec 4 - Data Structures

By Brian J Lee

Lec 4 - Data Structures

  • 525