---
# System prepended metadata

title: WEEK 2

---

# WEEK 2
[toc]

# Day 1: INTERMIDIATE PYTHON
## 1. List comprehension:
```
even_numbers =[]
for x in range(5):
  if x%2 == 0:
    even_numbers.append(x)
even_numbers
>[0, 2, 4]
```
==more consie way==
```
even_numbers = [x for x in range(5) if x % 2 == 0]
>[0, 2, 4]
even_squares = [x * x for x in even_numbers]
>[0, 4, 16]
```
==You can use it to create dictionaries or sets too:==

```
square_dict = {x: x*x for x in range(5)}
>{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
```

*>> **Different from set & list:** set will remove all the duplicate* <<
```
square_set  = {x * x for x in [-1, 1]}
>{1}
square_set = {x*x for x in [-5, -3, 3, 5]}
>{9, 25}

```
==If you dont need the value of the list:==
```
zeros = [0 for _ in even_numbers] #has the same length as even_numbers
>[0, 0, 0]
```
==A list comprehension can include multiple fors:==
```
for x in range(10):
    for y in range(10):
        print(x,y)
0 0
0 1
0 2
..
0 9
1 1
1 2
```
==same==
```
pairs = [   
            (x, y)
            for x in range(10)
            for y in range(10)
        ]                     #100 pairs
pairs
>  [(0, 1),
    (0, 2),
    (0, 3),
    (0, 4),
    ...
]
```
### 2. Iterables and Generators:
``` 
def generator_name():
    #statements
    yield something
```
**Generators:** 
- The normal function calls it one time and then it disappears.(for ... in)
- ==**The generator** when generate the number, it's waiting for you to call it again through FOR.== (which can be iterated over just like lists but generate their values lazily on demand.)
```
def generate_range(n):
    i = 0
    while i < n:
        yield i   # every call to yield produces a value of the generator
        i += 1

my_gen = generate_range(10) #my_gen is a pointer
for i in my_gen:
    print(i)
0
1
2
..
9
```
==A second way to create generators is by using for comprehensions wrapped in parentheses:==
```
evens_below_20 = (i for i in generate_range(20) if i % 2 == 0) #using () to create another generator, so even_below_20 is a 2nd generator.

for i in evens_below_20:
  print(i, end=' ')
  
[0 2 4 6 8 10 12 14 16 18]
```
==After you consume all numbers in a generator, it won't produce anymore result==
```
for i in evens_below_20:
  print(i, end=' ')   
  
#nothing will print out here after we loop through everything in that evens_below_20
  ```
==This generator will **generate infinite** list unlike the one above==
```
def natural_numbers():
    """returns 1, 2, 3, ..."""
    n = 1
    while True:
        yield n
        n += 1

data = natural_numbers()
evens = (x**2 for x in data if x % 2 == 0)
even_squares = (x ** 2 for x in evens)

for x in evens:
    if x == 20: #without the break, it will run forever!
        break
```

### 3. Automated Testing via assert 
```
assert 1 + 1 == 2
assert 1 + 1 == 3, "An error message"
```
==We can use assert to test our below code:==
```
def smallest_item(xs):
    return min(xs)    
    
assert smallest_item([10, 20, 5, 40]) == 5
assert smallest_item([1, 0, -1, 2]) == -1
```
==This is a very important philosophy in programming: "write test cases before you write code":==
```
def gcd(a, b):
    if b == 0:
        return a
    while b > 0:
        a, b = b, a % b
    return a

assert gcd(4, 6) == 2
assert gcd(6, 12) == 6
```
### 4. Randomness
```
import random
random.seed(10)  # this ensures we get the same results every time
for _ in range(5):
  print(random.random())

0.6858614854745947
0.6618463200523511
0.1329781447122128
0.7678378139439905
0.9824132490111909

```
==**if your want to get reproduce results:** make sure you calling seed only one time at the top of your program, **and never put seed in the loop**== 
```
for _ in range(5):
  print(random.random())

0.8235888725334455
0.6534725339011758
0.16022955651881965
0.5206693596399246
0.32777281162209315
```
```
print(random.randrange(10))    # choose randomly from range(10) = [0, 1, ..., 9]
print(random.randrange(3, 6))  # choose randomly from range(3, 6) = [3, 4, 5]

6
3
```
```
up_to_ten = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
random.shuffle(up_to_ten)
print(up_to_ten)

[5, 6, 9, 2, 3, 7, 8, 4, 1, 10]
```
```
my_best_instructor = random.choice(["Minh", "Tom", "Minh Anh", "Nhan"])
my_best_instructor #giving random name at a time
```
```
lottery_numbers = range(49)
winning_numbers = random.sample(lottery_numbers, 6)  # [16, 36, 10, 6, 25, 9]winning_numbers

winning_numbers

[18, 43, 16, 29, 11, 47]
```
### 5. Zip and Argument Unpacking
==The zip function transforms multiple iterables into a single iterable of tuples of corresponding function:==
**If the lists are different lengths, zip stops as soon as the first list ends.**
```
list1 = ['a', 'b', 'c']
list2 = [1, 2, 3, 4, 5, 6, 7]

> zip is lazy, so you have to do something like the following
[pair for pair in zip(list1, list2)]    # is [('a', 1), ('b', 2), ('c', 3)]

[('a', 1), ('b', 2), ('c', 3)]
```
**You can also “unzip” a list using a strange trick:**
```
pairs = [('a', 1), ('b', 2), ('c', 3)]

list(zip(*pairs))

[('a', 'b', 'c'), (1, 2, 3)]
```
==More example of unpair by using *(asterisk):==
```
def f(*args): 
  x, y, z = args
  print(z)

f(1, 2, 3)

3
```
```
[1,2,3] ->  1,2,3
{"coder":True, "school":False} -> "coder" = True, "school"=False
```
```
def f(*args, **kwargs):
    print('args = ', args) #args =  ('Minh', 'Tom')
    print('kargs = ', kwargs) # kargs =  {'coder': True, 'school': True, 'test': False, 'hihi': 6}
    result = []
    for name in args:
        result.append('Hi ' + name)
    for key in kwargs:
        print(key, kwargs[key])
    return result
f('Minh', 'Tom', coder=True, school=True, test=False, hihi=6)

coder True
school True
test False
hihi 6
['Hi Minh', 'Hi Tom']
```
==Have 3 lists and want to zip them up:==
```
letters, numbers, greeting = zip(('a', 1, 'hi'), ('b', 2, 'there'), ('c', 3, '!'))
letters
numbers
greeting

('a', 'b', 'c')
(1, 2, 3)
('hi', 'there', '!')
```
==The asterisk (*) performs argument unpacking==
```
a, b, *c = [10, 99, 199, 101, 123]
a, b, c

(10, 99, [199, 101, 123])
```

#### Question:
>Write a function flexible_compute(*args, **kargs) so that we can call the function like this:
flexible_compute(3, 2, divide_by_2=True, operator='Mul')
flexible_compute(3, 2, divide_by_2=True, operator='Add')
flexible_compute(3, 2, divide_by_2=False, operator='Minus')

```
def flexible_compute(*args, **kargs):
  #print(args, kargs)
  a, b = args
  div_2 = kargs['divide_by_2']
  operator = kargs['operator']
  if operator == 'Add':
    result = a+b
  elif operator == 'Mul':
    result = a*b
  else:
    result = a - b

  if div_2 == True:
    return result/2
  else: 
    return result

#flexible_compute(3, 2, divide_by_2=True, operator='Mul')

assert(flexible_compute(3, 2, divide_by_2=True, operator='Mul') == 3 )
assert(flexible_compute(3, 2, divide_by_2=True, operator='Add') == 2.5 )
assert(flexible_compute(3, 2, divide_by_2=True, operator='Minus') == 0.5 )
assert(flexible_compute(3, 2, divide_by_2=False, operator='Minus') == 1 )
assert(flexible_compute(3, 2, divide_by_2=False, operator='Add') == 5 )
```
### 6. Pointer in Python
==**List, Dictionary** are passed in a function which are their address value== (shallow copy of the address in the memory)!
```
a = [2]
def add_2(b):
    b[0] = b[0] + 2
    return b
print(add_2(a))
print(a)

[4]
[4]
```
==If you don't want the function to change the orignal list/dictionary then need to *deep copy*== (which create a new clone/copy of variable value in a memory)
```
a = [2]  #ab346
def add_2(b): #ab346
    b = b.copy() #jh895
    b[0] = b[0] + 2
    return b
print(add_2(a))
print(a)

[4]
[2]
```
```
a = [1, 2, 3]
b = a.copy()
a[0] = 9
print(a)
print(b)

[9, 2, 3]
[1, 2, 3]
```
==Also apply for dict!==
```
a = {'a': 1, 'b': 2}

def increase_by_3(n):
    n = n.copy()
    n['a'] += 3
    return n

print(increase_by_3(a))
print(a)

{'a': 4, 'b': 2}
{'a': 1, 'b': 2}
```
#### Question:

**1) How can I make sure any function won't change my input variable ?**
> We can convert it into Tuples, and Tuples is immutable
We can make a copy of that address, so it wont change the original

**2)If I want to change that variable in the function, how can I do that? I got a function like this:**
```
a = 'Coderschool'
def change(b):
  b = b.lower()
  return b
 
change(a) # change to 'coderschool'
print(a)  # but still 'Coderschool'
```
**Write 3 different ways to change the a variable to 'coderschool' instead of 'Coderschool'**
```
a = 'Coderschool'
def change(b):
    b = b.lower()
    return b
a = change(a) 
print(a)
```
==coderschool==
```
a = 'Coderschool'
def change(b):
    global a
    a = a.lower()
    return a
print(a)
```
==coderschool==
```
a = 'Coderschool'
a =[] 
a.append['Coderschool']
def change(b):
    b = b.lower()
    return b

print(change(a[0]))
```
==coderschool==
### 7. Python Decorators(Bonus)
==Functions can be passed around and used as arguments==
```
def say_hello(name):
    return f"Hello {name}"

def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

def greet_bob(greeter_func):
    return greeter_func("Bob")

print(greet_bob(say_hello))
print(greet_bob(be_awesome))

Hello Bob
Yo Bob, together we are the awesomest!
```
==It's possible to **define function inside other function.**==
```
def parent():
    print("Printing from the parent() function")

    def first_child():
        print("Printing from the first_child() function")

    def second_child():
        print("Printing from the second_child() function")

    second_child()
    first_child()

parent()
#second_child() # this function will be not defined in global scope because it is not in the same scope of parent function

Printing from the parent() function
Printing from the second_child() function
Printing from the first_child() function
```
==The magical Python decorator:==
```
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Something is happening before the function is called.")
        func(*args, **kwargs)
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def greeting(name):
    """Just a docstring"""
    print("Hi", name)

greeting('coderschool')



Decorator allows you to run it bf the function calls, call the function and run sth after the function is called
```

[link]https://www.youtube.com/watch?v=7otb3njzLwQ&feature=youtu.be

== https://regexone.com/

Lesson Notes
abc…	Letters
123…	Digits
\d	Any Digit
\D	Any Non-digit character
.	Any Character
\.	Period
[abc]	Only a, b, or c
[^abc]	Not a, b, nor c
[a-z]	Characters a to z
[0-9]	Numbers 0 to 9
\w	Any Alphanumeric character
\W	Any Non-alphanumeric character
{m}	m Repetitions
{m,n}	m to n Repetitions
*	Zero or more repetitions
+	One or more repetitions
?	Optional character
\s	Any Whitespace
\S	Any Non-whitespace character
^…$	Starts and ends
(…)	Capture Group
(a(bc))	Capture Sub-group
(.*)	Capture all
(abc|def)	Matches abc or def


---
waz{3,5}up
\? for real?
\. for real.
<key>? for any optional character
. for matching anything you want
.+ matching more than 1
^Mission: successful$  so ^and $ for start and ends of a string


# Day 2: DATA STRUCTURES AND ALGORITHMS

## Objectives

- Object Oriented Programming (OOP)
- Time complexity, Big O Notation
- Data structures and Algorithms
- Recursive, Binary Search, Quick Sort, Merge Sort

## 1. Time Complexity and BigO Notation
* **Time Complexity**: a measurement of how much time an algorithm takes.
* **Big O Notation**: commonly used notation for describing ==the limiting behavior of a function== when the argument tends toward infinity.

> Constant time: **O(1)**

(Recommend to use this one, regardless input it will run in one step)
> Logarithmic: **O(log(n))**
Linear time: **O(n)**

(Recommend to use these 3)
> Quadric: **O(n^2)**

(Run big input it will takes infinity longer)
> Exponential: **O(2^n)**

(It's growing exponetially)
 
 ![img](https://amitshahi.dev/static/756663638e3de206cc41988bfa13b7db/2d017/bigocomplexitychart.jpg)
 
Binary Search
---
Recursion (repeat it self)
- a function calling itself inside the body of that function -> recursion
- when you have stopping condition

factorial will run 4ever in case it has a condition to stop it
---
==dynamic programming== keyword to search>> 

## Sorting Algorithms

==Binary Sort==

---

==Quick Sort==
==Merge Sort== but recommend using this one, more efficient(they are better because they are the best application in the worst scenario)
Quick and Merge -> worst case or bad case are the same

Merge sort
- Only sort when merge items at the end
- Do not sort when splitting items

## OOP (Object Oriented Programming)

functions belong to a method we call it object??
calling the method we dont need to use self anymore.

* when we overide a default have to use underscore
* without the underscore can write anything you want when calling *def*
* if using underscore have to use the resigned name


DOg is inherited from anima
==Super==
weight? in herited
dont need to have the same nsame with the parent
sll the method need to add self in it.

using dir(str) or can google it <magic method>

what is default mean?? 




---
Matrix








[2X3]*[3X2]=[2X2]
2 ==2(last element)
3==3


---
# Day 3: LAB NUMPY
## 1. Import library

==import== numpy ==as== np

## 2. Basics
**Let's take a took at how to create tensors with NumPy.**

<div align="left">
<img src="https://i.imgur.com/VSCCjUx.png" width="650">
</div>

* **Scalar**
x = np.array(6)

x:  6
x ndim:  0
x shape: ()
x size:  1
x dtype:  int64

* **Vector**
x = np.array([1.3 , 2.2 , 1.7])

x:  [[1.3]
     [2.2]
     [1.7]]
x ndim:  2
x shape: (3, 1)
x size:  3
x dtype:  float64

* **Matrix**
x = np.array([[1,2], [3,4]])

x:
 [[1 2]
 [3 4]]
x ndim:  2
x shape: (2, 2)
x size:  4
x dtype:  int64

* **3-D Tensor**
x = np.array([[[1,2],[3,4]],[[5,6],[7,8]]])

x:
 [[[1 2]
  [3 4]]

 [[5 6]
  [7 8]]]
x ndim:  3
x shape: (2, 2, 2)
x size:  8
x dtype:  int64

* ==NumPy also comes with several functions that allow us to create tensors quickly.==
```
np.zeros((2,2)):
 [[0. 0.]
 [0. 0.]]
np.ones((2,2)):
 [[1. 1.]
 [1. 1.]]
np.eye((2)):
 [[1. 0.]
 [0. 1.]]
np.random.random((2,2)):
 [[0.37901834 0.7440362 ]
 [0.4541136  0.99075632]]
 ```
## 3. Indexing
<div align="left">
<img src="https://i.imgur.com/y0CL0S4.png" width="300">
</div>

* **Indexing**
x = np.array([1, 2, 3])
print ("x: ", x)
print ("x[0]: ", x[0])
x[0] = 0
print ("x: ", x)

x:  [1 2 3]
x[0]:  1
x:  [0 2 3]

* **Slicing**
x = np.array([[1,2,3,4], [5,6,7,8], [9,10,11,12]]) #Shape 3,4
print (x)
print (x[:, 1]) 
print (x[0, :]) 
print (x[0:2, 1:3])
print (x[:, 0]) 

[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]]
x column 1:  [ 2  6 10]
x row 0:  [1 2 3 4]
x rows 0,1 & cols 1,2: 
 [[2 3]
 [6 7]]
x first column: 
 [1 5 9]




## . Create Numpy array
==**1D Array**==
```
x = np.array([1, 2, 3])
print(x)
print(x.shape)

[1, 2, 3]
(3,)
```
==**2D Array**==
```
x = np.array([1, 2, 3],[4, 5, 6])
print(x)

[[1, 2, 3]
[4, 5, 6]]
```
```
x = np.array([[0,0,0,0]], dtype='float32')
print(x)
print(x.shape)

[[0. 0. 0. 0.]]
(1, 4)
```
==Using **np.ones** to create array==
```
x = np.ones((2,5))
print(x)
print(x.shape)

[[1. 1. 1. 1. 1.]
 [1. 1. 1. 1. 1.]]
(2, 5)
```
```
x = np.ones((2,2),dtype='int32')*7
print (x)

[[7 7]
 [7 7]]

-----
## DAY 5:

keys -> hash function -> buckets

Tuples can be in dict
Rules to

**print(values):**
for k in my_car:
    print(my_car[k])
for x in my_car.values()
    print(x)
**print key and the values**
for x,y in my_care.items()
    print(x,y)
    
**get indexes and the key**
for x, y in enumerate(my_car):
    print(x,y)
    
**get indexes, keys and values**
for (x,y,z) in enumerate(my_car)
    print(x,y,z)

# Check if key exists in dict
if "driven_by" in my_car:
    print("YES")
else:
    print("NO")
    
-- checking
def check_key(dict,key)
    return key in dict

**Adding more items in dict**
my_house["coolness"] = "The best of the best!"
my_house

**Removing an item in dict**
my_house.pop("coolness")
my_house

Removing the latest item just inserted (here which is the "coolness")
my_house.popitem()

**==Organize a list of dict==**
my_family_list = [child1,child2,child3]

for child_key in my_family_dict:
    print("child",child)
    for 
    
### 2.5 Beautiful Soup
 import re
 my_clean_string = re.sub('[^\w\s],',my_string)
 
 --------
 WEEKLY PROJECT
 1 table for category
     store a link inside category
 1 table for products
 can use sql "What is the largest numbers of products?"
 "What is the highest price of products?"
 
 
 --- Insert a row of data
 conn.commit
 make sure to commit after changing any content(insert,delete,change)
 
 sql use to figure out which one will be the last layer
 by using left join or right join