#!/usr/bin/env python # coding: utf-8 # # Lecture 2: Python Syntax # # Lecture 2: Python Syntax # - Syntax and basics of Python # - The Python interpreter # - Goal: kickstart your Python knowledge
# to the same level as M-students have in Matlab # # Recommended reading: # - [1] Chapter 1.6, 2.1-2.2, 3, 4, 5, 6, 8 # - [2] Section 3, 4, 5 # # [1] C. Horstmann: Python for everyone
# [2] Python tutorial: https://docs.python.org/3/tutorial/ # ## Contents # - Basic Types # - `int`, `float`, `str`, `type` # - type conversion # - Data structures # - `str`, `tuple`, `list`, `set`, `dict` # - indexing and slicing, `del` # - Control flow statements # - Conditionals `if`, `elif`, `else` # - `while`, `for`, `enumerate`, `zip`, `itertools`, iterators # - list comprehensions, one-liners # # - Operators # - Numerical operations, inplace operations # - Division in Python 2 and 3 # - Boolean operators, bitwise operators # - Functions # - `def`, `lambda`, `return`, scope # - `*args`, positional arguments # - `**kwargs`, keyword arguments (with default values) # - argument expansion, arbitrary argument list # - References and copies # - Introspection and help # - `dir`, `help` # - doc-strings # - Packages and modules # - `import`, `from x import y as z` # - Error messages # # Basic Types # - `int`, `float`, `str` # - `type` and type conversion # # Values and data types # You can check the type of almost any value # In[ ]: type(3.14) # In[ ]: type('Hello World') # In[ ]: type(100) # In[ ]: type(True) # The types `float`, `int`, `str`, `bool`, etc. have the type: `type` # In[ ]: type(int) # ## Type conversion # * Changing types is possible through the built in functions # In[ ]: x = int('123') type(x) # In[ ]: x = str(45.3) type(x) print(x, type(x)) # But only if there the conversion makes sense # In[ ]: x = int('Carl') # # Data structures # - `str` # - `tuple`, `list`, `set`, `dict` # - indexing and slicing # - `del` # ## Strings: `str` # - Similar to Matlab and other languages. # In[ ]: a = 'Hello World' b = "Hello World" c = """Hello World""" d = '''Hello World''' # - Strings are immutable in Python
# Immutable /ɪˈmjuːtəb(ə)l/:
*unchanging over time or unable to be changed* # In[ ]: a = 'Hello World' a[0] = 'Y' # * Line breaks and special characters just like in Matlab # In[ ]: a = 'Hello\nWorld' print(a.upper()) # * More on strings in Lecture 3 # * E.g. string methods # In[ ]: ', '.join(s for s in dir('') if not '__' in s) # Brain-teaser! :) # ## Tuples: `tuple()`, `()` # - Ordered (immutable) collection of objects # - Often used to store objects of different types # In[ ]: x = (3.14, int, 'John') x = 3.14, int, 'John' # paranthesis are optional print(x) # * Tuple-methods # In[ ]: ', '.join(s for s in dir(tuple()) if '__' not in s) # In[ ]: help(tuple().index) # In[ ]: t = 'A', 'B', 'C' t.index('C') # - Tuples are immutable (like strings)
# and can _not_ be changed in place # In[ ]: x = (3.14, int, 'John') x[0] = 3.1415 # ## Tuple unpacking # * A convenient syntax for splitting up all the components of a tuple # In[ ]: x = 3.14, int, 'John' a, b, c = x print(a) # * Also works for `list`, `set` and `dict`
though this usage is more rare # In[ ]: x = ['Hello', 'Foo', 'World'] a, b, c = x print(a, c) x = {1, 2, 2} a, b = x print(a, b) x = {'John': 37, 'Sara': 25, 'Lisa': 45} a, b, c = x.items() print(a, c) # ## Lists: `list()`, `[]` # - Lists are your standard storage option # - Mixing object types is possible but strongly discouraged (use `tuple`) # In[ ]: x = [43, 10, 15] len(x) # * Can store any object (including other lists) # In[ ]: y = ['Hello', 'World'] z = [[1, 2, 3], [4, 2], [9, 4, 2]] # * Lists can be changed in place # In[ ]: z[2] = 41 print(z) # ## `list` methods # In[ ]: ', '.join(s for s in dir(list()) if '__' not in s) # In[ ]: help(list().sort) # In[ ]: x = [4,67,2,10] x.sort() print(x) # ## Indexing and slicing # - Using square brackets `[]` # In[ ]: x = [43, 10, 15] x[1] # - **Note:** indexing starts from 0
# Index measures distance from start. # - Negative indices are counted from the end # - `-1` is the index of the last element # In[ ]: x[-1] # ### Slicing # - Similar to that of Matlab, but in the order -`start:end:increment`* # ``` # Matlab Python # x(a:b:c) ~ x[(a-1):c:b] # end ~ -1 or nothing: # ``` # - True for single index # ``` # x(end) ~ x[-1] # ``` # - But `-1+1 = 0` so when using slicing, the syntax is to leave it out # ``` # x(1:2:end) ~ x[::2] # x(7:end) ~ x[6:] # ``` # ## Slicing examples # - Note that ranges are given in a half open interval description, `a:b` $= [a,b)$ # In[ ]: x = ['my', 'short', 'list', 'of', 'strings'] # In[ ]: x[0:2] # In[ ]: x[1:] # In[ ]: x[:1] # In[ ]: x[:] # In[ ]: x[0::2] # In[ ]: x[0:2:3] # In[ ]: x[::-1] # ## Sets: `set()`, `{}` # * Sets are containers of unique items # In[ ]: x = {1, 2, 3, 2, 7, 2, 3, 2, 6} print(x) # In[ ]: x.add(8) print(x) # In[ ]: y = x.intersection({1, 4, 5, 6, 10}) print(y) # - `set`-methods # In[ ]: ', '.join(s for s in dir(set()) if '__' not in s) # In[ ]: help(set().intersection) # - Useful for "set operations": E.g. Find common elements in two lists # In[ ]: A = ['Ada', 'Beda', 'Emil', 'Emilia'] B = ['Ada', 'Emilia', 'Eva', 'Greta'] s = set(A).intersection(B) l = list(s) print(l) # ## Dictionaries: `dict()`, `{}` # - Important data structure that is very common in Python programs # - Set of "key-value" pairs # - Also called maps # ```python # x[key] = value # ``` # representing the mapping of $key \to value$ # In[ ]: x = {'John': 37, 'Sara': 25, 'Lisa': 45} # In[ ]: x['Lisa'] # It is very common to create an empty dictionary and
add new entries as required (e.g. values are read from a file) # In[ ]: x = dict() # or x = {} x['John'] = 37 x['Sara'] = 25 x['Lisa'] = 45 # In[ ]: print(x['John']) # * Check for existence of keys using `in` and `not in` # In[ ]: print( 'Lisa' in x ) print( 'Jeff' not in x ) # * You get access the `keys`, `values`, or `(key,value)` pairs
as iterators (more on this later): # In[ ]: print( x.keys() ) print( x.values() ) print( x.items() ) # ## Deleting: `del` # # - `del` can be used to remove elements in `list`, `dict` # In[ ]: x = [4,5,6,4,3,5,1] del x[3:] print(x) # In[ ]: x = {'John': 37, 'Sara': 25, 'Lisa': 45} del x['John'] print(x) # There are also methods for "consuming" elements, like `.pop` # In[ ]: sara_age = x.pop('Sara') print(sara_age) print(x) # - `del` can also be used to remove an object
(i.e. force garbage collection) # In[ ]: x = 10 del x print(x) # # Control flow statements # - Conditionals `if`, `elif`, `else` # - Indentation # - `while`, `for`, `enumerate`, `zip`, `itertools`, `.items()` # - Iterators `type(enumerate(['A', 'B', 'C']))` # - List comprehension # - Single statement, single line; # ## Conditionals: `if`, `elif`, `else` # - Works as expected # - Built in boolean values: `True`, `False` # In[ ]: if False: print('Stuff here') elif True: print("Let's print") print('many lines') else: print('Good bye!') # - Can be used directly in assignments # In[ ]: a = 1 x = 1 if a > 2 else print(x) # ## Indentation # - Indentation is **not** optional in Python.
# - It determines the scope in control flow! # - Using 4 spaces per indentation is **strongly** encouraged. # - Statements ending with colon `:` require a following indented block # Matlab conditionals # ```matlab # if x # f(); # elseif y # g(); # end # for x = y # disp('foo'); # end # if q # end # ``` # Python conditionals # ```python # if x: # f() # elif y: # g() # for x in y: # print('foo') # if q: # pass # no-operation, needed to avoid syntax errors # ``` # ## Boolean expressions # - `in`, `not`, `and`, `or` # In[ ]: print( 1 in [1,2,3] ) print( 7 not in (4,3,5) ) print( 'W' in 'World' ) # In[ ]: print( 6 != 5, 6 == 5 ) # In[ ]: print( 6 >= 5, 6 <= 5 ) # In[ ]: print( 6 > 5, 6 < 5 ) # In[ ]: print( True or False ) print( True and False ) print( not True ) # ## While loops: `while`, `break`, `continue` # - Syntax # ```python # while condition: # do_stuff # ``` # - `break` and `continue` work as in Matlab # - Q: What is printed below? # In[ ]: x = 0 while x < 10: x = x + 1 if x == 3: continue print(x, end=', ') if x > 6: break # ## For-loops: `for` # - Loop directly over the content in iteratable containers # - `tuple`, `list`, `set`, `dict`, etc. # In[ ]: foo = ['This', 'is', 'a', 'list'] for x in foo: print(x) # - `range(start, end+1, step)` iterator over integer ranges # - C.f. Matlab's colon operator: `range(a, b, c) ~ a : c : (b-1) ` # In[ ]: for i in range(5): print(i, end=', ') # In[ ]: for i in range(1, 10, 2): print(i, end=', ') # ### `for` ... `dict` # * Looping over dictionaries loops over the keys # In[ ]: foo = {'John': 37, 'Sara': 25, 'Lisa': 45} for x in foo: print(x) # * unless you specify otherwise # In[ ]: foo = {'John': 37, 'Sara': 25, 'Lisa': 45} for x in foo.items(): print(x) # * We can also directly unpack variables in for-loops # In[ ]: for key, value in foo.items(): print(key, "is", value, "years old") # ### `enumerate` and `zip` # - To get both the index and the value use `enumerate` # In[ ]: foo = ['This', 'is', 'a', 'list', 'of', 'strings'] for i, x in enumerate(foo): print('counter is', i, 'and value is', x) # - To loop over 2 (or more) sequences together use `zip` # - `zip` "zips" the sequences into a sequence of tuples # In[ ]: x = [3, 7, 4, 9, 3, 0] y = "qwerty" z = {1, 2, 3} for i, c, i2 in zip(x, y, z): print(i, c, i2) # ## Iterators # - For performance, `zip` doesn't actually create a list # - It returns an "iterator" that dynamically constructs tuples # In[ ]: z = zip(x,y) print(z) print(type(z)) # - If a list is **really** needed use `list` # In[ ]: z = list(zip(x,y)) print(z) # ## Single statement, single line # - `if`, `while`, `for` etc. can all be written on a single line
# when there is just 1 statement inside the block. E.g. # In[ ]: if 3 > 0: print('It works!') # In[ ]: for s in ['This', 'works', 'too!']: print(s, end=' ') # ## List comprehensions # - Short, convenient, and fast syntax for creating lists # - Use with care, _will_ make code unreadable # In[ ]: l = list() for i in range(5): l.append(i**2) print(l) # In[ ]: l = [k**2 for k in range(5)] print( l ) # In[ ]: l = [k**2 for k in range(10) if k % 2 != 0] print( l ) # * Also works for `set` and `dict` # In[ ]: d = {k**2 for k in range(10)} print(d) # In[ ]: l = ['John', 'Jeff', 'Carl'] d = { key : idx**2 for idx, key in enumerate(l) } print(d) d = {} for idx, key in enumerate(l): d[key] = idx**2 print(d) # # Operators # - Numerical operations; `+, -, *, /, **` # - Acting on: `list`, `tuple`, `str` # - Division in Python 2 and 3 # - Inplace; `+=, -=, *=, /=, //=` # - Boolean operators; `==, is, in, >, <, >=, >=` # - Bitwise operators; `>>, <<, &, |, ^` # ## Binary operators # In[ ]: print('4 + 3 =', 4 + 3) print('4 - 3 =', 4 - 3) print('4 * 3 =', 4 * 3) print('4 / 3 =', 4 / 3) # ### Less obvious operators # * `a ** b` $= a^b$ # * `a // b` $= \lfloor a/b \rfloor$ # * `a % b ` $= a \,\text{mod}\, b$ # In[ ]: print('5 ** 3 =', 5 ** 3) print('5 // 3 =', 5 // 3) print('5 % 3 =', 5 % 3) # ## **Note:** Division in Python v2 and v3 # - In Python 3 # - `a / b` is floating point division # - `a // b` is integer division # - In Python 2 # - `a / b` is **integer division** (unexpected!?) # - `a / float(b)` is floating point division # - In Python 2 `a / b` can be made floating point division by # ```python # from __future__ import division # ``` # Here: Python 3 # In[ ]: 4 / 3 # In[ ]: 4 // 3 # ## Inplace assignment operators # In[ ]: x = 1 x += 2 # x = x + 2 x *= 3 # x = x * 3 x /= 4 # etc. print(x) # In[ ]: x = 1 x //= 2 print(x) # ## Bitwise operators # - Binary representations: # $$41 = 2^5 + 2^3 + 2^0 = 101001_b$$ # $$28 = 2^4 + 2^3 + 2^2 = 011100_b$$ # In[ ]: a = 0b101001 b = 0b011100 print('a = {:d} = 0b{:06b}'.format(a, a)) print('b = {:d} = 0b{:06b}'.format(b, b)) # - Bit wise and `&`, or `|`, xor `^` # In[ ]: a = 0b101001 b = 0b011100 # In[ ]: print('a & b = 0b{:06b}'.format( a & b )) # In[ ]: print('a | b = 0b{:06b}'.format( a | b )) # In[ ]: print('a ^ b = 0b{:06b}'.format( a ^ b )) # - Bit shift operators `>>` and `<<` # In[ ]: print("41 >> 1 =", 41 >> 1) print("41 << 1 =", 41 << 1) # - `X >> 1` shifts the bits one step to the right # $$41 >> 1 = 101001_b >> 1 = 010100_b = 2^4 + 2^2 = 20$$ # # - This is equal to integer division by 2, `X >> 1 == X // 2` # # - In general: `a >> b == a // (2**b)` # ## Operators acting on non-scalars # - Operators are **not** the same as in Matlab # - Different types work differently with operators # - `string` # In[ ]: 'Hello World' * 3 # In[ ]: 'Hello' + ' ' + 'World' # - `list`, `tuple` # In[ ]: [1, 2, 3] * 3 # In[ ]: (1, 2, 3) + (4, 5) # # Functions # - `def` # - `return` # - scope # - `args`, positional arguments # - `kwargs`, keyword arguments (with default values) # - `*args`, `**kwargs`, positional and keyword argument expansion # - arbitrary argument list # ## Function definition: `def` # In[ ]: def print_value(x): print('The value is:', x) # In[ ]: print_value(13) # ## Return value(s): `return` # In[ ]: def my_sum(a, b): return a + b # In[ ]: print(my_sum(1, 2)) # ### Multiple return values # - Using `tuple` expansion (as advertised) # In[ ]: def multi_return_function(x): return x, x**2, x**3 # In[ ]: x = multi_return_function(3) print(x) # In[ ]: a, b, c = multi_return_function(3) print('a = ', a, ', b = ', b, ', c = ', c, sep='') # * Wrong number of arguments in the tuple expansion is an error # In[ ]: a, b = multi_return_function(3) # ### Optional arguments # - Has to be in the end of the function argument list # In[ ]: def my_function(a, b=2, c=3): # b and c have default values and are optional return a + b + c # * These all give the same result (and will all have `a=1, b=2` and `c=3`) # In[ ]: print(my_function(1, 2, 3)) print(my_function(1, 2)) print(my_function(1)) # ### Key-word arguments # - Set function parameters by name # In[ ]: print(my_function(a=1, b=2, c=3)) print(my_function( 1, b=2, c=3)) print(my_function( 1, 2, c=3)) print(my_function( 1, c=3, b=2)) print(my_function( 1, b=2)) print(my_function( 1, c=3)) print(my_function(a=1, c=3)) # * Parameters without a name **must** come before named parameters! # In[ ]: my_function(a=1, 2, 3) # Not OK! # In[ ]: my_function(1, b=2, 3) # Not OK! # ### Argument expansion # - Expand `list` and `tuple` to positional arguments using the `*` operator # In[ ]: def my_sum(a, b, c): return a + b + c # In[ ]: x = [2, 3, 4] s = my_sum(*x) # Uses: "a, b, c = x" print(s) # ### Key-word argument expansion # - Key-word arguments can be passed using a `dict` # - The key-value pairs are expanded using the `**` operator # In[ ]: def pet_info(name, kind=None, owner=None): """Returns a string summarizing the info on the pet.""" out = name if kind is not None: out += ' is a ' + kind if owner is not None: out += ' owned by ' + owner out += '.' return out # In[ ]: pets = [ dict(name='Snoopy', kind='dog', owner='Charlie'), dict(name='Garfield', kind='cat', owner='Jon'), dict(name='Bamse', kind='bear'), ] for pet in pets: print(pet_info(**pet)) # Expand dict to key-word arguments # ### Arbitrary arguments # - Functions taking any number of positional and key-word arguments # - Sometimes useful, but makes the function hard to read # In[ ]: def my_general_argument_function(*args, **kwargs): print('Got positional arguments:', args) print('Got key-word arguments:', kwargs) # In[ ]: my_general_argument_function( 'one', 'argument', 'at', 'a', 'time', key='words', are='very', important='!') # ## Function variable scope # - Functions inherit the local scope # In[ ]: c = 123 def sum_1(a): return c + a print(sum_1(1)) # In[ ]: def sum_2(a): c = 14 # c is a local variable (different from c above) return c + a print(sum_2(1)) print(c) # c, in the outer scope is unchanged # In[ ]: def sum_3(a): c += 17 # no modification of variables from the outer scope return c + a print(sum_3(1)) # ## Anonymous functions # - Also known as `lambda` functions # - C.f. Matlabs anonymous functions `@(x) x^2` # In[ ]: def axpy(a, x, y): # trivia: axpy is a standard name for a*x + y (in linear algebra packages) return a*x + y print(axpy(2, 3, 4)) # In[ ]: axpy_lambda = lambda a, x, y : a*x + y print(axpy_lambda(2, 3, 4)) # - Both are functions # - The only difference is the syntax # In[ ]: print(type(axpy), type(axpy_lambda)) # ## Everything is an object # ## Functions are objects! # In[ ]: def add_me(a, b): return a + b def mul_me(a, b): return a * b def div_me(a, b): return a / b functions = [add_me, mul_me, div_me] for f in functions: print(f(1, 2)) # # References and copies # - Assignment of primitive types gives copies # In[ ]: a = 10 b = a # b is a new int variable b += 1 print(a, b) # - Assignment of containers `list`, `dict` etc. are **references** # In[ ]: a = [10] b = a # b is a reference to the list a b[0] += 1 print(a, b) # - Important corner cases # In[ ]: x = [1, 2, 3] # Create a list object and binds x to that object. y = x # Binds y to that same object x y += [4, 5] # += for lists means "append to list" print(x) # In[ ]: x = [1, 2, 3] # Create a list object and bind x to that object. y = x # Bind y to that same object y = y + [4, 5] # Create a new object from the list, use + list operation and re-bind y to that new object print(x) # In[ ]: x = [1, 2, 3] # Create a list object and bind x to that object z = (x, x) # Create a tuple object which contains references which are bound to the same object print('1:', z) x += [4, 5] # Append to list print('2:', z) x = [7, 8] # Rebind name x to a new list print('3:', z) # - Well, the variable `z` has actually not changed (tuples are immutable) # - It's still pointing to the same two list-objects # In[ ]: x = [1, 2, 3] y = [x, x, [7, 8, 9]] print(y) # What will happen if we do: # In[ ]: y[0] = [4, 5, 6] print(x) print(y) # The equal sign is the "rebind variable" operation! # To really make a copy, use the slice `:`, or the `.copy()` method # In[ ]: x = [1, 2, 3] y = x[:] y += [4, 5] print(x, y) # In[ ]: x = [1, 2, 3] z = x.copy() z += [6, 7] print(x, z) # ## Functions and references # - Same behavior with references when it comes to function arguments # In[ ]: def foo(x): x[0] = 42 y = [1, 2, 3] foo(y) # This means foo(x = y), and x = y behaves as described earlier print(y) # * The equal sign `=` is variable name rebinding,
it doesn't modify the object that `x` was bound to previously. # In[ ]: def foo(x): x = [4, 5, 6] y = [1, 2, 3] foo(y) print(y) # # Modules # - In Python have few functions in the global scope (namespace) # - C.f. Matlab were everything is available # - Import modules to access functionality # - Even basic things in the Python standard library # ## Module namespace # - Each module has its own namespace (defaults to the library name) # - Namespaces are good, let's use them! # - They avoid name-collisions between common function names # ## Importing modules # In[ ]: import numpy numpy.sqrt(5) # ## Abbreviating the namespace # In[ ]: import numpy as np np.sqrt(5) # ## Selective importing # Example: Solve $x^2 - 1 = 0$ numerically # # This is colloquially called: "Root solving" # In[ ]: from scipy.optimize import root root(lambda x: x**2 - 1, x0=0.1) # In[ ]: from scipy.optimize import root as scipy_root res = scipy_root(lambda x: x**2 - 1, x0=0.1) print(res.x) # ## Wildcards # Using the wildcard, you import everything in that scope # ```python # from numpy import * # ``` # but this practice is discouraged!! Why? # - The `numpy` module contains many functions, e.g. a function called `all`. # - If `all` is defined in the program before the wildcard import, **it is lost!** # - Very hard to debug... # In[ ]: def all(): return 'Hi all!' print(all()) # In[ ]: from numpy import * print(all()) # ## The Python Standard Library # - Python comes with "batteries included" # - # # Examples: # In[ ]: from datetime import datetime print(datetime.now()) # In[ ]: from itertools import permutations for pair in permutations(['A', 'B', 'C'], 2): print(pair, end=', ') # In[ ]: from multiprocessing import Pool def my_calc(x): return x*x p = Pool(4) l = p.map(my_calc, [1, 2, 3, 4, 5, 6, 7, 8, 9]) print(l) # # Documentation # ## Reference manuals # PDF versions of the manuals for: # - [Python 3](https://docs.python.org/3/), # - [Numpy and SciPy](https://docs.scipy.org/doc/), and # - [Matplotlib](https://matplotlib.org/contents.html), as well as # - the [Lecture Notes](https://chalmers.instructure.com/courses/8757/modules) # # will be available on the exam. Check them out! # # Built in `help` # - `help` is a built in help function # - You can run `help` on most things, functions, libraries, variables # - Note that you need to pass the function or method (i.e. without parenthesis) # In[ ]: help(len) # ### Documenting your own functions # - Document your functions using "doc-strings" # - `help` will show this documentation # - NumPy has a good [style guide]() # In[ ]: def my_sum(a, b): """The sum of two numbers. Parameters ---------- a : int First number b : int Second number Returns ------- int Sum of a and b """ assert( type(a) == int ) assert( type(b) == int ) return a + b # In[ ]: help(my_sum) # # Errors # - Many examples in this lecture # - Syntax errors are probably going to be common # In[ ]: x = 123 + /34 # In[ ]: 132/0 # In[ ]: x = [1,2,3] x[8] # # The many ways of Python # There are many ways to use and interact with Python # - Running `.py` on the command line, using the `python` interpreter # - Interactively using the `python` interpreter # - Interactively using the "smart" `ipython` interpreter # # Demo! # - Interactively using the Jupyter notebook interface # - Using a development GUI like `PyCharm`, perfect for developing GUI applications! # # We warmy recommend `PyCharm` for learning in this course! (the free version)
# (It is possible to [apply](https://www.jetbrains.com/student/) for a "full" version as a student) # # Lecture 2: The End