Froschs Blog

Computer und was das Leben sonst noch so zu bieten hat

Zur Website | Impressum

Python: (Fast) alles ist eine Liste

16. November 2019 um 23:11 Uhr von Atari-Frosch

„In Python ist alles eine Liste“, erklärte mir zu Beginn Bison aus dem Chaosdorf, als ich gerade anfing, Python zu lernen. An die Denkweise konnte ich mich gut gewöhnen. In den letzten Monaten ist sie mir allerdings in einem, ähm, speziellen Fall so richtig auf die Füße gefallen, und zwar im Zusammenhang mit Feldabfragen in MySQL. Heute habe ich endlich die Lösung gefunden.

Seit einiger Zeit bauen mir die Scripte aus meinem Projekt htmlglue meine statische Website zusammen. Dabei bekommt die Startseite jedes Mal ein anderes Zufallsfoto, dessen Daten aus der Datenbank meiner Media-Website gezogen werden; die Bilder selbst stehen natürlich nicht in der Datenbank, aber die Datenbank kennt den jeweiligen Pfad im Filesystem. Die Media-Website läuft unter Piwigo, und dessen Datenbank ist, wie üblich, in MySQL angelegt (ich verwende allerdings MariaDB). Und mit den zusätzlichen Informationen zu den Bildern hatte ich gelegentlich so ein kleines, größeres Problem.

In den meisten Fällen funktionierte das Ganze bereits. Nur bei den Bildern, bei denen die Lizenz nicht gesetzt war, brach das Konfigurationsscript der Startseite und damit auch makesite.py ab und hinterließ die Startseite mit dem neuen Foto, aber den Daten für das vorherige. Denn der Link zu dem ausgewählten Foto wird als erstes aus der Datenbank gezogen, das Foto eingelesen, herunterskaliert und mit einem fest eingetragenen Dateinamen ins Bilderverzeichnis der Site abgelegt. Mit dem Abbruch der Bearbeitung wurde aber der Content-Teil der Startseite nicht neu erzeugt. Dafür bekam ich per Mail eine Fehlermeldung, die besagte, daß die ausgelesene Lizenz für das Foto nicht verarbeitet werden konnte. Genauer: TypeError: 'NoneType' object is not subscriptable.

Ich schaute mir die betreffende Datenbanktabelle mal genauer an:

MariaDB [x]> show columns from pwg_copyrights_media;
+----------+---------+------+-----+---------+-------+
| Field    | Type    | Null | Key | Default | Extra |
+----------+---------+------+-----+---------+-------+
| media_id | int(11) | NO   | PRI | NULL    |       |
| cr_id    | int(11) | NO   |     | NULL    |       |
+----------+---------+------+-----+---------+-------+

[x] ist der Name der Datenbank. Den muß ich ja nicht verraten. 🙂

OK. Wenn also keine Lizenz eingetragen wird, dann nimmt das Feld cr_id den Wert NULL an. Das ist eine etwas schräge Sache: NULL heißt, das Feld ist in einem undefinierten Zustand. In Python heißt das dann „None“.

Also sollte man meinen, wenn das Feld nicht gefüllt wurde, bekomme ich bei Abfrage des Feldes (für einen vorher gewählten Wert für media_id) eben NULL bzw. None zurück, und wenn ich das in der Variablen erkenne, kann ich einen generischen Output erzeugen, zum Beispiel „All rights reserved“. Also, theoretisch. In der Praxis flog das Script halt jedesmal ab, wenn der Feldinhalt NULL war, und ich hatte eine nicht zusammenpassende Kombination aus Foto und Beschreibung auf der Webseite stehen.

Was mich dabei am meisten irritierte, war die Tatsache, daß ich an anderen Stellen ebenfalls NULL-Werte zurück bekam und Python das völlig in Ordnung fand. Hier zum Beispiel:

if picinfo[3] == None:
    picdata['descr'] = " "
else:
    picdata['descr'] = picinfo[3].replace("\r\n", "<br />")

picinfo ist eine Liste, die aus einer Abfrage aus der Haupttabelle von Piwigo resultiert. Wenn an vierter Stelle dieser Liste (Python fängt bei Listen bei 0 an zu zählen) ein NULL = None rausfällt, dann ersetze das durch einen Non-Breaking-Space; ansonsten ersetze alle DOS-Zeilenumbrüche, die Piwigo in seinen Beschreibungen für Zeilenumbrüche benutzt, durch einen einfachen HTML-Zeilenumbruch. Der Output geht ja schließlich in ein HTML-Dokument, das kann mit DOS-Zeilenumbrüchen nichts anfangen und würde alle Zeilen trotzdem hintereinander schreiben.

Für die meisten Bilder habe ich noch keine Beschreibungen eingegeben, das Feld ist also üblicherweise NULL. Und Python so: Öhm ja, da kommt NULL raus, und?

Vor etwa einem Monat fragte ich dann mal ausführlicher auf Pluspora nach: Feldinhalt NULL aus MySQL in Python3 vergleichen und bekam schließlich eine plausibel klingende Antwort: Statt eines einfachen SELECT sollte ich das Feld mit SELECT COALESCE abfragen und diesem eine Liste mitgeben, die aus dem Feldnamen und dem Wert '0' besteht. Denn COALESCE heißt, daß der erste Nicht-NULL-Wert aus der Liste übergeben wird. Diese Lösung übernahm ich und …

… vor einer guten Woche bekam ich die altbekannte Fehlermeldung wieder in die Mail.

Das war's also nicht.

Heute ergänzte ich erst einmal in der Python-Konfigurationsdatei für die Startseite, in der das Modul eingesetzt wird, das Logging. Praktischerweise mußte ich dafür nur das Core-Modul logging importieren, ohne nochmal ausführlich einzutragen, was ich genau wie geloggt haben will; denn die Log-Einträge flossen danach wie durch Zauberhand direkt in das Log, das ich bereits von makesite.py schreiben ließ. Damit konnte ich das Problem weiter eingrenzen.

Schließlich kam ich sozusagen langsam tastend auf die Lösung: In den anderen Fällen frage ich jeweils mehrere Felder aus einer Tabelle in einem Rutsch ab. Bei der Lizenz frage ich jedoch nur ein Feld aus der weiter oben gezeigten separaten Tabelle ab: Die Angabe einer Lizenz ist im Piwigo-Core nicht vorgesehen, sondern wird erst durch ein Plugin ermöglicht, das sich dafür seine eigene Tabelle in der Piwigo-Datenbank anlegt.

Diese Abfrage sah zunächst so aus:

getlicense = "SELECT cr_id FROM pwg_copyrights_media WHERE media_id = " + photo
logging.debug('SQL statement for license code is %s', getlicense)
db = pymysql.connect(dbhost, dbuser, dbpass, dbname)
cursor = db.cursor()
cursor.execute(getlicense)
l = cursor.fetchone()
logging.debug('SQL answer to statement is %s', l)
db.close()

logging.debug('License code for %s is %s.', photo, l[0])
if l[0] == None:
    licensename = "All rights reserved."
elif l[0] == 1:
    licensename = "CC-BY"
elif l[0] == 2:
    licensename = "CC-BY-SA"
elif l[0] == 3:
    licensename = "CC-BY-ND"
elif l[0] == 4:
    licensename = "CC-BY-NC"
elif l[0] == 5:
    licensename = "CC-BY-NC-SA"
elif l[0] == 6:
    licensename = "CC-BY-NC-ND"

Als ich das Logging noch nicht drinstehen hatte, flog mir dieses Script beim ersten if-Statement ab. Nach Ergänzung des Loggings flog es dann schon in der Zeile obendrüber raus.

Die erzeugten Logeinträge lieferten mir dann endlich die Lösung:

DEBUG SQL statement for license code is SELECT cr_id FROM pwg_copyrights_media WHERE media_id = 6025
DEBUG SQL answer to statement is (5,)
DEBUG License code for 6025 is (5,).

Wenn da eine Zahl rausfällt, ist es tatsächlich eine Liste. Das erkennt man an den Klammern und dem Komma, das der Zahl folgt. Wenn aber NULL rausfällt, ist es keine.

Die Abfrage der Variablen l muß also anders ablaufen:

logging.debug('License code for %s is %s.', photo, l)
if l == None:
    licensename = "All rights reserved."
elif l[0] == 1:
    licensename = "CC-BY"
elif l[0] == 2:
    licensename = "CC-BY-SA"
elif l[0] == 3:
    licensename = "CC-BY-ND"
elif l[0] == 4:
    licensename = "CC-BY-NC"
elif l[0] == 5:
    licensename = "CC-BY-NC-SA"
elif l[0] == 6:
    licensename = "CC-BY-NC-ND"
else:
    licensename = "unknown"

Zunächst frage ich also l ab wie eine einfache Variable, um herauszufinden, ob da ein None zurückgekommen ist. Wenn das nicht der Fall ist, frage ich dieselbe Variable explizit als Liste (mit nur einem Eintrag) ab – und bekomme den darin gespeicherten Wert. Das else: am Ende kann ich mir vermutlich sparen, denn etwas anderes als NULL oder eine Zahl zwischen 1 und 6 kann in diesem Fall eigentlich nicht herausfallen.

Fazit: Wenn (zumindest aus Python heraus) ein einzelnes Feld aus einer MySQL-Datenbanktabelle abgefragt wird, ist der Rückgabewert eine einfache Variable, sofern der Feldinhalt NULL ist, aber eine Liste mit nur einem Eintrag, wenn er nicht NULL ist.

Zieht man dagegen mehrere Felder zugleich aus einer Tabelle und mindestens eine davon ist NULL, dann kann dieser Wert als „None“ in Python ganz problemlos abgefragt und verglichen werden.

So richtig logisch finde ich das jetzt nicht.

6 Kommentare zu “Python: (Fast) alles ist eine Liste”

  1. Daniel Rehbein quakte:

    Die Programmiersprache, in der alles eine Liste ist, heißt Lisp, nicht Python.


  2. Fritz Alfred Kersten quakte:

    Hat Python kein switch / case ?
    Diese if-elif-else-Kaskade sieht ja nicht gerade toll aus.


  3. Atari-Frosch quakte:

    @„Fritz Alfred Kersten“: Das hier noch, trotz Block, weil der Kommentar Unsinn ist. Ich kenne CASE aus GfA-Basic (Atari ST). Mehrere Werte abzufragen ist – auch mit CASE – Schreibarbeit. In GfA-Basic würde der Code etwa so aussehen:

    SELECT l% # angenommen, l sei hier ein Integer)
    CASE 1
      licensename$ = "CC-BY"
    CASE 2
      licensename$ = "CC-BY-SA"
    CASE 3
      licensename$ = "CC-BY-ND"
    CASE 4
      licensename$ = "CC-BY-NC"
    CASE 5
      licensename$ = "CC-BY-NC-SA"
    CASE 6
      licensename$ = "CC-BY-NC-ND"
    DEFAULT
      licensename$ = All rights reserved."
    ENDSELECT

    Mit einem anderen Variablentyp in l wäre GfA-Basic allerdings nicht klargekommen; Variablen sind da klar gekennzeichnet: zum Beispiel steht % hinter dem Variablennamen für Integer, $ für String.


  4. Daniel Rehbein quakte:

    Ich vermute, daß es Fritz Alfred Kersten um die Ästhetik des Codes ging. Das kenne ich durchaus auch von mir, daß ich mit Code nicht glücklich bin, wenn dieser nicht schön aussieht. Vor allem gilt das für Code, den ich selbst geschrieben habe, aber auch bei fremden Code stellt sich bei mir ein Empfinden ein, ob ich Sympathie für den Code habe oder nicht.

    Die Beurteilung von Schönheit oder Ästhetik ist natürlich immer eine subjektive Angelegenheit. Aber gerade deshalb finde ich es durchaus interessant, mal Meinungen dazu zu lesen, welchen Code andere Menschen als schön empfinden und welchen nicht.

    Von meinem persönlichen Empfinden kommt wohl auch meine Liebe zu der Sprache Lisp, weil ich diese Struktur der ineinander verschachtelten runden Klammern als sehr ästhetisch empfinde.

    In der Sprache Python gibt es tatsächlich kein Case- oder Switch-Statement wie in anderen Sprachen. Dafür gibt es aber das Konstrukt „elif“, was viele andere Sprachen nicht kennen. In anderen Sprachen müsste ich das mit „else if“ formulieren und hätte dann eine ständige Vergrößerung der Einrückungstiefe im formatierten Code, was ich als nicht schön empfinde. Das „elif“ führt dagegen zu bündigem Code, was wieder ein schön aussehender Code aus.

    Mir fällt allerdings auf, daß in der Abfrage bei allen Möglichkeiten fast dasselbe passiert: Es wird immer derselbe Variable ein Text zugewiesen, nur dessen Inhalt unterscheidet sich. Wäre das dann nicht schöner mit einem Array (bzw. in Python mit einer Liste) implementiert?

    Oder gibt es vielleicht sogar schon eine Methode, die zu der Zahl den Lizenztext zurückgibt? Wenn das Zusatzmodul zu Piwigo die zusätzliche Datenbanktabelle „pwg_copyrights_media“ anlegt, in der die Lizenzen als Zahlenwerte abgespeichert werden, müsste dieses Zusatzmodul dann nicht auch über eine Methode verfügen, die zu einer Lizenz den Zahlenwert ermittelt, und entsprechend auch über eine Methode verfügen, die zu dem Zahlenwert die Lizenz zurückgibt?


  5. Atari-Frosch quakte:

    @Daniel: Wieso sollte es mit „else if“ eine ständige Vergrößerung der Einrückungstiefe geben? „elif“ entspricht „else if“, das ist nur eine andere Schreibweise in anderen Sprachen für dasselbe Statement.

    Ja, ich könnte auch ein Dictionary bauen, das jedem Wert den entsprechenden String zuweist:

    license = {}
    license[1] = "CC-BY"
    license[2] = "CC-BY-SA"
    license[3] = "CC-BY-ND"

    usw. Weniger Schreibarbeit wäre das jetzt auch nicht, denn ich muß ja vorher das Dict aufschreiben. Was ich nicht beurteilen kann, ist, ob die elif-Folge oder ein Dictionary zeiteffizienter wäre. Die gesamte Site mit etwa 5 MB Content (ohne Bilder) baut makesite.py in schätzungsweise 10 – 15 Sekunden auf. Ob da eine Abfrage aus einem Untermodul jetzt 1/100 Sekunde langsamer oder schneller ist, kann mir da herzlich egal sein. Python ist sowieso nicht gerade eine Sprache für zeitkritische Anwendungen.

    Piwigo ist in PHP geschrieben. Bestimmt gibt es dort im Lizenz-Plugin eine Methode für das Auslesen und Zuordnen der Lizenzen, aber in Python nützt die mir eher wenig.

    Was die Schönheit des Codes angeht: Darauf lege ich durchaus auch großen Wert, allein schon deshalb, weil ich meine Ergüsse ja später auch wieder lesen können möchte, wenn ich was verändern will, und das, ohne daß ich das Bedürfnis verspüre, einfach alles nochmal neu zu machen. Du kannst Dir die gesamten Scripte ja gerne anschauen, sie liegen frei zugänglich auf meinem Gitweb. Generell finde ich es keine Mühe, in Python schönen Code zu produzieren (weshalb ich Python auch letztendlich PHP vorgezogen habe). Das heißt leider nicht, daß es alle tun: Ich habe auch schon fremden Python-Code gesehen, den ich völlig unlesbar fand.

    Was die Klammern in anderen Sprachen angeht: Ich bin ja schon genervt von den (allerdings geschweiften) Klammern in den Konfigurationsdateien für nginx. Da bin ich ganz froh, daß ich sowas in Python nicht auch noch brauche.

    Aber das Schöne ist ja, daß man die Wahl hat, nicht wahr? 🙂


  6. Daniel Rehbein quakte:

    Wenn Du die Lizenztexte als Dictionary aufbaust, dann musst Du tatsächlich für jeden Wert den Variablennamen erneut hinschreiben. Ein Dictionary bräuchtest Du aber nur, wenn die Zahlenwerte beliebig sind, tatsächlich sind sie aber in diesem Fall aufeinanderfolgende und bei Eins beginnende Werte. Du kannst also eine Liste hinschreiben, in der die Lizenztexte einfach mit Komma hintereinanderstehen.

    Wenn Du statt „elif“ die Formulierung „else if“ wählst, steht ja das „if“ nicht mehr linksbündig, sondern ist um fünf Zeichen eingerückt. Da in der Regel Wörter wie „then“, „else“ und „endif“ (sofern es sie in der jeweiligen Sprache gibt) bündig unter das „if“ geschrieben würde, stände nach einem „else if“ das nächste „else if“ um fünf Zeichen eingerückt, das darauf folgende „else if“ schon um 10 Zeichen eingerückt, und so weiter. Bei „elif“ dagegen steht das nächste „else“ oder „elif“ nicht bündig unter dem Wortbestandteil „if“, sondern bündig unter dem gesamten Wort „elif“ (weil dieses kein Leerzeichen enthält, sondern ein zusammenhängendes Wort ist), und somit bleibt die gesamte Konstruktion bündig am Zeilenanfang.


Kommentieren

Bitte beachte die Kommentarregeln!

XHTML: Du kannst diese Tags verwenden: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>