Intro To Python Programming Notes
Intro To Python Programming Notes
Intro To Python Programming Notes
Power:
Get input base, power
Set ans = 1 (multiplication)
Set count = 0 (addition/counter)
If count < power
1. Multiply answer by base
2. Add 1 to count
3. Go back to if line
Otherwise, output answer
→ ans = ans * base
Casting: x = bool(x)
False: 0, 0.0., ‘’
True: all else
Add strings “str” + “str” or “str“ + “int”
Float = decimal
Boolean = True, False → type matters (use ==)
5 / 3 = 1.6666667
5 // 3 = 1 (cuts off decimal part) → not rounding
5 % 3 = 2 (remainder)
Negative: find biggest multiple of -n that is still less than -m (find distance)
-5%10 = 5
Comments:
# single line
“““
multi-line
“““
x = x+2 → x += 2
x = x*2 → x *= 2 or if string: x = ‘2’ → x *= 2 = ‘22’
> < >= <= == : must be same type on both sides
- String comparison: later letter are greater “boo” > “ahh” (compare 1st, if same compare 2nd)
length doesn’t matter
Abstraction: agreement about how to represent information at different levels
Python code
Interpreter
Translate → something else
Output ← result
Parsing (levels of abstraction → what code we write, how Python reads it)
Function → operator → tokens
Tokenizing: breaking text into tokens/words
Parsing: imposing structure/meaning on tokens
(1) Runtime error (NameError) after parsing, after tokenizing, error while running code
Bytecode (set of instructions that computer knows how to run) → variables, literals, functions
Program memory
- Constant table, variable table, stack (stores stuff → loads in stack for operation)
- Once you use a loaded variable, is gets deleted (must reload)
- Only need to store once
Python Bytecode bucket #
X=5 LOAD_CONST 1 (5)
Y=7 STORE_FAST 0 (x)
Z = x+y LOAD_CONST 2 (7)
STORE_FAST 1 (y)
LOAD_FAST 0 (x)
LOAD_FAST 1 (y)
BINARY_ADD
STORE_FAST 2 (z)
Store_fast: loads const into variable
Load_fast: load variables
Can use dis.dis(myfunc) to see breakdown
(2) Logical Error: parses fine, runs fine, but is logically incorrect
** can’t be recognized by Python
(3) Syntax Error
Code will not run, even if error is in last line (Bytecode will not load)
Bit = a single 0 or 1
8 bits = 1 byte (max binary = 255)
4 bits/half-byte = hexadecimal system (base 16) 0 1 2 3 4 5 6 7 8 9 A B C D E F
A = 10
B = 11…
1F = 0001 1111
Color = pixels - RGB combinations → need 3 bytes to store information (8 bits each since 255)
Greyscale = 1 byte
Functions
function(arguments)
print( ) = side effect → NONE since it doesn’t return a value
Libraries: import __library__ (i.e. math) → math.degrees(math.pi) = 180.0
When making a function:
1. Name
2. Input
3. Body
4. Output
def birthdaySong(name):
print(“Happy birthday to you”)
print(“Happy birthday to you”)
print(“Happy birthday dear” + name)
print(“Happy birthday to you”)
(no need for output line)
birthdaySong(“Sam”)
print(“HERE”, birthdaySong(“Stella”)) = NONE
**function always returns something: even if you don’t return anything -> return “NONE”
def randomFunction(x):
x = x+20
return x /2 **more useful is you want to use the output for something else
print(randomFunction(4) + randomFunction(2))
Combining Booleans: logical operations → ___ and ___, ___ or ___, not( )
not(x<5) → x>=5
Nesting control
Bits = electrical voltage (low =1, high=1)
Logical gates: hardware’s boolean operations (input/output instead of true/false)
- AND (rounded square) A^B
- OR (rounded triangle) AVB
- NOT (triangle with circle at point) -A
- NAND (circle in front of AND) -(A^B)
- NOR (circle in front of OR) -(AVB)
- XOR (if exactly 1 and 0) A⊕B
1 1 **10
1 0 1
0 1 1
0 0 0
Half adder: 1 bit addition (9+7)
Full adder: 17 + 25 C_in = value carried in from previous computation
C_out = regular carry: need at least 2 1’s to be 1 = X⊕Y⊕Cin
N-bit adder: chain together full adders → replace each full adder with black box
Each black box makes Cout that becomes Cin for next black box
Each number (X,Y) in binary from switches on or off
1 1 1 0
1 1 0 0
1 0 1 1
1 0 0 1
0 1 1 0
0 1 0 0
0 0 1 1
0 0 0 0
For-range is best option: more options, not as complicated + needing more info
While loop necessary if range endpoint not known
Slicing
s[len(s)-1] = s[-1] = last character (negative indexes go from right to left of string)
s[1:len(s):1] = s[beginning:ending:step] → can be used to get substring of whole string
s[1::] = s[1:]
String parsing
for word in s.split(where you want to separate):
print(word)
Code syntax:
(1) if x < 5: → if multiple if’s all will run
(2) elif x == 5:
(3) else:
Binary:
Unsigned binary conversion:
- Divide by 2 | remainder
- Bring whole number factor down + continue
- Stop at ½ = 1
- Go bottom to up to determine binary
53 = 110101
53 26 1
26 13 0
13 6 1
6 3 0
3 1 1
1 1
_ _ _ _
Signed magnitude bit: 1 = negative, 0 = positive
Hold value
Circuits:
Look for patterns after fixing first input and combine to get boolean expression
(when x = 0, (Y__Z) notX)
def decodeFile(filename):
f = open(filename, “r”)
text = f.read( )
f.close( )
for line in text.split(“\n”):
nextWord = “”
for word in line.split(“ “):
num = chr(int(word))
nextWord = nextWord + num
message = message + “ “ + nextWord
return message
**no return → function implicitly returns none, so printing a function without return statement will print
NONE
Circuit abstraction:
Half Adders: adds two binary digits (11, 10, 01, 00)
1+1 = 10 → need another output column to store carried value 1
2 inputs of single binary digits, carry number, sum
Full Adders: 3 inputs (X, Y, Cin from previous addition), output (1’s digit), carry number
N-Bit Adders: chain together full adders (adding numbers of multiple bits in length by adding single digit
numbers one by one + carrying number over)
Flip-Flops
For truth table, break into chunks by columns and combine boolean expression
Errors:
Syntax you see right away (print(“Uh oh))
Runtime after you run the function (print(1/0))
Logical: python won’t notice it but you’re not expressing what you want correctly
Graphics
Edit shell configuration
Change gui = none: no GUI support
Change startDir
Starter code:
from tkinter import → tkinter = graphics library
Drawing a grid
Left: size*(number of circles that came before) → repeated loop variable
Right: left + size
Lists
Built-in functions:
len(lst)
min(lst), max(lst) → for strings too
sum(lst) → only for numbers
lst = [7, 5, 3]
3 in lst → returns boolean
lst.count(5) = 1
lst.index(3) = 2 → returns first index of occurance
def generateFibNumbers(numList):
fibList = [1,1]
while fibList[len(fiblist)-1] < max(numLst):
nextFib = fibList[len(fibList)-2] + fibList[len(fibList)-1]
fibList = fibList + [nextFib]
return fibList
lst.append( ) → adds to list, does NOT return anything, directly changes the list itself → MUTABILITY
L = [2,3,4]
Issue comes when we want to make a copy:
(1) M = L
(2) N = [2,3,4]
Alias → special kind of copy where 2 variables refer to same value
L.append(8) → changes L but also M
id(L) → unique number of value (location of where value is stored)
id(L) = id(M) , but not equal to id(N)
L is M → boolean for whether or not id’s of L and M are the same
L.remove( )
L.insert(index#, value)
lst.pop( ) → removes element at index, also returns value its removing
M=L
L.append(4)
L = L + [5] → changes L only, not M
sorted(M)
L[5] = “foo” → can directly change value at this location → mutable
Strings are NOT mutable → immutable
Want to remove all instances of a value: DON’T use for loop since length of list decreases per iteration
(same for adding elements to list → infinite loop)
L = [3,5,2,3,6,3]
for i in range(len(L)):
if L[i] == 3:
L.remove(3)
print(L)
s = ‘bananas’
lst = [ ]
for c in s:
ff c not in lst:
lst.append(c)
print(lst)
List of lists
lst = [ [1,2,3], [4,5,6], [7,8,9] ]
lst = [ ]
For row in range(3):
Tmp = [ ]
For col in range(3):
tmp.append(0)
lst.append(tmp)
print(lst)
Recursion
Remove a number each time until [ ] and then return back
Base case = [ ]
Recursive case = all other numbers
def recursiveSum(lst):
if len(lst) == 0:
return 0
else:
return lst[0] + recursiveSum(lst[1:]) → must be smaller than input list (taking away
element each time)
def recursionFunction(input):
if base_case: (smallest version)
return something
else:
Remove a part from input to make it smaller
Partial_solution = recursionFunction(input - part)
Return the part combined with the partial_solution
Towers of Hanoi:
def moveDiscs(discs, start, temp, end):
if discs == 1:
print(“move 1 disk from “ + start + “to” + end)
Else:
Steps = 0
Steps += moveDisks(discs - 1, start, end, temp)
Steps += moveDiscs(1, start, temp, end)
Steps += moveDisks(discs-1, temp, start, end)
Return steps
moveDiscs(4, “red”, “black”, “blue”)
Binary search: how many times can I divide my list in half until I exit (only 1 element left)
def binarySearch(lst, item):
if len(lst) == 0:
return False
mid = len(lst) // 2
if lst[mid] == item:
return True
elif item < lst[mid]:
return binarySearch(lst[:mid], item)
else: # item > lst[mid]
return binarySearch(lst[mid+1])
Linear search: best case = 1 (first element), worst = n steps (last or not in list)
Binary search: best case = 1 (middle element), worst = log 2 (n)
→ only add 1 more iteration by doubling the list length
Big - O notation:
Linear search → O(n) (linear in N) → number of operations = const. * n + const.
(n) + const
Binary search → O(log(n)) → number of operations = const. * log(n) const
(don’t care about constants)
[2 million] → log2(2 million) = 20 → doesn’t grow as much at large N
Sorting
def selectionSort(L): → swapping (index matters)
for i in range(len(L)-1):
minSoFar = L[i]
for j in range(i+1, len(L)):
if L[j] < minSoFar:
minSoFar = L[j]
tmp = L[minIndexSoFar]
L[minIndexSoFar] = L[i]
L[i] = tmp
runtime → O(N2)
def insertionSort(L): → moving next items into already sorted list + inserting
for i in range(1, len(L)):
tmp = L[i]
j=i-1
while j >= 0 and L[j] > tmp:
L[j+1] = L[j]
j=j-1
L[j+1] = tmp
runtime → O(N2)
def mergeSort(L): → merges sections of list and them compares first elements of each section to sort
if len(L) == 0 or len(L) == 1:
return None
mid = len(L) //2
left = L[:mid]
right = L[mid:]
mergeSort(left)
mergeSort(right)
leftIndex = 0
rightIndex = 0
i=0
while leftIndex < len(left) and rightIndex < len(right):
if left[leftIndex] < right[rightIndex]:
L[i] = left[leftIndex]
leftIndex = leftIndex + 1
else:
L[i] = right[rightIndex]
rightIndex = rightIndex + 1
i = i +1
while leftIndex < len(left):
L[i] = left[leftIndex]
leftIndex = leftIndex + 1
i = i +1
while rightIndex < len(right):
L[i] = right[rightIndex]
rightIndex = rightIndex + 1
i=i+1
runtime → O(Nlog2N) → much better than N2 at large N
Memory → same amount of size per element (know how to find any element)
Hashing → Hash function value = non-negative integer
hash(x) → i
x != y → hash(x) != hash(y) (usually return two different values)
def myHash(s):
num = 0
for c in s:
num += ord(c)
return num
Python already has built-in hash function: hash( )
Hash table
1. Put NONE in each index
2. Use hash function to determine value and place in specific index (replace NONE with value)
a. Let one index hold more than 1 unique item → inner list = “collision”
b. Don’t have duplicates
3. myHash(“zebra”) % 10 → to bring back into bounds of hash function
4. **Mutable values should not be in hash table → hash(x) != same index i every time
a. Can’t hash lists
Dictionary
Maps key → value (mailbox, dictionary)
- Keys are hashed (can look up)
- Can access value once key in located
Indexed by keys (which can take on any immutable value)
d={}
d[“apple”] = “red” → adds to dictionary : apple is key, red is value
del d{“apple”} → deletes from dictionary
“apple” in d → boolean
d.keys( )
len(d)
def getNumberCounts(lst):
d={}
for item in lst:
digit = item % 10 → one’s digit
if digit not in d:
d[digit] = [ ]
d[digit].append(item)
return d
def mostCommonDigit(d):
bestVal = None
bestCount = 0
for digit in d:
If len(d[digit]) > bestCount:
bestVal = digit
bestCount = len(d[digit])
return bestVal
Trees
Root → nodes → children, leaves
Define using dictionaries
- Single node = 2 keys
- 1 key = value
- 2 key = list of children (other dictionaries)
def searchTree(t,item):
if len(t[“children”]) == 0:
return t[“value”] == item
else:
if t[“value”] == item:
return True
for child in t[“children”]:
result = searchTree(child, item)
if result == True:
return True
return False
Place restrictions on #children + order of children → makes searching the tree more efficient
Binary tree: 0-2 children, left < right
- 3 keys: value, left, right ( : None if it doesn’t have child there)
def binarySearchTree(t,item):
if t[left] == None and t[“right”] == None:
Return t[value] == item
Else;
IF T[VALUE] == item:
Return True
Elif t[value] < item:
If t[right] != None:
Return binarySearchTree(t[right],item)
Else:
Return False
Else t[value] > item:
If t[left] != None:
Return binarySearchTree(t[left]),item)
Else:
Return False
More generally:
def sumTree(t}:
result = t[“value”]
for child in t[“children”]:
result = result + sumTree(child)
return result
Graphs
Connected nodes, fewer restrictions
Edges → directed (arrows), undirected (no arrows)
1. Dictionary approach: edges don’t have values (only values of nodes)
Each node (key) maps to connected nodes (values)
2. 2D List approach: edges have values
Adjacency matrix: column 1, row 1 is all connection of node A (connection of A with itself is
None) other elements of list are values of edges connected to A
nodes = [A, B, C, D, E, G, H] → indexing the actual nodes directly
If directed: backwards connection == None
# connections = len(matrix[node])
if matrix[i][j] != 0: count += count
Searching in graphs
BFS: start at first node
- Check directly connected nodes, then A, B’s connections
- Slowly seeping out from main node
- Check connected nodes that you just visited
DFS: start at first node
- Check a path all the way to the end
- If you can’t go further, backtrack to most recent node that had neighbors
BFS:
** recursion doesn’t work for graphs
1. Visit all of start’s neighbors
def breadthFirstSearch(g, start, item):
to_visit = []
visited = []
while len(to_visit) > 0:
next = to_visit[0]
to_visit.pop(0)
if next not in visited:
if next == item:
return True
else:
to_visit = to_visit + g[next]
visited.append(next)
return False
DFS:
def depthFirstSearch(g, start, item):
to_visit = []
visited = []
while len(to_visit) > 0:
next = to_visit[0]
to_visit.pop(0)
if next not in visited:
if next == item:
return True
else:
to_visit = g[next] + to_visit
visited.append(next)
Run time: O(n) (generally for a tree)
Tractability
P, NP, NP-complete
O(x//=2,3) = O(logn)
Recursion:
- number : 0/1
- String: “ “
- List: []
def addone(l):
If l = []
Return []
Else:
Curr = l[0]
Remainder = l[1:]
Curr_Addone = [curr +1]
Remainder_addone = addone(remainder)
Return curr_addone + remainder_addone
Concurrency
CPU - central processing unit/processor contains:
- ALU (adders, multipliers)
- Register holds info (bytecode, program counter, accumulator, data register)
- Control unit: regulate flow of info
Thing that helps run algorithm at lowest level (all the actions)
Deadlock: processes stop if they overlap resources since they can’t continue until resources being used
elsewhere get freed up
Solve deadlock: different circuits request different resource priorities
Pipelining: splitting tasks into series of subtasks (each task relies of data modified by task before it)
- Connected, sharing data → can’t start one until previous subtask finishes
- Relies on longest task to determine total time
If only 1 CPU → multitasking to run multiple processes, but you switch between them (only 1 process
running at a time)
- Appears that they all run at the same time since this switch is so fast
- Scheduler: determines when/where the switch occurs
- Transistors as switches → direct data/bits to different adders in circuits
- More transistors → faster the computer
- MOORE’S LAW
- Use multiple CPU’s instead to enhance speed → multi-core processors
MapReduce
1. Mapper algorithm: takes subset of data + produces results
2. Collected algorithm: takes results as input and combines them into a list
3. Reducer algorithm: takes in list from collector and returns the result that collector outputs
**generate dictionary in mapper for each value/break them into pieces and them combine
f = open(“icecreams.csv”, “r”)
#get the header
Header = f.readline().strip()
Col = header.split(“,”)
Prefs = []
For line in f:
Values = line.strip().split(“,”)
prefs.append(values)
Library
OneTen
isUpperClassman(name): True if name = upperclassman, false if not
getTA(s): TA associated with section s
def getUpperClassmen():
allTAs = getAllTAs()
uCTAs = [ ]
for TA in allTAs:
if (ot.isUC(TA)):
uCTAs.append(TA)
return uCTAs
import OneTen as ot
def getAllTAs():
sections = [“A”, “B”, … “P”]
allTAs = [ ]
for letter in sections:
allTAs.append(ot.getTA(letter))
return allTAs
ML: to teach a math model (can modify itself by learning from data) + predict future
Regression: predict numeric value
Classification: predict what category something will fall into
- Logistic regression: separates data
- SVM: want distance from line to data to match on both sides
- K nearest vector: point closest to each other will fall into same category
- Trees: can make a bunch of lines instead of 1 to divide data
Internet:
Computer → ISP → router
- Fault tolerance: design philosophy (ex. Packet goes missing)
- Packet: small bit of info sent across internet (carries location in total sequence, destination, who
sent it)
- Packet switching: packets bounce around/take random path to routers to get to final destination.
Can reorder them due to numbering/ordering scheme
- Relates to fault tolerance: even if one bad router, the packet reaches destination since
packet switching is random and will eventually send packet to good router
DNS (domain name server): stores mapping of IP addresses for each website (several IP addresses, so if
one goes down, you can reroute to a different IP address → fault tolerance)
Domain name: google, yahoo
Security + Privacy:
Encryption: Plaintext → ciphertext
Decryption: ciphertext → plaintext
- Symmetric key encryption: Caesar cipher, substitution cipher (depend on the same key)
- Both people need to have the same key (shared)
- Asymmetric key encryption/public key cryptography: RSA (different keys)
- Generate public key (to anyone → encrypt) + private key (to you only → decrypt)
- No mathematical relationship between the two keys
Caesar cipher: try all the shifts and look for a common word in English → Brute force attack
Substitution cipher: list of alphabets in random order + mapping → brute force much more difficult
Could look at how often the letter appears and match
Man in the Middle Attack: in between two endpoints + messing with info
DDOS:
Cloud:
Software as a Service → consuming (Google docs)
Platform as a Service → resources to build service (Squarespace, use Alexa to build App)
Infrastructure as a Service → basic framework (don’t want to deal with hardware)