Eigene Funktionen, Gültigkeitsbereiche, Import und Modulstruktur.
Kernideen
Parameter werden über die Position oder den Namen zugeordnet; Defaultwerte machen sie optional.
Ein Defaultwert wird einmal bei der Definition ausgewertet. Ein veränderlicher Default wird deshalb von allen Aufrufen geteilt.
*args und **kwargs sammeln beliebig viele Argumente ein und packen sie beim Aufruf wieder aus. Das Gegenstück in R ist ....
Gelesen wird von innen nach aussen, geschrieben wird immer lokal, ausser mit global oder nonlocal.
Funktionen sind Objekte: Man kann sie übergeben, speichern und aus anderen Funktionen zurückgeben.
Jede .py-Datei ist ein Modul. if __name__ == "__main__": trennt den ausführbaren Teil vom importierbaren.
Erklärung
Vorwissen:Python-Grundlagen, insbesondere der Unterschied zwischen veränderlichen und unveränderlichen Objekten und die Tatsache, dass ein Name nur ein Verweis ist. Beides erklärt die wichtigste Falle dieses Themas. Weiter geht es mit Objektorientierung in Python.
Wann eine Funktion fällig ist
Die Faustregel: Sobald dieselbe Folge von Schritten ein zweites Mal auftaucht, gehört sie in eine Funktion. Der Grund ist weniger das Tippen als die Korrektur. Steht eine Berechnung dreimal im Skript und stellt sich als falsch heraus, muss man alle drei Stellen finden. Steht sie einmal, genügt eine Änderung. Der zweite Grund ist die Benennung: Eine Funktion mit einem guten Namen macht aus fünf Zeilen eine, die sagt, was passiert.
Aufbau einer Funktion
def kennzahlen(werte, runden=2, einheit="mm"):"""Mittelwert als Text mit Einheit.""" mittel =sum(werte) /len(werte)returnf"{round(mittel, runden)}{einheit}"
Teil
Bedeutung
def
leitet die Definition ein
werte
Pflichtparameter, muss beim Aufruf angegeben werden
runden=2
Parameter mit Defaultwert, darf fehlen
Docstring
die Zeichenkette direkt unter def, erscheint in help(kennzahlen)
return
gibt den Wert zurück; fehlt es, liefert die Funktion None
In R entspricht das kennzahlen <- function(werte, runden = 2, einheit = "mm"). Dort ist der letzte ausgewertete Ausdruck der Rückgabewert, ein return() braucht es nur für einen vorzeitigen Ausstieg.
Wie Argumente zugeordnet werden
Python ordnet zuerst nach Position, dann nach Namen zu. Nach dem ersten benannten Argument darf kein unbenanntes mehr folgen. Ein Name, der in der Definition nicht vorkommt, ist ein Fehler. R ist hier grosszügiger: Es akzeptiert abgekürzte Namen, solange sie eindeutig sind (partial matching). Das spart Tipparbeit und ist eine Fehlerquelle, sobald eine Funktion einen zweiten Parameter mit demselben Anfang bekommt.
Die Falle mit veränderlichen Defaultwerten
Der Defaultwert wird beim Ausführen der def-Zeile erzeugt und an die Funktion gehängt, nicht bei jedem Aufruf neu. Solange er unveränderlich ist (Zahl, Text, Tupel, None), merkt man davon nichts. Ist er eine Liste oder ein Dictionary, teilen sich alle Aufrufe dasselbe Objekt. Der richtige Weg verwendet None als Signal und legt das Objekt im Funktionsrumpf an. R kennt die Falle nicht, weil dort Defaults bei jedem Aufruf neu ausgewertet werden.
Namensräume
Python sucht einen Namen in vier Schichten, von innen nach aussen: Lokal, Einschliessende Funktion, Global (Modul), Built-in. Das heisst LEGB-Regel; R nennt dasselbe Prinzip lexical scoping.
Lesen folgt dieser Kette. Schreiben legt ohne ausdrückliche Erklärung immer einen lokalen Namen an. Wer einen äusseren Namen verändern will, erklärt ihn mit global (Modulebene) oder nonlocal (einschliessende Funktion). In R übernimmt <<- beide Rollen.
Funktionen sind Objekte
Eine Funktion lässt sich einer Variablen zuweisen, an eine andere Funktion übergeben und aus einer zurückgeben. Darauf beruhen map, filter und das Argument key beim Sortieren. lambda ist eine Kurzschreibweise für eine namenlose Funktion mit einem einzigen Ausdruck; für alles Längere ist eine benannte Funktion lesbarer, und für die meisten map-Aufrufe ist eine Comprehension die klarere Wahl.
Vom Skript zum Modul
Sobald eine Datei wächst, lohnt die Aufteilung. Jede .py-Datei ist ein Modul und kann importiert werden. Beim Import wird die Datei einmal von oben nach unten ausgeführt; Definitionen entstehen, aber auch jeder Aufruf auf oberster Ebene läuft. Der Block
if__name__=="__main__": main()
schützt davor: __name__ ist "__main__", wenn die Datei direkt gestartet wird, und der Modulname, wenn sie importiert wird.
Für die Datenarbeit hat sich diese Aufteilung bewährt: Funktionen in ein Modul, das versioniert und getestet wird, und ein Notebook oder Quarto-Dokument, das diese Funktionen aufruft und die Erzählung liefert. So bleibt die Logik prüfbar und das Dokument lesbar.
Drei Messwerte 12.34, 15.67, 11.02 sollen als Mittelwert mit Einheit ausgegeben werden, einmal mit allen Defaults, dann mit geänderter Rundung, geänderter Einheit, vertauschter Reihenfolge und einem abgekürzten Namen.
Nach Position zugeordnet. Python schreibt eine gerundete Gleitkommazahl als 13.0, R lässt die Null weg.
einheit nach Name
13.01 cm
13.01 cm
runden bleibt beim Default, obwohl es in der Definition davor steht.
alles benannt, Reihenfolge vertauscht
13.0 cm
13 cm
Mit Namen spielt die Reihenfolge keine Rolle.
ein="cm"
TypeError
13.01 cm
Python kennt keinen Parameter ein. R ergänzt den eindeutigen Anfang zu einheit.
Interpretation und Ergebnissatz
Benannte Argumente machen einen Aufruf ohne Blick auf die Definition lesbar und unabhängig von der Reihenfolge. Bei mehr als zwei Argumenten sind sie fast immer die bessere Wahl. Das partial matching in R ist bequem, aber zerbrechlich: Bekäme die Funktion einen Parameter einzeln, wäre ein mehrdeutig und der Aufruf bräche ab.
Der Mittelwert der drei Messwerte beträgt 13.01 mm. Ein unbekannter Argumentname wie ein löst in Python einen TypeError aus, während R ihn stillschweigend zu einheit ergänzt.
Frage und Datenlage
Eine Funktion soll einen Wert an eine Liste anhängen und die Liste zurückgeben. Fehlt die Liste, soll eine neue beginnen. Was geschieht bei drei Aufrufen hintereinander?
print(sammeln_falsch.__defaults__) # dort haengt die geteilte Liste
([1, 2, 3],)
def sammeln(wert, bisher=None):if bisher isNone: bisher = [] bisher.append(wert)return bisherprint(sammeln(1))
[1]
print(sammeln(2))
[2]
Output Zeile für Zeile
Aufruf
erwartet
Python falsch
Python richtig
R
erster
[1]
[1]
[1]
1
zweiter
[2]
[1, 2]
[2]
2
dritter
[3]
[1, 2, 3]
3
__defaults__
([1, 2, 3],)
__defaults__ ist das Tupel der Defaultwerte, das an der Funktion hängt. Nach drei Aufrufen steht darin die gefüllte Liste: Der Default ist kein Rezept für eine neue Liste, sondern eine Liste, die bei jedem Aufruf weiterwächst.
Interpretation und Ergebnissatz
Der Fehler ist tückisch, weil der erste Aufruf richtig aussieht und Tests mit einem einzigen Aufruf ihn nie finden. Die Merkregel lautet: Als Defaultwert taugen nur unveränderliche Objekte; alles andere entsteht im Rumpf nach dem Muster if bisher is None: bisher = []. Dieselbe Falle gibt es bei veränderlichen Klassenattributen, siehe Objektorientierung.
Mit dem Default bisher=[] liefert der dritte Aufruf [1, 2, 3] statt [3], weil die Liste bei der Definition einmal erzeugt und dann geteilt wird. Mit None als Default und einer neuen Liste im Rumpf liefert jeder Aufruf eine eigene Liste; in R tritt das Problem nicht auf.
Frage und Datenlage
Eine Hilfsfunktion soll eine beliebige andere Funktion aufrufen und vorher protokollieren, womit. Dazu eine Summe über beliebig viele Zahlen und der umgekehrte Weg: Argumente aus einer Liste und einem Dictionary übergeben.
Python trennt in ein Tupel der unbenannten und ein Dictionary der benannten Argumente. R hält beides in einer Liste, unbenannte haben einen leeren Namen.
Ergebnis
2.0 kg
2 kg
Die Argumente kommen unverändert bei kennzahlen an.
Summe von 1, 2, 3
6
6
Summe ohne Argumente
0
0
Leeres Tupel beziehungsweise leere Argumentliste; die Summe von nichts ist 0.
summe_aller(*teile)
9
Der Stern im Aufruf packt die Liste in einzelne Argumente aus.
**optionen
2.0 g
2 g (über do.call)
Mittelwert 1.88, auf null Stellen gerundet. R braucht do.call, um eine Liste als Argumente zu übergeben.
Interpretation und Ergebnissatz
Der Stern hat zwei Rollen: In der Definition sammelt er ein, im Aufruf packt er aus. Gerechtfertigt ist *args beim Durchreichen an eine andere Funktion, deren Signatur man nicht wiederholen will, und bei beliebig vielen gleichartigen Werten. Verdächtig wird es, wenn damit verdeckt wird, dass eine Funktion zu viele verschiedene Dinge tut.
protokolliert erhält ein unbenanntes Argument im Tupelargs und die Einheit im Dictionarykwargs und reicht beide unverändert weiter; das Ergebnis ist 2.0 kg. Mit * und ** im Aufruf lassen sich umgekehrt Listen und Dictionaries als Argumente übergeben.
Frage und Datenlage
Ein globaler Zähler steht auf 0. Welche Funktionen können ihn lesen, welche verändern? Und wie baut man einen Zähler, der seinen Stand zwischen zwei Aufrufen behält, ohne einen globalen Namen zu benutzen?
def zaehler_fabrik(): stand =0def weiter():nonlocal stand stand +=1return standreturn weiterz = zaehler_fabrik()print([z(), z(), z()])
[1, 2, 3]
z2 = zaehler_fabrik()print(z2())
1
Output Zeile für Zeile
Funktion
Python
R
Erklärung
liest_nur()
1
1
Der äussere Zähler wird gelesen.
schreibt_lokal()
99
99
Die Zuweisung legt einen neuen lokalen Namen an.
zaehler danach
0
0
Der äussere Zähler ist unverändert.
zaehler += 1 ohne Erklärung
UnboundLocalError
1, äusserer bleibt 0
Python entscheidet beim Übersetzen: Wird ein Name in der Funktion zugewiesen, ist er dort lokal, auch beim Lesen davor. R liest den äusseren Wert und legt dann einen lokalen an.
mit global bzw. <<-
1
1
Jetzt wird der äussere Zähler verändert.
z() dreimal
[1, 2, 3]
1 2 3
Die innere Funktion behält stand aus ihrer Entstehungsumgebung.
neuer Zähler z2()
1
1
Jeder Aufruf der Fabrik erzeugt einen eigenen Stand.
Die innere Funktion samt der Umgebung, aus der sie ihre Namen bezieht, heisst Closure. Sie ist die saubere Alternative zum globalen Zähler: Der Zustand ist gekapselt, und zwei Zähler kommen sich nicht in die Quere.
Interpretation und Ergebnissatz
Beide Sprachen lesen von innen nach aussen und schreiben lokal. Der Unterschied liegt im Mischfall: R erlaubt, einen äusseren Wert zu lesen und unter demselben Namen lokal abzulegen, Python verbietet es mit einem UnboundLocalError. Versteckte Seiteneffekte sind die häufigste Ursache schwer auffindbarer Fehler; beide Sprachen machen sie zum Sonderfall, der ausdrücklich erklärt werden muss.
Ohne global legt eine Zuweisung in der Funktion einen lokalen Namen an, und der äussere Zähler bleibt bei 0; zaehler += 1 scheitert in Python mit einem UnboundLocalError. Eine Closure mit nonlocal zählt 1, 2, 3 und hält ihren Stand getrennt von jedem weiteren Zähler.
Frage und Datenlage
Drei Messreihen (Drossel 2, 4, 6, Ventil 1.6, 1.7, 9.3, Pumpe 5). Gesucht sind je Reihe Mittelwert und Anzahl, die Reihen sortiert nach Mittelwert, nur die Reihen mit mehr als einem Wert, und für das Ventil Minimum, Maximum und Mittelwert in einem Durchgang.
print([round(f(messungen["Ventil"]), 2) for f in (min, max, mittel)])
[1.6, 9.3, 4.2]
Output Zeile für Zeile
Aufgabe
Python
R
Ergebnis
Mittelwert je Reihe
Dictionary-Comprehension
sapply(messungen, mean)
Drossel 4.0, Ventil 4.2, Pumpe 5.0
Anzahl je Reihe
map(len, ...)
sapply(messungen, length)
3, 3, 1
nach Mittelwert sortiert
sorted(..., key=...)
sort() und names()
Drossel, Ventil, Pumpe
mehr als ein Wert
filter(lambda ...)
Filter(function ...)
Drossel, Ventil
drei Funktionen auf eine Reihe
Schleife über (min, max, mittel)
sapply über eine Liste von Funktionen
1.6, 9.3, 4.2
key= erwartet eine Funktion, die aus jedem Element den Sortierwert macht. Hier ist das Element der Name der Reihe, und die Lambda-Funktion schlägt dazu den Mittelwert nach. map und filter liefern in Python keine Liste, sondern einen Iterator; erst list() holt die Werte ab.
Interpretation und Ergebnissatz
Die letzte Zeile zeigt den Kern: min, max und mittel stehen in einem Tupel wie gewöhnliche Werte, und die Schleife ruft jede davon auf. Das Ventil hat mit 1.6 bis 9.3 die grösste Spannweite, aber mit 4.2 nur den mittleren Mittelwert, ein Hinweis auf einen Ausreisser, den ein einzelner Mittelwert verbergen würde.
Nach Mittelwert sortiert stehen die Reihen in der Folge Drossel (4.0), Ventil (4.2), Pumpe (5.0); nur Drossel und Ventil haben mehr als einen Wert. Weil Funktionen Objekte sind, lassen sich Minimum (1.6), Maximum (9.3) und Mittelwert (4.2) des Ventils in einem Durchgang berechnen.
Frage und Datenlage
Eine Datei kennzahlen_modul.py enthält eine Funktion, eine Ausgabe auf oberster Ebene und einen Testaufruf im __main__-Block. Was läuft beim Import, was beim zweiten Import, was beim direkten Start? Die Datei wird in einem temporären Ordner angelegt, damit das Beispiel ohne Vorbereitung läuft.
Modul wird ausgefuehrt, __name__ = kennzahlen_modul
print(kennzahlen_modul.mittel([2, 4, 6]))
4.0
import kennzahlen_modul # zweiter Importprint("zweiter Import ohne Ausgabe")
zweiter Import ohne Ausgabe
_ = runpy.run_path(str(pfad), run_name="__main__") # wie ein direkter Start
Modul wird ausgefuehrt, __name__ = __main__
Testaufruf: 2.0
print(__name__)
__main__
Output Zeile für Zeile
Zeile
Ausgabe
Erklärung
erster Import
Modul wird ausgefuehrt, __name__ = kennzahlen_modul
Die Datei läuft einmal ganz durch. __name__ ist der Modulname, der Testaufruf im __main__-Block bleibt aus.
Funktion aus dem Modul
4.0
Zugriff über modul.funktion.
zweiter Import
nichts
Python merkt sich geladene Module in sys.modules und führt die Datei nicht erneut aus. Wer eine geänderte Datei neu laden will, braucht importlib.reload.
direkter Start
__name__ = __main__ und Testaufruf: 2.0
Jetzt ist die Datei das Hauptprogramm, und der geschützte Block läuft.
__name__ im Dokument
__main__
Auch der Code dieser Seite läuft als Hauptprogramm, deshalb steht hier __main__.
R: sys.source
Datei wird ausgefuehrt
Auch in R läuft die Datei beim Laden ganz durch.
R: Funktion aus der Umgebung
4
sys.source führt die Datei in einer eigenen Umgebung aus, die als Modul dient.
R: exists("mittel_modul")
FALSE
Die Funktion ist nicht in den globalen Namensraum gelangt. Ein schlichtes source() hätte sie dort abgelegt.
Interpretation und Ergebnissatz
Der Import ist kein reines “Definitionen holen”: Er führt die Datei aus. Alles, was auf oberster Ebene steht, läuft mit, einmal je Sitzung. Genau deshalb gehören Testaufrufe, Dateizugriffe und Ausgaben in den __main__-Block oder in eine Funktion main(). R hat keinen solchen Mechanismus; dort trennt man Funktionsdateien und Auswertungsskripte, oder man schreibt ein Paket.
Beim ersten Import lief die Datei einmal durch und meldete __name__ = kennzahlen_modul, der Testaufruf blieb aus; der zweite Import blieb stumm. Erst beim direkten Start war __name__ gleich __main__, und der geschützte Block gab Testaufruf: 2.0 aus.
Verständnisfragen
Eine Funktion ist als def sammeln(wert, bisher=[]) definiert. Der erste Aufruf liefert [1], der zweite [1, 2]. Warum?
Der Defaultwert wurde einmal bei der Definition erzeugt und wird geteilt
Richtig. Die leere Liste hängt seither an der Funktion, sichtbar in __defaults__, und jeder Aufruf ohne zweites Argument hängt daran an.
Python merkt sich die vorherigen Aufrufe
Einen solchen Mechanismus gibt es nicht. Es liegt allein am geteilten Objekt.
Die Funktion ruft sich selbst auf
Jeder Aufruf ist unabhängig, nur das Defaultobjekt ist es nicht.
Wann ist *args gerechtfertigt und wann ein Zeichen für unklares Design?
Gerechtfertigt beim Durchreichen an eine andere Funktion
Richtig, etwa wenn man deren Signatur nicht wiederholen will, oder bei beliebig vielen gleichartigen Werten. Verdächtig wird es, wenn damit verdeckt wird, dass eine Funktion zu viele verschiedene Dinge tut.
Immer gerechtfertigt, es macht Funktionen flexibel
Flexibilität ohne erkennbare Signatur macht den Aufruf unlesbar.
Nie, benannte Parameter sind immer besser
Beim Weiterreichen wären sie eine Verdopplung, die bei jeder Änderung nachgezogen werden müsste.
In einer Funktion steht zaehler += 1, und zaehler ist nur global definiert. Python meldet UnboundLocalError. Warum?
Weil zaehler in der Funktion zugewiesen wird und damit lokal ist, auch beim Lesen davor
Richtig. Python entscheidet für die ganze Funktion, ob ein Name lokal ist. Abhilfe: global zaehler, oder besser den Wert als Argument übergeben und zurückgeben.
Weil globale Namen in Funktionen nicht lesbar sind
Lesen geht, wie liest_nur() in Beispiel 4 zeigt.
Weil += für ganze Zahlen nicht definiert ist
Ist es; das Problem ist der Gültigkeitsbereich.
Ein Skript enthält Testaufrufe am Dateiende und wird von einer anderen Datei importiert. Was passiert ohne if __name__ == "__main__"?
Die Testaufrufe laufen beim Import mit
Richtig. Beim Import wird der gesamte Dateiinhalt ausgeführt, wie die Ausgabe in Beispiel 6 zeigt. Das kostet Zeit, erzeugt unerwünschte Ausgaben und kann im schlimmsten Fall Dateien überschreiben.
Nichts, Funktionen werden beim Import nur definiert
Definitionen ja, aber Aufrufe auf oberster Ebene werden ausgeführt.
Der Import schlägt fehl
Er gelingt, nur eben mit Nebenwirkungen.
Eine Moduldatei wird geändert und im laufenden Notebook erneut mit import geladen. Die Änderung wirkt nicht. Warum?
Die Datei wurde nicht gespeichert
Möglich, aber nicht der typische Grund.
Ein zweiter import lädt aus dem Zwischenspeicher sys.modules und liest die Datei nicht neu
Richtig. Abhilfe ist importlib.reload(modul) oder ein Neustart des Kernels, der ohnehin der sicherere Weg ist.
Python erlaubt nur einen Import pro Sitzung
Weitere Importe sind erlaubt, sie tun nur nichts.
Warum ist sorted(leute, key=lambda p: p["jahr"]) einer eigenen Sortierschleife vorzuziehen?
Weil Lambda-Ausdrücke schneller sind
Der Gewinn liegt nicht am Lambda, sondern an der geprüften Sortierfunktion dahinter.
Weil die Absicht in einer Zeile steht und der Sortiercode geprüft ist
Richtig. Eine eigene Schleife müsste Stabilität, Sonderfälle und Effizienz nachbilden, ohne dass daraus etwas gewonnen wäre.