Last update: 2022-02-19

High-Level Overview

Environment Setup

Windows Setup
Linux Setup, e.g., Ubuntu
#  Installing Python 3.9 using apt
sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install python3.9

Resources

Python Syntax

Basics

Python uses whitespace to delimit control flow blocks (following the off-side rule).

#!/usr/bin/env python

"""
This is a docstrings (documentation strings) documenting a module, class, method or function.
"""

# a comment, variable declaration and assignment
i = 1

# input/ output
in_cnt = int(input("input a number: "))
print(f"You entered: {in_cnt}")

# control statements
if in_cnt >= 10:
    print("gte10")
elif in_cnt > 7:
    pass
elif in_cnt > 5:
    print("gt5")
else:
    print("lt5")

# loops
while i < in_cnt:
    i += 1
    if i >= 5 and i <8:
        continue
    print("*" * (i-1))
    if i == 8:
        break

for i in range(in_cnt):
    print("^" * i)

# function definition
def test(x, y=1):
    return x * y

# anonymous/ inline function
lambda num: num ** 2

# inline if statement / conditional expression / 'if' operator
var_a = 'a' if 'a' > 'b' else 'b'

# recursion
def fibonacci_of(n):
    if n in {0, 1}:  # Base case
        return n
    return fibonacci_of(n - 1) + fibonacci_of(n - 2)  # Recursive case
Data Types

Since Python is dynamically typed, values carry type (at the runtime). However, all variables in Python are also objects.

  • Text – str
  • Numeric – int, float, complex
  • Sequence – list, tuple, range, str
  • Mapping – dict
  • Set – set, frozenset
  • Boolean – bool
  • Binary – bytes, bytearray, memoryview
# Strings - ordered sequence of characters
my_str = 'HEllo WoRlD'
print(len(my_str))
words = my_str.split() # default sep = " " 

# String functions
print("Lowercase:", my_str.lower())
print("Uppercase:", my_str.upper())
print("Capitalized:", my_str.capitalize())
print("Title Case:", my_str.title())
print("Second char: " + my_str[1])

# String interpolation / f-strings (Python 3.6 +)
name = input("what is your name? ")
print(f'Hello, {name}!')

# Indexing strings -- string[index]
m = "test_string"
print("First char: " + m[0])
print("Last char: " + m[-1])
print("Middle char: " + m[int(len(m)/2)])

# Slicing strings -- string[start_index, stop_index, stride]
print("Even index chars: "  +m[::2])
print("Odd index chars:"+m[1::2])
print("Reversed message: " + m[::-1])

# Numbers - ints, floats
my_int = 1
my_float = 1.0
my_sci_num = 4.5e9    # 4.5 * (10**9)

# Bools - True/ False
my_bool = False
print(my_bool)

# Typecasting 
a = int("1")        # convert string to int
b = float("1.25")   # convert string to float
c = str(123)        # convert int to string
e = bool("value")   # convert value to bool (anything but 0 or empty = Treu)

# Other types include: ascii(), chr(), hex(), ord(), type()

Collections
  • Ordered sequential types
    • Lists (dynamic arrays)
    • Tuples (imutable sequence type, fixed length)
    • Strings
  • Unordered types (mappings)
    • Dictionaries
    • Sets
# Lists - sorted, ordered sequence of objects (mutable)
list1 = ["one", 2, 11, 4, 15, 6, 7]
list1.index(1)       # get the item under specified index
list1.append(8)      # append item to the end of the list
list1.insert(0, 1)   # insert item at the specified position
list1.pop()          # takes an index param, default =-1 i.e. the last element

# List functions
sorted_list = sorted(list1)             # new sorted list
reversed_list = list(reverse(list1))    # reverse iterator

# List slicing
list2 = list1[1:]  # slice from the 2nd element on

# List comprehension [ item_output expression for item_output in list ]
# can also add if() front of item_output
list3 = [x for x in range(100)]
powers_of_eight = [8**n for n in range(1, 6)]

# filter() -- takes a function and a list and applies function to the list
def square(num):
    return num ** 2

squares = list(map(square, my_num))
for n in filter(square, my_num):
    print(n)

# Dictionaries - unordered, unsorted, key-value pairs (mutable)
my_dict = {'key1': 'value1', 
           'key2': 'value2',
           'key3': ['one', 'two', 'three', 4, 5, 6]}

my_dict['key4'] = "value4"
my_dict['key1'] = "new value"
del my_dict['key2']

print(f"Keys: {list(my_dict.keys())}")
print(f"Values: {list(my_dict.values())}")
print(f"Items: {list(my_dict.items())}")

# Tuples - ordered sequence of objects (immutable)
tup = (1, 2, 3, 4, 4, 4)
print(tup[3])
print(tup.count(4))

# tuple unpacking
tup_list = [(1, 2), (3, 4), (5, 6)]
for (a, b) in tup_list:
    print(a)

# Sets - unordered collections of unique elements (mutable)
my_set = set()
my_set.add(1)
my_set.add(2)
my_set.add(2)

print(my_set)

Generators

Generators allow us to declare a function that behaves like an iterator and generate a sequence of values over time. The main difference in syntax is the yield statement, which returns one element at a time, and there’s no need to store the entire list in memory, e.g. range() is a generator.

"""
Intro to python generators - lazy evaluation 
"""

# in memory list example
def create_cubes(n):
    result = []
    for x in range(n):
        result.append(x ** 3)
    return result

print(create_cubes(10))

# generator - more memory efficient
def cubes(n):
    for x in range(n):
        yield x ** 3

print(cubes(10))

# generator objects (return of generator function) need to be iterated over
def gen_fibon(n):
    a = 1
    b = 1
    for _ in range(n):
        yield a
        a, b = b, a + b

print(list(gen_fibon(10)))

# char range generator examnple
def char_range(start, stop, step=1):
    stop_modifier = 1
    start_code = ord(start)
    stop_code = ord(stop)
    if start_code > stop_code:
        step *= -1
        stop_modifier *= -1
    for value in range(start_code, stop_code + stop_modifier, step):
        yield chr(value)
Decorators

A decorator @decorator_name is a function that takes another function as a parameter and extends the behaviour (adds new functionality to an existing object) of the latter function without explicitly modifying it.

"""
Intro to decorators
"""

# decorator - adding extra functionality on the runtime
# commonly used in web frameworks such as flask and django e.g. routing etc.
# @ is used to declare decorators

# returning a function from within a function
def func():
    print('upper function')
    def func2():
        print('nested function')
        def func3():
            print('nested 2 levels')
            return 72
        return func3()
    return func2()

test = func()
print(test)

def cool():
    def super_cool():
        return 'I''m so fancy'
    return super_cool

# pointer/ delegate
some_func = cool()
print(some_func)

# decorator example - long way using a wrapper function
def new_decorator(original_function):
    def wrap_func():
        print('Some extra code, before the original function')
        original_function()
        print('Some extra code, after the original function')
        return 42
    return wrap_func()

def func_needs_decorator():
    print('I need to be decorated')

decorated_func = new_decorator(func_needs_decorator)
print(decorated_func)

# short way using @ declaration
@new_decorator
def func_needs_decorator2():
    print('I want to be decorated 2')

Object Oriented Programming (OOP)
"""
Object oriented programming in python
"""

import unittest

# self. is the same as this.
# class names - use camel casing 
# methods - use lower casing and underscores
# class properties = class attributes
# constructor = __init__(self, args**) 

class Dog():
    # class object attributes/ constants
    species = 'mammal'
    def __init__(self, breed):
        self.breed = breed
    def speak(self):
        print('woof')
    def bark(self, num):
        for _ in range(num):
            print("woof ")

# inheritance
# pass base class as a parameter in derived class constructor 
# e.g. base class -- moreless an abstract class

class Animal():
    def __init__(self):
        print('animal created')
    def who_am_i(self):
        print('i am an animal')
    def eat(self):
        print('i am eating')

class MoreAbstractClass():
    def __init__(self, *args, **kwargs):
        pass
    def abstract_method(self):
        raise NotImplementedError('derived class must implement this method')

# e.g. deriver class

class Cat(Animal):
    def __init__(self):
        Animal.__init__(self)
        print('cat created')
    def speak(self):
        print('meow')

# polymorphism
# when object share the same methods
# pass an object as a parameter to the method 
# it'll be auto resolved based on its type e.g. 

def pet_speak(pet):
    print(pet.speak())

#  special methods (__ - dunder)
#  __init__(self) - constructor     __str__(self) - to string
#  __len__(self) - length           __del__(self) - delete

# access modifiers: global - public, _ - protected, __ private
gloabl a = "global, it's terrible, global scope e.g., inside function"
_b = "protected, it can still be accessed and modified from outside the class"
__c = "private, it cannot be accessed or modified from the outside"

# when using tuples in classes unpacking can be done in __init__ or any other method

# unittest - a built-in library
# unit testing using a test class e.g. 

class TestCap(unittest.TestCase):
    def test_one(self):
        text = 'python'
        result = str.capitalize(text)
        self.assertEqual(result, 'Python')

Common Problems

# How to reverse a string
def reverse_string(s):
    return s[::-1]

# Is given string a palindrome
def is_palindrome(item):
    item = str(item)
    return item == item[::-1]

# How do you prove that the two strings are anagrams
def are_anagrams(s1, s2):
    if len(s1) != len(s2): return False
    return list(s1).sort() == list(s2).sort()

# How to get count of each char in a string
def repeating_chars(s):
    m = {} 
    for i in (range(0, len(s))):
        if s[i] in m.keys():
            m[s[i]] += 1
        else:
            m[s[i]] = 1
    return {k:v for k,v in m.items() if v >1}

# Get the number of vowels and consonants in a string
def count_cons_vols(s):
    v_list = ['a', 'o', 'e', 'i', 'o', 'u']
    v, c = 0, 0
    for i in range(0, len(s)):
        if s[i] in v_list:
            v += 1
        else:
            c += 1
    return (c,v)

# Find the count for the occurrence of a particular character in a string.
def char_count(c, s):
    d = {}
    for i in range(0,len(s)):
        if s[i] in d.keys():
            d[s[i]] += 1
        else:
            d[s[i]] = 1
    return int(d[c])

# Find missing number in a given array
def missing(arr):
    n = len(arr)
    total = (n+1)*(n+2)/2
    sum_arr = sum(arr)
    return total - sum_arr

# Two Sum Problem
def sum_two(arr, target):
    hash_table = {}
    for i in range(len(arr)):
        comp = target - arr[i]
        if comp in hash_table:
            return (i, hash_table[comp])
        else:
            hash_table[arr[i]] = i

# FizzBuzz
def fizzbuzz(upper_number):
    for number in range(1, upper_number + 1):
        if number % 3 == 0 and number  % 5 == 0:
            print("FizzBuzz")
        elif number % 3 == 0:
            print("Fizz")
        elif number % 5 == 0:
            print("Buzz")
        else:
            print(number)

# Fibonacci Sequence using recursion
def fib(n):
   if n == 0: return 0
   elif n == 1: return 1
   else: return fib(n-1) + fib(n-2)

# Fibonacci Sequence using a generator
def gen_fibon(num):
    a, b = 0, 1
    for _ in range(0, num+1):
        yield a
        a, b = b, a + b # Adds values together then swaps them