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.