# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# ### Mutability
# ##### The capability of an object to be changed after it has been instantiated.

# Lists are mutable
my_list = list(range(10))
print(my_list)

# Change the element at the 4th index
my_list[4] = 0
print(my_list)

# Append a number to the end of a list
my_list.append(1)
print(my_list)

# ### Can Change Variables of Immutable Objects

x = 4
print(x)

x = 12
print(x)

x == x

4 == 12

# +
# Basically, when you assign a value to a variable, Python holds that 
# value in memory, then associates that variable name with that value. When
# you change the value the variable holds, Python just associates that 
# name with a different thing in memory.
# -

# ### Tuples

# +
# Let's create some tuples
a_tuple = 1, 2
another_tuple = (1, 2)
yet_another_tuple = tuple([1, 2])

print(a_tuple)
print(another_tuple)
print(yet_another_tuple)

# +
# Typle vs List
some_collection = [2,3,4,7,8,10]

# Grab evens from some collection using a list
evens = []
for element in some_collection:
    if element % 2 == 0:
        evens.append(element)
print(evens)

# +
# Grab evens from some collection using a tuple
evens = ()
for element in some_collection:
    if element % 2 == 0:
        evens.append(element)
        
# See the error, you cannot append to a tuple because it's immutable

# +
# Create a tuple
t = (1, 3.5, 'sam')

# Tuples can hold any object, just like a list
print(type(t[0]))
print(type(t[1]))
print(type(t[2]))

# +
# Create a tuple
t = (1, [1, 2])

# Tuples can hold lists inside them!
print(type(t[1]))

# +
# Create a tuple
t = (1, [1, 2])

# You can even update a list that is inside a tuple!
t[1][0] = 'hello'
print(t)

# You can store mutable objects inside immutable objects.

# +
# Let's do it again
my_tuple = (1, 5, ['erin', 'desmond'])

# We can use append because we indexed into the list
my_tuple[2].append('is so cool')

print(my_tuple)

# +
# The only methods strictly associated with tupes are count() and index()

tuple_demo = (3,5,7,7, ['a string in a list'])
print(tuple_demo)
# -

# count()
print('Number of times 7 appears in tuple:', tuple_demo.count(7))

# index()
print('Index of that weird list:', tuple_demo.index(['a string in a list']))

# +
# Class challenge answer

my_tuple = (1,4,7,3,['a list', 4], 56)

to_add = ['hello', 45]

for each in to_add:
    my_tuple[4].append(each)

print(my_tuple)
# -

# ### Dictionaries

# Let's build a dictionary of states and capitols 
# If you order them in a list, it'll look like this:
states_caps = [('Georgia', 'Atlanta'), ('Colorado', 'Denver'), ('Indiana', 'Indianapolis')]
print(states_caps)

# Cool. But what do you do with this? Is this very readable? 
# To search for the capitol of Colorado, you'd have to search the entirety
# of the list until you found it - this is inefficient.
search_state = 'Indiana'
capital = 'State not found'
for state, state_capital in states_caps:
    if state == search_state:
        capital = state_capital
        break
print(capital)

# +
# Scratch that, let's throw these into a dictionary.
states_caps_dict = {'Georgia': 'Atlanta', 'Colorado': 'Denver', 'Indiana': 'Indianapolis'}
print(states_caps_dict)

# Notice the {}, the : and the , 
# -

# Now let's search for the capitol of Colorad
print(states_caps_dict['Colorado'])

# If a key does not exist in the dictionary, it will throw an error
print(states_caps_dict['Hawaii'])

# How do you know if you have a specific key in your dict?
states_caps_dict.get('Hawaii', 'State not found')

# +
# Dictionaries are mutable! But their keys are not, and their keys MUST
# be unique

# Let's change the value associated with a key in a dictionary

# +
# Instantiate the dictionary
my_dict = {'thing': 1, 'other': 2}

# Print the value associated with the key 'thing'
print(my_dict['thing'])
# -

# Change the value associated with the key 'thing'
my_dict['thing'] = 3
print(my_dict)

# Add a key-value pair to our dictionary
my_dict['the coolest'] = 'erin'
print(my_dict)

# +
# Note: THE KEYS IN DICTIONARIES MUST BY IMMUTABLE AND UNIQUE
# This means, you cannot assign a list as a key in a dictionary

my_dict_test = {['a list']: 4}

# Notice the reference to hash maps, read more if you're interested
# -

# ### Dictionary Looping

# +
# Let's print out the keys of a dictionary

# Create a dictionary
ages_dict = {'sam':45, 'bob':20, 'erin':30, 'jenny': 21}

# This will print the keys in my dictionary
for name in ages_dict:
    print(name)
    
# same thing, just using .keys()
for name in ages_dict.keys():
    print(name)
    
# Usually these will print unordered

# +
# What if you wanted to print the values and not the keys? .values()

for age in ages_dict.values():
    print(age)

# +
# What if you wanted to print both? .items()

for name, age in ages_dict.items():
    print(name, age)
# -

# Maybe you want to print them together as tuples:
for people in ages_dict.items():
    print(people)

# +
# Class challenge answer

class_dict = {'erin': 30, 'sean': 32}
my_name = 'sally'
my_age = 34

if my_name not in class_dict.keys():
    class_dict[my_name] = my_age

print(class_dict)
# -

# ### Sets

# Let's make a set
my_set = set([1, 2, 3])
my_other_set = {1,2,3}
print(my_set, my_other_set)

# Let's see if those two are equal
my_set == my_other_set

# Let's explore set methods, press .tab
my_set

my_set = set([1, 2, 3])
my_other_set = {5,6,7}

# Let's check the union between the two sets
my_set.union(my_other_set)

# Let's add some number to our set
my_set.add(4)
print(my_set)

# Now let's update one set by adding another set to it
my_set.update(my_other_set)
print(my_set)

# Let's get rid of the number 5 in our set
my_set.remove(5)
print(my_set)

# How about the intersection?
my_set.intersection(my_other_set)

# ### Set Speed

# +
# Let's create a list of 10000 numbers
my_list = list(range(10000))

# Now let's turn that into a set
my_set = set(my_list)
# -

# time 1000 loops
timeit 1000 in my_list

# +
# Another way to time it:
# %timeit 1000 in my_list

# %timeit 1000 in my_set
# -

# ### Dictionary Comprehension

# +
# Let's create a dictionary with a for loop

squares_dict = {}
for num in range(1, 6):
    squares_dict[num] = num**2
print(squares_dict)

# +
# Now let's do the exact same thing with a dictionary comprehension

squares_dict_comp = {num: num**2 for num in range(1, 6)}
print(squares_dict_comp)
# -

# Class challenge answer
from random import randint
my_dict = {name: randint(1,100)**2 for name in names_list}









StarcoderdataJupyterScriptsDedupFiltered