Python Gotchas (C) 2018-2025 T.Birnthaler OSTC GmbH
==============
* Fließkommazahl mit "," statt "." schreiben --> Tupel mit 2 int statt 1 float:
f = 1,5
print(f, type(f)) --> (1, 5) <class 'tuple'>
GRUND: Tupel entsteht sobald ein Komma vorkommt, runde Klammern außenrum sind
nicht notwendig (aber optisch leichter erkennbare Form)
GRUND: Fließkommazahlen sind immer mit "." zu schreiben (jede Sprache).
* "," am Zeilenende --> Tupel statt Wert:
var = 123 --> var hat Wert 123
var = 123, --> var hat Wert (123,) d.h ein Tupel
* ";" am Zeilenende ist überflüssig (Statement-Abschluß):
var = 123
var = 123;
GRUND: In Python gilt das Zeilenende als Ende einer Anweisung.
* Ein vergessenes Komma "," in einer Sequenz von Strings kann dazu führen,
dass versehentlich 2 Elemente zu einem Element zusammengezogen werden:
var = ("aa" "bb") --> var hat Stringwert "aabb" (kein Tupel)
var = ("aa","bb") --> var hat Tupelwert ("aa", bb")
("a", "b", "c" "d", "e" "f") --> ("a", "b", "cd", "ef")
("a",
"b",
"c"
"d",
"e"
"f") --> ("a", "b", "cd", "ef")
GRUND: Direkt hintereinanderstehende Strings (ohne Operator dazwischen)
werden von Python zu einem String zusammengezogen (analog Verkettung per "+")
(auch auf mehreren Zeilen verteilte Strings falls sie in Klammern stehen)
* Verwechslung von "+=" und "=+" (analog C/C++/Java):
v = 5
v += 1 --> 6
v =+ 1 --> v = +1 --> v = 1 --> 1
Verwechslung von "-=" und "=-" (analog C/C++/Java):
v = 5
v -= 1 --> 4
v =- 1 --> v = -1 --> v = -1 --> -1
GRUND: Bei "=+" / "=-" zählt Plus/Minuszeichen als (überflüssiges) Vorzeichen.
* Nutzung von "++" oder "--" zum Inkrementieren/Dekrementieren von Werten
(wie in C/C++/Java üblich):
erg = ++v # OK, aber identisch zu: erg = v
erg = --v # OK, aber identisch zu: erg = v
erg = ++a * --b # OK, aber identisch zu: erg = a * b
erg = v++ # --> SyntaxError: invalid syntax
erg = v-- # --> SyntaxError: invalid syntax
erg = a++ * b-- # --> SyntaxError: invalid syntax
GRUND: Die Operatoren "++" und "--" gibt es in Python nicht (ABSICHTLICH!).
Präfix-Form ++v, --v als DOPPELTES Vorzeichen interpretiert --> wirkungslos.
Suffix-Form v++, v-- stellt einen Syntaxfehler dar.
* "assert" ist ein Schlüsselwort, keine Funktion, es erwartet 1 oder 2 durch
Komma getrennte Werte (typischerweise eine Bedingung + einen String)
(Klammern der Parameter führt zu Syntaxfehlern oder merkwürdigen Ergebnissen):
assert TEST # --> Korrekte Schreibweise
assert TEST, MSG # --> Korrekte Schreibweise
assert(TEST,MSG) # --> FEHLER!
assert(TEST) # --> FEHLER!
* "del" ist ein Schlüsselwort, keine Funktion
(Klammern sind zwar erlaubt, aber unüblich):
del V1 # --> Korrekte Schreibweise
del V1, V2, ... # --> Korrekte Schreibweise
del(V1) # --> OK, aber ungewöhnlich
del(V1, V2, ...) # --> OK, aber ungewöhnlich
* Ein 1-elementiger Tupel sind mit einem "überflüssigen" Komma zu schreiben:
("abc",) --> OK
("abc") --> Falsch da kein Tupel sondern ein Wert (geklammerter Ausdruck)
GRUND: Mehrdeutige Klammer (...) muss eindeutig gemacht werden:
a) Rechenausdruck (Expression): (1+2)*3 (123) (1+2+3)
b) Tupel (Komma notwendig): () (123,) (1, 2, 3)
c) Funktions-Aufruf: f() f(123) f(1, 2, 3)
* Eine LEERE MENGE ist speziell zu schreiben:
set() frozenset() # --> OK
set({}) frozenset({}) # --> OK
{} # --> FALSCH da Dictionary
GRUND: Mehrfachbedeutung von {...} = dict, set, frozenset
* Vergessene schließende Klammer (z.B. bei print(...), [...], {...}) oder
vergessenes schließendes Stringende "..." '...' """...""" '''...'''
--> Fehlermeldung "SyntaxError" evtl. irreführend erst viele Zeilen später,
da Aufteilung auf mehrere Zeilen innerhalb Klammern/String erlaubt ist.
* Methoden müssen in Klasse eingeschachtelt sein (korrekte Einrückung):
class KLASSE:
def METHODE1(self, ...): # Methode Gehört zur Klasse
...
def METHODE(self, ...): # Funktion von Klasse unabhängig
...
Nicht eingerückte Methoden nach Klasse sind Klassen-unabhängige Funktionen.
* Auf KLASSEN-VARIABLE kann per OBJEKT zugegriffen werden,
solange die Klassenvariable NUR GELESEN wird.
Sobald 1x per OBJEKT in die Klassenvariable GESCHRIEBEN wird,
hat Objekt EIGENE gleichnamige Variable mit eigenem Wert.
class C:
var = 111
c1 = C()
c2 = C()
print(C.var, c1.var, c2.var) # --> 111, 111, 111
c1.var = 222
print(C.var, c1.var, c2.var) # --> 111, 222, 111
C.var = 333
print(C.var, c1.var, c2.var) # --> 333, 222, 333
* KLASSEN-VARIABLE nur über den Klassennamen ansprechen, nicht über das Objekt
(wäre nur zum Lesen OK)
GRUND: Solange nur gelesen wird identisches Objekt,
sobald geschrieben wird --> verschiedene Objekte (COW)
class K:
attr = "klasse"
o1 = K()
o2 = K()
print(K.attr, o1.attr, o2.attr) # --> "klasse", "klasse", "klasse"
o1.attr = "objekt"
print(K.attr, o1.attr, o2.attr) # --> "klasse", "objekt", "klasse"
* Klammern nach Bezeichner setzen oder nicht ist ein Riesenunterschied:
var = name() # --> Funktions-Aufruf/Objekt-Konstruktor/...
var = name # --> Referenz auf Funktion/Klasse/...
* Python konvertiert Datentypen NIE automatisch, das ist immer manuell zu tun:
int(STR) <-> str(INT/FLOAT)
int("123.4") --> PENG!
Ausnahmen:
1) Alle Zahlentypen können in Ausdrücken gemischt werden:
bool <-> int <-> float <-> complex <-> Decimal <-> Fraction
2) Bei if/while wird Bedingung automatisch nach bool(...) konvertiert
if BED: ...
while BED: ...
3) print(...) konvertiert alle angegebenen Werte/Objekte per str(...)
--> Kann garantiert jeden Wert/jedes Objekt ausgeben!
* Python verhält sich bei Datei-Input/Output wie ein UNIX/Linux-System:
+ Zeilenenden bleiben erhalten
+ Zeilenende "\r\n" (Windows-OS) <-> "\n" (UNIX/Linux)
+ Verzeichnistrenner "\" (Windows-OS) <-> "/" (UNIX/Linux)
* Die Zeichensatz-Codierung von Source-Code, Eingabe- und Ausgabe-Daten
berücksichtigen (Python 3 geht standardmäßig von Unicode in UTF-8 Codierung
aus).
* Für Bezeichner möglichst nur ASCII-Zeichen verwenden (Code 32-127, Teil von
Unicode und hat UTF-8 Codierung).
KONSTANTE = 3.141529
variable = 123
class KlassenName: ...
def funktions_name
* Der Dateiname eines Moduls muss ein BEZEICHNER sein, damit es per "import"
geladen werden kann (nur die Zeichen "a-zA-Z_0-9" verwenden, insbesondere
kein Leerzeichen und kein Bindestrich "-" im Modulnamen nutzen!)
* Unterverzeichnis mit Modulen und Modul NICHT GLEICH nennen:
Programm-Code
# programm.py
import verz.unter.modul as vum
--> ModuleNotFoundError: No module named 'verz.unter'; 'verz' is not a package
Verzeichnis + Dateistruktur:
+-- programm.py
+-- verz
| +--- unter
| +--- modul.py
+-- verz.py
* Datentypen werden erst zur Laufzeit (Run-Time) überprüft, nicht während der
Übersetzungszeit (Compile-Time), generelle Eigenschaft von Skript-Sprachen.
--> Fehlermeldung TypeError, ValueError, ... erst zur Laufzeit
* Überschreiben von bereits vorhandenen Namen von Variablen, Funktionen, ...
ist jederzeit ohne Warnung möglich (inkl. Typänderung).
Ausnahme: Die 37 Schlüsselworte (if, else, ...) sind nicht umdefinierbar
Typische Kandidaten (versehentlich verwendete eingebaute/interne Namen):
len = 0
sum = 12345
min = 0
max = 999
any = True
all = []
str = "hallo"
int = 123
float = 123
list = [1, 2, 3]
tuple = (1, 2, 3)
dict = {"a": 1, "b": 2, "c": 3}
type = "int"
dir = "C:\\Users\\Administrator\\Documents"
* Eigenes Python-Skript nicht so nennen wie ein Builtin-Modul oder eine
bereits vorhandene interne Variable, Funktion, Klasse oder ...
GRUND: Namen können jederzeit neu definiert werden
(kommentarlos, ohne Fehlermeldungen)
TIP: Unterstrich hinten dranhängen (z.B. "if_")
KANDIDATEN: len sum min max any all str int list ... (Built-in Funktionen)
* Namenskonventionen und Reihenfolge-Vorgaben einhalten (PEP8):
cls # Klasse in Klassenmethode
self # Objekt in Methode
other # 2. Objekt in Methode
*args # Sammler für Positions-Argument in Funktions-Definition
**kwargs # Sammler für Schlüsselwort-Argument in Funktions-Definition
*_ # Sammler für zu ignorierende Elemente in Tupel-Zuweisungs
_ # Temporäre Variable (z.B. for _)
NAME # Public Attribut/Methode in Klasse
_NAME # Protected Attribut/Methode in Klasse
__NAME # Private Attribut/Methode in Klasse
__NAME__ # Python-interne Variable (nie für eigene Zwecke nutzen!)
KEYWORD_ # Schlüsselworte als Bezeichner (z.B. if_ while_)
funk_tion # Funktions-Bezeichner (Snake Case)
Klasse # Klassen-Bezeichner (Camel Case)
KONSTANTE # Konstanten-Bezeichner (Trump Case)
GRUND: Programm für nächsten (durchschnittlichen) Programmierer schreiben!
* Vollständigen Import aller Elemente aus Modul per * nicht benutzen:
from modul import *
GRUND: Überschreibt alle gleichnamigen bereits vorhandenen Namen!
Welche Namen werden überhaupt importiert und wie viele?
* Fehler NICHT UNTERDRÜCKEN, sondern abfangen und behandeln:
try: # Start
... # "Überwachter" CODE: Fehler --> except angesprungen
except: # Fängt ALLE Fehler ab --> zu viele (z.B. Syntax, Name, Type, Value, Assert, ...)!
pass # Unterdrückt ALLE Fehler --> Programmierer bekommt nichts mit!
GRUND: Alle Fehler-Meldungen werden abgefangen + vollständig unterdrückt!
Keine Fehler-Behandlung sondern Fehler-UNTERDRÜCKUNG!
Erschwert Fehlersuche stark!
* Maximal "Exception" abfangen, nicht "BaseException"!
except: ... # entspricht "except BaseException: ..."
GRUND: Programm mit exit()-Funktion und "Strg-C" (Cancel) nicht abbrechbar!
* Programm übersetzbar, aber Exception löst "NameError" aus
GRUND: Exception-Namen werden erst im Fehlerfall gesucht ob vorhanden
(nicht zur Übersetzungszeit)
Analog bei Funktionen und Klassen: Erst bei Nutzung werden Namen darin geprüft
* Exception-Fall unwirksam
GRUND: Vorher steht in der Kaskade eine allgemeinere Exception --> greift zuerst
Python überprüft nicht, ob Exception-Kasakade disjunkt ist
* Test auf True/False ist "over-engineered" und zu spezifisch
(Datentyp von "var" MUSS "bool" sein, sonst Ergebnis "False"):
if var == True: # --> if var:
if var != True: # --> if not var:
if var == False: # --> if not var:
if var != False: # --> if var:
while var == True: # --> while var:
while var != True: # --> while not var:
while var == False: # --> while not var:
while var != False: # --> while var:
BESSER: if var: # if --> Boolescher Kontext
if not var: # if --> Boolescher Kontext
GRUND: "Wahr" oder "Falsch" sind ALLE WERTE,
IDENTISCH mit "True" oder "False" ist fast KEIN Wert.
Der Ausdruck nach if/while wird automatisch nach bool() konvertiert.
* Test auf leeren/gefüllten CONTAINER ist "over-engineered" und zu spezifisch
(Datentyp MUSS "tuple", "list", "dict" sein, sonst Ergebnis "False"):
if tpl == (): # --> if not tpl:
if tpl != (): # --> if tpl:
if lst == []: # --> if not lst:
if lst != []: # --> if lst:
if dct == {}: # --> if not dct:
if dct != {}: # --> if dct:
if mng == set(): # --> if not mng:
if mng != set(): # --> if mng:
GRUND: Leere Container ergeben "False" in booleschem Kontext
Gefüllte Container ergeben "True" in booleschem Kontext
{} ist kein set, sondern ein dict --> Datentyp verschieden, Wert nicht!
* Defaultwert eines Funktions-Parameters mit MUTABLE Objekt ist problematisch:
def funk(var=[]): ...
BESSER: def funk(var=None):
if var is None:
var = []
GRUND: Objekt [] wird nur 1x erzeugt bei Funktions-Definition
und bei jedem Aufruf ohne Parameter wiederverwendet,
d.h. Änderungen daran bleiben zw. Funktions-Aufrufen erhalten.
* Eigene Funktion liefert "None" als Ergebnis zurück (unabhängig vom Aufruf).
GRUND: "return" ohne Wert oder "return ERGEBNIS" am Funktions-Ende vergessen!
Dann findet automatisch ein "return None" am Funktions-Ende statt!
* Folgende Initialisierungen sind gefährlich:
a = b = {}
a = [[] * 10]
GRUND: Gleiches Dictionary- oder Listen-Objekt wird mehrfach verwendet
BESSER: (a, b) = ({}, {})
(a, b) = ([], [])
a = list(map(lambda x: [], range(10))
* Test auf Datentyp einer Variablen nicht so durchführen:
if type(var) == int:
if type(var) == int or type(var) == float:
if type(var) in (int, float):
sondern BESSER so:
if isinstance(var, int):
if isinstance(var, (int, float)):
GRUND: Vererbung bei "type()" NICHT berücksichtigt, bei "isinstance()" schon!
* Selber zählen und indizieren vermeiden:
arr = ["hallo", "welt", "hier", "bin", "ich"]
for i in range(len(arr)): # --> for elem in arr:
print("ELEM", arr[i]) # print("ELEM", elem)
GRUND: "Verzählen" möglich, Performance, Speicherersparnis, Iterator, ...
ZITAT: "In Python zählt man nicht selber, sondern lässt Python zählen."
(mit enumerate, zip, sum, min, max, any, all, ...)
* Mit Iteratoren oder Generatoren arbeiten,
anstatt Listen/Tupel/... zu verwenden
GRUND: Performance + Speicherersparnis + ...
* Quellcode per "Copy-and-Paste" aus Webseiten/PDF/... kopieren ist gefährlich!
"Copy-and-Paste" von Code transferiert oft die EINRÜCKUNG nicht korrekt.
GRUND: Einrückung (Blockstruktur) geht (meist) verloren (teilweise oder
vollständig) und muss manuell korrigiert werden (es gibt keinen
Automatismus dafür, da die Einrückung Teil der Programm-Semantik ist)
GRUND: Leerzeichen/Tabulator falsch übertragen --> Andere Bedeutung möglich
--> Einrückung unbedingt Zeile für Zeile manuell überprüfen!
* "raise" vor FehlerKlasse vergessen:
AssertionError("Problem")
--> Anweisung wird ignoriert
* Raw- und Format-Strings falsch herum geschrieben sind normaler String:
"R..." # Korrekt: R"..."
"F..." # Korrekt: F"..."
* Es gibt 3 Funktionen zum Vergleich von REGEX mit String:
re.search() # Kein automatischer Anker
re.match() # Anker ^ automatisch vorne
re.fullmatch() # Anker ^ $ automatisch vorne + hinten
--> IMMER re.search und bei Bedarf die Anker ^ $ verwenden!
* Python verzögert Auswertungen möglichst lange:
print(range(10)) # --> "range(10)"
--> Muss daher manchmal dazu gezwungen werden:
print(list(range(10))) # --> "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
* Grenzen sind INKONSISTENT (obere Grenze erreicht oder nicht?):
range(10) # --> 0..9 (10 Werte!)
range(1, 10) # --> 1..9 ( 9 Werte)
var[0:10] # --> var[0] .. var[9] (9 Elemente)
random.randint(1, 10) # --> 1..10
TIP: Obere Grenze bei Range immer mit Addition/Subtraktion von 1 schreiben:
range(1, 10+1, 1) # --> 1..10
range(10, 0-1, -1) # --> 10..0
* Auf Wert "None" testen geht auf 2 Arten:
if var == None: # OK (un-pythonic)
if var is None: # OK + empfohlen (pythonic)
GRUND: Bedeutet das Gleiche (None ist SINGLETON, identisch mit Klasse)
* Datentyp "NoneType" gibt es ab Python 3.6 nicht mehr
--> Statt dessen type(None) verwenden falls notwendig
* Vergleich "==" statt Zuweisung "=" verwendet wird ohne Fehlermeldung ignoriert:
var == 123
GRUND: Wertet Vergleich aus und verwirft Ergebnis True/False.
--> Variablenwert bleibt gleich.
* Ausdrücke (Expressions) für sich ohne Zuweisung oder Funktions-Aufruf außenrum
werden ignoriert (sind aber kein Fehler).
"""docstring ...""" # Für mehrzeilige Kommentare + Docstrings benutzt
var == 123 # Vergleich (statt Zuweisung)
(1 + 2) * 3 ** 4 # Rechenausdruck (ohne Zuweisung)
AssertionError("...") # "raise" davor vergessen
* Walrus-Operator := ist keine Zuweisung, sondern ein Operator.
Verhält sich aber wie eine Zuweisung:
print(erg := 1 + 2 * 3 / 4) # OK: erg == 2.5
erg := 1 + 2 * 3 / 4 # FEHLER
(erg := 1 + 2 * 3 / 4) # OK
GRUND: Nur in geklammertem oder weiterverwendetem Rechenausdruck erlaubt!
* Zirkuläre Struktur ist möglich --> Entscheidung über Freigabe per Reference
Counting funktioniert nicht mehr --> Garbage Collector notwendig:
a = [] #
b = [a] #
a.append(b) #
print(a, b) # --> [[[...]]] [[[...]]]
* Vergleich "==" und "!=" vergleicht die WERTE von 2 Objekten.
Vergleich "is" und "is not" vergleicht die IDENTITÄT von 2 Objekten.
(OBJ1 is OBJ2 ist ABKÜRZUNG für id(OBJ1) == id(OBJ2)
* Bei "==" muss der Datentyp der Gleiche sein, sonst kommt IMMER "False" heraus.
(außer bei Zahlen bool <-> int <-> float <-> complex <-> Decimal <-> Fraction)
True == 1 # --> True
False == 0 # --> True
123 == 123.0 # --> True
123 == (123.0+0j) # --> True
123.0 == (123.0+0j) # --> True
123 == "123" # --> False
(1, 2, 3) == [1, 2, 3] # --> False
0 == "0" # --> False
"" == () # --> False
[] == () # --> False
* Gleicher Typ + Wert --> Meist nur 1x angelegt und mehrfach referenziert
--> Objekt nicht nur gleich, sondern identisch
a = "abc"
b = "abc"
print(a, b, a == b, a is b) # --> abc abc True True
Aber nicht immer:
c = "ab"
c += "c"
print(a, c, a == c, a is c) # --> abc abc True False
* Gleiche ID heißt auch gleicher WERT!
Gleicher WERT heißt aber nicht unbedingt gleiche ID!
id(OBJ1) is id(OBJ2) # --> OBJ1 == OBJ2
s1 = "hallo welt" # --> s1 = "hallo welt"
s2 = "hallo" # --> s2 = "hallo"
s2 += " welt" # --> s2 = "hallo welt"
s1 == s2 # --> True
s1 is s2 # --> False
* Gleiche Speicherstelle wird evtl. nacheinander für 2 verschiedene Objekte
ALTERNIEREND verwendet (wenn 1. Objekt nicht mehr referenzierbar ist und
2. Objekt den gleichen Typ hat und genauso viel Speicher benötigt).
a) String:
t = "hallo"
print(t, id(t))
while True:
del t
t = "zorro"
print(t, id(k))
b) Ganze Zahl:
i = 123456;
while True:
i += 1; print("I", i, id(i))
i += 1; print("I", i, id(i))
* Die Objekte -5 .. 256 sowie 0.0, 1.0, -1.0, True, False, None werden von
Python vorab beim Programmstart erzeugt (da oft benötigt).
--> Haben ungewöhnlich kleine + feste ID.
* Ganze Zahlen haben als ID ihren Wert.
var = 123
print(var, id(var)) # --> 123 123
* Type Annotation/Hint wird in Python verglichen mit anderen Programmiersprachen
"verkehrt herum" geschrieben und mit einem ":" (Doppelpunkt) zwischen dem
Bezeichner und dem zugehörigem Datentyp getrennt:
var: int = 123
def funk(a: int, b: str, c: float) -> bool:
In C/C++ schreibt man den Typ "anders herum" und ohne ":" (Doppelpunkt)
int var = 123
bool funk(int a, str b, float c)
GRUND: Erst sehr spät hinzugefügt (ab Python 3.5)
* Type Annotation/Hint wird von Python nur akzeptiert, aber komplett ignoriert.
--> Erst von Tools wie "mypy", "pylint", ... werden die Datentypen überprüft.