====================
Einführung in Python
====================

Stefan Schwarzer <sschwarzer@sschwarzer.com>

Chemnitzer Linuxtage 2011

Chemnitz, 2011-03-19


Überblick
---------

* zum Workshop

* erste Schritte im interaktiven Interpreter

* Namen

* Datentypen

* Bedingte Anweisungen (`if ... elif ... else`)

* Schleifen (`for`, `while`)

* Ausnahmen (`try ... except ... else`, `try ... finally`)

* Module

* eingebaute Funktionen

* Objektorientierte Programmierung (OOP)


Dieser Workshop
---------------

* Einführung in die Programmiersprache Python - ein Schnupperkurs

* keine Python-Kenntnisse erforderlich

* aber Programmierkenntnisse! (idealerweise auch in objektorientierter
  Programmierung)

* Ziel des Workshops: Teilnehmer sollen hinterher in der Lage sein,
  zumindest kleine Programme zu schreiben.

* Zeit ist *viel* zu knapp. :-/

  Folgen:

  - Erklärt werden nur die wichtigsten Konzepte und Bibliotheken.
  - Eventuell ist nicht genug Zeit, um mit den einzelnen Übungen fertig
    zu werden.
  - Nicht jedes Detail muss verstanden werden.
  - bitte Feedback zum Ablauf in der Pause (falls dringend, auch früher)

* verwendet wird Python 2.x (Python 3.2 ist aktuell, aber inkompatibel
  zu Python 2.x; der meiste Code ist aber für Python 2.x geschrieben)

* zwischendurch Zeit für eigenen Experimente/Übungen;
  bei Fragen fragen. ;-)

* Anmerkungen:

  - Code innerhalb von Fließtext schließe ich normalerweise in
    "umgekehrte" Hochkommas ("Backticks") ein. Ein Beispiel ist die
    Zuweisung `beispiel = 1`.
  - (G)Vim-Nutzer bekommen durch laden der Datei `talk.vim` Syntax-
    Highlighting wie hier.
  - Die Ausführung des meisten Beispiel-Codes in dieser Datei erledigt
    ein cleveres ;-) Skript `example.py`, das auch Bestandteil der
    Workshop-Dateien ist.


Wichtige Literatur / Links
--------------------------

* Python-Startseite: http://www.python.org/

* offizielle Python-Dokumentation

  - Startseite: http://docs.python.org/
  - Tutorial: http://docs.python.org/tutorial/
  - Library Reference: http://docs.python.org/library/
  - Language Reference: http://docs.python.org/reference/

* Python Style Guides

  - http://www.python.org/dev/peps/pep-0008/
  - http://www.python.org/dev/peps/pep-0257/

* Python Package Index: http://pypi.python.org/pypi

* Typische Python-Fehler:
  http://sschwarzer.com/download/robustere_python_programme_clt2010_print.pdf

* Buch "Learning Python"
  http://www.amazon.de/Learning-Python-Mark-Lutz/dp/0596158068/
  behandelt Python 2.x *und* 3.x ! :-)

* Buch "Python: Das umfassende Handbuch"
  http://www.amazon.de/x/dp/3836211106/

* deutschsprachige Portalseite zu Python: http://www.python.de/


Die Sprache Python
------------------

* ermöglicht kompakte, gut lesbare Programme

* ist relativ leicht zu lernen

* eignet sich für:

  - System-Administration
  - Web-Anwendungen
  - wissenschaftliche Anwendungen
  - das Verbinden verschiedener Systeme ("glue language")
  - und vieles mehr

* unterstützt diese Programmier-Paradigmen:

  - prozedural
  - objektorientiert
  - ein paar funktionale Elemente (aber keine "funktionale"
    Programmiersprache wie beispielsweise Haskell)

* ist bei vielen Linux-Systemen vorinstalliert (oft Python 2.6).


Erste Schritte im interaktiven Interpreter
------------------------------------------

* Python-Interpreter lässt sich interaktiv benutzen

* sehr gut für Experimente (teilweise leider Probleme bei
  Unicode-Strings, siehe unten)

* für den Einstieg in Python; Beispiel:

    $ python
    Python 2.6.5 (r265:79063, Apr 16 2010, 13:09:56)
    [GCC 4.4.3] on linux2
    Type "help", "copyright", "credits" or "license" for more information.
    >>> 1
    1
    >>> 2 * (3 + 4)
    14
    >>> print u"Hallo Welt!"
    Hallo Welt!
    >>> 1 + "2"
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: unsupported operand type(s) for +: 'int' and 'str'
    >>> def ausgabe(arg):
    ...   print arg
    ...
    >>> ausgabe(7)
    7
    >>> help("print")
    The ``print`` statement
    ***********************

       print_stmt ::= "print" ([expression ("," expression)* [","]]
                      | ">>" expression [("," expression)+ [","]])

    ``print`` evaluates each expression in turn and writes the resulting
    object to standard output (see below).  If an object is not a string,
    it is first converted to a string using the rules for string
    [viele weitere Zeilen]
    >>>

* aber auch oft für Fortgeschrittene, um Python-Module (Bibliotheken)
  auszuprobieren; Beispiel:

    $ python
    Python 2.6.5 (r265:79063, Apr 16 2010, 13:09:56)
    [GCC 4.4.3] on linux2
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import urllib
    >>> ufobj = urllib.urlopen("http://sschwarzer.com")
    >>> data = ufobj.read()
    >>> ufobj.close()
    >>> data[:50]
    '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Stric'

* verlassen des Interpreters mit Strg-D direkt am Prompt (`>>>`)

* Tipp: erweiterten Interpreter IPython installieren:

    $ sudo aptitude install ipython

  (hier für Debian/Ubuntu, bei anderen Distributionen entsprechend)


Bezeichner ("Namen")
--------------------

* bestehen aus A-Z, a-z, 0-9 und _

* erstes Zeichen darf keine Ziffer sein

* Groß-/Kleinschreibung wird unterschieden; `hallo` und `Hallo`
  sind unterschiedliche Bezeichner

* Schlüsselwörter *können* *nicht* als Namen verwendet werden

* Schlüsselwörter sind

    and         del         from        not         while
    as          elif        global      or          with
    assert      else        if          pass        yield
    break       except      import      print
    class       exec        in          raise
    continue    finally     is          return
    def         for         lambda      try

* eingebaute Funktionen *sollten* *nicht* als Namen verwendet werden
  siehe http://docs.python.org/library/functions.html

* PEP 8 sieht einige Konventionen für Namen vor. Einige der
  Verwendungen werden später erklärt.

  - Konstanten: `GROSSBUCHSTABEN_MIT_UNTERSTRICHEN`
  - Klassen: `EineKlasse`
  - Module: vorzugsweise `einmodul`, sonst `ein_model`
  - alles andere: `mein_toller_bezeichner`

  Gebrauch von Unterstrichen:

  - nicht-öffentlicher Name: `_interner_name`
  - "privates" Attribut in einer Klasse: `__total_intern`
  - spezieller, von der Sprache genutzter Name: `__str__`


Datentypen
----------

* einfache Datentypen

  - Zahlen (`int`, `long`, `float`, `complex`)
  - Boolsche Werte = Wahrheitswerte (`bool`)
  - Zeichenketten (`str`, `unicode`)

* zusammengesetzte Datentypen

  - Listen (`list`)
  - Tupel (`tuple`)
  - Dictionarys = assoziative Arrays (`dict`)
  - Sets (`set`)

* Funktionen, Klassen, Methoden

* Dateien

* Module

* und noch einige exotischere


Zahlen
------

* Beispiele:

    print 2 + 3 * 7  # 23
    print 8 / 4      # 2
    # Ganzzahl-Division: ganzzahliger Teil bleibt, Rest wird verworfen
    print 9 / 2      # 4
    print 9. / 2     # 4.5

* Den Unterschied `int` vs. `long` kann man normalerweise
  ignorieren; bei Bedarf werden `int`s in `long`s umgewandelt:

    klein = 2 ** 5
    print type(klein), klein
    gross = 2 ** 1000
    print type(gross), gross

* `float`-Werte entsprechen in etwa C's `double`-Typ und haben
  eine Genauigkeit von ca. 16 Dezimalstellen.

    print repr(1./7)

* `complex`-Werte bestehen aus Real- und Imaginärteil:

    z = (0+1j) ** 2
    print z, z.real, z.imag

* Bei Kombination von verschiedenen numerischen Datentypen werden
  die Operanden in den "höchsten" Datentyp konvertiert
  (`int` -> `long` -> `float` -> `complex`). Beispiele:

    print 1 + 2.1
    print 1 == 1.0
    print 1 == (1+0j)

* Interpreter-"Spielzeit"


Boolsche Werte
--------------

* Boolsche Werte sind die Konstanten `True` und `False`.

    wahr = (1 == 1)
    print type(wahr), wahr
    falsch = (1 == 0)
    print type(falsch), falsch

* Prinzipiell sind `True` und `False` aber nur verkappte Integerwerte:

    zwei = 1 + True
    print type(zwei), zwei
    null = False * 9
    print type(null), null

  Solche Ausdrücke können den Code schlecht lesbar machen; man sollte
  sich gut überlegen, ob es auch übersichtlicher geht.

* Boolean-Werte lassen sich mit `or`, `and` und `not` verknüpfen
  (Reihenfolge zunehmender Priorität).

    a = 1
    b = 2
    print (a >= 1) and not (b < 2)

* `and` und `or` sind "short-circuit-Operatoren", das heißt, der
  zweite Operand wird nicht ausgewertet, wenn das Ergebnis schon
  feststeht. Das Ergebnis der Kombination ist dann der erste
  Operand (aber nicht notwendigerweise `True` oder `False`).

* weiter unten mehr zu `bool`-Werten


Zeichenketten
-------------

* Python kennt keinen besonderen Typ für einzelne Zeichen
  (wie `char` in C)

* Zeichenketten werden in Anführungszeichen eingeschlossen:

  "Test"

  'Er sagte, "Hallo!"'

  "Wie geht's?"

  """Eine
    "mehrzeilige"
      Zeichenkette"""

  '''Und
      noch eine'''

* Diese so genannten "triple-quoted Strings" können alle anderen
  Arten von Anführungszeichen enthalten außer "sich selbst". Folgendes
  geht also *nicht*:

    """Triple-quoted Zeichenketten """der gleichen Art""" lassen
    sich nicht schachteln."""

* Anführungszeichen können mit Backslashes maskiert werden:

    print "Er sagte, \"Hallo!\""

* Alle oben genannten Zeichenketten sind vom Typ `str`. Es gibt auch
  noch Unicode-Strings (siehe unten).

* Es gibt (ähnlich wie in anderen Sprachen) ein paar besondere
  Zeichenketten:

    \"  maskiertes doppeltes Anführungszeichen
    \'  maskiertes einfaches Anführungszeichen
    \n  Zeilenumbruch (newline)
    \t  Tabulatorzeichen
    \r  Wagenrücklauf ("\r\n" ist ein unter Windows anzutreffendes
        Zeilenwechselzeichen)
    \b  Backspace

  und zur Definition von Zeichen durch Zeichencodes

    \xhh        Byte mit Hexadezimalcode hh
    \uhhhh      Unicode-Zeichen mit Code Point hhhh
    \Uhhhhhhhh  dito, mit 8 Hexadezimalziffern
    siehe
    http://docs.python.org/reference/lexical_analysis.html#string-literals

* Steht vor dem Anführungszeichen, das eine Zeichenkette einleitet,
  ein `r`, ist die folgende Zeichenkette ein so genannter Raw-String.
  In diesem Fall werden Maskierungen ignoriert. Beispiel:

    print """abc\ndef"""
    print
    print r"""abc\ndef"""

* Strings können mit Vergleichsoperatoren `==`, `!=`, `<`, `>=`
  usw. verglichen werden.

* Interpreter-Spielzeit


Mal so zwischendurch: Unicode
-----------------------------

* Die Zeiten von eindeutigem "plain text" sind lange vorbei.

* Unicode definiert etliche tausend Zeichen und ordnet jedem einen
  Zahlencode (Code Point) zu. Dies ist nur ein abstrakter Wert und hat
  zunächst nichts mit der Speicherung des Unicode-Textes zu tun.

* Zum physikalischen Speichern von Zeichen oder Übertragen über ein
  Netzwerk müssen diese in ein so genanntes Encoding gebracht werden.
  Unter Unix-Systemen am häufigsten sind UTF-8, ISO-8859-1 (Latin 1)
  und ISO-8859-15 (Latin 9).

* Also: Unicode --- encode ---> Bytes
        Bytes   --- decode ---> Unicode

* Empfehlenswerte Links:

  - http://www.p-nand-q.com/python/unicode_faq.html
  - http://docs.python.org/howto/unicode.html
  - http://www.joelonsoftware.com/articles/Unicode.html


Unicode-Strings
---------------

* Die bisher behandelten Zeichenketten (Strings) waren so genannte
  Bytestrings (Typ `str`), also Byte-Folgen.

* Python kennt aber auch Unicode-Strings (Typ `unicode`).

* Um ein Unicode-Literal zu schreiben, muss man ein `u` vor
  das Anführungszeichen schreiben:

    byte_string = "Byte-String"
    print type(byte_string), byte_string
    unicode_string = u"Unicode-String"
    print type(unicode_string), unicode_string

    print

    # Nimm an, dass der Byte-String in UTF-8 kodiert ist.
    decoded_string = byte_string.decode("UTF-8")
    print type(decoded_string), decoded_string
    # Konvertiere den Unicode-String nach UTF-8
    encoded_string = unicode_string.encode("UTF-8")
    print type(encoded_string), encoded_string

* Kommen in einem Ausdruck Byte-Strings *und* Unicode-Strings vor,
  wird im Zweifelsfall nach Unicode konvertiert.

    s = "abc"
    u = u"def"
    print s < u
    zusammen = s + u
    print type(zusammen), zusammen

* Wenn man im Quelltext Unicode-Literale verwenden will, muss
  man das Encoding in einem speziellen Kommentar innerhalb der
  ersten zwei Zeilen der Datei angeben. Beispiel:

    #! /usr/bin/env python
    # encoding: UTF-8
    ...

  Fehlt die Encoding-Zeile, wird ASCII angenommen. Dann können
  effektiv keine "Sonderzeichen" in String-Literalen verwendet werden.

* *Anmerkung* Python 3 verwendet nur noch Unicode für Zeichenketten
  und UTF-8 als Default-Encoding für Python-*Quelltexte*.


Nochmal Zeichenketten (Byte- und Unicode-Strings)
-------------------------------------------------

* Zugriff auf einzelne Zeichen:

    uni = u"Dies ist eine Zeichenkette"
    print uni[0]    # Indizes starten bei 0
    print uni[3]
    print uni[-1]   # negative Indizes zählen vom Ende
    print uni[-3]
    print uni[100]  # Fehler

* Strings sind unveränderlich ("immutable"):

    uni = u"Test"
    uni[0] = u"F"  # Fehler!

* Mit so genannten Slices kann man auf Bereiche zugreifen:

    uni = u"Dies ist ein Test."
    print uni[5:8]  # erster Index, letzter Index plus 1
    # Den Satz kann man auch so schreiben.
    print uni[0:5] + uni[5:9] + uni[9:13] + uni[13:len(uni)]

    print

    # Fehlt ein Index wird der String-Anfang bzw. das Ende angenommen.
    print uni[:3]
    print uni[13:]
    print uni[:]    # eine Kopie des Strings
    print uni[:-2]  # alles ohne die letzten zwei Zeichen

* Slices lassen noch ein drittes "Argument" als Schrittweite zu

    print u"umgedreht"[::-1]

* Spielzeit. Anregungen:

    uni = u"abcdef"
    print 1, uni[2:2]
    print 2, uni[:100]
    print 3, uni[4:2]
    print 4, uni[:len(uni)]
    print 5, uni[:-len(uni)+1]
    print 6, uni[:-len(uni)]
    print 7, uni[:-100]


Listen
------

* Listen nehmen null bis beliebig viele Objekte beliebigen Typs auf:

    L = []
    print "Leere Liste:", L
    L = [1, "abc", u"def", [9]]
    print "Liste mit Liste:", L

* Durch Indizes kann man auf einzelne Listen-Elemente zugreifen.
  Da Listen veränderbar ("mutable") sind, kann man, anders als
  bei Strings, auch Teile verändern.

    L = [1, 2, "abc"]
    print L[1]  # Indizes zählen ab 0
    L[2] = 7
    print L     # [1, 2, 7]

* Slices funktionieren wie bei Strings:

    L = range(10)  # die Liste [0, 1, 2, ..., 8, 9] (ohne 10!)
    print L[:3]    # [0, 1, 2]
    L[2:6] = range(3)  # linke und rechte Seite müssen nicht gleich lang sein
    print L        # [0, 1, 0, 1, 2, 6, 7, 8, 9]

    print

    print u"Noch mal in Zeitlupe ..."
    L = range(10)
    print u"Ursprüngliche Liste:", L
    print u"Zu ersetzendes Stück:", L[2:6]
    print u"Neues Stück:", range(3)
    L[2:6] = range(3)
    print u"Ergebnis:", L  # von oben

* Hinzufügen von Elementen zu einer Liste am Ende geht mit den
  `append`- und `extend`-Methoden:

    L = [1, 2]
    print u"Ursprüngliche Liste:", L
    L.append(3)
    print L
    L.extend([4, 5])
    print L

* Mit `insert` lassen sich auch Werte "in der Mitte" einfügen:

    L = [1, 2, 3]
    L.insert(1, 7)
    print L

* Listen sind gleich, wenn sie die gleichen Werte in der gleichen
  Reihenfolge enthalten.

    L1 = [1, 2, 3]
    L2 = [1, 2]
    print L1 == L2
    L2 = [1, 3, 2]
    print L1 == L2
    L2 = [1, 2, 3.0]
    print L1 == L2

* Spielzeit. Was passiert hier?

    L = range(8)
    print L[:]
    print L[:-2]
    print L[3:3]
    L[:] = [3, 2, 1]
    print L

    print

    L = range(8)
    L[3:2] = [-1, -2]
    print L


Tupel
-----

* Tupel verhalten sich ganz ähnlich wie Listen, sind aber
  unveränderlich.

    t = (1, 2, 3)
    print t[1]
    print t[:2]
    t[1] = 7  # Fehler

* leere Tupel und solche mit einem Element

    leeres_tupel = ()
    ein_element = (1,)  # Komma zur Unterscheidung von geklammertem Ausdruck

* Oft explizit oder implizit verwendet wird das so genannte
  "Tupel Unpacking" (das auch mit Listen auf der rechten Seite
  funktioniert).

    a, b = 1, 2
    print a, b

    a, b = b, a
    print a, b

    gruss = u"Hallo Welt!"
    erstes_wort, zweites_wort = gruss.split()
    print erstes_wort
    print zweites_wort


Dictionarys
-----------

* Dictionarys enthalten Schlüssel-Wert-Paare.

    d = {}
    d = {2: "abc", 1: 2}
    print d
    print d[1]
    print d[2]
    print d[3]

  Es gibt *keine* garantierte Reihenfolge der Paare!

* Jeder Schlüssel kann nur einmal vorkommen bzw. wird erneut dem
  gleichen Schlüssel zugewiesen, wird der alte Wert ersetzt.

    d = {2: "abc", 1: 2}
    print d[1]
    d[1] = "def"
    print d

* Zuweisungen an einen neuen Schlüssel erzeugen einen neuen Eintrag.

    d = {1: 2}
    d["a"] = 17
    print d

* Schlüssel müssen unveränderlich sein.

    d = {}
    d[(1, 2)] = 1
    print d
    d[[1, 2]] = 2

* Dictionarys sind gleich, wenn sie die gleichen Schlüssel-Wert-Paare
  enthalten.

    d1 = {1: 2, 3: 4}
    d2 = {3: 4.0, 1.0: 2}
    print d1 == d2


Sets
----

* Sets ähneln Dictionarys

    s = set([1, 2, 3])
    print s  # das ist *keine* Liste
    s.add(4)
    print s

* Sets nehmen nur Werte auf, keine Paare

* es gibt *keine* garantierte Reihenfolge

* Werte müssen immutable sein

    s = set()
    s.add((1, 2))
    print s
    s.add([1, 2])

* Sets (Mengen) dienen (auch) zur Berechnung von Mengen-Operationen.
  Beispiel: Welche Werte sind in Set 1, aber *nicht* in Set 2?

    s1 = set(range(5))     # 0, 1, 2, 3, 4
    s2 = set(range(3, 8))  # 3, 4, 5, 6, 7
    print s1 - s2
    print s1.difference(s2)

  Welche Werte sind in Set 1 *und* Set 2?

    s1 = set(range(5))
    s2 = set(range(3, 8))
    print s1 & s2
    print s1.intersection(s2)


Namen und Zuweisungen
---------------------

* *Python kopiert nicht!* (außer auf ausdrückliche Anfrage)

* Eine Zuweisung verbindet einen Namen (links) und ein Objekt (rechts).

* Hilfreich: Der Operator `is` stellt fest, ob zwei Objekte
  *identisch* und nicht nur wertmäßig gleich sind.

    x = 1.0
    y = x
    print x is y
    y = 1.0
    print x is y  # kann falsch sein (hängt von Python-Implementierung ab)

* Folgender Code ist legal (aber normalerweise *nicht* zu empfehlen):

    a = 1       # int zuweisen
    a = "abc"   # str zuweisen
    a = [1, 2]  # list zuweisen

  Hier wird jedesmal ein anderes Objekt mit dem Namen `a` verknüpft.
  Das vorher verbundene Objekt wird "entsorgt", falls es sonst nicht
  mehr benötigt wird.


Namen und Zuweisungen - Unveränderliche Objekte
-----------------------------------------------

* Zuweisungen

    x = 1.0
    y = x       # x und y zeigen aufs gleiche Objekt
    y = 1.0     # erzeuge ein neues Objekt 1.0 und lass y darauf zeigen

* nach der ersten Anweisung:

  x ---> 1.0

* nach der zweiten Anweisung:

  x ---+
       +---> 1.0
  y ---+

* nach der dritten Zuweisung

  x ---> 1.0  (das zuerst erzeugte Objekt)

  y ---> 1.0  (das in der dritten Zeile erzeugte Objekt)


Namen und Zuweisungen - Veränderliche Objekte
---------------------------------------------

* Was passiert hier?

    L1 = [1, 2]
    L2 = L1
    L1.append(3)
    print L1
    print L2  # wurde auch verändert!

* Nach der ersten Anweisung ist `L1` ein Name für die Liste `[1, 2]`,
  *die während der Zuweisung erzeugt wird* (bzw. unmittelbar vor der
  eigentlichen Zuweisung).

  L1 ---> [1, 2]

* Nach der zweiten Zuweisung ist die Liste unter zwei Namen bekannt.

  L1 ---+
        +---> [1, 2]
  L2 ---+

* Der Aufruf der `append`-Methode verändert diese eine Liste.

* Diese "Anomalie" ist bei unveränderlichen Objekten nicht möglich.

* Spielzeit: Was passiert hier?

    L1 = [1, 2]
    L2 = L1
    t1 = (L1,)
    t2 = t1
    L1.append(3)
    print L1, L2
    print L1 is L2
    print t1, t2
    print t1 is t2


String-Methoden
---------------

* Schon seit etwa acht Jahren haben Strings Methoden (gleich mehr
  dazu). Die Verwendung des `string`-Moduls, um die gleichen Aufgaben
  auszuführen, ist *veraltet*.

    print "abc def ghi".split()

    # VERALTET!
    import string
    print string.split("abc def ghi")

  Das Modul selbst kann noch nützlich sein. Zum Beispiel enthält es
  ein paar nützliche Konstanten:

    import string

    print string.digits
    print string.ascii_letters   # `string.letters` kann je nach Locale
                                 # noch andere Zeichen enthalten

* einige nützliche String-Methoden

    s = u"Dies ist ein String"
    print s.count(u"i")
    print s.encode("UTF-8")  # vgl. decode
    print s.startswith(u"Dies")
    print s.endswith("ing")
    print s.lower()
    print s.upper()
    teile = s.split()
    print teile
    print u"*".join(teile)
    print u"abc\ndef\nghi".splitlines()

* noch mehr Nützliches

    print u"abc" + u"def"
    print 10 * "=-"

    s = u"Dies ist ein String"
    print len(s)
    print u"ist" in s


String-Formatierung
-------------------

* Zeichenketten lassen sich sehr vielseitig miteinander und mit
  anderen Objekten kombinieren.

* Syntax: `string_objekt % objekt`

* Beispiele:

    print u"eins: %d" % 1

    print u"%s %s" % (u"eins", u"zwei")
    print u"%d plus %d ist %d" % (2, 3, 2+3)
    print u"rechtsbündig mit zwei Nachkommastellen: %10.2f" % (1./3)

    # Achtung! Anomalie bei Tupeln mit einem Element
    print u"ein Integer: %s" % 3
    print u"ein String: %s" % u"string"
    print u"eine Liste: %s" % [1]
    print u"ein Dictionary: %s" % {1: 2}
    print u"ein Tupel: %s" % (u"1",)

* Variante: benannte Platzhalter und Dictionary

    name = {"vorname": u"Stefan", "nachname": u"Schwarzer"}
    print u"%(vorname)s %(nachname)s" % name
    print u"%(nachname)s, %(vorname)s" % name

  näheres unter
  http://docs.python.org/library/stdtypes.html#string-formatting-operations


Listen-Methoden
---------------

* schon gesehen: `append(wert)` und `extend(iterable)`

* `iterable`: alles, über was iteriert werden kann; wird später
  genauer erklärt. Bei `extend` ist `iterable` in der Praxis
  meist eine Liste.

* `sort` sortiert eine Liste "in-place", das heißt, die Liste
  selbst wird verändert. Zurückgegeben wird der spezielle Wert
  `None`:

    L = [3, -1, 9, 7]
    print L.sort()  # das ist wirklich `None`, aber `print`
                    # macht daraus implizit den String "None"
    print L


  `sort` akzeptiert ein optionales Argument `key`. Dabei
  handelt es sich um eine Funktion, die auf alle Listenelemente
  angewendet wird. Die Listenelemente selbst werden dabei nicht
  verändert (aber natürlich ihre Reihenfolge).

    L = ["def", "ABC", "Def", "abc"]
    L.sort(key=str.lower)
    print L

* `reverse` vertauscht die Reihenfolge der Liste "in-place"
  und gibt `None` zurück.

* `index(wert)` gibt den Index des ersten Elements zurück, dessen
  Wert gleich `wert` ist.

    L = [1, 7, 3, 2]
    print L.index(3)
    print L.index(-1)

* ebenfalls nützlich:

    L = [1, 2, 3]
    del L[1]
    print L
    L = [1, 2, 3]
    del L[1:]
    print L

    print

    L = [1, 7, 3, 2]
    print len(L)

    L1 = [1, 2]
    L2 = [2, 3]
    print L1 + L2

    L = [0]
    print 10 * L

  *Achtung* Bei der "Multiplikation" wird wie üblich *nicht* kopiert!
  Das heißt, das/die betreffende/n Objekte werden mehrfach
  referenziert:

    innere_liste = [1, 2]
    aeussere_liste = 5 * [innere_liste]
    print aeussere_liste
    innere_liste.append(3)
    print aeussere_liste

* Spielzeit


Tupel-Methoden
--------------

Tupel haben einige Methoden, die auch Listen haben, aber keine,
die das Tupel verändern. (Tupel sind unveränderlich!)


Dictionary-Methoden
-------------------

* `keys` liefert eine Liste aller Dictionary-Schlüssel, `values`
  liefert die Werte, und `items` eine Liste von
  `(schluessel, wert)`-Paaren. Die Reihenfolgen aller drei
  Methoden ist konsistent, *solange das Dictionary nicht
  zwischendurch verändert wird*.

    d = {"str": "abc", "int": 1, "liste": [1, 2, 3]}
    print d
    print d.keys()
    print d.values()
    print d.items()

    print

    d["tupel"] = (2, 3, 4)
    print d
    print d.keys()
    print d.values()
    print d.items()

* `get(schluessel, default)` gibt den Wert zu `schluessel` zurück
  oder `default`, wenn der Schluessel nicht im Dictionary ist. Wird
  `default` nicht angegeben, ist der Rückgabewert bei nicht
  vorhandenem Schlüssel `None`.

    d = {1: 2}
    print d.get(1)
    print d.get(2)  # nicht im Dictionary
    print d.get(2, 3)
    print d

* `setdefault(schluessel, default)` funktioniert wie `get`, jedoch
  wird außerdem bei *nicht vorhandenem* Schlüssel ein Schlüssel
  `schluessel` mit Wert `default` ins Dictionary eingefügt.

    d = {1: 2}
    print d.setdefault(1)
    print d.setdefault(2)  # nicht im Dictionary
    print d.setdefault(2, 3)
    print d

* `update(dict2)` aktualisiert das Dictionary mit den Schlüssel-
  Wert-Paaren aus dict2. Existieren Schlüssel in beiden Dictionarys,
  "gewinnt" `dict2`.

    d1 = {1: 2, 3: 4}
    d2 = {3: 17, 5: 6}
    d1.update(d2)
    print d1

  Das Dictionary `d1` wird dabei in-place verändert und `None`
  zurückgegeben.

* ebenfalls nützlich:

    d = {1: 2, 3: 4}
    print (3 in d)
    print (5 in d)

    print len(d)

    del d[1]
    print d


Übung
-----

Welchen Wert hat das Dictionary `d` am Ende des folgenden Codes?

    L = range(5)
    L[:-1] = range(10, 12)
    t = (1, 2, 3)
    d = {1: L, 2: t}
    L.append(u"Python ist toll".split()[0])
    L = 2 * L
    t.append(2)


Anweisungsblöcke
----------------

* Anweisungsblöcke werden *nicht* in `{ ... }` oder `begin ... end`
  etc. eingeschlossen.

* Anweisungsblöcke entstehen durch gleichartige Einrückung; Beispiel:

    for wert in liste:
        if was_weiss_ich:
            a = wert + 1
            b = wert + 2
        else:
            a = wert + 3
            b = wert + 4
        eine_funktion(a, b)

* nach Python Style Guide (PEP 8) *vier* *Leerzeichen* pro
  Einrückungsebene

* Der allergrößte Teil des existierenden öffentlichen Python-Codes
  hält sich an die Konvention.

* Viele Programmiereditoren stellen sich beim Bearbeiten von
  Python-Code automatisch passend ein. Zum Beispiel werden dann
  durch Drücken der Tabulatortaste entsprechend viele Leerzeichen
  eingefügt.

* sonst den Editor bitte entsprechend konfigurieren :-)


Bedingte Anweisungen
--------------------

* Die Struktur der `if`-Anweisung ist

    if bedingung1:
        block1
    elif bedingung2:
        block2
    elif bedingung3:
        block3
    else:
        block4

* Die `elif`- und `else`-Zweige sind optional (`elif` ohne
  `else` ist auch möglich).

* Die Bedingungs-Ausdrücke müssen *nicht* eingeklammert werden.

* Nach den Bedingungen und dem `else` steht jeweils ein
  Doppelpunkt.

* Die Bedingungs-Ausdrücke müssen *nicht unbedingt* einen
  boolschen Wert liefern.


Wahrheitswerte
--------------

* Nicht nur `True` und `False` lassen sich als Ergebnisse eines
  Bedingungs-Ausdrucks verwenden.

* Folgende Ausdrücke gelten in Python als falsch:

  - `False`
  - `None`
  - alle Werte, die numerisch Null sind: 0  0.0  0+0j
  - leere Strings: ""  u""
  - leere Container: []  ()  {}  set()  frozenset()
  - der Vollständigkeit halber:
    - Objekte benutzerdefinierter Klassen, die `__nonzero__`
      definieren und dafür `False` zurückgegeben
    - Objekte benutzerdefinierter Klassen, die `__len__`, aber
      nicht `__nonzero__` definieren und `__len__` 0 liefert

* Alle anderen Werte sind wahr.

* leicht zu testen mit `bool(wert)`

    print bool([])
    print bool("")
    print bool(set())
    print bool(0)
    print
    print bool([1])
    print bool(1e-50)


Fortsetzungszeilen
------------------

* "normalerweise" endet eine Python-Anweisung am Zeilenende

* Ausnahmen:

  - am Ende der Zeile steht ein Backslash `\` (funktioniert *nicht*,
    wenn der Backslash in einem Kommentar steht)
  - wenn das Ende einer Zeile erreicht wird, sind noch Klammern offen

* Beispiele:

    a = 1  # eine Zeile, eine Anweisung

    b = 2 + \
        3

    c = 2 +  # das funktioniert *nicht* \
        3

    d = (2 +
         3)

    e = [1,
         2,
         3,  # (Komma nach dem letzten Element ist erlaubt)
        ]


Schleifen
---------

* `while`-Schleife:

    while bedingung:
        block1
    else:
        block2

  Achtung: Doppelpunkte nach der Bedingung und `else`

  Der `else`-Zweig ist optional und wird selten verwendet.

* überspringen einer Iteration mit `continue` (danach erneute
  Evaluierung der Bedingung)

* abbrechen der Schleife mit `break`

* `continue` und `break` beziehen sich bei geschachtelten Schleifen
  (einschließlich `for`-Schleife) auf die innerste Schleife, in der
  sie stehen.

* `for`-Schleife:

    for wert in iterable:
        block1
    else:
        block2

  Achtung: Doppelpunkte nach der Bedingung und `else`

  Auch hier ist `else` optional und unüblich.

* `wert` kann auch ein Tupel sein -> Tupel-Unpacking

    for erstes, zweites in [(1, "a"), (2, "b")]:
        print erstes,
        print zweites
        print

* Bei Verwendung von `continue` wird die Schleife mit dem
  nächsten Wert aus dem Iterable fortgesetzt.

* Durch die Verwendung der `for`-Schleifen-Syntax ist es selten
  nötig, mit Indizes zu arbeiten. Falls man doch Indizes braucht,
  zum Beispiel fürs Schreiben in Listen, gibt es die Funktion
  `enumerate`:

    L = range(5)
    print L
    for index, wert in enumerate(L):
        L[index] = wert + 1
    print L

* Es gibt auch noch Schleifen innerhalb eines Ausdrucks, so
  genannte List Comprehensions. Als Beispiel berechnet der
  folgende Code die Quadrate aller geraden Zahlen von 0 bis 10:

    quadrate = [i**2 for i in xrange(10+1) if i % 2 == 0]
    print quadrate

  Prinzipiell kann man mehrere `for`- und `if`-Fragmente nutzen,
  ich empfehle jedoch als Faustregel, maximal
  
  - ein `for` und ein `if` oder
  - zwei `for`
    
  zu verwenden und sonst lieber explizite `for`- und `if`-Anweisungen.


Übung
-----

Schreiben Sie ein Skript zur Berechnung der Fakultät einer Zahl. Der
Ausgangswert soll auf der Kommandozeile übergeben werden.

Die Fakultät `n!` ist definiert als

    n! = 1 * 2 * 3 * n
    0! = 1

Für negative Zahlen ist die Fakultät nicht definiert. Für diesen
Fall bitte eine Fehlerbehandlung ausdenken. :-)

Tipp: Mit

    import sys

    n = int(sys.argv[1])

bekommt man das erste Kommandozeilen-Argument als Integer `n` ins
Skript.

Hier eine mögliche Lösung (mit `n` als Konstante, da ich bei meinem
Editor-Trick keine Kommandozeilen-Argumente übergeben kann):

    n = 7

    if n < 0:
        print u"Argument %d ungültig!" % n
    else:
        fakultaet = 1
        # `range(1, 1)` ist eine leere Liste - daher
        # keine Sonderbehandlung für n == 0 nötig
        for i in range(1, n+1):
            fakultaet = fakultaet * i
        print fakultaet

Mit kleinen Optimierungen:

    n = 7

    if n < 0:
        print u"Argument %d ungültig!" % n
    else:
        fakultaet = 1
        # `xrange` erzeugt keine Liste, sondern ein Iterable, das
        # wenig Speicher benötigt
        for i in xrange(1, n+1):
            # so genanntes "augmented assignment"
            fakultaet *= i
        print fakultaet


Funktionen
----------

* allgemeine Syntax

    def name(argumentliste):
        block

* die wohl einfachste Funktion:

    def leer():
        # `pass` tut nichts (Behelfs-Block).
        pass

    print leer()

* mit einem Argument:

    def addiere_eins(n):
        return n + 1

    print addiere_eins(2)

* mit zwei Argumenten, davon ein Default-Argument:

    def addiere(n, summand=1):
        return n + summand

    print addiere(2)
    print addiere(2, 4)

* Argumentliste kann beliebig viele Argumente enthalten

* Default-Argumente müssen am Ende stehen. Folgendes ist *ungültig*:

    def addiere(summand=1, n):
        return n + summand

    print addiere(4, 2)

* Default-Argumente werden nur *einmal* - während der Definition -
  ausgewertet!

    def zwei_dazu(liste=[]):
        liste.append(2)
        return liste

    print zwei_dazu([1, 2, 3])
    print zwei_dazu()
    print zwei_dazu()
    print zwei_dazu()

* Mit der `return`-Anweisung kann man einen Wert zurückgeben. Das
  kann auch ein Tupel sein (um effektiv mehrere Werte zurückzugeben).

* Eine Funktion darf mehrere `return`-Anweisungen enthalten. Diese
  können auch Werte verschiedener Typen zurückgeben. Ob man das auch
  tun will, *sollte man sich gut überlegen*.

* Fehlt die `return`-Anweisung oder wird sie von keinem Wert gefolgt,
  ist der Rückgabewert `None`. Will man `None` zurückgeben, sollte
  man das besser explizit mit `return None` tun.

* Wenn man im Aufruf Argument-Namen verwendet, kann man auch eine
  andere Reihenfolge wählen, zum Beispiel

    def komisches_append(liste, wert):
        liste.append(wert)
        return None

    L = [1, 2, 3]
    komisches_append(L, 4)
    print L

    L = [1, 2, 3]
    komisches_append(wert=4, liste=L)
    print L

* Gibt man direkt nach dem Funktionskopf einen String an, wird dieser
  zum so genannten Docstring der Funktion.

    def stupid_function():
        """Return `None`.

        This function isn't really useful. It's only supposed to
        show how a docstring is defined and used.

        Docstrings (and program code in general) are usually
        written in English.
        """
        return None

    print stupid_function.__doc__

* Mit der Syntax `*args` und `**kwargs` kann man beliebige Positions-
  und Schlüsselwort-Argumente übergeben.

    def zeige_argumente(*args, **kwargs):
        """Demonstriere Positions- und Schlüsselwort-Argumente."""
        print u"Positions-Argumente:", args
        print u"Schlüsselwort-Argumente:", kwargs

    zeige_argumente(1, 2, "abc", a=7, x=(3, 4), b=[1, 2])


Übung
-----

Schreiben Sie eine Funktion zur Fakultätsberechnung. Die Funktion soll
als optionales Argument einen Fehlertext akzeptieren, der im Fall von
n < 0 verwendet wird.

    def fakultaet(n, fehlertext=u"Argument %d ungültig!"):
        """Gib die Fakultät des Arguments n zurück.

        Optional kann man als zweites Argument einen Fehlertext
        angeben, der als Platzhalter für das ungültige Argument
        %d, %r oder %s enthalten muss.
        """
        if n < 0:
            print fehlertext % n
            # Später kommt auch eine "richtige" Fehlerbehandlung.
            return None
        fakultaet = 1
        for i in xrange(1, n+1):
            fakultaet *= i
        return fakultaet

    print fakultaet(0)
    print fakultaet(1)
    print fakultaet(7)
    print fakultaet(-1)
    print fakultaet(-1, fehlertext=u"Blöder Wert %s!")


Ausnahmen
---------

* um Programme zu schreiben, fehlt noch etwas: Ausnahmebehandlung

* Ohne Ausnahmebehandlung würde dieser Code zum Programmabbruch führen:

    fobj = open("/nicht_da")

* Syntax für Ausnahmebehandlung:

    try:
        block1
    except ausnahme_klasse1, ausnahme_objekt1:
        block2
    except ausnahme_klasse2, ausnahme_objekt2:
        block3
    else:
        block4

* Der Teil `, ausnahme_objekt` kann auch fehlen, wenn er nicht
  gebraucht wird (später mehr zu Ausnahme-Klassen und -Objekten).

* Es muss mindestens ein `except`-Block vorhanden sein, beliebig
  viele können folgen.

* Der `else`-Zweig ist optional. Er wird ausgeführt, falls *keine*
  der Fehlerbedingungen zutrifft.

* Beispiel:

    try:
        fobj = open("/nicht_da")
    except IOError, exc:
        # `exc` ist kein String, wird aber implizit umgewandelt
        print u"Datei nicht gefunden: %s" % exc
    else:
        data = fobj.read()
        fobj.close()

* Unbedingte Ausführung: Mit `try ... finally` wird ein Code-Block
  auf jeden Fall ausgeführt, egal ob eine Ausnahme auftritt oder
  nicht.

    try:
        block1
    finally:
        block2

* Beispiel:

    try:
        fobj = open("python_workshop.txt")
    except IOError, exc:
        print u"Datei nicht gefunden: %s" % exc
    else:
        # Nur dieser Teil ist neu!
        try:
            data = fobj.read()
        finally:
            # Wird auch ausgeführt, falls in `fobj.read` ein Fehler auftrat.
            print "Closing ..."
            fobj.close()

* Mit `raise` kann eine Ausnahme erneut ausgelöst werden:

    try:
        fobj = open("/nicht_da")
    except IOError, exc:
        # `exc` ist kein String, wird aber implizit umgewandelt
        print u"Datei nicht gefunden: %s" % exc
        raise
    else:
        data = fobj.read()
        fobj.close()

* Man kann auch selbst Ausnahmen auslösen:

    raise ValueError("ungültiger Wert!")

* Anmerkung: Oft benötigt man solch ein Konstrukt:

    try:
        ...
        try:
            ...
        except ...:
            ...
        except ...:
            ...
    finally:
        ...

  *Seit Python 2.5* kann man stattdessen schreiben:

    try:
        ...
    except ...:
        ...
    except ...:
        ...
    finally:
        ...

* Es gibt auch noch die Syntax

    try:
        ...
    except:
        ...

  Hiermit werden (fast) alle Ausnahmen abgefangen. Allerdings auch
  Ausnahmen, die man wahrscheinlich nicht fangen möchte. ;-)

    try:
        fobj = opne(eine_datei)
    except:
        print u"Datei nicht gefunden"

  *Hier wird ein `NameError` verschleiert!*


Übung
-----

Schreiben Sie die Fakultätsfunktion so um, dass sie eine
Ausnahmebehandlung verwendet. Im Fall eines ungültigen Arguments soll
ein `ValueError` ausgelöst werden. (Ich gehe später auf einen besseren
Ansatz ein.)

    def fakultaet(n, fehlertext=u"Argument %s ungültig!"):
        """Gib die Fakultät des Arguments n zurück.

        Optional kann man als zweites Argument einen Fehlertext
        angeben, der als Platzhalter für das ungültige Argument
        %r oder %s enthalten muss.
        """
        if n < 0:
            kompletter_fehlertext = fehlertext % n
            # Hack, weil `raise` nicht richtig mit Unicode umgehen kann :-/
            if isinstance(kompletter_fehlertext, unicode):
                kompletter_fehlertext = kompletter_fehlertext.encode("UTF-8")
            raise ValueError(kompletter_fehlertext)
        fakultaet = 1
        for i in xrange(1, n+1):
            fakultaet *= i
        return fakultaet

    print fakultaet(0)
    print fakultaet(1)
    print fakultaet(7)
    try:
        print fakultaet(-1)
    except ValueError:
        print u"Und weiter geht's ..."
    print fakultaet(-1, fehlertext=u"Blöder Wert %s!")


Eigene Ausnahmeklassen
----------------------

* Normalerweise definiert man eigene Ausnahmeklassen, damit ein
  Nutzer des Codes diese speziell handhaben kann.

* Hier erstmal als Rezept ... Eine eigene Ausnahme lässt sich mit
  folgendem Konstrukt bauen:

    # normalerweise `Error` am Ende des Namens
    class CLTLocationError(Exception):
        pass

* Sie kann dann so verwendet werden:

    def clt(uni):
        ...
        # Hoffentlich nicht!
        if uni_abgebrannt(uni):
            raise CLTLocationError("Tagungsort abgebrannt")

  Der Aufrufer könnte folgendermaßen reagieren:

    try:
        clt("TU Chemnitz")
    except CLTLocationError:
        clt("Sporthalle")


Module
------

* Module sind Dateien, die Python-Code aufnehmen.

* Die Python-Distribution enthält über 400 Module, weitere sind
  über den Python Package Index (PyPI, siehe oben) erhältlich.

* Eine Datei, die mit `.py` endet, kann sowohl Modul als auch
  "direkt ausführbares Hauptprogramm" sein.

* Module werden mit der `import`-Anweisung geladen. Auf die Namen
  innerhalb des geladenen Moduls kann dann mit der Notation
  `modul.name` zugegriffen werden.

    import example

    # Auch Module können einen Docstring haben.
    print example.__doc__
    print example.EXAMPLE_FILENAME

* Natürlich lässt sich nicht nur auf Konstanten zugreifen, sondern
  auf alle Namen, die im importierten Modul existieren.

* Es gibt auch noch Varianten des Imports:

    from example import EXAMPLE_FILENAME

    print EXAMPLE_FILENAME

  Das spart zwar etwas Schreibarbeit, aber ist nicht zu empfehlen,
  da mit der ersten Notation viel klarer wird, woher welche Namen
  kommen.

* Auch die Kurzform `from example import *` ist normalerweise nur
  für die Verwendung im interaktiven Interpreter gedacht.

* Ein Modul kann dem besonderen Namen `__all__` eine Liste der
  Namen zuweisen, die mit der `*`-Form exportiert werden sollen.

    __all__ = ['schlaue_funktion', 'PRAKTISCHE_KONSTANTE']

* Auch, wenn die Form `from modul import *` nicht empfehlenswert ist,
  ist die Angabe von `__all__` dennoch nützlich, um anzudeuten, welche
  Namen Bestandteil der Modul-Schnittstelle sind und welche nur intern
  verwendet werden (sollen).

* Module können nur importiert werden, wenn sie

  - im selben Verzeichnis liegen wie das Modul, das importiert
  - im Modul-Suchpfad enthalten sind (`sys.path`, kann auch über
    die Umgebungsvariable `PYTHONPATH` ergänzt werden)

    $ export PYTHONPATH=/home/ich/python/meine_module

* *Achtung* *Nie* das gleiche Modul aus zwei oder mehr Verzeichnissen
  innerhalb des Modulpfades importieren, auch nicht implizit!

  Beispiel: Das aktuelle Verzeichnis ist `meine_module`, gleichzeitig
  ist `PYTHONPATH=/home/ich` gesetzt und `meine_module` ist ein
  Unterverzeichnis davon. Die `import`-Anweisungen

    import modul2
    from meine_module import modul2

  führen dann aus Sicht von Python zu *zwei unterschiedlichen*
  Einträgen in `sys.modules` und damit zwei Modul-Identitäten und
  damit einer Art Schizophrenie.

* Viele Python-Programme haben als letzte Zeilen etwas wie

    if __name__ == '__main__':
        tu_was()

  Die Anweisungen in der `if`-Anweisung werden nur ausgeführt, wenn
  die Datei als "Hauptprogramm" ausgeführt wird, aber nicht beim
  Import als Modul. (Hintergrund: `__name__` enthält den Namen des
  Moduls, in dem die `if`-Anweisung steht; im Fall des
  "Hauptprogramms" ist dieser Name `__main__`.)

* Anmerkung: Als Erweiterung des Modul-Gedankens können Module
  wiederum in "Packages" gesammelt werden. Normalerweise wird ein
  Package definiert, indem in einem Verzeichnis eine Datei
  `__init__.py` abgelegt wird.

    paketverzeichnis
        __init__.py
        modul1.py
        modul2.py

  Module aus dem Package können dann so importiert werden:

    import paketverzeichnis  # importiert implizit __init__.py
    import paketverzeichnis.modul1
    # üblichste Form
    from paketverzeichnis import modul2
    from paketverzeichnis import modul2 as anderername
    # eher nicht empfehlenswert (siehe oben)
    from paketverzeichnis.modul2 import name1, name2

* Die Import-Anweisung kann auch mit einer "Umbenennung" verknüpft
  werden, um Namenskollisionen zu vermeiden:

    from paket1 import modul as paket1_modul
    from paket2 import modul as paket2_modul


Übung
-----

Verlegen Sie die Fakultäts-Funktion in ein Modul. Schreiben Sie eine
weitere Python-Datei, die die Funktion aus dem Modul verwendet.

    # encoding: UTF-8
    # fakultaet.py

    __all__ = ['fakultaet']


    def fakultaet(n, fehlertext=u"Argument %s ungültig!"):
        """Gib die Fakultät des Arguments n zurück.

        Optional kann man als zweites Argument einen Fehlertext
        angeben, der als Platzhalter für das ungültige Argument
        %r oder %s enthalten muss.
        """
        if n < 0:
            kompletter_fehlertext = fehlertext % n
            if isinstance(kompletter_fehlertext, unicode):
                kompletter_fehlertext = kompletter_fehlertext.encode("UTF-8")
            raise ValueError(kompletter_fehlertext)
        fakultaet = 1
        for i in xrange(1, n+1):
            fakultaet *= i
        return fakultaet


    # encoding: UTF-8
    # fakultaet_rechner.py

    import fakultaet


    argumente = [0, 1, 2, 5, 10]
    erwartete_ergebnisse = [1, 1, 2, 120, 3628800]

    for argument, erwartet in zip(argumente, erwartete_ergebnisse):
        ergebnis = fakultaet.fakultaet(argument)
        if ergebnis != erwartet:
            print u"Fehler! %s != %s" % (ergebnis, erwartet)

    try:
        ergebnis = fakultaet.fakultaet(-1)
    except ValueError:
        pass
    else:
        print "Fehler! fakultaet hätte ValueError auslösen sollen."


Wichtige Module
---------------

* Eine Python-Installation stellt einige hundert Module zur Verfügung.
  Eine Liste der Module ist unter http://docs.python.org/modindex.html

  Die Module `sys` und `os` kommen in praktisch jedem Python-Programm
  vor. Die darauf folgenden Module sind alphabetisch sortiert.

* `sys`: System-Informationen, zum Beispiel `sys.path`, `sys.modules`,
  `sys.argv`

* `os`: verschiedene Datei- und Prozess-Operationen, zum Beispiel
  liefert `os.listdir` eine Liste der Verzeichnisse und Dateien im
  aktuellen Verzeichnis

  `os.path`: Dateioperationen, zum Beispiel
  `os.path.join("dir1", "dir2", "datei")` -> `"dir1/dir2/datei"`
  mit den passenden Trenn-Symbolen. `os.path` wird durch `import os`
  automatisch geladen.

* `cgi`: einfache Tools für CGI-Skripte. Es gibt viele Web-Frameworks
  für Python, dennoch ist `cgi` noch nützlich. Zum Beispiel maskiert
  `cgi.escape` HTML-Sonderzeichen wie <, > und &.

* `ConfigParser`: lesen von Konfigurations-Dateien im Ini-Stil

* `csv`: Unterstützung für CSV-Dateien (character-separated values)

* `email`: Parsen und Zusammenbauen von E-Mails

* `fnmatch`, `glob`: Dateien nach bestimmten Mustern finden

* `gzip`, `zip`, `zlib`, `tarfile`: Unterstützung für entsprechende
  Archivdateien

* `logging`: Logging-Framework (weitreichend konfigurierbar!)

* `math`, `cmath`: mathematische Operationen

* `operator`: Funktionen, die Operatoren entsprechen sowie "erfundene"
  Operatoren; praktisch für List Comprehensions und
  Callback-Funktionen

* `pickle`, `shelve`: Persistierung von Python-Objekten

* `profile`: Profiler für Laufzeit-Analysen

* `random`: Zufallszahlen, Auswahloperationen etc.

* `re`: reguläre Ausdrücke (ein Beispiel ist in `contents.py`)

* `shutil`: Datei-Operationen (eher High-Level, zum Beispiel
  `shutil.copytree`)

* `StringIO`: Strings mit einer Schnittstelle wie bei Dateiobjekten
  versehen

* `subprocess`: Aufruf externer Programme, einschließlich Lesen aus
  und Schreiben in Pipes

* `time`, `datetime`, `calendar`: Zeit- und Datumsoperationen

* `threading`, `multiprocessing`: Unterstützung für Nebenläufigkeit
  auf Thread- bzw. Prozessebene

* `unittest`, `doctest`: Frameworks für automatisierte Tests

* `xml`: verschiedene XML-Parser


Eingebaute Funktionen
---------------------

* Außer den in Modulen definierten Funktionen und Klassen enthält
  Python einige "eingebaute" Funktion (built-ins). Hier ein paar
  wichtige.

* `complex`, `float`, `int`, `str` und `unicode`, um in Objekte des
  genannten Typs umzuwandeln:

    print complex("17")
    print float("7.7")
    print int("17"), int("0xf", 16)
    print str(17), str(str), type(str(17))
    print unicode(17), unicode(unicode), type(unicode(17))

  `str` wird auch implizit von der `print`-Anweisung und bei
  Verwendung von `%s` in Format-Strings aufgerufen.

  `int` kann tatsächlich auch implizit in `long` umwandeln.

  Neben `str` und `unicode` erzeugt auch `repr` ein `str`-Objekt.
  Im Gegensatz zu der Darstellung von `str` ist die von `repr` aber
  eher "low-level" und fürs Debugging geeignet.

    print str("Test")
    print repr("Test")

* `range` gibt eine Liste von numerischen Werten zurück. Die Funktion
  akzeptiert bis zu drei Parametern. Nähere Infos unter
  http://docs.python.org/library/functions.html#range .

* `xrange` akzeptiert die gleichen Parameter, liefert aber keine
  Liste, sondern ein `xrange`-Objekt. `xrange` wird üblicherweise für
  Schleifen verwendet, um den Speicher für den Aufbau einer Liste
  einzusparen.

* `len` gibt die Länge des Arguments zurück.

* `open(name, mode)` öffnet eine Datei. `name` ist der Dateiname,
  `mode` der Modus.

  - `r` ist der Default und steht für Lesen; `w` steht für Schreiben
    und `a` für Anhängen.
  - Ein `b` nach dem Buchstaben gibt an, dass die Datei im
    "Binärmodus" geöffnet werden soll.
  - Normalerweise werden Dateien im Textmodus geöffnet, das heißt,
    dass Zeilenendezeichen `\r\n` aus Dateien beim Lesen in `\n`
    gewandelt werden bzw. beim Schreiben umgekehrt. Dies hat zwar nur
    unter Windows einen Einfluss, aber trotzdem sollte auch unter
    Unix/Linux laufender Code der Portabilität wegen bei Binärdateien
    `b` verwenden.

  `file` ist ein Synonym für `open`, `open` ist aber der empfohlene
  Name.

* `sorted`, angewendet auf das Iterable-Argument, gibt eine sortierte
  Liste der ursprünglichen Werte zurück. Dieses wird nicht verändert.
  Wie die `sort`-Methode von Listen kann `sorted` ein optionales
  Argument `key` verarbeiten.

* `getattr`, `setattr`, `delattr` dienen dem Lesen, Setzen, Löschen
  von Attributen von Objekten. `hasattr` prüft, ob ein Objekt ein
  bestimmtes Attribut besitzt. Das Besondere an diesen Funktionen ist,
  dass sie einen Attributnamen als Zeichenkette verwenden. Man kann
  damit auf zur Laufzeit *berechnete* Attribute von Objekten zugreifen.

* `globals` gibt ein Dictionary mit den Modul-globalen Namen und
  zugeordneten Objekte zurück.

* `locals` liefert ebenfalls ein Dictionary, nämlich für die lokalen
  Namen (zum Beispiel in einer Funktion).

  Im Gegensatz zu dem von `globals` gelieferten Dictionary handelt es
  sich beim Ergebnis von `locals` um eine "flache Kopie" (shallow
  copy), so dass sich Änderungen am Rückgabewert nicht auf die
  tatsächlichen lokalen Namen auswirken.

* `zip` fügt zwei oder mehrere Listen zu einer Liste von Tupeln
  zusammen. Näheres siehe
  http://docs.python.org/library/functions.html#zip .

* `raw_input` liest eine Zeichenkette von der Standard-Eingabe
  (stdin).

* `id` gibt eine Objekt-Id zurück. Zum Test, ob zwei Objekte
  dieselben sind, sollte man trotzdem den `is`-Operator verwenden,
  da Objekte durch die Garbage Collection "zwischendurch" zerstört und
  eine bestimmte Id dann für ein anderes Objekt vergeben werden kann.

* `abs`, `max`, `min` und `sum` berechnen Absolutbetrag, Maximum,
  Minimum oder die Summe einer Liste von Werten. (Weitere mathematische
  Funktionen befinden sich in den Modulen `math` und `cmath`.)

* weitere Builtin-Funktionen unter
  http://docs.python.org/library/functions.html

* Spielzeit


Namensauflösung
---------------

* Wird im Code ein Name verwendet, ermittelt Python das zugehörige
  Objekt nach einer definierten Such-Reihenfolge.

* Die Regel lautet LEGB:

  - L - local (zum Beispiel innerhalb einer Funktion, in der der
    Name steht)
  - E - enclosing (in "umgebenden" Funktionen/Methoden; man kann
    Funktionen schachteln!)
  - G - global (im Modul-globalen Namensraum suchen)
  - B - builtin (nach Builtin-Funktionen oder ein anderen
    Builtin-Objekten suchen)

* zum besseren Verständnis:

    # Modul mein_modul.py

    G = 1                          # Modul-global

    def aeussere_funktion(liste):
        E = liste                  # enclosing
        def innere_funktion():
            L = 3                  # local
            return L + E + len(E)  # len wird in den Builtins gefunden
        return innere_funktion

* Soll von einer Funktion aus ein Modul-globaler Name angesprochen
  werden, muss die `global`-Anweisung verwendet werden:

    # Modul-Namensraum

    wert = 1

    def f():
        global wert   # Ohne `global` würde nur ein lokaler Wert
        wert = 2      # innerhalb der Funktion gesetzt werden.

    f()


Datei-Objekte
-------------

* Datei-Objekte werden mit der `open`-Funktion erzeugt (s. o.). Es
  folgen die (meines Erachtens) wichtigsten Methoden.

* `__iter__` und `next` werden normalerweise nicht direkt verwendet,
  sondern implizit, um über die Zeilen einer Textdatei zu iterieren:

    fobj = open("example.py")
    try:
        for line in fobj:
            print line,
    finally:
        fobj.close()

  Anmerkung: Der obige Code lässt sich mit der `with`-Anweisung
  (vorhanden ab Python 2.5) kürzer schreiben:

    with open("example.py") as fobj:
        for line in fobj:
            print line,

* `close` schließt eine Datei (s. o.)

* `readlines` liefert eine Liste aller Zeilen (ab der Stelle, bis zu
  der früher gelesen wurde) aus der Datei. Die Zeilen-Strings enden
  mit einem `\n`.

* `read` liest den Inhalt der noch "verbleibenden" Datei in einen
  einzigen String. Mit einem optionalen Argument kann man die Anzahl
  der maximal zu lesenden Bytes(!) bestimmen.

* `write` schreibt einen Byte-String in die Datei. Dazu muss sie zum
  Schreiben geöffnet sein. `writelines` verhält sich ähnlich wie
  eine Mischung von `readlines` und `write`: das Argument ist eine
  Liste von Zeichenketten. Symmetrisch zu `readlines` müssen die
  Zeilenendezeichen `\n` bereits vorhanden sein.


Übung
-----

Erweitern Sie das Ergebnis der vorherigen Übung, indem die Argumente
zeilenweise aus einer Datei gelesen werden. Schreibe eine Datei, die
ebenfalls zeilenweise die Ergebnisse erhält.

Wenn also die Eingangsdatei so aussieht:

0
1
5

soll die Ergebnisdatei hinterher so aussehen:

1
1
120

Eine mögliche Lösung:

    # encoding: UTF-8

    import fakultaet


    # Dateien werden durch die `with`-Anweisungen automatisch geschlossen,
    # sonst müssten sie explizit geschlossen werden.
    with open("fakultaet_eingabe.txt") as eingabe:
        with open("fakultaet_ausgabe.txt", 'w') as ausgabe:
            for line in eingabe:
                argument = int(line)
                ergebnis = fakultaet.fakultaet(argument)
                ausgabe.write("%d\n" % ergebnis)


Objektorientierte Programmierung (OOP)
--------------------------------------

* Überblick: Wer von den Anwesenden hat schon objektorientiert
  programmiert?

* Objektorientierung ist einfacher am Beispiel zu erklären als zu
  beschreiben. Hier trotzdem ein Versuch. ;-)

* *Objekte* sind ein Hilfsmittel für das Software-Design. Sie
  repräsentieren "Dinge".

* Objekte haben so genannte *Attribute*, die den aktuellen Zustand
  beschreiben und *Operationen* (auch *Methoden* genannt), die etwas
  mit dem Objekt tun können oder Daten aus den Eigenschaften des
  Objekts erzeugen können.

* In den meisten Programmiersprachen, auch in Python, werden Objekte
  aus einer "Vorlage", einer *Klasse* erzeugt.

* Beispiel:

    class Person(object):
        """`Person` beschreibt eine Person."""

        def __init__(self, vorname, nachname):
            self.vorname = vorname
            self.nachname = nachname

        def name(self):
            return "%s %s" % (self.vorname, self.nachname)

        def verheiraten(self, neuer_nachname):
            self.nachname = neuer_nachname

        def __str__(self):
            # besondere Methode, gibt einen String zurück; wird
            # implizit von `print` verwendet
            return self.name()


    willi = Person("Willi", "Wusel")
    print type(willi)
    # `willi.__class__` ist Willis Datentyp.
    print willi.__class__
    # `willi.__class__.__name__` ist ein String.
    print willi.__class__.__name__
    print
    # `print` druckt den von `name` zurückgegebenen String.
    print willi.name()
    # `print` ruft implizit `__str__` auf, um das Objekt zu drucken.
    print willi
    willi.verheiraten("Schmidt")
    print willi

Ein paar wichtige Punkte ...

* Im Gegensatz zur allgemeinen Terminologie in der objektorientierten
  Programmierung sind in Python auch Methoden Attribute, nur eben
  aufrufbare.

* Alle Attribute sind zugänglich und veränderbar. Es gibt im Gegensatz
  zu manchen anderen OOP-Sprachen keine "protected" oder "private"
  Attribute. (Es gibt nur *Konventionen* wie `_meine_sache`, s. o.)

    class Person(object):
        """`Person` beschreibt eine Person."""

        # Konstruktur
        def __init__(self, vorname, nachname):
            self.vorname = vorname
            self.nachname = nachname

        def name(self):
            return "%s %s" % (self.vorname, self.nachname)

        def verheiraten(self, neuer_nachname):
            self.nachname = neuer_nachname

        # Besondere Methode, gibt einen String zurück; wird
        # implizit von `print` verwendet.
        def __str__(self):
            return self.name()


    willi = Person("Willi", "Wusel")
    willi.nachname = "Schulze"
    print willi

    def anonymer_name():      # Methode des *Objekts*, kein `self`;
        return "<anonym>"     # "bound method"
    willi.name = anonymer_name
    print willi

    # Man kann auch die Klasse statt des Objekts verändern.
    def anonymer_name(self):  # Methode der *Klasse*, mit `self`;
        return "<anonym>"     # "unbound method"
    Person.name = anonymer_name

    stefan = Person("Stefan", "Schwarzer")
    print stefan

  *Achtung* Dass etwas *möglich ist* heißt nicht, dass man es machen
  muss. :-) Der Austausch von Methoden von Laufzeit ist auch eher die
  Ausnahme in Python-Programmen, normal ist aber das Hinzufügen von
  nicht-aufrufbaren Attributen (Zahlen, Strings, Listen etc.).

* Jede in einer Klasse definierte Methode hat `self` als erstes
  Argument (ausgenommen bei Klassen-Methoden und statischen Methoden,
  die ich hier nicht behandle). `self` ist im Moment des Aufrufs das
  jeweilige Objekt.

* Wie in anderen objektorientierten Sprachen ist Vererbung möglich,
  das heißt einer Ausgangsklasse (*Basisklasse*) werden Attribute
  hinzugefügt. Die neue Klasse nennt man *abgeleitete Klasse*. Ein
  Beispiel:

    class Person(object):
        """`Person` beschreibt eine Person."""

        # Konstruktur
        def __init__(self, vorname, nachname):
            self.vorname = vorname
            self.nachname = nachname

        def name(self):
            return "%s %s" % (self.vorname, self.nachname)

        def verheiraten(self, neuer_nachname):
            self.nachname = neuer_nachname

        # Besondere Methode, gibt einen String zurück; wird
        # implizit von `print` verwendet.
        def __str__(self):
            return self.name()


    class Vortragender(Person):

        def __init__(self, vorname, nachname):
            # Konstruktor der Basisklasse aufrufen.
            super(Vortragender, self).__init__(vorname, nachname)
            self.thema = "<unbekannt>"

        def vortrag_halten(self, thema):
            self.thema = thema
            print "%s hält einen Vortrag über %s." % (self.name(), thema)

        def __str__(self):
            name = super(Vortragender, self).__str__()
            return '%s mit Thema "%s"' % (name, self.thema)


    stefan = Vortragender("Stefan", "Schwarzer")
    stefan.vortrag_halten("Python")
    print stefan

* Vererbung sollte nur benutzt werden, wenn die Objekte der
  abgeleiteten Klasse auch für Objekte der Basisklasse stehen könnten.

  Man sollte also *nicht* von einer Klasse ableiten, nur um in der neuen
  Klasse die Attribute der Basisklasse verwenden zu können. In diesem
  Fall greift man lieber zur *Aggregation*, das heißt, man nimmt
  Objekte der einen Klasse als Attribute in eine andere Klasse auf.

  Beispiel:

    class Rad(object):

        def aufpumpen(self):
            print "Rad aufpumpen ..."


    class Pkw(object):

        def __init__(self):
            self.raeder = [Rad() for i in xrange(4)]

        def wartung(self):
            for rad in self.raeder:
                rad.aufpumpen()


    pkw = Pkw()
    pkw.wartung()


Spezielle Methoden
------------------

* Wie bereits gesehen, können Klassen spezielle Methoden besitzen, die
  das Verhalten bestimmter Anweisungen und Funktionen von Python
  beeinflussen. Beispiele sind `__init__` und `__str__`. Hier ein paar
  weitere solcher Methoden ...

* `__call__` definiert das Verhalten von `obj(...)`

* `__len__` definiert `len(obj)`

* `__getattr__`, `__setattr__` und `__delattr__` beeinflussen
  Ausdrücke und Anweisungen wie

    obj.attr            # ruft __getattr__
    obj.attr = wert     # ruft __setattr__
    del obj.attr        # ruft __delattr__

* `__getitem__`, `__setitem__`, `__delitem__` steuern das Verhalten von

    obj[index]
    obj[index] = wert
    del obj[index]

* `__contains__` definiert `wert in obj`

* `__repr__`: wird implizit von der eingebauten Methode `repr`
  aufgerufen, oder wenn in einem Format-String `%r` vorkommt

* `__unicode__` wird aufgerufen, wenn `unicode` auf das Objekt
  angewendet wird

* `__del__`: wird aufgerufen, wenn das Objekt vom Garbage Collector
  gelöscht wird

* `__lt__`, `__le__` etc.: steuern das Ergebnis von
  Vergleichsoperatoren (hier `<` und `<=`)

* `__add__`, `__sub__` und viele mehr legen das Verhalten bei
  Anwendung mathematischer und anderer Operatoren fest

* `__nonzero__` legt den Wahrheitswert eines Objekts fest und wird
  zum Beispiel implizit von `bool`, `if` und `while` verwendet

* Eine ausführliche Liste befindet sich unter
  http://docs.python.org/reference/datamodel.html#special-method-names .


Übung
-----

Leiten Sie von der Klasse `Person` ab und fügen Sie eine Methode
`gehe` hinzu. Diese Methode soll eine Objektvariable (= Instanzvariable)
`tempo` setzen. In der Ausgabe von `print person` soll auch
erscheinen, welches Tempo die Person gerade hat.

Teste die Klasse mit folgendem Code:

    willi = BeweglichePerson("Willi", "Wusel")
    print willi.tempo  # sollte 0 ausgeben
    willi.gehe(3)      # sollte das Tempo auf 3 km/h setzen
    print willi.tempo  # sollte 3 ausgeben
    print willi        # sollte das aktuelle Tempo enthalten

Eine mögliche Lösung:

    class Person(object):
        """`Person` beschreibt eine Person."""

        # Konstruktur
        def __init__(self, vorname, nachname):
            self.vorname = vorname
            self.nachname = nachname

        def name(self):
            return "%s %s" % (self.vorname, self.nachname)

        def verheiraten(self, neuer_nachname):
            self.nachname = neuer_nachname

        # Besondere Methode, gibt einen String zurück; wird
        # implizit von `print` verwendet.
        def __str__(self):
            return self.name()


    class BeweglichePerson(Person):

        def __init__(self, vorname, nachname):
            super(BeweglichePerson, self).__init__(vorname, nachname)
            self.tempo = 0.0

        def gehe(self, tempo):
            self.tempo = float(tempo)

        def __str__(self):
            name = super(BeweglichePerson, self).__str__()
            return "%s geht mit %s km/h." % (name, self.tempo)


    willi = BeweglichePerson("Willi", "Wusel")
    print willi.tempo  # sollte 0 ausgeben
    willi.gehe(3)      # sollte das Tempo auf 3 km/h setzen
    print willi.tempo  # sollte 3 ausgeben
    print willi        # sollte das aktuelle Tempo enthalten


Übung
-----

Verwenden Sie das Modul `urllib`, um eine beliebige HTML-Seite zu
holen. Benutzen Sie dann das Modul `re` oder Zeichenketten-Operationen,
um die HTTP-Links innerhalb der Seite zu ermitteln und anschließend
zeilenweise auszugeben. Selbst, wenn Sie bisher keine regulären
Ausdrücke kennen, kommen Sie evtl. mit dem Code in `contents.py`
weiter.

Für die Übung ist es nicht nötig, zu unterscheiden, ob die URLs
innerhalb von Links (`a`-Tags) vorkommen oder nicht.

Nutzen Sie Funktionen, Klassen oder Methoden, um den Code
übersichtlich zu strukturieren.

Hier zur Unterstützung ein paar Links:

* `urllib`: http://docs.python.org/library/urllib.html#module-urllib

* `re`: http://docs.python.org/library/re.html#module-re

* gültige Zeichen in einem URL: http://www.ietf.org/rfc/rfc2396.txt

  Der Einfachheit halber gehen Sie bitte davon aus, dass ein URL nach
  dem Beginn `http://` folgende Zeichen enthalten darf:

  - Groß- und Kleinbuchstaben aus dem ASCII-Zeichensatz
  - Ziffern
  - `;  /  ?  :  @  &  =  + $  , -  _  .  !  ~  *  '  (  ) %`

*Optionale* Erweiterungen, um die Übung interessanter zu machen ;-)

* Sortieren Sie die URLs vor der Ausgabe alphabetisch.

* Entfernen Sie doppelte URLs, so dass kein URL mehrfach ausgegeben
  wird.

* Übergeben Sie den auszulesenden URL per Kommandozeile.

Eine mögliche Lösung:

    import os
    import re
    import string
    import sys
    import urllib


    URL = "http://sschwarzer.com/publications"
    VALID_URL_CHARS = string.ascii_letters + string.digits + \
                      ";/?:@&=+$,-_.!~*'()%"

    def url_regex():
        """Return a regular expression for matching URLs."""
        return re.compile(r"http://[%s]+" % re.escape(VALID_URL_CHARS))

    def find_urls(text):
        """Return a list of URL contained in the text `text`."""
        regex = url_regex()
        return regex.findall(text)

    def get_text(url):
        """Return the text found at the URL `url`."""
        fobj = urllib.urlopen(url)
        try:
            text = fobj.read()
        finally:
            fobj.close()
        return text

    def print_urls(url_list):
        """Print each of the URLs in the list `url_list` to stdout."""
        # Remove duplicates.
        url_list = list(set(url_list))
        for url in sorted(url_list):
            print url

    def main(url):
        """Print links in URL `url`."""
        text = get_text(url)
        urls = find_urls(text)
        print_urls(urls)


    if __name__ == '__main__':
        # Make this runnable via `example.py`.
        main(URL)
        sys.exit()
 
        # Get URL from command line.
        try:
            url = sys.argv[1]
        except IndexError:
            # `sys.argv[0]` contains the script name.
            print "Usage: %s <url>" % os.path.basename(sys.argv[0])
            sys.exit(1)
        main(url)

In der Praxis würde man das Programm wohl nicht derart weit aufteilen,
ich möchte hier nur ein paar Anregungen für "Trennstellen" geben.


Python-"Philosophie"
--------------------

* einige Tipps bekommt man mit

  import this

* EAFP statt LBYL (EAFP = "it's easier to ask for forgiveness than
  permission", LBYL = "look before you leap")

  Beispiel:

    # LBYL
    if os.path.exists(filename):
        fobj = open(filename)
        ...

    # EAFP (Python-typisch)
    try:
        fobj = open(filename)
        ...
    except IOError:
        ...

* Duck Typing ("if it walks like a duck, swims like a duck and quacks
  like a duck, it must be a duck")

  Der Ansatz meint, dass zwei Objekte, die eine ähnliche Schnittstelle
  bieten sollen, nicht von Klassen stammen müssen, die von einer
  gemeinsamen Basisklasse abgeleitet sind. Ein Beispiel macht es
  klarer.

  Zuerst ein "Java-artiger" Ansatz:

    class A(object):
        def gehe(self, tempo):
            raise NotImplementedError("gehe ist undefiniert")

    class B(A):
        def gehe(self, tempo):
            ...

    class C(A):
        def gehe(self, tempo):
            ...

  Python-typisch (Duck Typing):

    class B(object):
        def gehe(self, tempo):
            ...

    class C(object):
        def gehe(self, tempo):
            ...

  Ein bekanntes Beispiel aus der Python-Bibliothek sind die Klassen
  `file` und `StringIO`, die zwar viel gemeinsam haben, aber nicht
  voneinander oder einer gemeinsamen Basisklasse ableiten.


Ausblick
--------

Einige Themen, die hier nicht behandelt wurden:

* Mehrfach-Vererbung und Mixins

    class A(object):
        pass

    class B(object):
        pass

    class C(A, B):
        pass

  - http://www.python.org/download/releases/2.2.3/descrintro/

* Generatoren und Generator-Ausdrücke

    def even_numbers(max_number):
        for i in xrange(0, max_number+1, 2):
            yield i

    print type(even_numbers)
    print type(even_numbers(10))

    for i in even_numbers(10):
        print i


    even_numbers = (i for i in xrange(0, 10, 2))
    print even_numbers
    print type(even_numbers)
    print list(even_numbers)

  - http://docs.python.org/glossary.html#term-generator
  - http://docs.python.org/library/itertools.html#module-itertools

* Dekoratoren

    def print_name(func):
        def new_func(*args, **kwargs):
            print func.__name__
            return func(*args, **kwargs)
        # Besser functools.wrap benutzen.
        new_func.__name__ = func.__name__
        new_func.__doc__ = func.__doc__
        return new_func

    @print_name
    def example_func(max_n):
        return range(max_n)

    print example_func(5)

  - http://docs.python.org/glossary.html#term-decorator
  - http://docs.python.org/reference/compound_stmts.html#function
  - http://docs.python.org/library/functools.html#module-functools

* Propertys

    class A(object):

        def __init__(self):
            self.__x = None

        def _get_x(self):
            print "Get x"
            return self.__x

        def _set_x(self, value):
            print "Set x"
            self.__x = value

        x = property(_get_x, _set_x)

    a = A()
    print a.x
    a.x = 7
    print a.x

  - http://www.python.org/download/releases/2.2.3/descrintro/
  - http://docs.python.org/reference/datamodel.html#implementing-descriptors

* Metaklassen (werden allerdings auch in Produktionscode kaum genutzt)

    class A(type):
        def __new__(new_class, name, bases, dict_):
            special = ['__init__', '__module__', '__metaclass__']
            for key in dict_:
                if key in special:
                    continue
                if not key.startswith("example_"):
                    raise TypeError(
                      "attribute name '%s' doesn't start with \"example_\"" %
                      key)
            return type.__new__(new_class, name, bases, dict_)

    class B(object):
        __metaclass__ = A

        def __init__(self):
            print "We're fine."

        def example_test(self):
            print "Example"

    print "alles ok soweit"

    class C(B):
        def forbidden(self):
            pass

  - http://docs.python.org/glossary.html#term-metaclass
  - http://docs.python.org/reference/datamodel.html#metaclasses

* Ferner:

  - Deskriptoren
    http://www.python.org/download/releases/2.2.3/descrintro/

  - Coroutinen
    http://docs.python.org/reference/expressions.html#yield-expressions


Anhang: Ausführen der Programm-Schnipsel aus GVim
-------------------------------------------------

* Das folgende Makro zum Ausführen von Code reicht vom allein
  stehenden `.` bis zum allein stehenden `..`, enthält also zwei
  Leerzeilen am Ende.

?^\.$
jV/^\.\.
k:w !./example.py



* Den Text-Bereich markieren und mit "ey als Makro e speichern.

* Sofern
  
  - `example.py` im aktuellen Verzeichnis liegt,
  - `gnome-terminal` installiert ist und
  - darin ein Profil "Talk" angelegt ist,
    
  kann man nun einen Code-Schnipsel, innerhalb dessen der Cursor
  steht, durch Drücken von @e ausführen.