Python Gotchas (C) 2018-2024 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) 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.