sampledoc's Scientific Python Tools

Table Of Contents

Previous topic

Introduction

Next topic

First introduction to NumPy

This Page

First introduction to Python

Fast-track Python

The python source code is run by an interpreter either interactively or as script. The common python interpreter can be launched by the python command from any shell window. In general we recommend not to use the bare python interpreter but an enhanced and much more user friendly version called IPython. Start an interactive IPython session by running the ipython command:

ifmp14@ifmp14:~$ ipython
Python 2.7.6 (default, Mar 22 2014, 22:59:38)
Type "copyright", "credits" or "license" for more information.

IPython 2.2.0 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]:

In Python everything is an object and the types are dynamic

In [1]: a = 1.5   # The rest of this line is a comment

In [2]: type(a)
Out[2]: float

In [3]: a = 1 # redefine a as an integer

In [4]: type(a)
Out[4]: int

In [5]: a = 1e-10 # redefine a as a float with scientific notation

In [6]: type(a)
Out[6]: float

In [7]: a = 1+5j # redefine a as complex

In [8]: type(a)
Out[8]: complex

In [9]: print("a="+str(a))
a=(1+5j)

In [10]: print(a)
(1+5j)

In [11]: a
Out[11]: (1+5j)

In [12]: print(2**6)
64

In [13]: a**8
Out[13]: (-3824-456960j)

In [14]: 2**6
Out[14]: 64

str, unicode - string types (immutable):

In [15]: s1 = 'hello'

In [16]: s2 = u'φ(α) + ψ(α+β+γ)' # prepend u, gives unicode string

In [17]: s1[0]
Out[17]: 'h'

In [18]: s1[0], s1[1]
Out[18]: ('h', 'e')

In [19]: type(s1)
Out[19]: str

In [20]: type(s2)
Out[20]: unicode

In [21]: print(s2)  # The 'print' shows the string using the actual special characters
φ(α) + ψ(α+β+γ)

In [22]: s2         # This way we just see the unicode encoding
Out[22]: u'\u03c6(\u03b1) + \u03c8(\u03b1+\u03b2+\u03b3)'

list - mutable sequence:

In [23]: ell = [1,2,'three'] # make list

In [24]: type(ell)
Out[24]: list

In [25]: type(ell[0])
Out[25]: int

In [26]: type(ell[2])
Out[26]: str

In [27]: ell[2] = 3

In [38]: ell.append(4)

In [29]: ell
Out[29]: [1, 2, 3, 4]

tuple - immutable sequence:

In [30]: tempty = ()

In [31]: toneelement = (1,)  # Note the trailing comma here (and only here)

In [32]: ttwoelement = (1, 2)

In [33]: tempty
Out[33]: ()

In [34]: toneelement
Out[34]: (1,)

In [35]: ttwoelement
Out[35]: (1, 2)

In [36]: t = (1, 2, 'four')

In [37]: t[2]
Out[37]: 'four'

In [38]: t[2] = 4
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-34-56c309f1aa91> in <module>()
----> 1 t[2] = 4

TypeError: 'tuple' object does not support item assignment

python - loop:

In [39]: for element in xrange(1,21,2):
   ....:     print(element)
   ....:
1
3
5
7
9
11
13
15
17
19

In [40]: for element in xrange(1,21,2):
   ....: print(element)
   ....:
  File "<ipython-input-43-ff776174c35a>", line 2
    print(element)
        ^
IndentationError: expected an indented block

hence indentation is the delimiter for blocks. Usually IPython takes care for you. Pressing Enter is enough.

python - functions:

A pythonic way of adding first n numbers:

def sf(n):
    k = 0
    result = 0

    while True:
        print("current number to add =" + str(k))
        result += k
        k += 1
        if k > n:
            break

    return result

In the above example note that the indentation delimits the blocks and that no semi-colons are needed.

We can enter this in our interactive ipython session

In [50]: def sf(n):
   ....:     k = 0
   ....:     result = 0
   ....:     while True:
   ....:         print("current number to add = " + str(k))
   ....:         result += k
   ....:         k += 1
   ....:         if k > n:
   ....:             break
   ....:     return result
   ....:

In [51]: sf(10)
current number to add = 0
current number to add = 1
current number to add = 2
current number to add = 3
current number to add = 4
current number to add = 5
current number to add = 6
current number to add = 7
current number to add = 8
current number to add = 9
current number to add = 10
Out[51]: 55

Note how nicely ipython handles indentation for us when we start new nested blocks. On the other hand we can not make empty lines in this interactive mode because this would be interpreted as end of the input. (Actually, we can make empty lines but only directly after opening a new nested block.)

For larger pieces of code there is much advantage in copying the above definition of the function sf into a file, let us call it sumy.py. Python source code files have the extention .py. Then we start ipython in the same directory, and type:

In [1]: from sumy import *

to load the code. Then we can call the function like:

In [2]: sf(3)
current number to add = 0
current number to add = 1
current number to add = 2
current number to add = 3
Out[2]: 6

Mutable and immutable objects

Warning

Arguments are always passed by assignement.

Python’s pass-by-assignment scheme isn’t quite the same as C++’s reference parameters option, but it turns out to be very similar to the C language’s argument-passing model in practice:

  • Immutable arguments are effectively passed “by value.” Objects such as integers and strings are passed by object reference instead of by copying, but because you can’t change immutable objects in-place anyhow, the effect is much like making a copy.
  • Mutable arguments are effectively passed “by pointer.” Objects such as lists and dictionaries are also passed by object reference, which is similar to the way C passes arrays as pointers – mutable objects can be changed in-place in the function, much like C arrays.

Of course, if you’ve never used C, Python’s argument-passing mode will seem simpler still – it involves just the assignment of objects to names, and it works the same whether the objects are mutable or not.

In [1]: def f(a):    # a is assigned to (references) the passed object
   ...:     a = 99   # changes local variable a only: here simply resets 'a' to a completely different object
   ...:

In [2]: b = 88

In [3]: f(b)         # 'a' and 'b' both reference same 88 initially

In [4]: print(b)     # b not changed
88

Assignment to an argument name inside a function (e.g., a=99) does not magically change a variable like b in the scope of the function call. Argument names may share passed objects initially (they are essentially pointers to those objects), but only temporarily, when the function is first called. As soon as an argument name is reassigned, this relationship ends. This is also related to the copy-on-write semantics.

That is the case for assignment to argument names themselves. When arguments are passed mutable objects like lists and dictionaries, we also need to be aware that inplace changes to such objects may live on after a function exits, and hence impact callers. Here’s an example that demonstrates this behavior:

In [5]: def changer(a,b):   # Arguments assigned references to objects
   ...:     a = 2           # Changes local name's value only
   ...:     b[0] = 'spam'   # Changes shared object in-place
   ...:

In [6]: X = 1

In [7]: L = [1, 2]

In [8]: changer(X,L)        # Pass immutable and mutable objects

In [9]: X                   # X is unchanged, L is different!
Out[9]: 1

In [10]: L
Out[10]: ['spam', 2]

If we don’t want in-place changes within functions to impact objects we pass to them, though, we can simply make explicit copies of mutable objects. For function arguments, we can always copy the list at the point of call:

In [11]: L = [1, 2]

In [12]: changer(X, L[:])   # Pass a copy, so our 'L' does not change

In [13]: X
Out[13]: 1

In [14]: L
Out[14]: [1, 2]

We can also copy within the function itself, if we never want to change passed-in objects, regardless of how the function is called:

In [15]: def changer(a, b):
   ....:     b = b[:]        # Copy input list so we don't impact caller
   ....:     a = 2
   ....:     b[0] = 'spam'   # Changes our list copy only
   ....:

In [16]: L = [1, 2]

In [17]: changer(X, L)

In [18]: X
Out[18]: 1

In [19]: L
Out[19]: [1, 2]

Both of these copying schemes don’t stop the function from changing the object – they just prevent those changes from impacting the caller. To really prevent changes, we can always convert to immutable objects to force the issue. Tuples, for example, throw an exception when changes are attempted:

In [20]: changer(X, tuple(L))   # Pass a tuple, so changes are errors
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-26-f94776bffe6b> in <module>()
----> 1 changer(X, tuple(L)) # Pass a tuple, so changes are errors

<ipython-input-15-a06105498d20> in changer(a, b)
      2     b = b[:]
      3     a = 2
----> 4     b[0] = 'spam' # Changes our list copy only
      5

TypeError: 'tuple' object does not support item assignment

Elements of writing good Python style are on Google Python Style Guide or PythonStyle.

Number formatting

You generally do not want to display a floating point result of a calculation in its raw form, often with an enormous number of digits after the decimal point, like 23.457413902458498. You are likely to prefer rounding it to something like 23.46.

Python features sprintf()-style formatting, which you might know already from Matlab or C. The most common format specifiers are %d for integers, %f for floating point numbers, %e for scientific notation and %s for strings.

Examples

A float rounded to 2 digits after the comma with 6 digits in total:

In [1]: '%+6.2f' % 0.2
Out[1]: ' +0.20'

Scientific notation with 2 digits after the comma:

In [2]: s = '%.2e' % 0.2
Out[2]: 2.00e-01

Integer with a fixed size of 5 digits and left-padded with zeros:

In [3]:
s = '%05d' % 34
print(s)
Out[3]: 00034

It is also possible to replace more than one number in a string:

In [4]: 'GMRES solver converged to a rel. residual of %.3e after %d iterations in %.2f seconds.' % (1.2323e-3, 1000, 5.1)
Out[4]: 'GMRES solver converged to a rel. residual of 1.232e-03 after 1000 iterations in 5.10 seconds.'

An exhaustive list of options can be found in section 5.6.2. String Formatting Operations: of the python documentation.

Formatting tables with str.format() (Optional)

This section is optional and mainly intended as a reference for formatting plain text tables.

The str.format() is an alternative way in python for string formatting, it replaces fields contained in a string marked with curly braces {}. A field is specified by the following syntax:

{[keyword] : [alignment][format specifier]}

The format specifier is identical to the previous section, except that the % has to be omitted now. Valid alignment options are >, <, ^, resp. left, right and center.

  • Example:

    In [1]: '{Class:5s} has {NStudents:d} students'.format(Class='Numerische Methoden', NStudents=200)
    Out[1]: 'Numerische Methoden has 200 students'
    

    keyword is optional and can be omitted completely or replaced by the argument number:

    In [43]: '{:5s} has {:d} students'.format('Numerische Methoden', 200)
    Out[43]: 'Numerische Methoden has 200 students'
    
  • Example: formatting a table with 3 columns and proper alignment

    In [2]: title = '{0:>15s}\t{1:>15s}\t{2:>15s}'.format('Refinement','Mesh width', 'Abs. error')
       ...: print(''.join(len(title)*['-'])) # separator '-------'
       ...: print(title)
       ...: print(''.join(len(title)*['-']))
       ...: for i in xrange(5):
       ...:     h = 2**(-i)
       ...:     err = h**5
       ...:     row = '{0:15d}\t{1:15.2f}\t{2:15.2e}'
       ...:     print(row.format(i, h, err))
       ...: print(''.join(len(title)*['-']))
    
    Out [2]:
    
    -----------------------------------------------
         Refinement      Mesh width      Abs. error
    -----------------------------------------------
                  0            1.00        1.00e+00
                  1            0.50        3.12e-02
                  2            0.25        9.77e-04
                  3            0.12        3.05e-05
                  4            0.06        9.54e-07
    -----------------------------------------------
    

For more options see section 7.1.3. Format String Syntax of the python documentation.

Some nice extra specialities (Optional)

python - list comprehensions:

Often we want to construct lists in a very systematic way, for example a list of all square numbers. This can be done easily by a so called list comprehension:

In [5]: L = [k**2 for k in xrange(10)]

In [6]: L
Out[6]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

It is even possible to attach a condition, for example we can compute squares of even numbers only:

In [83]: L = [k**2 for k in xrange(10) if k%2 == 0]

In [84]: L
Out[84]: [0, 4, 16, 36, 64]

python - generators:

The python documentation states:

They [generators] are written like regular functions but use the yield statement whenever they want to return data. Each time next() is called, the generator resumes where it left-off [...]

Let’s just make a trivial example:

In [37]: def squares(n):
   ....:     for i in xrange(n):
   ....:         yield i**2
   ....:

In [38]: S = squares(10)

In [39]: S.next()
Out[39]: 0

In [40]: S.next()
Out[40]: 1

In [41]: S.next()
Out[41]: 4

In [42]: S.next()
Out[42]: 9

Until we reach the end, where it raises a StopIteration exception

In [48]: S.next()
Out[48]: 81

In [49]: S.next()
^[[A
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-49-8d33773269b4> in <module>()
----> 1 S.next()

StopIteration:

This way we can iterate over generated values, and for example combine this with a list comprehension to find the first square numbers:

In [55]: L = [s for s in squares(10)]

In [56]: L
Out[56]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Generators can be useful to represent infinite data structures lazily. For example we can compute all square numbers, one after the other:

In [71]: def squares():
   ....:     i = 0
   ....:     while True:
   ....:         yield i
   ....:         i = i+1
   ....:

In [72]: S = squares()

In [73]: S.next()
Out[73]: 0

In [74]: S.next()
Out[74]: 1

In [75]: S.next()
Out[75]: 2

ad infinitum ...

python - Anonymous functions (lambda functions):

A lambda function is a (usually small) function without an explicit name. The are defined like regular function but use the lambda keyword instead of def and have no name. Let’s look at an example:

In [90]: lambda x,y : x + y - 2
Out[90]: <function __main__.<lambda>>

Of course we can assign this function object to any variable name we like:

In [91]: f = lambda x,y : x + y - 2

This is now equivalent to a regular definition but involes less typing:

In [55]: def f(x, y):
   ....:     return x+y-2
   ....:

Lambda functions have a few restrictions, for example we can not do any variable assignment or control blocks inside. But still it’s possible to do some fancy stuff:

In [59]: f = lambda n: [i for i in xrange(n)]

In [60]: f(10)
Out[60]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Finally, the Zen of Python (hat you might find useful outside of Pyhton, too):

In [35]: import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

More on the Python language

You can find more on Python in on-line tutorials and books. I enjoyed Learning Python by M. Lutz and Python in a Nutshell by A. Martelli.

There as the very good official python tutorial covering most of the important stuff and also some things not so important for us:

You should definitely read at least the following three chapters:

For the ones of you interested in object-oriented programming in the python laguage, there is also a chapter on classes and inheritance.

  • Classes Covers the topics related to object-oriented programming in python.

Finally, python comes with a huge standard library included.

And if this is still not enough, there is a central storage called PyPI containing thousands of python packages for almost any job