# SI 413 Fall 2012
# Lab 11
# Python introduction

'''
This Python program will serve as a basic introduction to the language.
It has some basic changes for you to make too.

Note that this is a Python 3 program. You have to run it using the
"python3" command.

First things first, you notice that single-line comments in Python start
with a # symbol, like above. What we're in here is actually a multi-line
string, which is started and ended with 3 single or double quotes. But
multi-line strings like this one that aren't actually used anywhere can
also serve as comments. Like this!
'''

##### DEFINITIONS #####

'''
Variable declarations and redeclarations look exactly the same!
And there are no declared types, of course. (Everything here is just an
int.)
You will notice that statements do NOT end in a semicolon or anything
else! A single line = a single statement.
'''

x = 10
x *= 2
y = x + 3

# Use the print statement to... print things.
print("----- DEFINITIONS -----")
print(x)
print(x,y)
print() # Print with no arguments does an empty line.

'''
Function definitions use the "def" keyword. The standard Python syntax
is that a colon marks the beginning of a block (on the next line).

One truly innovative syntactic idea in Python is that WHITESPACE
MATTERS! Every line in a block must be indented the same amount of
whitespace. If your text editor automatically converts spaces to tabs,
use caution, because they are not the same to the Python interpreter.

Although the whitespace for indentation can be any amount (as long as it
is consistent), the official standard is to use 4 spaces (NO TABS!) for
each indentation level.
'''

def fun(x):
    return x + x*3

print(fun(12))
print(fun(fun(y)))
print()

'''
Python has lexical scope and functions are first-class. We can define
functions within functions, return functions from functions, and there
is even lambda for anonymous functions!
'''

def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

C = make_counter()
print(C(), C(), C(), C())

'''
Notice something odd in the function above? The "nonlocal" keyword
indicates that the name "count" should not be a local variable to
increment(), but rather should use the pre-existing local variable from
make_counter(). The reason we have to do things like this is that Python
doesn't distinguish syntactically between variable declaration and
re-assignment. By default, the first use of a variable within a scope
makes a declaration. The "nonlocal" overrides this behavior.

There is a similar keyword "global" that can be used to indicate
variables should have global scope instead of local.
'''

fun_alt = lambda y: y + y*3
print(fun(7), fun_alt(7))

print()

##### DATA TYPES #####
print('----- DATA TYPES -----')

'''
Python includes a number of built-in data types such as int, float,
list, tuple, and dict. To learn about their built-in operations, you can
type something like "help(int)" inside the interpreter to get the full
documentation. 

You can also covert between many of the built-in types; to do so, just
use the name of the type as a function. Let's start with numbers.
'''

print(float(20))
print(int(13.5))

# Most arithmetic operators work like you're used to.
print(1+2*3-4)

# The division operator / always does EXACT division
# To do integer division (quotient), use //
print(4/3, 4//3)

# Exponentiation is built-in using the ** operator.
print(3**2)

# The "mod" operator is what you're used to, but there is also a special
# global function divmod that produces the quotient and remainder
# at the same time.
print(28//5, 28%5)
print(divmod(28,5))

print()

'''
Booleans are pretty straightforward. True and False are the
literals, and you can do "and", "or", and "not".
'''

print(True, False, 3<5, True and True or not False)
print()

'''
Lists are an extremely important data type. You create lists with square
brackets, and can manipulate them in all sorts of wonderful ways. (Run
help(list) to see more!)
'''

L=[]
print(L, len(L)) #len is an extremely useful global function!

L=[1,2,3,4]
L.append(5)
print(L, len(L))

L.sort()
print(L)
L = [5,3,1]
print(sorted(L), L) 
# sorted just returns a sorted copy, whereas sort modifies the original.

L.extend(['types','can','be','mixed'])
L[1] = 'CHANGED!'
print(L)
print(['lists','can'] + ['be','added'])

# Besides the usual access like L[1], you can also use negative numbers
# to access from the back of the array.
print("First element is", L[0])
print("Last element is", L[-1])
# And you can get ranges using the "slice" notation with a colon.
print("First 2 elements are", L[:2])
print("Last 2 elements are", L[-2:])
print("Middle 2 elements are", L[2:4])

print()

'''
Tuples are like lists, except that they are immutable. They can be
quickly defined using parentheses. This can be slightly awkward since
parentheses are also used to override precedence. To make a one-element
tuple, you have to throw in an extra comma.
'''

T = (1,2,3)
print(T, len(T)) # len works for just about anything.
print(T + (4,5) + (6,))

# Notice the extra parens below.
# sorted always returns a list, even if its input is something else.
print( sorted((9,8,7)) )

print()

'''
Dictionaries are your basic hash map. You create them with curly
brackets. You can use the square brackets on a dictionary to insert or
retrieve elements.
'''

D = { 
    'key': 'value', 
    'another key': 'another value',
    5: ['types','can','mix'] 
}

print(len({}), len(D))

print(D['key'], D[5])
D[20] = 7
D.pop('another key')
print(D)

# The "in" operator can be used to see if a key is there.
print(20 in D)
print(5 in D)
print('value' in D)

print()

##### CONTROL STRUCTURES ####
print('----- CONTROL STRUCTURES -----')

'''
if and if/else statements are fairly normal. You will notice the special
keyword 'elif' to do an else-if case.
'''

if 1 < 4:
    print('1 is less than 4')

if False:
    print('Not here!')
else:
    print('Here instead!')

if 1 == 2:
    print('no')
elif 2 == 2:
    print('yes')
else:
    print('no')

print()

'''
while loops are just like you would expect. Like if's, no parens are
needed around the conditional.
'''

L=['a']
while len(L) < 5:
    L = L + L
print(len(L), L)

print()

'''
for loops are a little different. The only kind of for loop in Python is
a for-each loop to loop over all the items in some kind of "iterable"
collection.
'''

for a in [5,3,1]:
    print("For loop fun", a)

# "range" makes a range to loop over.
# This is how you do a typical for loop
for i in range(5, 10):
    print(i,end=' ') # this makes it end with space not newline

# Of course you can convert a range to a list.
# And the third argument to range changes the "increment" value.
print(list(range(10)))
print(list(range(5, 10, 2)))
print(list(range(10, 5, -1)))

# You can loop over JUST ABOUT ANYTHING YOU WANT

# remove the l's from hello
noels=''
for x in "hello":
    if x != 'l':
        noels = noels+x
print(noels)

for k in {1:2, 3:4}:
    print('The key is', k)

print()

##### LIST COMPREHENSIONS #####
print('----- LIST COMPREHENSIONS -----')

'''
List comprehensions are an awesome and special feature of Python that
lets you make a list a fill it up with the contents of anything that is
iterable. It is a very powerful and convenient syntax. The basic idea is
to write [(some expression) for (name) in (iterable)].
Let's see some examples!
'''

print([x for x in range(4)])
print(['really ' + s for s in ('I', 'like', 'Python')])

# zip is a useful utility to iterate over the tuples choosing one from
# each input list.
L1 = [4,5,6]
L2 = [7,8,9]
print([a*b for a,b in zip(L1,L2)])

# There is also a "map" function in Python to do something similar
print(list(map(len,['help','me','rhonda'])))

# You can add conditionals to the list comprehension for awesomeness!
# (This makes a list with all multiples of 3.)
print([x for x in range(20) if x % 3 == 0])

print()

##### GENERATORS #####
print('----- GENERATORS -----')

'''
You can make your very own "iterable" type of thing with a generator
function. The idea is that you replace a "return" statements with
"yield" statements, and then your function will keep going to yield more
and more things. Get excited for this one!
'''

def longwords():
    with open('/usr/share/dict/words', 'r') as English:
        # By the way, that's how you open a file for reading.
        for word in English:
            # By the way, that's how you loop over the lines in a file.
            word = word.strip()
            # This strips trailing whitespace.
            if len(word) > 18 and "'" not in word:
                yield word

# Now we can loop over this FUNCTION like it's any other iterable thing
for w in longwords():
    print(w)

# You can also get things out of an iterable manually, without a forloop.
gen = longwords()
print("The first long word is", next(gen))
print("The second long word is", next(gen))

print()


