Python: Unterschied zwischen == und is Operatoren

Prämisse: Variablen sind Pointer auf Objekte im Speicher.

  • == vergleicht die Werte von zwei Variablen und schaut nicht, ob es sich um das selbe Objekt im Speicher handelt.
  • is hingegen vergleicht bzw. prüft, ob es sich um das selbe Objekt im Speicher handelt.

Folgender Python-Code veranschaulicht den Unterschied:

a = [1, 2, 3]
b = [1, 2, 3]

print(id(a), id(b))

print(a == b)
print(a is b)

und erzeugt folgende Ausgabe:

2691913515776 2691915438912
True
False

Der Unterschied zwischen Operatoren sollte unbedingt bekannt sein, da dieser unter Umständen zu ungewolltem Verhalten führen kann.

Ausnahmen und Sonderfälle

Gleichsetzen von Variablen

Wenn Variablen gleich gesetzt werden, dann zeigen sie auf das gleiche Objekt im Speicher.

Folgender Python-Code veranschaulicht dies:

a = [1, 2, 3]
b = [1, 2, 3]

a = b

print(id(a), id(b))

print(a == b)
print(a is b)

und erzeugt folgende Ausgabe:

140497929272704 140497929272704
True
True

Überschreiben von __eq__ bei selbst geschriebenen Klassen

Bei dem Operator == wird die dunder-method __eq__ verwendet, welche rein theoretisch überschrieben werden kann. Dies bedeutet, dass man nicht immer davon ausgehen sollte, dass == so funktioniert, wie man es erwartet. is hingegen funktioniert immer auf die gleiche Art und Weise und vergleicht die Objekte, auf die im Speicher gezeigt wird.

Der folgende Code sollte das veranschaulichen:

class TestKlasse:
    def __init__(self, wert) -> None:
        self.testVar = wert

    def __eq__(self, other) -> bool:
        return False


c = TestKlasse(wert="Hallo")
d: TestKlasse = c


print(id(c), id(d))

print(c == d)
print(c is d)

durch folgende Ausgabe:

140356530760144 140356530760144
False
True

Es wird also auf dasselbe Objekt gezeigt (siehe id), aber weil die __eq__Methode überschrieben wurde, funktioniert == nicht mehr, wie man es erwarten würde.

Interning des Python Interpreters

“In computer science, interning is re-using objects of equal value on-demand instead of creating new objects”

Wikipedia: https://en.wikipedia.org/wiki/Interning_(computer_science)

Je nach Plattform und Python-Interpreter kann es sein, dass sich der Interpreter dazu entscheidet, den Pointer von zwei Variablen auf die selbe Speicherstelle verweisen zu lassen, sofern der Wert gleich ist.

Der folgende Code konstruiert diese Situation:

e = "Hallo"
f = "Hallo"

print(id(e), id(f))

print(e == f)
print(e is f)

und zeigt es mit der Ausgabe:

140639697683504 140639697683504
True
True

Obwohl zwei unterschiedliche Variablen (e und f) definiert werden, wird auf dieselbe Speicherstelle verwiesen, wodurch der is Operator anders funktioniert, als man intuitiv meinen möchte. Interning ist ein sogenanntes Implementierungsdetail und keine Regel. Das Verhalten kann also immer wieder anders sein.

Wann sollte man welchen der beiden Operatoren verwenden?

Anhand der Bespiele lässt sich ableiten, dass man den == Operator dann nutzen sollte, wenn man z.B. zwei Strings vergleichen möchte und/oder wenn man sich sicher sein kann, dass die __eq__ Methode nicht überschrieben wurde.

Den is Operator sollte man eigentlich nur bei Vergleichen mit singleton objects, wie z.B. None, True oder False, verwenden, da es immer nur ein Objekt dieser singleton objects im Speicher geben kann. Dies wird auch in PEP8 explizit so definiert.

Die Tupel-Fallgrube

Was viele nicht wissen, ist dass bei der Deklarierung eines Python Tupels die runden Klammern eigentlich nicht zum syntaktischen Konstrukt gehören, sondern nur zur optischen Abgrenzung verwendet werden. Dies bedeutet folgendes:

Eingabe:

x = (1, 2, 3)
print(x)

Ausgabe:

(1, 2, 3)

Doch ebenso:

Eingabe:

x = 1, 2, 3
print(x)

Ausgabe:

(1, 2, 3)

Dies erscheint erst einmal nicht schlimm, oder sogar vielleicht elegant, da man sich Tipparbeit sparen kann, doch diese quasi Inkonsistenz in der Syntax kann zu schwierig zu findenden Fehlern führen, wie z.B. folgendem:

Eingabe:

x = min(1, 2, 3),
print(x)

man beachte das “,” hinter dem call von min(). Dies führt dann hierzu:

Ausgabe:

(1,)

also einem Tupel, obwohl man einen Integer-Wert erwartet. Ein solcher Bug kann schwer zu finden bzw. zu erkennen sein.
Deshalb ergibt type hinting auch bei Python Sinn.

Dictionaries und shallow copies in Python

Wenn man mit dictonaries in python arbeitet, sollte man das Konzept der shallow copy kennen, da dieses Feature zu fiesen und vielleicht schwer zu findenden bugs führen kann. Mit der Funktion copy() kann man von einem Dictionary eine Kopie, aber eben “nur” eine shallow copy erzeugen.

Bei einer shallow copy handelt es sich zwar um zwei separate Objekte, aber die einzelnen Elemente der beiden Dictionaries verweisen auf dieselbe Speicherstelle. Diesen Umstand stellt folgender Beispielcode dar:

# Dictionaries definieren:
my_dict: dict[int, list[str]] = {0: ["a", "b"], 1: ["c", "d"]}
dict_copy: dict[int, list[str]] = my_dict.copy()

# ids der Dictionaries ausgeben:
print(id(my_dict))
print(id(dict_copy))

# Inhalte der beiden Dictionaries ausgeben
print(my_dict)
print(dict_copy)

Ausgabe:

2513461433280
2513483301312
{0: ['a', 'b'], 1: ['c', 'd']}
{0: ['a', 'b'], 1: ['c', 'd']}

Wir sehen anhand der unterschiedlichen IDs der Objekte, dass es sich in der Tat um zwei unterschiedliche Objekte handelt, die aber den gleichen Inhalt haben.

# Bei der Kopie des Dictionaries das erste Item des ersten Dictionary-Eintrags verändern
dict_copy[0][0] = "test"

# Erneut die Inhalte der beiden Dictionaries ausgeben:
print(my_dict)
print(dict_copy)

Ausgabe:

{0: ['test', 'b'], 1: ['c', 'd']}
{0: ['test', 'b'], 1: ['c', 'd']}

Wie wir sehen, wurde nicht nur die Kopie des Dictionaries (dict_copy) verändert, sondern auch das ursprüngliche Dictionary. Dies verdeutlicht, dass die Items im Objekt Verweise auf dieselbe Stelle im Speicher sind. Es ist also Vorsicht geboten, wenn man mit copy() arbeitet und dann einzelne Items der Dictionaries manipuliert.

Vektorisierte Operationen mit Pandas

Aktuell beschäftige ich mich intensiv mit Python im Kontext von Data Science.
Hierbei kommt man natürlich nicht an Pandas vorbei. Und natürlich gibt es, wie so gut bei jedem Problem, welches man durch Programmcode lösen möchte, mehrere Wege zum Ziel.

Bei der Verwendung von Pandas Dataframes könnte man auf die Idee kommen, folgenden Code zu schreiben, um alle Werte einer Spalte zu verarbeiten und das Ergebnis in eine neue Spalte zu schreiben:

df['wert_quadrat'] = df.apply(lambda row: row['wert'] ** 2, axis=1)

Der Code quadriert den Wert in der Spalte “wert” und erzeugt dann eine neue Spalte “wert_quadrat” mit dem Ergebnis.
Dies funktioniert, doch es gibt eine elegantere und vor allem performantere Lösung: vektorisierte Operationen!

Hier die gleiche Operation als vektorisierte Operation:

df['wert_quadrat'] = row['wert'] ** 2

Je nach Größe des Datensatzes kann man hierdurch erhebliche Performancegewinne erzielen, da bei vektorisierten Operationen kein Python-Loop durchgeführt wird, bei dem für jede Zeile die entsprechende Python-Funktion ausgeführt wird, sondern die Operation wird an die zugrundeliegende numpy arrays deligiert, welche die Operation in Maschinencode ausführt.

pylint – ‘Unable to import’ – import-error

Wenn man mit VScode Python entwickelt und für das linting pylint verwendet, dann kann es vorkommen, dass der Editor die Imports als falsch bzw. nicht aufgelöst anzeigt und dies mit der Einblendung:

Unable to import {Modulname} pylint(import-error)

quittiert. Dies liegt daran, dass pylint nicht den korrekt Python-Interpreter bzw. den Pfad dorthin kennt.

Lösung: Man öffnet das VScode Menü mit F1 bzw. Strg + Shift + P und wählt über die Option Python: Select Interpreter den korrekten Python-Interpreter aus.

Brave Browser: Globaler Dark-Theme-Mode ohne Extension

Der Brave Browser ist mittlerweile ein sehr beliebter Browser und übertrifft die althergebrachten Browser wie Google Chrome und FireFox in Sachen Sicherheit und Funktionsumfang. Wenn man durch das Netz surft, dann können weiße bzw. sehr hell gehaltene Seiten für viele Menschen störend sein. Um hier Abhilfe zu schaffen, kann man ganz einfach den zwar aktuell noch experimentellen, aber bereits im stable build enthaltenen globalen Dark-Theme-Mode aktivieren. Hierfür muss man in die Adresszeile einfach folgende Zeile eintippen oder kopieren.

chrome://flags/#enable-force-dark

bzw.

brave://flags/#enable-force-dark

Hat man dies getan, muss man die erscheinende Option per drop-down noch auf “enabled” setzen und schon hat man out-of-the-box den globalen Dunkelmodus aktiviert. Viel Spaß und angenehmes Surfen hiermit!

Map-Kompilierung mit Trenchbroom

In dumptruck_ds’ Video-Tutorial-Serie zum Thema “Quake-Mapping mit Trenchbroom” wird in der ersten Folge der Editor und die Tools zur Compilierung der Maps vorgestellt und eingerichtet. Dabei müssen mehrere Tools nacheinander mit dem Output des jeweils vorhergehenden Tools aufgerufen werden. Zur schnellen und effizienten Kompilierung der Map-Files ist also eine Verkettung und damit Automatisierung des Kompilierungsprozesses notwendig. Unter Windows gibt es hier für Necros’ Compiling GUI, welche auch im gerade genannten Video vorgestellt und erklärt wird. Leider gibt es dieses Tool nicht für Linux (oder zumindest habe ich es nirgends gefunden ;-)). Das soll aber auch kein Problem sein, da der Editor Trenchbroom die gewollte Funktionalität selbst beherrscht. Das Setup und die Verkettung der Compiling-Tools ist vielleicht nicht intuitiv, weshalb ich das hier kurz beschreiben will:

Die notwendigen Optionen findet man unter:

Hauptmenüleiste: Run > Compile

Wählt man diese Option aus, dann öffnet sich ein neues Fenster. Hier möchten wir ein neues Profil anlegen, wozu wir auf das “+” des Containers “Profiles” klicken. Es erstellt sich ein neues Profil, welches nun einen aussagekräftigen (oder auch nicht ;-)) Namen erhält. Zudem definieren wir hier das working directory für alle involvierten Tools. Ich habe das Profil “standard” genannt und als working directory:

/home/frederic/quakedev/working/

angegeben. Ist das Profil so angelegt, können nun die einzelnen Schritte des Kompilierungsprozesses definiert werden, indem im rechten Container “Details” wiederrum das kleine “+” betätigt wird und die gewünschte Schrittart ausgewählt wird. Hier schlage ich folgende Schritte vor:

1. Export Map
Mit diesem Schritt wird die mit Trenchbroom erstellte Map in ein anderes Verzeichnis verschoben. Wir verschieben hiermit die Map-Datei in unser working directory, damit die darauf folgenden tools diese verwenden können. Der Wert im target-Feld sieht bei mir wie folgt aus:

${WORK_DIR_PATH}/${MAP_BASE_NAME}-compile.map

${WORK_DIR_PATH} und ${MAP_BASE_NAME} sind in diesem Fall Variablen, die der Editor mit den entsprechenden Werten füllt. Also für WORK_DIR_PATH wird das working directory eingesetzt, welches wir zuvor definiert haben und für MAP_BASE_NAME wird der Name der Map-Datei eingesetzt.

2. Run Tool
Als erstes Tool des eigentlichen Compilierungsprozesses definieren wir qbsp. Dies ist ein Tool, welches die Geometrie und die Kollisionsdaten der Map berechnet und als output eine .bsp-Datei zur Verfügung stellt. Man muss hier den vollständigen Pfad zur Binärdatei angeben. Ich habe folgende Werte definiert:

Pfad: /home/frederic/quakedev/tools/ericw-tools-v0.18.1-32-g6660c5f-Linux/bin/qbsp
Parameters: ${MAP_BASE_NAME}-compile.map ${MAP_BASE_NAME}.bsp

Die Werte im Parameters-Feld werden dem Tool (definiert im Pfad) so übergeben, wie also ob man das das Tool mit diesen Parametern über die command line aufrufen würden. In diesem Fall ist ${MAP_BASE_NAME}-compile.map das Map-File, welches durch den Export-Job im working directory hinterlegt wurde. ${MAP_BASE_NAME}.bsp ist der Name der output-Datei. Man beachte, dass die Parameter (wie auf der command line) durch ein Leerzeichen getrennt werden.

3. Run Tool
Als nächstes Tool definieren wir vis. Dies ist ein Tool, welches die potentiell sichtbaren Bereiche der Map berechnet.

Pfad: /home/frederic/quakedev/tools/ericw-tools-v0.18.1-32-g6660c5f-Linux/bin/vis
Parameters: ${MAP_BASE_NAME}.bsp

Als Parameter übergeben wir nur den Namen der .bsp-Datei, da die Datei mit diesem Namen gelesen und wieder geschrieben wird.

4. Run Tool
Das letzte Tool des eigentlichen Compilierungsprozesses ist light, welches – wie der Name schon vermuten lässt – die Beleuchtung der Map berechnet und in der Map hinterlegen.

Pfad: /home/frederic/quakedev/tools/ericw-tools-v0.18.1-32-g6660c5f-Linux/bin/light
Parameter: -soft -extra4 ${MAP_BASE_NAME}.bsp

Die Parameter -soft und -extra4 werden übergeben, damit die Schatten in der Map relativ weich gezeichnet werden. Es gibt hier einige mögliche Parameter, die das Ergebnis der Beleuchtungsberechnung stark beeinflussen können.

5. Copy Files
Der letzte Job ist das Kopieren der fertig kompilierten Map- bzw. .bsp-Datei in das Verzeichnis, auf das Quake später zugreifen wird.

Source: ${WORK_DIR_PATH}/${MAP_BASE_NAME}.bsp
Target: /home/frederic/quakedev/id1/maps/

Source ist eben die fertig kompilierte .bsp-Datei im working directory.
Target ist das Ziel der Kopier-Operation.

Wer genauere Beschreibungen der Tools und vorallem der übergebbaren Parameter haben möchte, der sollte sich z.B. die Dokumentation der Tools von EricW durchlesen.

Neverwinter Nights Enhanced Edition von GoG will nicht (direkt) starten

Ich hatte mir vor ein paar Tagen die Neverwinter Nights Enhanced Edition bei GoG gekauft, als so ziemlich alle D&D-Spiele im Angebot waren und weil ich einen guten D&D-Simulator haben wollte ;-).
Nun wollte ich das Spiel heute das erste Mal unter Linux Mint 19 starten, und musste leider feststellen, dass es hier noch einige Probleme gibt.
Wenn man das Spiel über den Desktop-Shortcut, welcher durch das Installationsscript erstellt wird, startet, dann startete das Spiel zunächst gar nicht. Um hier etwas mehr Informationen über die dahinter liegenden Probleme zu erhalten, kann man das Spiel über die command line starten:

~$ <installationsverzeichnis>/start.sh 

oder

~$ <installationsverzeichnis>/game/bin/linux/nwmain-linux

Zunächst wurde ich damit konfrontiert, dass anscheinend noch libs fehlen, da es sich hier um 32bit Spiel handelt. Es werden hierbei z.B. Fehler, wie folgender geworfen:

ALSA lib conf.c:3523:(snd_config_hooks_call) Cannot open shared library libasound_module_conf_pulse.so

Dies lässt sich aber schnell durch Nachinstallation der fehlenden libs beheben:

sudo apt install libopenal1:i386
sudo apt install libasound2-plugins:i386

Nach der Installation der fehlenden Bibliotheken waren zwar die Fehlermeldungen, die beim Starten über die command line geworfen wurden, verschwunden, aber ich erhielt nun einen schwarzen Bildschirm, den ich nur nach vielfachem “Raus-Tab-Versuch” verlassen durfte ;-). Was war hier nun das Problem? Nach längerer Recherche habe ich herausgefunden, dass die Linux-Version anscheinend ein Problem mit dem Full-Screen-Modus hat. Die Lösung ist relativ simpel, und wenn man es weiß auch logisch, man muss einfach den Full-Screen-Modus, der standardmäßig initial gestartet wird, deaktivieren. Dies geht ganz einfach, indem man die nwn.ini-Datei entsprechend editiert. Die findet sich in der Regel unter:

~/.local/share/Neverwinter\ Nights/nwn.ini

Hier müssen die folgenden Zeilen entsprechend angepasst werden (sofern man ein Vollbild-Erlebnis haben möchte):

FullScreen=0
FullScreenDesktopMode=1
Borderless=1

Speichert man diese Änderungen, dann startet das Spiel, wie es soll. Unter z.B. XFCE kann es sein, dass bei dieser Konfiguration die Taskleiste einen Teil des Bildes verdeckt. Das ist störend und vielleicht sogar behindernd, da hier Menüflächen des Spiels überdeckt werden. Hier kann man dann aber ganz einfach per Rechts-Klick auf die Taskschaltfläche des Spiels klicken und die Option “Immer im Vordergrund” wählen. Und TADA, wir haben ein wunderbar laufendes Spiel im Vollbild. Viel Spaß!

Hinweis:
Wie immer übernehme ich keine Verantwort oder Garantie für Veränderungen, die ihr motiviert durch diese kurze Lösungsanleitung an eurem System durchführt. Sobald man mit root-Rechten agiert, ist immer Vorsicht geboten!

Brogue 1.7.5 und die fehlenden libs

Brogue ist ein wirklich hervorragendes Roguelike, jedoch kommt es bei der aktuellen Version 1.7.5 häufig zu folgenden Fehlern beim Versuch das Spiel zu starten:

$ ./brogue
./brogue: error while loading shared libraries: libncurses.so.6: cannot open shared object file: No such file or directory
./brogue: error while loading shared libraries: libtinfo.so.6: cannot open shared object file: No such file or directory

Hier versucht Brogue libraries zu verwenden, die so nicht auf dem System vorhanden sind. Sucht man dann nach der fehlenden lib:

$ find / -iname "*ncurses*"
[...]
/lib/x86_64-linux-gnu/libncurses.so.5.9

findet man meist die lib in einer anderen Version.
Da Brogue nicht zwingend die lib in der Version 6 benötigt, gibt es eine einfache und schnelle Lösung zu diesem Problem. Man erstellt einfach einen symbolic link auf die vorhandene lib:

$ sudo ln -s /lib/x86_64-linux-gnu/libncurses.so.5.9 /lib/x86_64-linux-gnu/libncurses.so.6
$ sudo ln -s /lib/x86_64-linux-gnu/libtinfo.so.5.9 /lib/x86_64-linux-gnu/libtinfo.so.6

Startet man nun Brogue:

$ ./brogue

Startet das Spiel und man kann in die Tiefen der Dungeons of Doom hinabsteigen, um das Amulet von Yendor zu suchen.

Hinweis:
Da bei der Erstellung der symbolic links mit sudo, also mit root-Rechten hantiert wird, ist natürlich Vorsicht geboten. Man kann hier leicht etwas kaputt machen. Gerade bei der Reihenfolge der Parameter von ln kann man schnell durcheinanderkommen. Ich übernehme keine Garantie und Verantwortung dafür, dass die hier vorgeschlagene Lösung Schäden verursacht oder ungewünschte Nebeneffekte mitsich bringt.