11.2 FALLGRUBEN, REGELN & TRICKS

 

 

EINFÜHRUNG

 

Wie in jeder Programmiersprache, so gibt es auch in Python einige Fallgruben, in die selbst erfahrene Programmierer fallen können. Du kannst sie umgehen, wenn du sie als potentielle Gefahrenquelle kennst.

 

 

DAS VARIABLEN-SPEICHERMODELL VON PYTHON

 

In Kapitel 2.6 hast du eine intuitive Vorstellung von Zahlenvariablen kennengelernt, die auf dem Boxmetapher beruht. Dabei stellst du dir  vor, dass bei einer Variablendefinition wie a  = 2 ein Speicherplatz im Hauptspeicher reserviert wird, den du wie eine Box oder Schublade (für einen einzelnen Wert) auffassen kannst, in welchem der Wert 2 gespeichert ist. a ist dabei ähnlich wie in der Mathematik ein Bezeichner (Name, Platzhalter) für diesen Wert.
Diese einfache Vorstellung ist allerdings für Python nicht ganz korrekt, da in Python alle Daten, also auch Zahlen, als Objekte aufgefasst werden. Objekte haben aber nicht nur einen einzelnen Wert, sondern definieren auch Verfahren (Funktionen, Methoden). So "wissen" Zahlenobjekte beispielsweise, wie man Werte addiert. Dass diese Vorstellung richtig ist, siehst du an folgendem Test:

a = 100
a: 100
a.__add__
built-in method __add__ of int object at 0x2>

Man spricht auch davon, dass 2 die Adresse des Objekts ist, in Python id genannt:

id(a)
2

Definierst du in Python eine Variable a, so ist a ein Name, der auf auf ein int-Objekt "verweist", das sich an der Speicherstelle  (Adresse) 0x2 befindet. Statt von einem Namen spricht man auch von einem Alias (oder in anderen Programmiersprachen von einem Zeiger oder einer Referenz). Du kannst dir dies bildlich so vorstellen:

Der Unterschied zum Boxmetapher wird bei folgender Zuweisung deutlich:

b = a
b: 100

Es ist nämlich nicht so, dass jetzt eine zweite Box mit den Namen b entsteht, wo die Zahl 2 hineinkopiert wird, sondern es verweisen jetzt zwei Namen a und b auf dasselbe Objekt.

is(b)
2

Bildlich sieht die Situation also wie folgt aus:

Die Kenntnis dieses objektorientierten Speichermodells ist dann von grosser Wichtigkeit, wenn es sich nicht um Zahlen, sondern um  kompliziertere (strukturierte) Datentypen, beispielsweise um Listen handelt.  Zuerst definierst du wieder eine Liste a und machst nachher durch eine Zuweisung an b einen zweiten Namen für dieselbe Liste.

a = [1, 2, 3]
a: [1, 2, 3]
b = a
b: [1, 2, 3]

Wie du richtig vermutest, gehört dazu folgendes Bild:

Veränderst du nun mit dem Namen b die Liste

b.append(4)
b
[1, 2, 3, 4]

sieht es bildlich so aus:

Die Liste mit dem Namen a hat sich also ebenfalls verändert!

a
a: [1, 2, 3, 4]

Diese gegenseitige Abhängigkeit der beiden Variablen a und b führt zu vielen subtilen Programmierfehlern. Unkritisch ist es allerdings, wenn du b neu definierst. Dann wird nämlich erwartungsgemäss ein neues Objekt erzeugt und die beiden Objekte a und b sind nun völlig unabhängig voneinander:

a = [1, 2, 3]
a: [1, 2, 3]
b = a
b = [1, 2, 3]
b: [1, 2, 3]

oder bildlich:

Nun veränderst du die Liste b:

b.append(4)

und a ist selbtverständlich gleich geblieben:

wie du wie folgt bestätigst:

b
b: [1, 2, 3, 4]
a
a: [1, 2, 3]

Für Zahlen macht sich die gegenseitige Abhängigkeit nicht bemerkbar, da du bei der Veränderung eine neue Zahl erzeugst.

a = 100
a: 100
b = a
b: 100
b = 200
b: 200
a
100
 

Entgegen der intuitiven Vorstellung mit dem Boxmetapher wird also bei der gegenseitigen Zuweisung von Variablen keine Kopie des Wertes durchgeführt.
Du kannst dich nochmals überzeugen, wie problematisch die Zuweisung bei Listen ist, die Strings enthalten, da bei der Zuweisung das Objekt selbst nicht kopiert wird:

myGarden = ["Rose", "Lotus"]
yourGarden = myGarden
yourGarden[0] = "Hibiskus"
myGarden
["Hibiskus", "Lotus"]
yourGarden
["Hibiskus", "Lotus"]

Darum gilt

Regel 1a:
Die Kopieroperation mit dem Gleichheitszeichen ist  meist falsch. Ausnahmen sind die unveränderlichen Datentypen (siehe unten).

Willst du für Listen eine unabhängige Kopie (auch Klone genannt) erstellen, so musst du entweder die Elemente mit eigenem Code explizite in eine neue Variable kopieren oder du kannst dazu die Funktion deepcopy() aus dem Modul copy verwenden:

import copy
myGarden = ["Rose", "Lotus"]
yourGarden = copy.deepcopy(myGarden)
yourGarden[0] = "Hibiskus"
myGarden
["Rose", "Lotus"]
yourGarden
["Hibiskus", "Lotus"]

Regel 1b:
Für veränderliche Datentypen sollte man beim Kopieren die Funktion copy.deepcopy() verwenden.

Regel 1 wird oft im Zusammenhang mit der Parameterübergabe missachtet. Übergibt man einer Funktion den Wert eines nicht elementaren Datentyps, so kann die Funktion den Wert problemlos verändern:

def show(garden)
    print "garden:", garden
    garden[0] = "Hibiskus"
myGarden = ["Rose", "Lotus"]
show(myGarden)
myGarden
["Hibiskus", "Lotus"]

Nach dem Aufruf von show() haben sich die Werte des übergebenen Parameters verändert! Wie in der Medizin handelt es sich meist um eine unerwartete und unerwünschte Nebenwirkung oder um einen Seiteneffekt.

Regel 2:
Es gehört zum guten Programmierstil, dass man in einer Funktion den übergebenen Parameterwert nicht mittels Seiteneffekten verändert. Es handelt sich dabei meist um Schnellschüsse, die zu leicht zu einem unkontrollierbaren Chaos führen.

 

 

PACKING & UNPACKING

 

Auf den ersten Blick scheinen sich Tuples nicht wesentlich von Listen zu unterscheiden. In der Tat handelt es sich bei Tuples grundsätzlich um unveränderliche Listen und damit sind alle Listenoperation, die zu keiner Veränderung der Liste führen,  auch für Tuples anwendbar. Mit Tuples gibt es aber spezielle Schreibweisen unter Verwendung des Kommas.

Bei der Erzeugung von Tuples kann das runde Klammerpaar weggelassen werden:

t = 1, 2, 3
t
(1, 2, 3)

Hier wird das Komma also als Syntaxzeichen verwendet, um die Elemente voneinander zu trennen. Man spricht von automatischen Verpacken (packing). Du kannst elegant davon Gebrauch machen, um mehrere Funktionswerte als Tuple zurückzugeben:

import math
def sqrt(x):
    y = math.sqrt(x)
    return y, -y
sqrt(4)
(2,0, -2,0)

Du kannst den Kommaoperator auch bei der Variablendefinition verwenden, falls hinter dem Gleichheitszeichen ein Tuple steht. In diesem Fall spricht man vom automatischen Entpacken (unpacking):

import math
def sqrt(x):
    y = math.sqrt(x)
    return y, -y
y1, y2 = sqrt(2)
y1
1.41421356237330951
y2
-1.41421356237330951

Das Verpacken ist praktisch, um mehrere Variablen gleichzeitig zu definieren:

a, b, c = 1, 2, 3
a
1
b
2
c
3

Dabei werden auf der rechten Seite die drei Zahlen verpackt und auf der linken Seite wieder entpackt, so wie es hier explizite gemacht wird:

t = 1, 2, 3
a, b, c = t
a
1
b
2
c
3

Das Entpacken funktioniert übrigens auch mit Listen:

li = [1, 2, 3]
a, b, c = li
a
1
b
2
c
3

Mit dieser Syntax können zwei Zahlen elegant vertauscht werden, ohne dass eine Hilfsvariable nötig ist:

li = [1, 2, 3]
a, b = b, a
a
2
b
1

 

 

VERÄNDERLICHE UND UNVERÄNDERLICHE DATENTYPEN

 

Zur Verbesserung der Datensicherheit durch unerwünschte Seiteneffekte, gibt es in Python zwei veränderliche (mutable) und unveränderliche (immutable) Datentypen. Zu letzteren gehören die Zahlen, Strings (str), byte und tuple. Versuchst du beispielsweise bei einem String einen Buchstaben zu ändern, so ergibt sich eine Fehlermeldung

s = "abject"
s: "abject"
s[0] = "o"
TypeError:can't assign to immutable object

Um s zu korrigieren, musst du den ganzen String neu definieren.

s = "abject"
s: "abject"
s = "object"
s: "object"

Dabei wird eines neues String-Objekt erzeugt und der Bezug zum alten geht verloren (sein Speicherplatz wird durch einen internen Aufräumvorgang freigegeben).

 

 

ZWEIDIMENSIONALE LISTEN, MATRIZEN

 

Matrizen werden in vielen Programmiersprachen aus Arrays konstruiert. Die Zeilen der Matrix sind Arrays und die Matrix selbst ein Array aus diesen Zeilenarrays. Es ist naheliegend, in Python statt Arrays Listen zu verwenden. Dabei ist aber besondere Vorsicht geboten, denn Listen verhalten sich nicht wie elementare (unveränderliche) Datentypen, sondern wie Referenztypen. Bereits bei der Erstellung der Matrix fällst du in eine bekannte Fallgrube. Ohne Argwohn erzeugst du in der Python-Konsole eine 3x3 Matrix mit Nullen.

A = [[0] * 3] * 3
A
[[0, 0, 0],
   [0, 0, 0],
   [0, 0, 0]]

Veränderst du nun mit einer Zuweisung den letzten Wert der ersten Zeile, so bemerkst du mit Erstaunen, dass alle Zeilen verändert wurden.

A[0][2] = 1
A
[[0, 0, 1],
 [0, 0, 1],
 [0, 0, 1]]

Was ist da passiert? Etwas Nachdenken hilft dir auf die Sprünge. Die Erzeugung von A hätte auch in zwei Schritten erfolgen können:

z = [0] * 3
A = [z] * 3
A
[[0, 0, 0],
   [0, 0, 0],
   [0, 0, 0]]

Zuerst wird eine Liste z mit drei Nullen erzeugt. Dann wird eine Liste A mit dreimal derselben Zeilenreferenz gebildet. Alle verweisen also auf dieselbe Liste! Änderst du eine davon ab, so sind auch die anderen betroffen.

Regel 3:
Verwende bei verschachtelten Listen nie das Listenmultiplikationszeichen.

Um die Fallgrube zu umgehen, kannst du die List Comprehension einsetzen. Wie du nachprüfen kannst, verhält sich die Matrix nun richtig:

A = [[0 for x in range(3)] for y in range(3)]
A
[[0, 0, 0],
   [0, 0, 0],
   [0, 0, 0]]
A[0][2] = 1
A
[[0, 0, 1],
   [0, 0, 0],
   [0, 0, 0]]

 

 

FUNCTION DECORATORS

 

In Python kann man mit einer Zeile, die mit einem At-Symbol @ beginnt, eine Funktion speziell auszeichnen oder "ausschmücken". Eine solche Zeile nennt man einen Decorator. Du verwendest den Function Decorator, um einer Funktion zusätzliche Eigenschaften zu geben. Man spricht auch von einem Funktionswrapper, da die Ersatzfunktion üblicherweise im Inneren die bestehende Funktion verwendet. Im folgenden Programm gehst du von einer Funktion trisect(x) aus, die du so "dekorierst", dass sie für x = 0 den Wert 0 zurückgibt und alle Werte auf 2 Stellen rundet.

def tri(func):
    # inner function
    def _tri(x):
        if x == 0:
            return 0
        return round(func(x), 2)
    return _tri
 
@tri
def trisect(x):
    return 3 / x
 
for x in range(0, 11):  
   value = trisect(x)
   print(value)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Im Kapitel 3.7 hast du gesehen, dass du Decorators verwenden kannst, um eine Funktion so auszuzeichnen, dass sie automatisch als Callback registriert wird. Im Kapitel 7.2 wurde der Decorator @staticmethod dazu verwendet, um eine Funktion als statische Methode auszuzeichnen.

 

 

FUNKTIONALES UND MODULARES PROGRAMMIEREN

 

Du hast gelernt, dass ein Programm so strukturiert wird, dass du für wiederkehrende Tätigkeiten Funktionen definierst, die du dann mehrmals in deinem Programms aufrufen kannst. Meist benötigen die Funktionen Werte, die du über Parameter oder globale Variablen zur Verfügung stellst. Die Rückgabe von Werten erfolgt mit dem Funktionsrückgabewert oder über eine Zuweisung von globalen Variablen. Wie du in der Regel 2 bereits gelernt hast, ist es aber schlechter Programmierstil,  wenn eine Funktion äussere Variablenwerte verändert und man spricht in Anlehnung an unerwünschte Wirkungen von Medikamenten von einem Neben- oder Seiteneffekt. Um diese Gefahr zu vermeiden, gilt daher:

Regel 4:
Es gehört zum guten Programmierstil, dass eine Funktion keine globalen Variablen verwendet. Besonders gefährlich ist das Zuweisen globaler Variablen, also die Verwendung des Schlüsselworts global.

Man spricht von Funktionalem Programmieren, wenn Funktionen keine Seiteneffekte bewirken. Solche Funktionen haben aber noch einen weiteren wichtigen Vorteil: Sie lassen sich ohne Probleme in eine separate Programmdatei auslagern und können dann von irgend einem Programm verwendet werden. Eine solche Datei mit Funktionen (und Klassen) nennt man auch ein Modul. Ein etwas grösseres Informatikprojekt besteht üblicherweise aus vielen Modulen, die von verschiedenen Personen eines Teams entwickelt werden. Modulares Programmieren gehört daher zu den Grundprinzipien eines guten Programmentwurfs.

Da die Funktionen in einem Modul in verschiedenen Gebieten verwendet werden können, spricht man bei einem Modul oder einer Sammlung von Modulen von einer Programmbibliothek oder Programmlibrary. Dabei sind für den Anwender der Library die Einzelheiten des Programmcodes unwichtig. Er muss nur aus  einer präzisen Bibliotheks-Dokumentation entnehmen können, wie die Funktionen heissen, mit welchen Parametern sie aufzurufen sind und was sie zurückgeben. Da der Programmcode der Library für den Anwender "verdeckt" bleiben kann, spricht man auch von einer Black Box [mehr... Der Code ist oft nicht sichtbar, weil er nur in kompilierter Form vorliegt, beispielsweise als Maschinen- oder Bytecode]. Man könnte dieses Programmierparadigma anschaulich auch so beschreiben, dass es zwischen dem Anwender A und dem Entwickler E der Library einen Vertrag gibt. Hält sich A an die Voraussetzungen (Preconditions), so liefert die Library auch, was E verspricht (Postconditions) [mehr... Es handelt sich um das Programmierparadigma "Programming by contract"]

Hast du Funktionen gemäss den Regeln des funktionalen Programmierens (also möglichst ohne Seiteneffekte) geschrieben, so kannst du sie in ein Modul mit irgend einem geeigneten vielsagenden Namen auslagern. In einem anderen Programm musst du dieses Modul importieren, bevor du die Funktionen verwenden kannst. Zur Demonstration schreibst du ein Modul starlib.py, mit der die Turtle verschieden grosse Sterne zeichnet. Im Hauptprogramm importierst du mit from starlib import * die Funktionen des Moduls. (Wenn du nur eine brauchst, so kannst du auch from starlib import star schreiben.) [mehr... Dieser explizite import ist sogar besser, da nur genau die spezifizierten Funktionen importiert werden].

# starlib.py

from gturtle import *

def star(size): 
    startPath()
    for i in range(5): 
        forward(size) 
        left(144) 
    fillPath() 
Programmcode markieren
from gturtle import *
from random import randint
from starlib import *
        
makeTurtle()
ht()
for i in range(500):
    setPos(randint(-400, 400), 
           randint(-400, 400))
    star(randint(10, 50))        
Programmcode markieren

Es gibt zwei verschiedene Schreibweisen, um ein Modul zu importieren, entweder mit from ... oder mit import ...Im zweiten Fall musst du beim Funktionsaufruf den Modulnamen mit einem Punkt voranstellen. Das importierte Modul muss sich im gleichen Verzeichnis wie dein Programm befinden. Falls du es für Programme, die sich in verschiedenen Verzeichnissen befinden,. verwenden möchtest, so kannst du es in das Unterverzeichnis Lib kopieren, in dem sich tigerjython2.jar befindet [mehr... Wie du sehen kannst, wird bei der ersten Ausführung eine Klassendatei starlib$py.class erzeugt.
Diese kann als Bibliotheksdatei an Stelle der Sourcedatei verwendet werden
] .

 
from gturtle import *
from random import randint
import starlib
        
makeTurtle()
ht()
for i in range(500):
    setPos(randint(-400, 400),
       randint(-400, 400))
    starlib.star(randint(10, 50))
Programmcode markieren