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

# ***IMPORTANT***
#
# Make sure you activate the `ml-workshop` virtual environment.

# # Data Types
# - `int`
# - `float`
# - `list`
# - `tuple`
# - `dict`
# - `str`
# - `set`
# - `complex`

# ## Integers

type(1)

# Basic math operations: `+`, `-`, `*`, `/`

1 + 1

4 - 7

5 * 2

3 / 2

# ## Floats

type(0.5)

0.5 + 1

# Basic math operations: `+`, `-`, `*`, `/`

1.5 + 0.2

# Be careful of numerical round-off

1 / 3

1 / 3 == 0.33

# ## Lists

# Basics list construction

[1, 2, 3]

# Convert somthing to a list

range(10)

list(range(10))

# Lists can be non-homogeneous

[1, 2.5, 'yellow', ['a', 'b', 'c']]

# Lists have a length

len([1, 2, 3])

# Lists are "iterable"

for item in ['hello', 'world']:
    print(item)

# Lists can be can grow

my_list: list[int] = []
my_list

# Using the `append` method

my_list.append(1)
my_list

# Using `+=` operator overloading

my_list += [2]
my_list

# Lists can be sorted

unsorted_list = [6, 9, 1, 5, 23]
sorted(unsorted_list)

# Lists can be reversed, but the `revered` function returns an `iterator`.

reversed([1, 2, 3, 4, 5])

list(reversed([1, 2, 3, 4, 5]))

# The elements of lists can be modified

x = [1, 2, 3]
x[0] = -4
print(x)

# Lists can be indexed

y = ['a', 'b', 'c', 'd']
y

y[0]

# Lists can be sliced
#
# **IMPORTANT:** Python indicies are **left inclusive, right exclusive**
#
# This means, the left index (before the `:`) is included, but the right index (after the `:`) is **excluded**.

y[1:3]

# Lists can be indexed and/or sliced in reverse using negative numbers.

y[-1]

# ## Tuples

# Basically, all the rules of lists apply to tuples, but have some differences.

# Tuples are constructed using parentheses.

(1, 2, 3)

# This is not a valid tuple

not_a_tuple = (1)
type(not_a_tuple)

# A trailing comma is required to create a tuple with only *one element*

my_tuple = (1,)
my_tuple

type(my_tuple)

# Tuples are immutable
#
# Use a `try-except` block to catch the error and print it without halting the program.

# +
import traceback

x = (1, 2, 3)
try:
    x[0] = -4  # will raise a TypeError
except TypeError as err:
    traceback.print_exc()  # print the error message without stopping the program
# -

# ## Dictionaries

# Dictionaries are constructed with curly braces and colons (`{'key': value}`)

my_dict = {
    'first_name': 'Mark',
    'last_name': 'Todisco'
}
my_dict

# Dictionary `values` are accessed by their `keys`

my_dict['first_name']

my_dict['last_name']

# ## Strings

# Strings can use single or double quotes, they are equivalent.

'hello' == "hello"

# Strings can be concatenated

# +
first_name = 'Mark'
last_name = 'Todisco'

full_name = first_name + ' ' + last_name
full_name
# -

# String can be indexed and sliced

full_name[:4]

# ## Sets

# Sets are also constructed from curly braces, but **do not* use colons: `{elem1, elem2, ...}`

my_set = {'a', 'b', 'b', 'c'}
my_set

# We can test if an element is in a set

'a' in my_set

'd' in my_set

# We can do expected mathematical operations on sets

new_set = {'a', 'd'}
my_set.union(new_set)

# ## Complex

type(1j)

my_complex_number = 3 + 2j
my_complex_number

# Complex numbers have `real` and `imag` properties

my_complex_number.real

my_complex_number.imag

# The complex conjugate is calulcated using the `conjugate` method

my_complex_number.conjugate()

# # Iterators

# Lists, tuples, and strings are "iteratble"

my_list = [1, 2, 3]
for item in my_list:
    print(item)

my_tuple = (1, 2, 3)
for item in my_tuple:
    print(item)

my_string = '123'
for item in my_string:
    print(item)

# # For Loops

# Inneficient (matlab) way to loop over an iterable

my_data = ['a', 'b', 'c']
my_data

for i in range(len(my_data)):
    print(my_data[i])

# Better way to loop over an iterable

for item in my_data:
    print(item)

# # While Loops

# +
i: int = 0

while i < 5:
    print(i)
    i += 1
# -

# You can specify an `else` clause to execute once the loop exits

# +
j: int = 0

while j < 5:
    print(j)
    j += 1
else:
    print('The loop ended')
# -

# # List Comprehensions

# Inefficient method

# +
even_numbers: list[int] = []

for i in range(20):
    if i % 2 == 0:
        even_numbers.append(i)

even_numbers
# -

# This method is faster and requires less code

even_numbers = [i for i in range(20) if i % 2 == 0]
even_numbers

StarcoderdataJupyterScriptsDedupFiltered