Freitag, 30. Dezember 2022

ein snap ausprobieren / testweise installieren

ein snap testen

Hinweis: das im folgenden beschriebene Vorgehen ist explizit mit Ubuntu 22.04 LTS durchgeführt worden. Da snapd, der Daemon, der für snaps zuständig ist, aber von Canonical für alle unterstützen Ubuntuversionen gleichermaßen gepflegt wird, sollte das Vorgehen auch für andere Ubuntuversion funktionieren. Ebenso sollte das Vorgehen für andere Linuxdistributionen funktionieren, auf denen snapd installiert ist.

snap, das Paketformat von Canonical erlaubt es, ein Programm auszuprobieren, ohne dass das snap komplett installiert werden mus.

Im Vergleich zu einer normalen Installation eines snaps über snap install unterscheidet sich dieses Verfahren in vier Punkten:

  1. Es werden keine Konfigurationsdateien im Homeverzeichnis des Nutzers unter ~/.config angelegt - es werden aber trotzdem Dateien unter ~/snap für das snap angelegt, das man testet.
  2. Das snap wird bei einem snap refresh nicht aktualisiert.
  3. Man könnte Dateien, die zum snap gehören, zur Laufzeit des snaps editieren und diese Änderung würden beim Start des Programms berücksichtigt. Was sich aber nicht ändern lässt ist das Confinement sowie die Connections eines snaps. 1.. Wenn man das Programm über die Kommandozeile startet, erhält man dort gegebenenfalls die Ausgaben des Programms wie Fehler- oder Warnmeldung. Dies kann bei der Fehlersuche hilfreich sein, wenn ein über ein snap installiertes Programm Probleme machen sollte.

Hinweis: Diese Verfahren ist dazu gedacht, Programme auszuprobieren, die noch nicht regulär installiert sind. Möchte man zwei verschiedene Versionen eines snap parallel nutzen sind Hinweis dazu im Blogartikel snaps parallel installieren zu finden.

Vorgehen

Im folgenden wird das Inkscape snap für das Vektorzeichenprogramm Inkscape als Beispiel genutzt. Die snap Version ist 10512. Für andere snaps sind einfach der Name und die Versionsnummer entsprechend anzupassen. Für alle folgenden Schritte gehe ich davon aus, dass man sich in seinem eigenen Homeverzeichnis ~/ befindet.

Die folgenden Befehle müssen alle im Terminal ausgeführt werden, teilweise sind dafür Root-Rechte notwendig.

Herunterladen und testweise einbinden

Zuerst lädt man das snap herunter, ohne es zu installieren, mit Hilfe des Befehls download:

$ snap download inkscape

Dabei werden zwei Dateien heruntergeladen: das snap an sich, in diesem Beispiel namens inkscape_10512.snap und die zugehörige Assertion-Datei (Verifizierungsdatei) inkscape_10512.assert, welche die kryptographischen Schlüssel und Validierungsdaten enthält, die bei einer Installation des snaps geprüft würden. "10512" ist hierbei die Versionsnummer zur Zeit des Downloads aktuelle, stabilen snaps. Der Befehl download kennt auch ein Option für Channels, d.h. man kann auch eine Beta- oder Testversion herunterladen - je nach dem was für das jeweilige snap bereit gestellt wird.

snaps in ein SquashFS Dateisystem verpackt, welches zuerst in ein Verzeichnis entpackt werden muss. Das notwendige Programm Namens unsquashfs ist bei Ubuntu standardmäßig installiert (wenn zuerst nein muss über die Paketverwaltung zuerst das Paket "squashfs-tools" installiert werden):

$ unsquashfs inkscape_10512.snap

Im Verzeichnis ist jetzt ein neues Unterverzeichnis namens squashfs-root angelegt worden, welches das entpackte snap enthält. Bei Interesse kann man durch das Verzeichnis und dessen Unterverzeichnisse navigieren, um zu sehen, wo welche Dateien liegen.

Zum Ausprobieren von Inkscape muss man den folgenden Befehl ausführen:

$ sudo snap try squashfs-root

Auch, wenn man sich im eigenen Homeverzeichnis befindet, sind Root-Rechte notwendig. Anschließend kann Inkscape regulär im Terminal, die GNOME-Shell bzw. Weg der jeweiligen Desktopumgebung den gestartet werden.

Das testweise laufende snap erscheint jetzt auch in der Liste des installierten snaps, auch wenn es nicht richtig installiert ist:

$ snap list

Name             Version                           Revision  Tracking    Herausgeber  Hinweise  
...  
inkscape         1.2.2-b0a8486541-2022-12-01-cust  x1        -           -            try  
...  

Die Revisionsnummer für snap zum Ausprobieren ist x1 und in der Spalte "Hinweis" ist zu sehen, dass das Inkscape snap im try-Modus ist.

Ein so eingebundenes snap wird auch bei einem Systemneustart wieder eingebunden, wie andere, regulär installierte snaps auch. Das deaktivieren / deinstallieren wird im folgenden Abschnitt beschrieben.

snap wieder entfernen

Das testweise installierte snap kann wie ein reguläres snap deinstalliert werden:

$ snap remove inkscape

Danach kann man das Verzeichnis ~/squashfs-root und das Verzeichnis inklusive Unterverzeichnissen ~/snap/inkscape löschen. Des Weiteren kann man die heruntergeladenen Dateien inkscape_10512.snap und inkscape_10512.assert löschen.

Was tun, wenn versehentlich auch ein produktiv genutztes snap ausprobiert wurde?

Hat man - versehentlich oder bewusst - ein snap testweise installiert, welches in einer anderen Version schon regulär installiert war (z.B. das Firefox snap), dann wird regulär installierte gegebenenfalls durch das testweise installierte ersetzt. Dies lässt sich aber einfach rückgängig machen, im folgenden am Beispiel des Firefox snaps gezeigt:

Zuerst prüft man, ob noch eine reguläre Version des snap installiert ist. Dies sollte normalerweise der Fall sein, da immer mindestens eine ältere Version eines snap noch vorhanden ist:

$ snap list firefox --all

...  
firefox  107.0.1-1  2154      latest/stable  mozilla✓     deaktiviert,try  
firefox  108.0b9-1  x1        latest/stable  -            try  
...  

Die Ausgabe zeigt, das es die testweise installierte Version x1 und eine weitere, reguläre Version 2154 auf dem System gibt. Mit den folgenden beiden Befehlen kehrt man zur regulären Version zurück und löscht das snap der Testversion:

$ sudo snap revert firefox $ sudo snap remove firefox --revision=x1

Für das vollständige Entfernen des testweise installierten Version löscht man noch die Dateien wie weiter oben im Abschnitt "snap wieder entfernen" beschrieben.

Dienstag, 27. Dezember 2022

snaps parallel installieren

snaps parallel installieren

Hinweis: das im folgenden beschriebene Vorgehen ist explizit mit Ubuntu 22.04 LTS durchgeführt worden. Da snapd, der Daemon, der für snaps zuständig ist, aber von Canonical für alle unterstützen Ubuntuversionen gleichermaßen gepflegt wird, sollte das Vorgehen auch für andere Ubuntuversion funktionieren. Ebenso sollte das Vorgehen für andere Linuxdistributionen funktionieren, auf denen snapd installiert ist.

snap, das Paketformat von Canonical erlaubt es, mehrere Versionen eines Programms parallel zu installieren. Die Voraussetzung ist dabei, dass die snaps aus verschiedenen Channels stammen. Verschiedene Channels können z.B. eine Extend Support Version eines Programms sein und der reguläre / aktuelle, stabile Release.

Verschiedene Channels sind aber in der Regel nicht die aktuelle, stabile Version ein Programms und die Betaversion, da diese bei vielen / den meisten snaps im selben Track sind und damit nicht parallel installierbar

Warum parallel installieren?

Ob man ein snap in mehreren Version parallel installieren möchte (oder muss), muss man letztenslich für sich selber entschieden. Wirklich nötig ist es - zumindest in meinen Augen - wohl eher selten. Interessant ist es vielleicht für diejenigen, die zu Testzwecken mehrere Versionen eines Programms benötigen, wie z.B. den auch im folgenden als Beispiel verwendeten Firefox in der jeweils aktuellen und ESR-Version.

Risiko einer Parallelinstallation

Das Risiko kann, je nach snap, sein, dass die eine Version des snaps Konfigurationsdateien des anderen snap überschreibt oder ändert, was zu Problemen führen könnte (aber nicht muss).

Vorgehen

Im folgenden wird als Beispiel das Firefox snap aus dem "stable/latest" (=das standardmäßig installiere) und dem "esr/latest" Track/Channel, so dass zwischen der jeweils aktuellsten und aktuellsten ESR (=Extended Support Release) Version gewechselt werden kann. Das Vorgehen ist auf andere snap und andere Channels übertragbar.

Der Wechsel erfolgt über Befehle, die im Terminal eingegeben werden müssen. Zum Wechsel zwischen zwei Versionen dient der Befehl

$ snap switch --channel=TRACK/CHANNEL NAME_DES_SNAPS

Nach dem Ausführen von switch muss immer anschließend noch ein

$ snap refresh

ausgeführt werden. Zum Wechsel auf den stabilen ESR-Track des Firefox führt man die Befehle

$ snap switch --channel=esr/stable firefox $ snap refresh

aus. Da die ESR-Version des Firefox noch nicht installiert ist, wird diese heruntergeladen und aktiviert. Gleichzeitig wird der latest/stable Firefox deaktiviert, aber nicht deinstalliert. Beim nächsten Start des Firefox nutzt man die ESR Version.

Um wieder zum latest/stable Track zurück zu wechseln führt man wieder die beiden Befehle

$ snap switch --channel=lastest/stable firefox $snap refresh

aus.

einen Channel wieder deinstallieren

Möchte man doch nur einen Channel, im folgenden latest/stable nutzen und die Programmversion des anderen deinstallieren, dann wechselt man zuerst in den zu deinstallierenden Channel

$ snap switch --channel=esr/stable firefox $ snap refresh

und deinstalliert dann den Firefox

$ snap remove firefox $ snap switch --channel=latest/stable firefox $ snap refresh

Anschließend muss der Firefox aus dem latest/stable Channel gegebenenfalls noch aktiviert werden. Steht in der Ausgabe von

$ snap list firefox --all

in der Spalte "Hinweise" der Eintrag disabled, dann führt man noch den Befehl

$ snap activate firefox $ snap refresh

aus.

Mittwoch, 8. Juni 2022

Python und Geschwindigkeit auf verschiedenen Intel und AMD CPUs - und einem Apple Bionic Chip

Vor ein paar Jahren gab es hier im Blog einen Post, der die Geschwindigkeit eines Pythonprogramms zum Number-Crunching (in Form eines Primzahlentests) beschriebt (Link zum Artikel).

Ich hatte jetzt kürzliche die Gelegenheit, die Ausführungsgeschwindigkeit auf vier verschiedenen Mobil CPUs verschiedener Hersteller und Generation zu vergleichen, nämlich:

Die Prozessoren sind also eine zum Zeitpunkt des Schreibens des Artikels eine ca. 7 Jahre alte CPU (der Core i5-5200U, erschienen 2015), ein Core i7 Prozessor der 10. Generation (erschienen zweite Jahreshälfte 2019), ein Core i7 Prozessor der 11. Generation (erschienen 2. Jahreshälfte 2021)  sowie die ebenfalls 2021 erschienene Ryzen 7 5700U CPU.

Außerdem habe ich noch einen Test auf einem iPad Air gemacht. Mehr dazu am Ende des Artikels.

Verglichen habe ich nur die Ausführungsgeschwindigkeit des Python-Programms ohne Optimierungen / Beschleunigungen, also ohne PyPy, Cython, JIT Compiler etc.

Als Betriebssystem diente für den Core i5-5200U und den AMD Ryzen 7 5700U Ubuntu 22.04 mit Python 3.10.4 aus den offiziellen Ubuntu Quelle und für die beiden Intel Core i7 Windows 11, ebenfalls mit Python 3.10.4 aus dem Windows App Store, also die Python-Version, die direkt von der PSF bereit gestellt wird.
Das ganze wurde immer bei ruhendem Desktop gemessen, d.h. außer dem Terminal, in dem das Python-Programm läuft, läuft kein weiteres Programm. Es wurde jedes Skript 3x ausgeführt, die weiter unten auf geführten Zeitangabe sind der Mittelwert der Ausführungszeiten.

Als erste habe ich die Ausführungsgeschwindigkeit mit nur einem Prozess gemessen, das Programm sieht wie folgt aus:

import math
from time import time

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419,
    777777722155555333,
    777777722155555335,
    9999999900000001,
    2327074306453592351,
    2327074306453592353]

def is_prime(n):
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    for number in PRIMES:
        prime = is_prime(number)
        print('{} is prime: {}'.format(number, prime))

if __name__ == '__main__':
    start = time()
    main()
    end = time()
    print(f'Time: {end - start}')

Die Ergebnisse sind wie folgt:

  • Core i5-5200U: 204 Sekunden
  • Core i7-10510U: 111 Sekunden
  • Core i7-1165G7: 68 Sekunden
  • Ryzen 7 5700U: 78 Sekunden

Wie zu erwarten war, sind die neueren CPUs deutlich schneller. Interessant ist auch die relative große Unterschied zwischen dem Core i7 10. Generation und 11. Generation, letzterer ist etwas 60% schneller. Hier hat Intel deutlich in Sachen Leistung nachgelegt, zumindest was die Performance von nur einem Kern angeht.
Zweite Beobachtung: im weiter oben verlinkten Blogpost wurde der gleiche Code ebenfalls auf dem gleichen Core i5-5200U ausgeführt, nur damals unter Python 3.6. Die Geschwindigkeit war damals quasi identisch mit der hier gemessenen. Auch wenn Python (bzw. genau genommen die Referenzimplementierung CPython) einige Verbesserungen in Sachen Geschwindigkeit bekommen hat, spielen diese zumindest für dieses Szenario keine Rolle, der Code ist auf der gleichen CPU bzw. dem gleichen Rechner wie damals quasi gleich schnell.

Im zweiten Test wurde das Programm modifiziert, so dass mit Hilfe des concurent.futures Moduls mehrere Prozesses zum Rechnen gestartet. Da die Core i5 5200U CPU und auch die neueren Core i7 CPUs "nur" vier Kerne haben, wurde als Prozessanzahl vier gewählt, damit im Idealfall jeder Prozess auf einem Kern läuft.

Der Code sieht wie folgt aus:

import math
from time import time
import concurrent.futures

WORKERS = 4

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419,
    777777722155555333,
    777777722155555335,
    9999999900000001,
    2327074306453592351,
    2327074306453592353]

def is_prime(n):
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    with concurrent.futures.ProcessPoolExecutor(max_workers=WORKERS) as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print('%d is prime: %s' % (number, prime))


if __name__ == '__main__':
    print(f'using {WORKERS} workers for calculation')
    start = time()
    main()
    end = time()
    print(f'Time: {end - start}')

Die Ergebnisse für den Multiprozessansatz sind:

  • Core i5-5200U: 111 Sekunden
  • Core i7-10510U: 89 Sekunden
  • Core i7-1165G7: 50 Sekunden
  • Ryzen 7 5700U: 51 Sekunden

Auch hier ist der Leistungsunterschied zwischen dem Core i7 10. und 11. Generation erheblich, der neuere ist ca. 80% schneller. Der Core i7-1168G7 und der Ryzen 7 5700U sind quasi gleich schnell.

Ein zusätzlicher Test mit dem Ryzen 7 und acht Workern brachte keine weitere Beschleunigung, das Ergebnis war quasi identisch mit dem mit vier Workern. Dies liegt im gegebenen Test aber ziemlich sicher daran, dass die ersten sechs Prüfungen relativ schnell abgeschlossen sind, d.h. man zieht für die restlichen fünf Zahlen keinen echten Nutzen aus mehr Kernen, weil nicht mehr so viele Prozesse laufen.

Festzuhalten bleibt aber auch: natürlich sind neuere Prozessoren schneller, aber bringen (bei weitem) nicht so viel wie der Einsatz von schnelleren Python-Implementierungen und JIT Compilern.
Alleine der simple Einsatz von PyPy statt CPython auf dem "alten" Core i5 Prozessor führt den Code doppelt so schnell aus wie die Core i7 CPU der 11. Generation oder der Ryzen 7 5700U.

Zusätzlich zu den diversen Intel und der AMD CPU habe ich den nicht-parallelen Code auf einem iPad Air 3. Generation, iOS 15.5 und Python 3.6.1 aus der Pythonista ausgeführt. Dieses iPad hat eine A12 Bionic CPU. Der parallelisierte Code funktioniert ja leider nicht unter iOS, weil, zumindest zum Zeitpunkt des Schreibens des Artikels, ein Prozess keine weiteren Prozesse starten darf.

Der A12 Bionic braucht für den obigen Code ca. 104 Sekunden - und ist damit ca. 10% schneller der getestet Intel Core i7 der 10. Generation. Schon ganz ordentlich für einen Prozessor, der ca. ein Jahr vor dem Intel Prozessor erschienen ist, wobei allerdings auch der Takt der leistungsstärkeren Kerne des A13 mit 2,,49 GHz (deutlich) höher ist als die 1,80 GHz Core i7-10510U.

Donnerstag, 18. Januar 2018

Python und Geschwindigkeit (oder: Beschleunigung ist ganz einfach)

Es heißt ja schon mal gerne "Python ist langsam". Mal abgesehen davon, dass solch pauschale Aussagen generell fragwürdig sind, ist in der Aussage ein entscheidender Fehler: Python beschreibt die (Programmier-) Sprache an sich - aber es gibt diverse Implementierung, die sich in Sachen Geschwindigkeit teilweise erheblich unterscheiden.

Richtig ist, dass im allgemeinen "Python" mit der Referenzimplementierung CPython gleichgesetzt wird, welches die (mit Abstand) am meisten genutzte Implementierung ist. Auch verwende ich hier im Blog in der Regel das Wort Python als Synonym für CPython.

CPython arbeitet Code in zwei Schritten ab: zuerst wird der Quellcode in (plattformunabhängigen) Bytecode übersetzt, mit Hilfe eines Compilers. Der Bytecode wird dann in einer virtuellen Maschine (nicht zu verwechseln mit VirtualBox, VMware & Co) Anweisung für Anweisung ausgeführt. Und CPython ist in der Tat nicht auf Geschwindigkeit optimiert, der Fokus der Entwicklung liegt auf anderen Punkten.

Im folgenden geht es darum, wie man die Ausführung von Python-Skripten beschleunigen kann. Dazu sei noch angemerkt: Es werden im folgenden in erster Linie einige Möglichkeiten gezeigt. Es geht nicht darum, jede noch mögliche (Mikro-) Optimierung zu nutzen, um noch die ein oder andere Zehntelsekunde extra heraus zu holen.

Auch habe ich keine (aufwendigen) Benchmarks genutzt, sondern einfach nur einen recht rechenintensiven Code zur Prüfung von Primzahlen. Die absoluten Werte der Laufzeit des Codes sind auch sekundär, interessanter ist der relative Unterschied zwischen den verschieden Methoden der Ausführung. Alle Messungen wurden auf meinem Laptop gemacht, welcher einen Intel® Core™ i5-5200U CPU Prozessor mit 2.20 GHz und vier Kernen hat. Das Betriebssystem ist Ubuntu 16.04 (mit ruhendem Desktop, d.h. es liefen keine weiteren Programme), die CPython-Version ist Python 3.5.3, was Ubuntu 16.04 standardmäßig installiert hat.

Der Code ist an ein Beispiel aus der Python-Doku angelehnt:

import math

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419,
    777777722155555333,
    777777722155555335,
    9999999900000001,
    2327074306453592351,
    2327074306453592353]

def is_prime(n):
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    for number in PRIMES:
        prime = is_prime(number)
        print('{} is prime: {}'.format(number, prime))

if __name__ == '__main__':
    main()

Es wird also einfach nur für elf Zahlen geprüft, ob diese eine Primzahl sind (Tipp: wer diesen Code auf einem langsamen Rechner wie z.B. dem Raspberry Pi ausführen möchte, sollte die Liste kürzen und z.B. nach 777777722155555333 Schluss machen, damit der Code nicht zu lange läuft).

Dieser Code braucht bei mir auf meinem Laptop 3 Minuten 23 Sekunden. Das ist der Referenzwert für die im folgenden besprochenen Methoden zur Beschleunigung.

Natürlich lässt sich dieses Problem sehr gut parallelisieren. Dem concurrent.futures Beispiel aus der Python-Doku folgend zum Beispiel, in dem man das Rechnen auf mehrere Prozesse verteilt:

import concurrent.futures
import math

...

def is_prime(n):
    ...

def main():
    with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print('%d is prime: %s' % (number, prime))

if __name__ == '__main__':
    main()

Hier werden vier parallele Prozesse gestartet und damit gerechnet.

Ergebnis: 2 Minuten 15 Sekunden, also etwas mehr als 1 Minute schneller.

Nun lassen sich aber nicht alle Programme bzw. Berechnungen so leicht parallelisieren wie das Prüfen von Primzahlen. Was auch nicht weiter schlimm ist, denn es gibt auch Methoden, nicht-parallelisierten Code zu beschleunigen.

Die für Python wahrscheinliche bekannteste Methode ist der Einsatz von PyPy. PyPy ist kompatibel zu CPython, der Code wird aber von einem Just-in-Time Compiler übersetzt. Der Aufruf des Codes erfolgt einfach durch:

$ pypy name_des_skripts.py

Das Skript aus dem ersten Beispiel (also ohne Parallelisierung) wird von PyPy (Version 5.10.0) in 24 Sekunden ausgeführt. D.h. die Ausführungsgeschwindigkeit ist um einen Faktor von ca. 8,5 (!) höher, nur durch Einsatz der Python-Implementierung PyPy, ohne weitere Änderungen am Code.

Eine weitere Möglichkeit der Beschleunigung ist der Einsatz des Just-in-Time Compilers Numba. Dieser ist standardmäßig bei der Python-Distribution Anaconda an Bord. Numba kann zwar auch manuell installiert werden, dass ist aber vergleichsweise aufwendig, weswegen für den Test von Numba die Version 0.36.2 aus Anaconda (in Kombination mit Python 3.6.3) zum Einsatz kam.

Um Code bzw. Funktionen mit Numba zu beschleunigen, müssen diese lediglich mit einem Dekorator versehen werden:

import math
from numba import jit

PRIMES = [
    ...]

@jit
def is_prime(n):
    ...

def main():
    for number in PRIMES:
        prime = is_prime(number)
        print('{} is prime: {}'.format(number, prime))

if __name__ == '__main__':
    main()

Diese unscheinbare Änderung bewirkt, dass die Funktion is_prime vom Numba Just-in-Time Compiler ausgeführt wird. Das Ergebnis ist eine Laufzeit von 19 Sekunden, also eine Beschleunigung vom eine Faktor von ca. 10,5.

Jetzt gibt auch die Möglichkeit, Python zu C-Code kompilieren lassen. Dazu wurden der Python-Compiler Nuitka und eine Kombination aus Cython und gcc getestet.

Nuitka (welches ich bis dahin noch nie verwendet hatte) hat zum Ziel, einen "extremely compatible Python compiler" bereit zustellen, entsprechend liegt der Entwicklungsfokus darauf, (Geschwindigkeits-) Optimierungen sollen später erst einfließen.

Entsprechend ist auch das Ergebnis: die Laufzeit ist 3 Minuten und 7 Sekunden, also unwesentlich schneller als CPython. Die eingesetzte Nuitka Version ist 0.5.24. Laut Aussage auf der Homepage ist Nutika-Homepage ist Nuitka bei größeren / umfangreicheren Benchmarks aber doch spürbar schneller als CPython.

Zweiter Testkandidat ist eine Kombination aus Cython (Version 0.27.3) und dem gcc Compiler (Version 5.4.0). Dazu wurde der Code aus dem ersten Beispiel zuerst einfach mit Cython in C-Code übersetzt und dann mit gcc zu einer ausführbaren Datei kompiliert:

$ cython prime_code_linear_cython.py --embed
$ gcc -Os -I /usr/include/python3.5m -o prime_code_linear_cython prime_code_linear_cython.c
-lpython3.5m -lpthread -lm -lutil -ldl

Das Ergebnis ist eine Laufzeit von 2 Minuten 39 Sekunden, also rund 1 Minute schneller als CPython. Nicht schlecht, aber für kompilierten Code auch nicht wirklich viel - jedenfalls nicht im Vergleich zu PyPy und Numba.

Das ganze kann man aber verbessern, wenn man von der dynamischen Typisierung, die Python standardmäßig hat, Abschied nimmt und statisch typisiert. Dadurch sind Optimierung seitens Cython möglich, welche die Ausführung (erheblich) beschleunigen. Ändert man den Code wie folgt:

import math

PRIMES = [
    112272535095293,
    ...]

cdef bint is_prime(long n):
    cdef int sqrt_n, i
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    cdef long number
    cdef bint prime
    for number in PRIMES:
        prime = is_prime(number)
        print('{} is prime: {}'.format(number, prime))

if __name__ == '__main__':
    main()

und kompiliert erneut, dann liegt die Ausführungsgeschwindigkeit bei 18 Sekunden, das ist die schnellste Ausführung (wenn auch nur unwesentlich schneller als Numba).

Der Nachteil: Der Code funktioniert tatsächlich nur noch, wenn die Primzahlen vom Typ long sind, d.h. der Code wird unflexibler. Fügt man z.B. eine 5 in die Liste der Primzahlen ein, bricht die Übersetzung mit Cython von Python nach C mit einer Fehlermeldung ab, weil 5 vom Typ int ist (und kein long). Wer mehr Infos zu Cython und Beschleunigung mittels statischer Typisierung sucht, für den ist die Seite Faster code via static typing in der Cython-Doku ein guter Startpunkt.

Zum Abschluss noch mal alle Ergebnisse auf einen Blick:

  • CPython 3.5.3: 3 Minuten 23 Sekunde
  • CPython + concurrent.futures.ProcessPoolExecutor: 2 Minuten 15 Sekunden
  • PyPy 5.10.0: 24 Sekunden
  • Python 3.6.3 + Numba 0.36.2: 19 Sekunden
  • Nuitka 0.5.24: 3 Minuten 7 Sekunden
  • Cython 0.27.3 + gcc 5.4.0: 2 Minuten 39 Sekunden
  • Cython mit statischer Typisierung + gcc: 18 Sekunden

Fazit

Es gibt verschiedenen Möglichkeiten rechenintensive / CPU-intensive Python-Skripte zu beschleunigen, teilweise erheblich zu beschleunigen.

Die einfachste Möglichkeit ist sicherlich PyPy - hier muss am Code nichts geändert werden, das Skript muss lediglich mit PyPy statt CPython aufgerufen werden. Wie auch Guido van Rossum (der "Erfinder" von Python) sagte: “If you want your code to run faster, you should probably just use PyPy.”

Der tatsächliche Beschleunigungsfaktor des eigenen Codes kann natürlich auch noch vom Code an sich abhängen. Gegenfalls macht es dann Sinn, die verschieden Optionen - PyPy, Numba und Cython + gcc - zu prüfen, um die für sich beste herauszufinden.

Sonntag, 17. Dezember 2017

Tkinter und Threads

Der Raspberry Pi ist ein recht populärer Minirechner, besonders für Hobbybastler und Leute, die gerne mit elektrischen Schaltung, welche vom Rechner gesteuert werden, herumspielen.
In Supportforen taucht öfters die Frage auf, wie man Messwerte von Sensoren am besten in graphischen Oberfläche (GUI) visualisiert werden können. Da Tkinter bei Python standardmäßig an Bord ist, wird dieses gerne eingesetzt (auch, wenn Qt oder GTK+ leistungsfähiger sind).

Das grundsätzliche Problem: Das Auslesen von Sensoren kann, je nach Sensor, mehr oder minder lang dauern. Liest man den Sensor über eine Funktion in der GUI aus, kann es sein, dass das Warten auf den Wert des Sensor die GUI bzw. deren Mainloop blockiert. Dies kann man recht einfach dadurch um gehen, dass das Auslesen der Sensoren in einen eigenen Thread auszulesen und die Werte über eine Queue an die GUI zu senden.

Der entsprechende Code kann z.B. wie folgt aussehen:

import tkinter as tk
from queue import Queue
from threading import Thread, Event
from time import sleep
from random import random

def generate_data(my_queue, event):
    while not event.is_set():
        data ={}
        data['value_sensor_1'] = random()
        data['value_sensor_2'] = random()
        my_queue.put(data)        
        sleep(0.7)
      

class Application(tk.Frame):
    def __init__(self, my_queue, thread_kill_event, master=None):
        super().__init__(master)
        self.my_queue = my_queue        
        self.thread_kill_event = thread_kill_event
        self.pack()
        self.create_widgets()
        self.update_labels()

    def create_widgets(self):
        self.label_sensor_1 = tk.Label(self, bg='purple', height=5, width=10, 
                                       text='Sensor 1')
        self.label_sensor_1.pack(side='left')
        self.label_sensor_2 = tk.Label(self, bg='yellow', height=5, width=10,
                                       text='Sensor 2')
        self.label_sensor_2.pack(side='right')
        self.quit = tk.Button(self, text='QUIT', fg='red',
                              command=self.exit_cleanup)
        self.quit.pack(side='bottom')

    def update_labels(self):
        self.master.after(500, self.update_labels)
        if not self.my_queue.empty():
            data = self.my_queue.get()
            if data['value_sensor_1'] < 0.5:
                self.label_sensor_1['bg'] = 'purple'
            else:
                self.label_sensor_1['bg'] = 'pink'
            if data['value_sensor_2'] < 0.5:
                self.label_sensor_2['bg'] = 'yellow'
            else:
                self.label_sensor_2['bg'] = 'blue'

    def exit_cleanup(self):
        self.thread_kill_event.set()
        self.master.destroy()

def main():
    my_queue = Queue()
    my_event = Event()
    my_thread = Thread(target=generate_data, args=(my_queue, my_event, ))
    my_thread.start()
    root = tk.Tk()
    app = Application(my_queue, my_event, master=root)
    app.master.title('TKinter Thread Demo')
    app.mainloop()

if __name__ == '__main__':
    main()

Von Prinzip her ist das eigentlich recht einfach:
Die Funktion generate_data , welche in einem eigenen Thread läuft, produziert alle 0,7 Sekunden einen zufälligen Wert zwischen 0 und 1 für zwei fiktive Sensoren, schreibt die Werte in ein Dictionary, welches dann in die Queue "geschoben" wird. Die 0,7 Sekunden Wartezeit "simulieren" dabei dir Trägheit des Sensors.
Die Klasse Application ist die eigentliche Tkinter-Anwendung. Diese besteht nur aus zwei Labeln und einem Quit-Button.
Die Methode update_labels ist die, welche die Werte aus der Queue holt und in Abhängigkeit vom Wert data['value_sensor_1'] bzw. data['value_sensor_2'] die Farbe des jeweiligen Labels ändert. Wichtig ist die Zeile

self.master.after(500, self.update_labels)

mit der festgelegt wird, dass die Methode alle 500 Millisekunden aufgerufen wird, so dass die Labels periodisch aktualisiert werden.
In der Funktion exit_cleanup wird dann noch der Thread sauber beendet, indem ein Event an die Funktion generate_data gesendet wird, worauf hin diese sich beendet, dann wird noch das Tkinter Fenster gekillt.

Wie zu sehen ist, ist das Entkoppeln von Messwerterfassungen und Messwertvisualisierung nicht weiter schwierig. Hat man mehrere verschiedene Sensoren, lässt sich das obige Beispiel auch einfach auf mehrere Threads erweitern.

Sonntag, 24. September 2017

Learning Concurrency in Python - Buchrezension

Der Name des englischsprachigen Buchs ist Programm: "Learning Concurrency in Python" hat das Ziel, eine Einführung in die nebenläufige Programmierung mit Python 3 zu geben.

Das ganze erfolgt recht umfassend auf rund 340 Seiten. Das Buch beginnt mit einem knappen Überblick über die verschiedenen Möglichkeiten, die Python bzw. zusätzliche Python-Module in Sachen nebenläufiger Programmierung bieten. Danach folgt ein allgemeiner (Python-unabhängiger) Überblick, welche Möglichkeiten und Limitierungen die Hardware bezüglich Nebenläufigkeit haben kann.
Ab dem 3. Kapitel wird es dann konkreter: es werden die Themen Threads in Python, Synchronisation, Ressourcenteilung und Kommunikation von Threads und Multiprocessing behandelt. Threads und Multiprocessing werden dabei über die jeweiligen Module threading und multiprocessing erläutert, als auch (etwas kürzer) über das Modul concurrent.futures.
Das folgenden Kapitel heißt "Event-Driven Programming" und behandelt primär das asyncio-Modul, am Ende des Kapitels gibt es aber auch (relativ kurze) Abschnitte zu den Modulen Twisted und Gevent.
Die folgenden beiden Kapitel behandeln "Reactive Programming" mittels der Module RxPY und PyFunctional sowie die Nutzung der GPU über die Module PyCUDA, Numba, Theano und PyOpenCL.
Zum Abschluss des Buchs gibt es nochmal auf rund zehn Seiten eine Zusammenfassung inklusive der Erläuterung der Vor- und Nachteile der verschiedene Wege zur nebenläufigen Programmierung mit Python.

Es werden also so ziemlich alle gängigen Bereich in Sachen Python und nebenläufiger Programmierung abgedeckt. Gut ist, dass das Buch sehr viel Bespiele enthält, welche der Autor auch alle erläutert, so dass eigentlich kaum Fragen offen bleiben. Persönlich fand ich das Kapitel zu asyncio sehr gut, da es zumindest mir (enorm) geholfen hat, dass Thema (endlich) besser zu verstehen.

Trotz der sehr guten und umfassenden Erklärungen hat das Buch aber einen ziemlich großen Mangel: in vielen Listings stimmen die Einrückungen nicht. Was so viel heißt wie, dass dort, wo syntaktisch Einrückungen sein müssen, keine sind (womit der Code in der abgedruckten Form nicht lauffähig wäre). Die Fehler sind zwar leicht zu erkennen, da immer "nur" eine Ebene fehlt. Aber für ein Fachbuch zu Python, wo Einrückungen essentiell wichtig sind, ist dieser Fehler schwer zu entschuldigen.

Nichts desto trotz ist das Buch durchaus empfehlenswert, wenn man einen umfassenden Überblick, Einblick und Einstieg ins Thema "concurrent programming" mit Python sucht. Um das Buch zu verstehen und effektiv nutzen zu können, sollte man fortgeschrittene Kenntnisse haben oder zumindest ein fortgeschrittener, ambitionierter Einsteiger sein (vorher braucht man wahrscheinlich so wie keine nebenläufigen Programme...).
Das Buch liest sich recht flüssige und sollte auch mit "normalen" Englischkenntnissen gut zu verstehen sein.
Wer sich für das Thema des Buchs interessiert, der sollte einen Blick hinein werfen.

Buchinfos:

Titel: Learning Concurrency in Python
Autor: Elliot Forbes
Verlag: Packt Publishing
ISBN: 9781787285378
Preis: 42,99 Euro (gedruckte Ausgabe + E-Book), 29,15 Euro (nur E-Book)
Erstauflage: August 2017

Sonntag, 14. August 2016

Motorola Moto G 3. Gen - Bilder werden im Dateimanager nicht angezeigt

Ein Verwandter hatte sich kürzlich ein Motorola Moto G 3. Generation als Smartphone gekauft. Da dieses Moto G einen Slot für Micro-SD Karten hat, wurde direkt eine 32 GB Karte eingelegt.

Das Smartphone hat dann nach dem ersten Start noch ein Update auf Android 6.0 gemacht und läuft völlig problemlos. Geschossene Fotos werden dabei auf die externe Speicherkarten gespeichert.

Jetzt sollten die Fotos auf den Computer übertragen werden. Also wurde das Handy per USB-Kabel an den Computer angeschlossen. Dieser erkannte das Smartphone und die SD-Karte auch direkt. Aber auf der SD-Karte wurden keine Fotos gefunden bzw. der Ordner "DCIM/Camera" wurde erst gar nicht angezeigt. Das Problem trat gleichermaßen unter Windows 10 als auch unter Ubuntu Linux 14.04 auf. Es macht auch keinen Unterschied, ob man am Smartphone als Modus für den Datentransfer "PTP" oder "MTP" auswählt.

Eine kurze Recherche im Internet zeigt, dass dieses Probleme wohl viele Nutzer haben. In einem englischsprachigen Forum wurde gesagt, dass dies ein Bug sei, der im Zusammenhang mit dem Upgrade auf Android 6.0 steht. Aber, und das ist das gut, es wurde direkt auch eine Lösung gezeigt, welche auch funktioniert hat. Diese ist im folgenden aufgeführt:

Hinweis: Auch wenn es beim Beschreiten des Lösungswegs normalerweise nicht zu Datenverlust kommen sollte, ist es dringend anzuraten, die auf der SD-Karte befindlichen Fotos vorher zu sichern (z.B. via Google Drive, Dropbox etc.).

Die Lösung des Problems besteht darin, dass man von Android zwischengespeicherte Daten löscht und somit das Betriebssystem dazu "bewegt" die Daten neu zu lesen und anzulegen. Anschließend werden die Verzeichnisses im Dateimanager auch wieder wie gewohnt angezeigt. Das ganze geht so:

Man geht in die "Einstellungen" und wählt dort den Punkt "Apps". Hier klickt man rechts oben die drei senkrechten Punkte und wählt den Menüpunkt "Systemprozesse anzeigen" aus.
Nun sucht man in der langen Liste der Prozesse die Punkte "Externer Speicher" und "Medienspeicher".
Beide Einträge öffnet man, klickt auf das Feld "Speicher" und klickt dann auf "Daten löschen" und "Cache leeren" .

Jetzt muss man das Smartphone noch ausschalten und dann wieder einschalten. Bevor man es an den Computer anschließt sollte ein paar Minuten (ca. 8-10) gewartet werden, damit Android die Daten neu anlegen kann (wie lange dies wirklich dauert weiß ich nicht, aber das englischsprachig Forum empfiehlt, so lange zu warten).

Schließt man das Moto G 3. Gen jetzt an den Computer an und wählt unter "USB zum Aufladen"  die Option "Dateien übertragen (MTP)", dann sollte man wieder alle Ordner auf der SD-Karte sehen. Inklusive des Ordners, in dem die Fotos liegen. Jedenfalls hat es bei meinem Verwandten so einwandfrei funktioniert.

Samstag, 30. Januar 2016

Python, WTForms, JavaScript: Formulare dynamisch erweitern

Folgender Fall: in einer Webanwendung sollen Daten über ein Formular eingegeben werden. Die Felder des Formulars sind klar definiert, aber wie viele Daten eingegeben werden sollen, ist offen.

Ein Beispiel hierfür wäre z.B. eine Webanwendung für Rezepte. Es gibt Rezepte, die haben eine handvoll Zutaten. Aber es gibt auch Rezepte, die haben mehrere Dutzend Zutaten. Hier macht es also wenig Sinn, die Anzahl der Eingabefelder für das Formular fix festzulegen. Vielmehr gilt es, die Möglichkeit für den Nutzer zu schaffen, das Formular dynamisch zu erweitern.

Die Zutaten hier dafür: Python 3.4, Bottle 0.12, WTForms 2.1 , JavaScript und jQuery 2.2. Bottle als Webframework ist hier übrigens nur Mittel zum Zweck, das Beispiel lässt sich genau so mit anderen Python Webframeworks umsetzen.

Das Beispiel besteht aus vier Codeteilen:
  • der Bottle Hauptanwendung, die das Routing etc. festlegt
  • den Formularklassen, erstellt mit WTForms
  • den beiden Templates zur Dateneingabe und zur Datenausgabe
Wer das Beispiel nachstellen möchte: wie bei Bottle üblich wird erwartet, dass die Templates im Verzeichnis views liegen. Das benötigte jQuery wird im Verzeichnis static erwartet. Alternativ könnte man natürlich auch jQuery von einem der vielen CDN-Servern online laden.

Die Hauptdatei sieht so aus:

import os
from bottle import route, run, template, debug, static_file, request
from my_forms import InputForm

BASE_DIR = os.path.dirname(os.path.dirname(__file__))

@route('/ingredients')
def index():
    form = InputForm()
    return template('dyn_form_input_wt.html', form=form)

@route('/ingredients', method='POST')
def post_data():
    form_data =  request.forms
    form = InputForm(form_data)
    if not form.validate():
        errors = form.errors['ingredients']
    else:
        errors = None
    ingredients = []
    for i in range(0, int(len(form_data)/3)):
        ingredients.append((form_data['ingredients-'+str(i)+'-description'],
                            form_data['ingredients-'+str(i)+'-unit'],
                            form_data['ingredients-'+str(i)+'-quantity']))
    return template('output.html',
                    ingredients=ingredients,
                    errors=errors)

@route('/static/<filename>')
def server_static(filename):
    return static_file(filename, root=os.path.join(BASE_DIR, 'static'))

debug=True
run(host='localhost', port=8080, reloader=True)

Hier gibt es eigentlich nicht viel zu sagen. Eine Route liefert das Formular aus, die Route mit method='POST' nimmt die Formulardaten auf und sortiert diese zurück in eine Liste. In einer realen Anwendung würde man an dieser Stelle die Daten z.B. in eine Datenbank schreiben.
Wichtig ist hierbei aber, dass davon ausgegangen wird, dass die name-Attribute der Formularfelder a) der Namensgebung von WTForms FieldList entsprechen - also Name des Formulars - Zähler - Name des Felds - , b) der Zähler bei Null beginnt und c) die Zählung lückenlos ist (also 0, 1, 2, 3 usw.). Dass das tatsächlich auch so ist, dafür sorgt der JavaScript in der weiter unten gezeigten Template-Datei dyn_form_input_wt.html.

Die Datei my_forms.py, welche die Formularklassen enthält, sieht so aus:

from wtforms.form import Form
from wtforms.fields import StringField, SelectField, DecimalField, FormField, \
    FieldList
from wtforms.validators import NumberRange, InputRequired

UNITS = [('kg', 'kg'),
         ('g', 'g'),
         ('TL', 'TL'),
         ('EL', 'EL'),
         ('St', 'St')] 

class IngredientForm(Form):
    description = StringField('description',
                              [InputRequired()])                                  
    unit = SelectField('unit', choices=UNITS)
    quantity = DecimalField('quantity',
                            [InputRequired(), NumberRange(min=0)],
                             places=3)
                            
class InputForm(Form):
    ingredients = FieldList(FormField(IngredientForm), min_entries=1)

Das ist soweit alles WTForms Standard ohne Tricks und Kniffe. In der Klasse InputForm wird die IngredientForm per FormField zu einem Formular-Feld zusammengefasst, FieldList wiederum fasst mehrere Formular-Felder zusammen. Mehr Infos dazu sind in der Dokumentation von WTForms zu finden.

Das Template dyn_form_input_wt.html ist für die Eingabe der Zutaten sowie das dynamische Hinzufügen und Entfernen von Eingabefeldern zuständig. Das Template sieht so aus:

<!DOCTYPE html>
<html>
<head>
    <title>Dynamic Forms</title>
    <script src="static/jquery-2.2.0.min.js"></script>
</head>
<body>
<form action="/ingredients" method="POST">
    <div class="input_fields_wrap">
    <!-- here goes the form fields -->
    </div>
    <button class="add_field_button">Add field</button>
    <button class="remove_field_button">Remove field</button>
    <input type="submit">
</form>
<script>
$(document).ready(function() {
    var max_fields = 20; //maximum input boxes allowed
    var wrapper = $(".input_fields_wrap"); //Fields wrapper
    var addButton = $(".add_field_button"); //Add button ID
    var removeButton = $(".remove_field_button"); //Add button ID
    var htmlString = '<div id="input_fields_0">{{ !form.ingredients }}</div>';
    var field_counter = 0;
    $(wrapper).append(makeString()); //add the first form
    $(addButton).click(function(e){ //on add input button click
        e.preventDefault();
        if(field_counter < max_fields){ //max input box allowed
            field_counter++;
            $(wrapper).append(makeString());}
        else { window.alert('max number of ingredients reached!')}
    });
    $(removeButton).click(function(e){ //on remove input button click
        e.preventDefault();
        if(field_counter > 0){ //make sure at least one field is there
            $('#input_fields_'+field_counter).remove();
            field_counter--;}
        else { window.alert('Cannot delete, one input has to remain.')}
    });
    function makeString() {
        var myString = htmlString;
        return myString.replace(/0/g, field_counter);
    };
});
</script>
</body>
</html>

Wie zu sehen ist, besteht das Template aus zwei Sektionen: dem HTML-Teil und dem JavaScript Teil. Erster ist ziemlich "straight forward" und bedarf wohl keiner weiterer Erklärung, letzter macht die Hauptarbeit und liefert die Dynamik.

Im HTML-Teil wird nur der "Rahmen" für das Formular angelegt, es werden aber keine Formularfelder erzeugt. Dies geschieht ausschließlich per JavaScript / jQuery.

Den Ausgangspunkt bildet die Definition der Variablen htmlString, in der auch das WTForms Formular gerendert wird, über {{ !form.ingredients }}

Die Zeile $(wrapper).append(makeString()); erzeugt den ersten Satz Formularfelder, der Zähler field_counter steht hier noch auf Null.

Zum Hinzufügen weitere Felder ist eine JavaScript-Funktion an den Button "Add field" gebunden, welche a) checkt, ob nicht die per max_fields festgelegte Anzahl an Feldern schon erreicht ist, b) den Feldzähler field_counter um eins erhöht und dann c) einen Satz Formularfelder hinzufügt.

Zum Löschen, welches über eine an den Button "Remove Fields" gebundene Funktion erfolgt, ist der Ablauf ähnlich. Hier wird zuerst geprüft, ob mehr als ein Satz Formularfelder vorhanden ist. Wenn ja wird das letzte entfernt und der field_counter Zähler um eins dekrementiert.

Die Funktion makeString ist dafür verantwortlich, dass die Nummerierung der Formularfelder (bzw. genau genommen deren Attribute wie id, name etc. gem. dem aktuellen Stand von field_counter angepasst werden. Dazu wird einfach die String-Methode replace() mit den entsprechenden Werten auf den Ausgangsstring angewendet und als neuer String zurück geliefert.

Wenn das Formular abgesendet wurde, wird in diesem Beispiel hier das Template output.html aufgerufen, welches die eine Liste der Zutaten mit Menge und Einheit sowie mögliche Fehler ausgibt. Das Template sieht so aus:

<!DOCTYPE html>
<html>
<head>
<title>Dynamic Forms Output</title>
</head>
<body>
<p>Received ingredients from form data:</p>
<ul>
% for ingredient in ingredients:
<li>{{ ingredient[0] }}: {{ ingredient[2] }} {{ ingredient[1] }}
% end
</ul>
% if errors:
<p>The form has the following errors:
<ul>
% for error in errors:
<li>{{ error }}
% end
</ul>
% end
</body>
</html>

Das Template ist hier auch nur Mittel zum Zweck, bei Produktiv-Code würde man hier sicherlich anders vorgehen.

Wie hier gezeigt wurde, ist es nicht so schwierig und aufwendig, dynamisch erweiterbare Formular zu erzeugen, auch wenn diese in den ansonsten "starren" Klassen eines Python Formularframeworks definiert sind.
Dazu benötigt werden aus WTForms insbesonders die Klassen FormField und FieldList sowie auf der Client-Seite ein bisschen JavaScript mit jQuery.


Samstag, 3. Oktober 2015

systemd timer unter Raspbian

Ende September ist eine neue Raspian Version erschienen, welche auf Debian Jessie basiert. Und damit ist auch systemd als Init-Dienst standardmäßig an Bord.

Davon bekommt man als "normaler" Nutzer erst mal wenig mit, weil man in der Regel wenig "Berührung" mit dem Init-System hat.

Durch den Einsatz von systemd bietet sich aber die Möglichkeit, dessen Timer zu verwenden. systemd Timer sind ein Dienst, welcher periodisch bestimmte, frei festzulegende Aktion ausführen kann - und sind damit eine Alternative zu cron.

Das wird im folgenden anhand eines Beispiels gezeig.

Unter Verwendung eines systemd Timer soll das folgende Python Skript alle 5 Minuten ausgeführt werden:

#!/usr/bin/env python3

import sqlite3
import datetime
from sense_hat import SenseHat

s = SenseHat()
conn = sqlite3.connect('/home/pi/code/ambient_data.db')
sql = 'INSERT INTO data VALUES (?, ?, ?, ?)'
with conn:
    conn.execute(sql, (s.get_temperature(),
                       s.get_pressure(),
                       s.get_humidity(),
                       datetime.datetime.now()))
conn.close()

Das Skript liest den Temperatur-, Luftdruck- und Luftfeuchtigkeitssensor eines SenseHAT aus und schreibt die Daten in eine SQLite-Datenbank.
Das Skript heißt ambient_temperature_logger.py und liegt unter /home/pi/code.
Die zugehörige Datenbank ambient_data.db liegt im gleichen Verzeichnis.

Damit das Skript automatisch ausgeführt wird, muss man noch zwei Dateien für systemd erstellen:
  • eine .service Datei, welche das Skript aufruft
  • eine .timer Datei, welche die Daten für das automatische Ausführen enthält.
Beide Dateien legt man mit Root-Rechten im Verzeichnis /etc/systemd/system an.

Die Datei ambient_data_logger.service bekommt folgenden Inhalt:

[Unit]
Description=SenseHat Ambient Data Logger

[Service]
Type=simple
ExecStart=/home/pi/code/ambient_data_logger.py

Der Inhalt ist wohl weitestgehend selbsterklärend:
Im Abschnitt [Unit] steht eine Beschreibung, was passiert. Im Abschnitt [Service] wird bei Type der Typ festgelegt, was bei Skripten wie in diesem Beispiel normalerweise simple ist. Unter ExecStart wird der Befehl zum Aufruf des Skripts angegeben.

Die Datei ambient_data_logger.timer bekommt den folgenden Inhalt:

[Unit]
Description=Runs the ambient_data_logger Python script every 5 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Unit=ambient_data_logger.service

[Install]
WantedBy=multi-user.target

Unter [Unit] wird wieder ein Beschreibung hinterlegt, unter [Install] WantedBy, welchem target der Timer zugeordnet wird. Für Skripte wie diese ist multi-user.target eine gute Wahl, weil der (aktiverte) Timer dann beim Systemstart mit gestartet wird.

Im Abschnitt [Timer] wird festgelegt, wann was passieren soll:
  • OnBootSec legt fest, wann das Skript (bzw. im Jargon von systemd die "Unit") ausgeführt werden soll - hier also nach 2 Minuten.
  • OnUnitActiveSec legt fest, nach wie viel Minuten die Unit jedes weitere Mal ausgeführt werden soll - hier also alle 5 Minuten
  • Unit legt fest, welche .service-Datei durch den Timer aufgerufen wird.
Jetzt muss man die .timer und die .service-Dateien noch ausführbar machen:

$ sudo chmod +x ambient_data_logger.*

und den Timer mit den folgenden beiden Befehlen aktivieren:

$ sudo systemctl start ambient_data_logger.timer
$ sudo systemctl enable ambient_data_logger.timer

Der erste Befehl startet den Timer, der zweite stellt sicher, dass der Timer auch nach einem Reboot aktiviert wird.
Zum Deaktivieren des Timers ersetzt man im ersten Befehl start durch stop und im zweiten Befehl enable durch disable.

Um zu prüfen, ob der Timer auch aktiv ist und läuft, kann man sich alle aktiven Timer anzeigen lassen:

$ systemctl list-timers

Das Anlegen von Timer von systemd ist also ziemlich gradlinig und nicht weiter schwierig.

Da systemd bei anderen Distribution (z.B. Fedora, Arch) schon länger im Einsatz ist, findet man im Internet reichlich Beispiele zu systemd und Timern. Da systemd weitestgehend unabhängig von den jeweiligen Eigenheiten der darüber liegenden Distribution ist, sind die Beispiele auch auf Raspbian übertragbar.
Außerdem sind die Man-Pages zu systemd und systemd.timer recht ausführlich.

Samstag, 15. August 2015

Python: PIL/Pillow - Bild anzeigen mit Image.show() ( xv vs. xli vs. command vs. imagemagick)

Möchte man sich mit Hilfe von Python ein Bild anzeigen lassen, dann geht das mit drei Zeilen Code und dem Pillow-Modul. Pillow ist ein aktiv entwickelter "friendly fork" von PIL (=Python Image Library). PIL bzw. Pillow sind der quasi-Standard unter Python, wenn es das Handling und Manipulieren von Grafik-Dateien geht.

Die folgenden drei Codezeilen zeigen das Bild "foo.png" an:

>>> from PIL import Image
>>> im = Image.open('foo.png')
>>> im.show()

So zumindest die Theorie. In der Praxis passiert aber zumindest unter Ubuntu 14.04 und auch unter Raspbian nichts. Nach dem Aufruf von im.show() wird sofort wieder der Prompt des Python-Interpreters angezeigt. Keine Fehlermeldung, aber auch kein Bild.

Ein Blick in die Doku von Pillow brachte dann ans Tageslicht, dass die show-Funktion den Bildbetrachter xv aufruft. Dieser war in den 90er Jahren des vorherigen Jahrhunderts wohl mal der de-facto Bildbetrachter auf Unix-System, ist heute jedoch (ziemlich) veraltet. Und damit auch in den meisten Distributionen wie Debian, Ubuntu etc. nicht mehr enthalten.

Um trotzdem Bilder mittels show() anzeigen zu können, gibt es zwei alternative Wege:

Einer ist, das Paket imagemagick zu installieren. Dieses ist für quasi alle Linux-Distributionen in den Paketquellen enthalten.
Danach funktioniert der Aufruf von show(), es wird display aus dem ImageMagick-Paket zur Anzeige des Bilds verwendet. Leider ist dieser Weg in der Doku von Pillow nicht dokumentiert. Ein Blick in den Quellcode von von Image zeigt aber, dass Pillow auf Linux-Systemen explizit nach display sucht und diese - sofern vorhanden - verwendet.

Eine andere Alternative ist im Raspberry Pi Forum zu finden. Hier wird also das Paket xli (xli="command line tool for viewing images in X11") installiert und ein Softlink von xv auf xli angelegt:

$ sudo apt-get install xli
$ cd /usr/local/bin
$ sudo ln -s /usr/bin/xli xv


Dies funktioniert sowohl unter Raspbian als auch unter Ubuntu (und vermutlich auch bei den meisten anderen Linux-Distros, die xli in den Paketquellen haben.

Die show-Funktion kennt das optionale Argument command=, mit dem der Bildbetrachter explizit vorgegeben werden. So steht es jedenfalls in der Dokumentation. Allerdings hat das auf keinem meiner Systeme funktioniert.