Python Gotchas (C) 2018-2022 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 "," in einer Sequenz 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") GRUND: Direkt hintereinanderstehende Strings (kein Operator dazwischen) werden von Python zu einem String zusammengezogen (analog Verkettung per "+") * Verwechslung von "+=" und "=+" (analog C/C++/Java): v = 5 v += 1 --> 6 v =+ 1 --> v = +1 --> v = 1 --> 1 GRUND: Bei "=+" zählt das Pluszeichen als (Überflüssiges) Vorzeichen. * assert ist ein Schlüsselwort, keine Funktion: assert(TEST,MSG) --> IMMER OK (da 2-elem Tupel) --> assert TEST, MSG assert(TEST) --> FEHLER da assert Tupel erwartet, (TEST) aber keines ist assert TEST --> Korrekte Schreibweise assert TEST, MSG --> Korrekte Schreibweise * 1-elem Tupel 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) Funktionsaufruf: f() f(123) f(1, 2, 3) * Leere Menge 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" irreführend und erst viele Zeilen später, da Formatierung innerhalb Klammern/String frei ist. * Auf KLASSENVARIABLE 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 * Klassenattribut nur über Klassennamen ansprechen, nicht über Objekt (wäre aber prinzipiell OK) GRUND: Solange nur gelesen wird identisches Objekt, sobald geschrieben wird --> verschiedene Objekte (COW) class K: attr = "klasse" pass 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() # --> Funktionsaufruf/Objekt-Konstruktor/... var = name # --> Referenz auf Funktion/Klasse/... * Python konvertiert Datentypen NIE automatisch, immer manuell zu tun: int(STR) <-> str(INT/FLOAT) int("123.4") --> PENG! Ausnahmen: Alle Zahlentypen können in Ausdrücken gemischt werden: bool <-> int <-> float <-> complex Bei if/while wird Bedingung automatisch nach bool(...) konvertiert if BED: while BED: print(...) wandelt alle Werte nach str(...) um --> Kann jeden Wert ausgeben * Python verhält sich bei Input/Output wie UNIX/Linux-System: + Zeilenenden bleiben erhalten + Zeilenende "\r\n" (Windows-OS) <-> "\n" (UNIX/Linux) + Verzeichnistrenner "\" (windows-OS) <-> "/" (UNIX/Linux) * Zeichensatz (Codierung) von Source-Code, Eingabe- und Ausgabe Daten berücksichtigen (Python 3 geht standardmäßig von Unicode in UTF-8 Codierung aus). Möglichst nur ASCII-Zeichen verwenden (Code 0-127, ist Teil von Unicode und hat UTF-8 Codierung). * Dateiname von Modul muss ein BEZEICHNER sein, damit es per "import" geladen werden kann (nur "a-zA-Z_0-9", insbesondere kein Bindestrich "-" im Namen!) * Datentypen werden erst zur Laufzeit geprüft, nicht zur Übersetzungszeit (generelle Skript-Sprachen-Eigenschaft) --> Fehlermeldung TypeError, ValueError, ... erst zur Laufzeit * Überschreiben von Namen von Variable, Funktionen, ... jederzeit ohne Warnung möglich (inkl. Typänderung) Ausnahme: Die 37 Schlüsselworte (if, else, ...) sind nicht umdefinierbar Typische Kandidaten (versehentlich verwendete 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 so nennen wie Builtin-Modul oder Variable oder Funktion oder 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 Reihenfolgevorgaben 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 (Lower Case) Klasse # Klassen-Bezeichner (Camel Case) KONSTANTE # Konstanten-Bezeichner (Trump Case) GRUND: Programm für nächsten (durchschnittlichen) Programmierer schreiben * Generellen Import aus Modul nicht nutzen: from modul import * GRUND: Überschreibt gleichnamige bereits vorhandene Namen! Welche Namen werden überhaupt importiert, wie viele? * 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! * Programm ist mit "Strg-C" (Cancel) nicht abbrechbar GRUND: except: verwendet == except BaseException: Maximal "Exception" abfangen, nicht "BaseException"! * Test auf True/False ist "over-engineered" und zu spezifisch (Datentyp von "var" MUSS "bool" sein, sonst Ergebnis "False"): if var == True: while var == True: if var != True: while var != True: if var == False: while var == False: if var != False: while var != False: BESSER: if var: # if --> Boolescher Kontext if not var: GRUND: "Wahr" sind fast alle Werte, identisch mit "True" ist fast keiner. Ausdruck nach if/while wird automatisch nach bool konvertiert. * Test auf leeren/gefüllten Containerist "over-engineered" und zu spezifisch (Datentyp von "var" 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 st == {}: --> if not st: if st != {}: --> if st: GRUND: Leere Container ergeben "False" in booleschem Kontext Gefüllte Container ergeben "True" in booleschem Kontext {} ist kein set, sondern ein dict --> Typ unterschiedlich, nicht Wert! * Defaultwert ist ein mutable Objekt: 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. Funktionsaufrufen erhalten. * 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 (indizieren): arr = ["hallo", "welt", "hier", "bin", "ich"] for i in range(len(arr)): print("ELEM", arr[i]) GRUND: "Verzählen" vermeiden, 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: Speicherersparnis, Performance * 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 * 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] 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 if var is None: # OK + empfohlen 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, verwirft Ergebnis True/False, Variable bleibt gleich * Ausdrücke (Expressions) für sich ohne Zuweisung oder Funktionsaufruf außenrum werden ignoriert (sind aber kein Fehler). """docstring ...""" # Bewusst für mehrzeilige Kommentare + Docstrings benutzt var == 123 (1 + 2) * 3 ** 4 AssertionError("raise davor vergessen") * Walrus-Operator := ist keine Zuweisung, 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 möglich --> Entscheidung über Freigabe per Reference Counting funktioniert nicht mehr --> Garbage Collector notwendig: a = [] b = [a] a.append(b) print(a, b) --> [[[...]]] [[[...]]] * Vergleich "==" vergleicht die WERTE von 2 Objekten. Vergleich "is" vergleicht die ID 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) 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 * Gleicher Typ + Wert --> Meist nur 1x angelegt und mehrfach referenziert 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. für 2 verschiedene Objekte verwendet (wenn das 1. Objekt nicht mehr referenzierbar ist und das 2. Objekt den gleichen Typ hat und genauso viel Speicher benötigt). t = "hallo" i = 0; print("I", i, id(i)) print(t, id(t)) i += 1; print("I", i, id(i)) del t i += 1; print("I", i, id(i)) z = "zorro" i += 1; print("I", i, id(i)) print(z, id(z)) 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. * Type Annotation/Hint in Python "verkehrt herum" geschrieben und mit ":" (Doppelpunkt) zwischen Bezeichner und zugehörigem Datentyp: var: int = 123 def funk(a: int, b: str, c: float) -> bool: In C/C++ schreibt man den Typ "anders rum" 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 "pylint", "mypy" werden die Datentypen überprüft.