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: Fließkommazahlen sind immer mit "." zu schreiben (jede Sprache).

* Verwechslung von "+=" und "=+" (analog C/C++/Java):
    v =  5
    v += 1  -->  6
    v =+ 1  -->  v = +1  -->  v = 1  -->  1
  GRUND: Bei "=+" zaehlt das Pluszeichen als (Überflüssiges) Vorzeichen.

* D.h. 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 werden von Python zu
  einem String zusammengezogen (analog Verkettung per "+")

* "," 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

* assert ist ein Schlüsselwort, keine Funktion:
    assert(TEST,MSG) --> IMMER OK (da 2-elem Tupel) --> assert TEST, MSG
    assert(TEST)     --> FEHLER da asset 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: Mehrfachbedeutung von (...) = Geklammerter Ausdruck, Funktionsaufruf, Tupel

* Leere Menge schreiben
    set()       --> OK
    set({})     --> OK
    {}          --> Falsch da kein Set sondern 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
  (ware 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/Objektkonstruktor/...
    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:
    Zahlen 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
  + Zeilende "\r\n" (Windows-OS) <-> "\n" (UNIX/Linux)    # Nur im Textmodus!
  + 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!)

* Typen werden 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
    min = 0
    max = 999
    sum = 12345
    str = "hallo"
    list = [1, 2, 3]
    type = "int"
    dir = "C:\\Users\\Administrator\\Documents"

* 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      # Protected-Attribut/Methode in Klasse
    __NAME     # Private-Attribut/Methode in Klasse
    __NAME__   # Python-interne Variable
    KEYWORD_   # Schlüsselworte als Bezeichner (z.B. if_  while_)
    funk_tion  # Funktions-Bezeichner (LowerCase)
    Klasse     # Klassen-Bezeichner (CamelCase)
    KONSTANTE  # Konstanten-Bezeichner (TrumpCase)
  GRUND: Programme für nächsten (durchschnittlichen) Programmierer schreiben

* from modul import *
  GRUND: Uberschreibt evtl. viele bereits vorhandene Namen
         Welche Namen werden überhaupt importiert? 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!

* Test auf True oder False ist "over-engineered" und zu spezifisch
  (Datentyp von var MUSS bool sein, sonst immer False als Ergebnis):
    if var == True:
    if var != True:
    if var == False:
    if 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.

* 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: dir len list int dict sum min max any all (Built-in Funktionen)

* 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 = list(map(lambda x: [], range(10))

* Test auf Datentyp einer Variablen nicht so:
    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 wird sonst nicht berücksichtigt.

* Selber zählen (indizieren):
    arr = ["hallo", "welt", "hier", "bin", "ich"]
    for i in range(len(arr)):
        print("ELEM", arr[i])
  (OK sind enumerate, zip, sum, min, max, any, all, ...)
  GRUND: "Verzählen" vermeiden, Performance, Speicherersparnis, Iterator, ...
  Man zählt nicht selber, sondern lässt Python zählen.

* 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!
  GRUND: Einrückung (Blockstruktur) geht verloren (meist) und muss
         manuell korrigiert werden (es gibt keinen Automatismus dafür
         da die Einrückung Teil der Programm-Semantik ist)

* "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]

* range(10) --> 0..9
  range(0, 10) --> 0..9
  var[0:10] --> var[0] .. var[9]
  random.randint(1,10) --> 1..10
  INKONSISTENT!

* Auf Wert "None" testen geht auf 2 Arten:
    if var == None:
    if var is None:
  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

* Vergleich == statt Zuweisung = verwendet wird ohne Fehlermeldung ignoriert:
    var == 123
  GRUND: Wertet Vergleich aus, verwirft Ergebnis True/False, Variable bleibt gleich

* Walrus-Operator := ist keine Zuweisung, verhält sich aber wie eine Zuweisung.
    print(erg := 1 + 2 * 3 / 4)
  Nur in einem Rechenausdruck erlaubt.

* "Copy-and-Paste" von Code transferiert oft die Einrückung nicht korrekt.
  Leerzeichen fehlerhaft übertragen --> Statt Syntaxfehler andere Bedeutung möglich
  Einrückung zeilenweise manuell überprüfen (kein autom. Formatierung möglich)

* Zirkuläre Struktur erstellen möglich --> Entscheidung über Freigabe per Reference
  Counting funktioniert nicht mehr --> Garbage Collector notwendig:
    a = []
    b = [a]
    a.append(b)
    print(a, b)    --> [[[...]]] [[[...]]]

* Der Vergleich "==" vergleicht die WERTE von 2 Objekten,
  Der 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).

* Ausdrücke (Expressions) für sich ohne Zuweisung oder Funktionsaufruf außenrum
  werden ignoriert (sind aber kein Fehler).
    """docstring ..."""
    var == 123
    (1 + 2) * 3 ** 4
    AssertionError("raise davor vergessen")