Basics


# Comments start with a # symbol.

"""
  Comment sections are bounded by three double quotes
"""

####################################################
## 1. Primitive Datatypes and Operators
####################################################

# integer
3

# float (floating-point number)
3.14

# Math is what you would expect
1 + 1  # => 2
8 - 1  # => 7
10 * 2  # => 20
35 / 5  # => 7.0

# Enforce precedence with parentheses
(1 + 3) * 2  # => 8

# Boolean Operators
# Note they are
True and False # => False
False or True # => True

# negate with not
not True  # => False
not False  # => True

# Equality is ==
1 == 1  # => True
2 == 1  # => False

# Inequality is !=
1 != 1  # => False
2 != 1  # => True

# More comparisons
1 < 10  # => True
1 > 10  # => False
2 <= 2  # => True
2 >= 2  # => True

# Comparisons can be chained!
1 < 2 < 3  # => True
2 < 3 < 2  # => False

# A string (text) is created with " or '
"This is a string."
'This is also a string.'

# Strings can be concatenated too!
"Hello " + "world!"  # => "Hello world!"

tblFigTitles = 'Line1\nLine2\nLine3\n'
print(tblFigTitles.endswith('\n'))

# Links to string functions:
#   https://www.geeksforgeeks.org/python-strings/
#   https://www.tutorialspoint.com/python/python_strings.htm
#   https://docs.python.org/2/library/string.html#string-functions
#   https://www.geeksforgeeks.org/string-endswith-python/

# None means an empty/nonexistent value
None  # => None

# F-strings allow embedding python expressions inside
# string literals for formatting.  F-strings are fast!
name = 'Mark'
age = '55'
msg = f'Hello, {name}, you are {age}.'
print(msg)  # Hello, Mark, you are 55.
# '\t' tab


####################################################
## 2. Variables, lists, dicts, tuples, sets, arrays
####################################################

# print() displays the value in your command prompt window
from datetime import datetime
# Codes listed at:  http://strftime
print('Prints in Python {:03.1f}x and {:03.1f}x versions as of {:%b %Y}'.format(2.7,3.5,datetime(2018,11,12,16,25)))
# Prints in Python 2.7x and 3.5x versions as of Nov 2018
# See also examples in "print (1).py"

# Variables
some_var = 5
some_var  # => 5
some_var += 1  # => 6

# Choosing list vs tuple vs dictinary vs array:
#   key/value pairs: Dictionary
#   add,remove,sort,count items: List
#   Unique list, add(), clear(), no del or change, check if item exists.  Set
#   Integer indexed list, no changes, speed!.  Tuple

# List vs Tuple vs Dictionary va Array:
#   List: mutable list of mixed types.  li=[1,'two',3.3]
#   Tuple: immutable list of mixed types.  Fast!  tup1=('a',3.5,4)
#   Dictionary: mutable list of key/value pairs.  dic1={'month': 'Jan'}
#   Sets: mutable unordered unique collection of the same type.  set1=set('a','b')
#   Array: mutable collection of items of the same type.
#
# (mutable = can be changed)
# liMyVar, tuMyVar, dicMyVar, setMyVar, arrMyVar

# Lists store a mutable sequence of mixed types.
# empty list: li = []
li = [1, 'two', 3.3]

# Add stuff to the end of a list with append
li.append(4)    # li is now [1, 'two', 3.3, 4]

## Access a list like you would any array
#li[0]  # => 1
# Assign new values to indexes that have already been initialized with =
li[0] = 42
#li # => [42, 'two', 3.3, 4]

# You can add lists
other_li = [4, 5, 6]
li + other_li   # => [42, 'two', 3.3, 4, 5, 6]

# Get the length with "len()"
print('len(li) = ', len(li))    # len(li) =  4

print('\nli:')
for item in li:
    print(item)

print('\nli:')
for i, val in enumerate(li):
    print(i, ', ', val)

# List comprehensions create a list from an existing list or any iterable.
squared_numbers = [x**2 for x in range(1, 6)]
print(squared_numbers)
#[1, 4, 9, 16, 25]

# Dictionaries store mappings (key/value pairs)
# https://realpython.com/iterate-through-dictionary-python/
empty_dict = {}
# Here is a prefilled dictionary
filled_dict = {
    'name': 'Lancelot',
    'quest': "To find the holy grail",
    'favorite_color': "Blue"
}

# Look up values with []
filled_dict['name']   # => 'Lancelot'

# Check for existence of keys in a dictionary with "in"
'name' in filled_dict   # => True
'age' in filled_dict   # => False

# set the value of a key with a syntax similar to lists
filled_dict["age"] = 30  # now, filled_dict["age"] => 30

print('\nfilled_dict:')
for key in filled_dict:
    print(key, ' -> ', filled_dict[key])

# A Set is an unordered collection of data type that is
# iterable, mutable and has no duplicate elements.
# Use a set to quickly check if a specific element exists.
# https://www.geeksforgeeks.org/python-sets/
set1 = set("GeeksForGeeks")
print('set1 = ', set1) # {'e', 'r', 'k', 'o', 'G', 's', 'F'}
# Note: set1 contains only unique elements (duplicates removed)
# Add element to set1
set1.add('a')   # {'a', 'e', 'r', 'k', 'o', 'G', 's', 'F'}
# See also .update() .remove() .discard() .pop() .clear() .union() ...
print("aer" in set1) # True
print("Elements of set1: ")
for i in set1:
    print(i, end=" ")

# A Tuple is a collection of Python objects separated by commas.
tup1 = ('python', 'geeks')   # or tup1 = 'python', 'geeks'
print('\ntup1: ', tup1)  # ('python', 'geeks')
tup2 = (5, 6, 7.2, 8)
print(tup2)
print(tup1 + tup2)
# nesting
tup3 = (tup1, tup2)
print(tup3)
print('tup2[2] = ', tup2[2])
# Do much more with tuples.  See: https://www.geeksforgeeks.org/tuples-in-python/

# Arrays are a mutable collection of items of the same type. 
import array as arr
#array(data_type, value_list)
arrFloat = arr.array('d', [2.5, 3.2, 3.3])
print('\narrFloat: ', end = '')
for i in range (0, len(arrFloat)): 
    print (arrFloat[i], end =" ")
# See also .insert() .remove() .pop() .index()

# list of dictionary
posts = [
    {
        'author': {'username': 'John'},
        'month': 'Jan',
        'year': '2020',
    },
    {
        'author': {'username': 'Susan'},
        'month': 'Dec',
        'year': '2019',
    }
]

# dictionary of lists
json = {
    "chartData": {
        "labels": [
        "sunday",
        "monday",
        "saturday"
        ],
    "thisWeek": [
        20000,
        14000,
        12000,
        15000,
        ]
    }
}
print(type(json))     # <class 'dict'>
print(type(json["chartData"]["labels"])) # <class 'list'>
print(type(json["chartData"]["thisWeek"])) # <class 'list'>


# The itertools module provides a set of functions to work with iterators
import itertools 
numbers = [1, 2, 3] 
result = list(itertools.permutations(numbers)) 
#output all the permutations 
#[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]


# Generators are a type of iterable defined using the keyword 'yield' that generate values on-the-fly, instead of storing them in memory.
### Generators created using yield keyword 
def fibonacci_series(n):
    a, b = 0, 1
    for i in range(n):
        yield a
        a, b = b, a + b

# Driver code to check above generator function 
for number in fibonacci_series(10):
    print(number)
"""
0
1
1
2
3
5
8
13
21
34
"""


####################################################
## 3. Control Flow
####################################################
# See also: https://docs.python.org/3/tutorial/controlflow.html
print('\nControl Flow:')

# Let's just make a variable
some_var = 5

# Here is an if statement.
# prints "some_var is smaller than 10"
if some_var > 10:
    print("\nsome_var is totally bigger than 10.")
elif some_var < 10:    # This elif clause is optional.
    print("\nsome_var is smaller than 10.")
else:           # This is optional too.
    print("\nsome_var is indeed 10.")

if some_var == 5: print('some_var = 5')
if some_var != 9: print('some_var != 5')

bResult = True
if bResult == True: print('bResult is True')

if bResult == True and some_var < 10: print('bResult = True and some_var < 10')

"""
SPECIAL NOTE ABOUT INDENTING
In Python, you must indent your code correctly, or it will not work.
All lines in a block of code must be aligned along the left edge.
When you're inside a code block (e.g. "if", "for", "def"; see below),
you need to indent by 4 spaces.

Examples of wrong indentation:

if some_var > 10:
print("bigger than 10." # error, this line needs to be indented by 4 spaces


if some_var > 10:
    print("bigger than 10.")
 else: # error, this line needs to be unindented by 1 space
    print("less than 10")

"""


"""
For loops iterate over lists
prints:
    1
    4
    9
"""
for x in [1, 2, 3]:
    print(x*x)


# enumerate function
fruits = ['peach', 'orange', 'mango']
for index, fruit in enumerate(fruits): 
	print(index, fruit)
"""
#0 peach
#1 orange
#2 mango
"""

# zip() aggregates elements from each of the iterables and returns an iterator of tuples. 
list1 = [1, 2, 3] 
list2 = ['a', 'b', 'c'] 
for x, y in zip(list1, list2):
    print(x, y)
"""
#1 a
#2 b
#3 c
"""

"""
"range(number)" returns a list of numbers
from zero to the given number MINUS ONE

the following code prints:
    0
    1
    2
    3
"""
for i in range(4):
    print(i)

# The 'break' statement will break out of a for or while loop
# The 'continue' statement, continues with the next iteration of the loop

# Stop program execution with 'raise SystemExit'

# pause executation
import time
time.sleep(5)   #pauses execution for 5 seconds


####################################################
## 4. Functions
####################################################

# Use "def" to create new functions
def add(x, y):
    print('x is', x)
    print('y is', y)
    return x + y

# Calling functions with parameters
add(5, 6)   # => prints out "x is 5 and y is 6" and returns 11

# Lambda functions are anonymous one time functions that use the lambda keyword.
add = lambda x, y: x + y 
result = add(3, 4)
print(result)
#7

# Decorators are a way to modify the behavior of a function or a class.
# Defined using the @ symbol, they can add functionality to a function.
def log_function(func):
    def wrapper(*args, **kwargs):
        print(f'Running {func.__name__}')
        result = func(*args, **kwargs)
        print(f'{func.__name__} returned {result}')
        return result
    return wrapper
@log_function
def add(x, y):
    return x + y
print(add(5,7))
#Running add
#add returned 12
#12


####################################################
## 5. List comprehensions
####################################################

# We can use list comprehensions to loop or filter
numbers = [3,4,5,6,7]
[x*x for x in numbers]  # => [9, 16, 25, 36, 49]

numbers = [3, 4, 5, 6, 7]
[x for x in numbers if x > 5]   # => [6, 7]

####################################################
## 6. Modules
####################################################

# You can import modules
import random
print(random.random()) # random real between 0 and 1


# Links for tutorials:
#   http://introtopython.org/

####################################################
## 6. Class
####################################################

class MyClass:
    'description of the class'

    def __init__(self):
        self.aHref = ''

    def __bool__(self):
        # The optional __𝐛𝐨𝐨𝐥__ dunder method is used to define the behavior of an object when used in a boolean context. 
        return len(self.aHref)>0

my_class = MyClass()
print('self = "',my_class.aHref,'"')
if my_class:
    print('True')
else:
    print('False')