a <- c(1, 2, 3)
b <- a
b[4] <- 4
list(a = a, b = b)$a
[1] 1 2 3
$b
[1] 1 2 3 4
# Auch verschachtelt kopiert R beim Aendern
tief <- list(c(1, 2), c(3, 4))
flach <- tief
flach[[1]] <- c(flach[[1]], 99)
tief[[1]][1] 1 2
==, sondern mit einer Toleranz.Vorwissen: keines. Diese Seite ist der Einstieg in Python. Wer von R herkommt, findet in jedem Beispiel den Vergleich; wer hier anfängt, kann den R-Reiter überspringen. Weiter geht es mit Funktionen und Module in Python.
In vielen Sprachen deklariert man eine Variable mit einem Typ, und dieser Typ gilt. Python macht es anders: Ein Name ist nur ein Verweis auf ein Objekt, und derselbe Name kann nacheinander auf Objekte ganz verschiedener Art zeigen.
Das macht den Einstieg schnell und verschiebt Fehler von der Übersetzungs- in die Laufzeit. Ein Tippfehler in einem Namen fällt erst auf, wenn die Zeile ausgeführt wird. Die Konsequenz für die Praxis: früh und oft ausführen, statt lange zu schreiben und dann zu starten.
Die zweite Folge ist die wichtigere. Schreibt man b = a, entsteht kein neues Objekt, sondern ein zweiter Name für dasselbe. Ändert man das Objekt über b, sieht man die Änderung auch über a. R verhält sich hier anders (copy on modify), und genau das macht Beispiel 1 zur ersten Stolperstelle beim Sprachwechsel.
| Typ | Schreibweise | geordnet | veränderlich | Zugriff über | wofür |
|---|---|---|---|---|---|
| Liste | [1, 2, 3] |
ja | ja | Position | der Normalfall |
| Tupel | (1, 2, 3) |
ja | nein | Position | feste Wertegruppen, Rückgabe mehrerer Werte |
| Dictionary | {"a": 1} |
ja (Einfügung) | ja | Schlüssel | benannte Daten |
| Set | {1, 2, 3} |
nein | ja | nur Mitgliedschaft | Eindeutigkeit, Mengenoperationen |
Ein Dictionary-Schlüssel muss unveränderlich sein. Ein Tupel taugt deshalb als Schlüssel, eine Liste nicht. Das ist der eigentliche Grund, warum es beide Typen gibt.
| Python | R | Unterschied, der beisst |
|---|---|---|
Liste [3, 1, 4] |
atomarer Vektor c(3, 1, 4) |
der R-Vektor rechnet elementweise, die Liste nicht |
| Liste mit gemischten Typen | list(...) |
|
| Dictionary | benannte Liste list(a = 1) |
fehlender Schlüssel: Python KeyError, R NULL |
| Set | unique(), union(), intersect() |
R hat keinen eigenen Mengentyp |
werte[0] |
werte[1] |
Zählung ab null gegen ab eins |
werte[-1] |
werte[length(werte)] |
werte[-1] heisst in R “ohne das erste” |
Python bildet Blöcke mit Einzug. Das erzwingt lesbaren Code und bestraft gemischte Tabulatoren und Leerzeichen sofort mit einem IndentationError.
Eine Schleife, die nichts tut als eine Liste zu füllen, schreibt man als Comprehension: [w ** 2 for w in werte if w > 10]. Das ist kein Stilmittel, sondern die idiomatische Form. R braucht dafür keine Schleife, weil dort jede Rechnung auf dem ganzen Vektor läuft.
| Umgebung | wofür sie taugt | Grenze |
|---|---|---|
| Konsole | einzelne Ausdrücke ausprobieren | nichts bleibt erhalten |
| Jupyter | Analyse mit Text, Code und Ausgabe in einem Dokument | Ausführungsreihenfolge kann von der Lesereihenfolge abweichen |
| Spyder | Skripte mit Variablenübersicht, nahe an RStudio | weniger für grosse Projekte |
| PyCharm, VS Code | mehrere Dateien, Tests, Refactoring | schwergewichtig für eine kurze Auswertung |
Die Falle bei Notebooks ist real. Wer Zellen in beliebiger Reihenfolge ausführt, erzeugt einen Zustand, der sich beim Durchlauf von oben nicht reproduzieren lässt. Die Probe: Kernel neu starten und alles von oben ausführen. Läuft es dann nicht durch, war das Ergebnis nie reproduzierbar. Mehr dazu unter Reproduzierbarkeit.
| Aufgabe | R | Python |
|---|---|---|
| Liste filtern und umformen | x[x %% 2 == 0]^2 |
[v ** 2 for v in x if v % 2 == 0] |
| über Schlüssel und Werte laufen | for (n in names(p)) p[[n]] |
for n, w in p.items(): |
| Text mit Werten | sprintf("%s: %.2f", n, w) |
f"{n}: {w:.2f}" |
| Häufigkeiten | table(x) |
Counter(x) |
| zwei Listen paarweise | for (i in seq_along(a)) |
for u, v in zip(a, b): |
| Wert mit Ersatz holen | if (is.null(p$d)) 0 else p$d |
p.get("d", 0) |
| Typ abfragen | class(x) |
type(x) |
Eine kleine Liste wird einem zweiten Namen zugewiesen und über diesen verändert. Was passiert mit dem Original, in Python und in R? Und reicht copy(), wenn die Liste selbst Listen enthält?
{'a': [1, 2, 3, 4], 'b': [1, 2, 3, 4], 'a_is_b': True}
{'a': [1, 2, 3, 4], 'c': [1, 2, 3, 4, 5], 'a_is_c': False}
{'x': [1, 2, 3], 'y': [1, 2, 3, 4], 'x_is_y': False}
{'tief': [[1, 2, 99], [3, 4]], 'echt': [[1, 2], [3, 4]]}
| Zeile | Python | R | Erklärung |
|---|---|---|---|
a nach Änderung über b |
[1, 2, 3, 4] |
1 2 3 |
In Python sind a und b dasselbe Objekt (a_is_b ist True). R kopiert in dem Moment, in dem b verändert wird. |
a nach c = a.copy() |
[1, 2, 3, 4] |
copy() erzeugt ein neues Objekt, a_is_c ist False, das Anhängen an c lässt a in Ruhe. |
|
x nach y = y + [4] |
[1, 2, 3] |
+ baut eine neue Liste und bindet y neu. append dagegen verändert das Objekt, auf das beide zeigen. |
|
tief nach flach[0].append(99) |
[[1, 2, 99], [3, 4]] |
1 2 |
copy() kopiert nur die äussere Liste. Die inneren Listen sind weiterhin geteilt. Erst copy.deepcopy trennt vollständig, echt bleibt [[1, 2], [3, 4]]. |
Merkregel: Methoden wie append, extend, sort und Zuweisungen an liste[i] verändern das Objekt, und alle Namen sehen es. Operatoren wie + und Funktionen wie sorted() liefern ein neues Objekt. Wer unsicher ist, prüft mit a is b.
Die Falle ist nicht die Zuweisung, sondern die Kombination aus Zuweisung und veränderndem Methodenaufruf. In R kann sie nicht auftreten, weil dort jede Änderung eine Kopie erzeugt, sobald ein zweiter Name auf das Objekt zeigt. Wer Code von R nach Python überträgt, muss deshalb an genau diesen Stellen eine Kopie einbauen.
Nach
b = aundb.append(4)enthält auchadie 4, weil beide Namen auf dieselbe Liste zeigen; in R bleibtabei1 2 3.copy()trennt nur die äussere Ebene: Bei verschachtelten Listen braucht escopy.deepcopy().
Fünf Messwerte 12.1, 14.3, 11.8, 15.0, 13.6. Wie holt man den ersten, den letzten, einen Ausschnitt, und was passiert bei einem Index jenseits des Endes?
12.1
[14.3, 11.8]
13.6
[14.3, 11.8, 15.0, 13.6]
[13.6, 15.0, 11.8, 14.3, 12.1]
IndexError list index out of range
[15.0, 13.6]
| Absicht | Python | Ergebnis | R | Ergebnis |
|---|---|---|---|---|
| erstes Element | werte[0] |
12.1 | werte[1] |
12.1 |
| zweites und drittes | werte[1:3] |
[14.3, 11.8] |
werte[2:3] |
14.3 11.8 |
| letztes | werte[-1] |
13.6 | werte[length(werte)] |
13.6 |
| alle ausser dem ersten | werte[1:] |
vier Werte ab 14.3 | werte[-1] |
vier Werte ab 14.3 |
| umgekehrt | werte[::-1] |
[13.6, 15.0, 11.8, 14.3, 12.1] |
rev(werte) |
dieselben Werte |
| Index 10 | werte[10] |
IndexError |
werte[10] |
NA |
| Ausschnitt über das Ende | werte[3:10] |
[15.0, 13.6] |
Drei Unterschiede stecken in dieser Tabelle:
werte[1] ist in Python der zweite Wert.werte[1:3] liefert die Positionen 1 und 2, also zwei Werte. Der Vorteil: Die Länge des Ausschnitts ist die Differenz der Grenzen, und werte[:k] und werte[k:] ergeben zusammen die ganze Liste ohne Überlappung.werte[-1] liefert in Python einen Wert, in R vier.Python bricht bei einem ungültigen Einzelindex ab, R gibt stillschweigend NA zurück und rechnet weiter. Das Python-Verhalten ist lauter und deshalb sicherer: Ein falscher Index fällt sofort auf, statt als fehlender Wert in einem Mittelwert zu verschwinden. Slicing ist dagegen tolerant und kürzt einfach ab.
Der Ausschnitt
werte[1:3]enthält die zwei Werte 14.3 und 11.8, weil Python ab null zählt und die obere Grenze ausschliesst.werte[-1]ist in Python der letzte Wert 13.6, in R dagegen der Vektor ohne das erste Element.
In einem Prüfprotokoll stehen die Teiletypen A, B, A, C, A, B. Welche Typen kommen vor, wie oft, welcher am häufigsten? Dazu ein Lagerbestand je Typ, der um einen Eintrag ergänzt und nach einem fehlenden Typ gefragt wird.
['A', 'B', 'C']
Counter({'A': 3, 'B': 2, 'C': 1})
[('A', 3)]
{'A': 120, 'B': 45, 'C': 8}
0
KeyError 'D'
Zürich
TypeError
| Frage | Python | R | Container |
|---|---|---|---|
| welche Typen | ['A', 'B', 'C'] |
"A" "B" "C" |
Set, in R unique() |
| wie oft | Counter({'A': 3, 'B': 2, 'C': 1}) |
Tabelle mit 3, 2, 1 | Counter ist ein Dictionary mit Zählfunktionen |
| häufigster | [('A', 3)] |
"A" |
most_common(1) liefert eine Liste von Tupeln aus Wert und Anzahl |
| Lager nach Ergänzung | {'A': 120, 'B': 45, 'C': 8} |
Liste mit drei Einträgen | Dictionary, in R benannte Liste |
fehlender Typ D mit get |
0 |
get mit Ersatzwert ist der übliche Weg |
|
| fehlender Typ D direkt | KeyError 'D' |
NULL |
Python bricht ab, R liefert ein leeres Objekt |
| Tupel als Schlüssel | Zürich |
Tupel sind unveränderlich und damit als Schlüssel zulässig | |
| Liste als Schlüssel | TypeError |
Listen sind veränderlich und nicht hashbar |
Warum sorted(set(...)) und nicht nur set(...): Ein Set hat keine Reihenfolge. Die Ausgabe eines Sets aus Zeichenketten kann sich zwischen zwei Programmläufen ändern. Wer eine reproduzierbare Ausgabe will, sortiert.
Die Wahl des Containers ist eine Frage an die Daten: Zählt die Reihenfolge (Liste), soll sich nichts ändern (Tupel), wird über Namen zugegriffen (Dictionary), oder geht es nur um Vorkommen (Set)? Mit der richtigen Wahl werden die Rechnungen Einzeiler.
Im Protokoll kommen die drei Typen A, B und C vor, A dreimal und damit am häufigsten. Der fehlende Typ D führt bei direktem Zugriff zu einem
KeyError; mitlager.get("D", 0)erhält man stattdessen den Ersatzwert 0.
Fünf Werte 12, 45, 7, 31, 88 sollen in die Klassen tief, mittel und hoch eingeteilt, quadriert, gefiltert und gezählt werden. Dazu die Probe, was werte * 2 tut.
werte art
1 12 mittel
2 45 hoch
3 7 tief
4 31 mittel
5 88 hoch
[1] 144 2025 49 961 7744
[1] 45 31 88
[1] 24 90 14 62 176
[1] 3
12 -> mittel
45 -> hoch
7 -> tief
31 -> mittel
88 -> hoch
[144, 2025, 49, 961, 7744]
[45, 31, 88]
{12: 144, 7: 49}
[12, 45, 7, 31, 88, 12, 45, 7, 31, 88]
3
| Aufgabe | Python | R | Ergebnis |
|---|---|---|---|
| Klassen | for mit if/elif/else |
ifelse() verschachtelt |
12 und 31 mittel, 45 und 88 hoch, 7 tief |
| quadrieren | [w ** 2 for w in werte] |
werte^2 |
144, 2025, 49, 961, 7744 |
| filtern | [w for w in werte if w > 30] |
werte[werte > 30] |
45, 31, 88 |
| Dictionary bauen | {w: w ** 2 for w in werte if w < 20} |
{12: 144, 7: 49} |
|
| mal zwei | werte * 2 |
werte * 2 |
Python: die Liste zweimal hintereinander, zehn Elemente. R: 24, 90, 14, 62, 176 |
| zählen | sum(1 for w in werte if w > 30) |
sum(werte > 30) |
3 |
f"{w:>3}" in der Schleife ist ein f-String: Der Ausdruck in geschweiften Klammern wird eingesetzt, :>3 richtet ihn rechtsbündig auf drei Stellen aus. Deshalb stehen die Pfeile untereinander.
In R zählt sum(werte > 30) die TRUE-Werte, weil ein logischer Vektor beim Summieren zu 0 und 1 wird. Python kann dasselbe mit sum(w > 30 for w in werte), denn auch dort ist True gleich 1.
Die Zeile werte * 2 ist der klassische Stolperstein beim Wechsel von R: Auf einer Liste wiederholt der Stern die Liste, statt die Elemente zu multiplizieren. Wer elementweise rechnen will, nimmt eine Comprehension oder wechselt zu einem NumPy-Array, das sich dann wie ein R-Vektor verhält.
Drei der fünf Werte liegen über 30 (45, 31 und 88).
werte * 2verdoppelt in Python die Länge der Liste auf zehn Elemente; elementweise rechnet erst eine Comprehension oder ein NumPy-Array.
Drei Dinge, die in beiden Sprachen überraschen: was 7 / 2 und die ganzzahlige Division liefern, warum 0.1 + 0.2 == 0.3 falsch ist, und wie round(2.5) rundet.
[1] 3.5 3.0 1.0 -4.0 1.0
[1] "numeric" "integer" "numeric"
[1] FALSE
[1] 0.30000000000000004
[1] TRUE
[1] 0.00 2.00 2.00 2.67
3.5 3 1 -4 1
float int int
False 0.30000000000000004
True
0 2 2 2.67
2.67499999999999982236
| Ausdruck | Python | R | Erklärung |
|---|---|---|---|
7 / 2 |
3.5 | 3.5 | Echte Division, in beiden Sprachen eine Gleitkommazahl. |
| ganzzahlige Division | 7 // 2 = 3 |
7 %/% 2 = 3 |
Abrunden auf die nächste ganze Zahl. |
| Rest | 7 % 2 = 1 |
7 %% 2 = 1 |
|
| negativ geteilt | -7 // 2 = −4 |
-7 %/% 2 = −4 |
Abgerundet wird nach unten, nicht Richtung null. −3.5 wird zu −4. |
| Rest bei negativ | -7 % 2 = 1 |
-7 %% 2 = 1 |
Der Rest hat das Vorzeichen des Teilers, damit (a // b) * b + a % b == a gilt. |
Typ von 5 |
int |
numeric |
Eine ganze Zahl ohne Nachsatz ist in R eine Gleitkommazahl. Ganzzahlig wird sie erst mit 5L. |
0.1 + 0.2 == 0.3 |
False |
FALSE |
0.1 und 0.2 sind binär nicht exakt darstellbar. Die Summe ist 0.30000000000000004. |
| Vergleich mit Toleranz | math.isclose → True |
all.equal → TRUE |
So vergleicht man Gleitkommazahlen. |
round(0.5), round(1.5), round(2.5) |
0, 2, 2 | 0, 2, 2 | Auf die gerade Zahl gerundet (Banker’s Rounding), nicht kaufmännisch. Das verhindert einen systematischen Aufwärtsfehler bei vielen Rundungen. |
round(2.675, 2) |
2.67 | 2.67 | Kein Rundungsfehler: 2.675 ist intern 2.67499999999999982236, also knapp unter der Mitte. |
Beide Sprachen folgen demselben Gleitkommastandard, deshalb verhalten sie sich bei Rundung und Vergleich gleich. Der einzige echte Unterschied in diesem Beispiel ist der Typ: Python unterscheidet int und float schon am Literal, R nicht. Das zählt, wenn Werte als Index oder in einer Datenbank landen.
0.1 + 0.2ergibt in beiden Sprachen 0.30000000000000004, weshalb der Vergleich mit==scheitert und eine Toleranz (math.isclose,all.equal) nötig ist.round(2.5)liefert 2, weil auf die gerade Zahl gerundet wird.
werte = [3, 1, 4, 1, 5]. Was liefert werte[0:2]?
[3, 1]
[3, 1, 4]
werte[1:3] in R. Beim Sprachwechsel ist das die häufigste Falle.
Nach a = [1, 2], b = a und b.append(3): Was enthält a?
[1, 2, 3]
b = a erzeugt keine Kopie, beide Namen zeigen auf dieselbe Liste, und append verändert diese Liste. Beispiel 1 zeigt den Nachweis mit a is b.
[1, 2]
b = a.copy().
a nicht verändert wurde
Wann ist ein Tupel der Liste vorzuziehen?
TypeError auslöst.
Was ergibt [0] * 3?
[0, 0, 0]
[0], weil null mal drei null ist
Ein Skript prüft if summe == 0.3: nach summe = 0.1 + 0.2. Was passiert?
math.isclose(summe, 0.3)
isTRUE(all.equal(summe, 0.3)).
Ein Code-Stück aus R lautet werte[-1] und wird wörtlich nach Python übertragen. Was ändert sich?
werte[1:].