1. Python Overview
    1. What it's good for
      1. Most similar syntactically of the scripting languages to a conventional programming language
      2. Works well in creating graphical user interfaces
      3. Works well as a convenient calculator
    2. What it's not so good for: Also designed to work with shell-like tasks, but I'd probably prefer Perl
  2. Invoking the Python Interpreter
    1. Command-line interpreter: Type "python"-useful as a calculator and trying out snatches of code
    2. Invoked with a file:
      python foo.py
      
      1. Argument Passing
        1. arguments can be accessed via sys.argv
        2. import sys
        3. argv[0] contains the name of the file
  3. Comments
    1. # starts a one-line comment
    2. """...""" starts a multi-line comment: also used for documentation strings
  4. Python as a Calculator (or how it works with numbers)
    1. Supports *, /, -, +, %
    2. ** is exponentiation
    3. / is floating point arithmetic even if both operands are integers. Python pre-3.0 / was integer arithmetic if both operands are integers.
    4. // is floor division and will get you integer division if both operands are integers
    5. float(x) converts a number to a float and int(x) converts a number to an integer
    6. _ holds the last computed result, so you don't have to assign an expression to a variable
  5. Strings: Treated like a primitive type
    1. Types
      1. Single quotes
      2. Double quotes
      3. Triple quotes--span multiple lines
      4. Use \ to escape quotes
      5. Use r'...' to include \'s in your strings: r stands for "raw string"
    2. String Operations
      1. Strings are immutable: assignment is really assignment of a pointer
      2. + is concatenation
      3. Can use 0-based indexing to access individual chars
      4. Can use negative indexing starting at -1 to access chars from the end of the string
      5. len(s): returns length of a string
      6. slicing allows you to get a substring
        1. s[3:6]: string with characters 3 to 5, excludes 6
        2. s[:6]: start of string to character 5
        3. s[3:]: from character 3 to end of string
        4. s[-3:]: from 3rd to last character to end of string
      7. str.split([delimiter]): Splits a string into fields based on the delimiter. The default delimiter is a space. Unlike some other scripting languages, split treats consecutive delimiters as a single delimiter. For example:
        >>> a = "3    5     8"
        >>> a.split()
        ['3', '5', '8']
        
        split is a nice way to break input lines into fields.
      8. str.strip(): returns a new string with the leading and trailing whitespace stripped away:
        >>> a = "brad vander zanden, yifan tang, george brett"
        >>> names = a.split(',')
        >>> names
        ['brad vander zanden', ' yifan tang', ' george brett']
        >>> names[1].strip()
        'yifan tang'
        
      9. Interpolation: not supported in Python. You must use a string's format() method instead.
    3. Converting values to strings
      1. str(val): converts val to a human readable string
      2. repr(val): converts repr to a representation that can be read by the Python interpreter
      3. Example:
        >>> x = 1
        >>> s = 'the value of x is ' + x
        TypeError: cannot concatenate 'str' and 'int' objects
        >>> s = 'the value of x is ' + str(x)
        >>> s
        'the value of x is 1'
        
  6. Lists: A built-in data type
    1. Syntax: a = [3, 4, 5, 6]
    2. Individual elements can be accessed using array notation: a[2]
    3. len(a): returns length of the list
    4. a[1:3]: returns a sublist starting at index 1 and ending at index 2
    5. lists are mutable
      1. a[2] = 64: replaces the element at index location 2 with 64
      2. a[1:3] = [4, 5, 6, 7]: replaces the slice 1:2 with the list 4,5,6,7
      3. a.insert(3, 8): Inserts 8 at index position 3: Items starting at position 3 are moved to the right
      4. a.append(3): appends 3 to the end of the list
    6. lists may be nested: [3, 4, [6, 7, 8], [9, 10], 3]--b[2][1] would return 7
    7. lists are pointed to by pointers, although you cannot get access to the pointers
      1. when you use assignment, shallow copying is used, so modifying a variable will cause all other variables pointing to the same list to "change"
        a = [3, 4, 5]
        b = a
        b[2] = 8
        a    // prints [3, 4, 8]
        
    8. lists are implemented as arrays so inserting/deleting anywhere but at the end can be slow
  7. A Short Program
    a, b = 0, 1
    while b < 10:
        print (b)
        a, b = b, a+b
    
    Comments
    1. You can use multiple assignment, and the right hand side is completely evaluated before the values are assigned to the left hand side--this makes things like swap much easier to perform
    2. : terminates the condition in control constructs: () not needed around conditions
    3. indentation rather than braces used to show grouping
      1. shortens programs
      2. avoids implied grouping errors in languages that use braces
      3. is controversial
    4. print
      1. prints a comma separated list of arguments with spaces between the arguments. Early versions of Python (< 3.0) did not require ()'s. Modern version of Python, including the version used in this course, do.
      2. suppress a newline by ending the arguments with a comma
  8. Boolean operations
    1. True/False: boolean constants
    2. relational operators: use for both numbers and strings
    3. boolean operators: all boolean operators are short-circuit operators
      1. a and b: logical and
      2. a or b: logical or
      3. not a: logical not
    4. conditions can be chained
      10 <= a <= 20    # true if a between 10 and 20
      a < b == c       # true if a < b and b == c
      
    5. using lists to avoid multiple or's:
      if name in ['frank', 'george', 'ralph']: 
      
      is equivalent to
      if (name == 'frank') or (name == 'george') or (name == 'ralph'):
      

  9. if statement
    1. syntax:
      if condition:
          statements
      elif condition:
          statements
      elif condition:
          statements
      ...
      else:
          statements
      
    2. Must use elif rather than else if
    3. Note that all control statements (if, elif, else) end with :
    4. Python has no switch statement
  10. for loop
    1. for loop iterates over a sequence:
      for var in sequence:
          statements that operate on var
      
      For example, the following code sums the elements of a list:
      sum = 0
      for value in data:
          sum = sum + value
      
    2. there is no equivalent of C's counting for loop
    3. use the range command if you want a counting loop
      1. range(x): returns a list of numbers from 0 to x-1. For example:
        range(4)  # yields [0, 1, 2, 3]
        
      2. range(x,y): returns a list of numbers from x to y-1
      3. range(x,y,n): returns a list of numbers from x to y-1 incremented by n each time.
        range(0, 10, 3) # yields [0, 3, 6, 9]
        range(10, 0, -3) # yields [10, 7, 4, 1]
        
    4. Example: to sum the numbers from 1 to 10:
      sum = 0
      for i in range(1,11):
          sum = sum + i
      

    5. how to iterate over different types of sequences

      1. enumerate(): gives you both the position index and value
        >>> for i, v in enumerate(['tic', 'tac', 'toe']):
        ...     print (i, v)
        ...
        0 tic
        1 tac
        2 toe
        
      2. zip(): Allows you to loop over two or more sequences at the same time--pairs corresponding entries from each sequence. The following code computes the dot product of two vectors:
        >>> vector1 = [10, 20, 30, 40, 50]
        >>> vector2 = [5, 10, 15, 20, 25]
        >>> for v1, v2 in zip(vector1, vector2):
        ...   product = product + v1 * v2
        ... 
        >>> product
        2750
        
      3. reversed(): returns a iterable that iterates through a sequence in reverse:
        for v1 in reversed(vector1):
           print (v1)
        
      4. dictionaries:
        1. use dict.keys() to iterate through the keys
        2. use dict.iteritems() to iterate through the key value pairs:
          >>> people = { 'brad': 45, 'yifan': 36, 'smiley' : 5 }
          >>> for k, v in people.iteritems():
          ...     print (k, v)
          ... 
          yifan 36
          smiley 5
          brad 45
          
    6. break, continue, and else on a loop
      1. break: breaks out of the most enclosing loop
      2. continue: continues with the next iteration of the loop
      3. else on a loop: executes when the loop's condition becomes false, but not if the loop is terminated by a break statement. Here's an example that prints whether each number in a range is a prime number:
        for n in range(2, 10):
             for x in range(2, n):
                 if n % x == 0:
                     print (n, 'equals', x, '*', n/x)
                     break
             else:
                 # loop fell through without finding a factor
                 print (n, 'is a prime number')
        
  11. Functions
    1. Syntax
      def functionName(args):
          """ Documentation String """ (optional)
          function body
          return value
      
    2. Native types are passed by value
    3. Objects are passed by reference. If you modify the object, then you have modified the argument, because you accessed the object through the reference.
    4. Variables are local by default. Python implements this by introducing a local symbol table for each function
      1. Any time you assign a value to a variable, the variable is written to the local symbol table
      2. When you access a variable, Python first checks the local symbol table. If the variable is not in the local symbol table, then Python checks the global symbol table.
      3. If you want to change a global variable, not a local variable, then declare the variable as global at the beginning of the function
        def foo():
           global x
           ... statements that modify x ...
        
    5. The documentation string has a standard format that allows documentation tools to automatically extract information about the function for documentation manuals

      1. The first line should be a concise statement of the function's purpose
      2. The second line should be blank, to create a visual separation
      3. The remaining lines should provide further documentation about how the function is implemented, its side effects, its parameters, etc.
      4. Here's an example:
        def dfs(x, y):
            """ Perform a depth first search starting at x and ending at y
        
                Parameters 
                   x: The node from which to start the search
                   y: The node at which to terminate the search
        
                Side-Effects: None
            """
            ...
        
    6. Variable-length argument lists: You can define variable length argument lists but we won't use them in this class
    7. Lambda functions: Lambda functions allow you to create anonymous functions. For example, suppose you were implementing a spreadsheet and wanted to make the input the user provides in a cell an executable formula. You might write:
      def makeFormula(formula):
          return lambda: formula
      
      newFormula = makeFormula("a + b")
      
      Here's another example that creates an incrementer:
      def makeIncrementer(n):
          return lambda x: x + n
      
      f = make_incrementor(42)
      f(1)   # returns 43
      
      1. lambda functions must be a single expression that computes a value
      2. cannot use conventional if-then-else. Instead use:
        lambda x,y: x if x < y else y
        
      3. can't use loops in lambda functions

    8. Generator Functions: You can create a function that incrementally returns results using the yield statement. For example:
      >>> def fib():
      ...    yield 1
      ...    yield 1
      ...    current, prev = 1, 1
      ...    while True:
      ...      current, prev = current + prev, current
      ...      yield current
      ... 
      >>> for i in fib():
      ...   if i > 100:
      ...     break
      ...   print (i)
      ... 
      1
      1
      2
      3
      5
      8
      13
      21
      34
      55
      89
      
      1. When you call a generator function, it creates and returns a generator object. The generator object retains its state information between calls
      2. How to use a generator function

        1. In a loop as above
        2. Assign the generator object to a variable and then call .next() each time you want the next value
          >>> a = fib()
          >>> next(a)
          1
          >>> next(a)
          1
          >>> next(a)
          2
          >>> next(a)
          3
          >>> next(a)
          5
          
      3. You cannot easily use the yield statement in a recursive function, because when you think you are calling the function recursively, you are really creating a new generator function, rather than calling the existing function. For example:
        def factorial(n):
           if n == 1:
              yield 1
           else
              yield n * fact(n-1)
        
        This won't work like you think because you expect fact(n-1) to return a number, but it in fact returns a generator object and hence the multiplication fails with a type error.

        There are sophisticated solutions beyond the scope of this course for doing recursive generator functions. An easier way is to declare a nested function that returns a list of the results, and then returns the results one at a time:

        def factorial(n):
           def fact(n):
              if n == 1:
                 return [1]
              else:
                 result = fact(n-1)
                 result.append(n*result[-1])
        	 return result
           results = fact(n)
           for i in results:
              yield i
        
  12. Coding Style: We won't cover it in class, but you should read the generally accepted conventions for Python's coding style.