Dienstag, 12. Dezember 2023

Django: Übersetzungen von Forms im Template anzeigen

Neulich habe ich zum ersten Mal eines meiner mit Django erstellten Projekt übersetzt. Bis jetzt waren die komplett in englisch gehalten, jetzt habe ich eine deutsche Übersetzung hinzugefügt.

Wie Übersetzungen / Internationalisierung mit Django funktionieren ist ja ausführlich in der Doku beschrieben (Link zur Django 4.2 Doku). Das hat bei mir auch alles einwandfrei funktioniert - nur wurden die Labels von Formularen aus der Datei forms.py im Template nicht übersetzt angezeigt.

Die forms.py sah so aus:

from django import forms
from django.utils.translation import gettext as _


class SelectByDimensionsForm(forms.Form):
    width = forms.IntegerField(label=_('Inner Width, mm'))
    width_tolerance = forms.IntegerField(label=_('tolerance for width, mm'),
                                         initial=20)
    (...)

und das Template so:

{% extends 'laundercalc/base.html' %}
{% load i18n %}

{% block content %}
{% translate "Please enter your data in the following form:" %} (...)
{% csrf_token %} {{ form.as_p }} (...)
(...)

Während der Text aus dem Template übersetzt angezeigt wurde (wie das "Please enter ...") erschienen die Labels des Formular nach wie vor in Englisch. In der Übersetzungsdatei waren die Labels übersetzt hinterlegt.

Die Lösung, wie man die Übersetzung angezeigt bekommt, ist ganz einfach: man muss nur gettext_lazy in der forms.py importieren, also from django.utils.translation import gettext_lazy as _ (statt ...import gettext as _) verwenden.

Dann wird die Übersetzung erst abgerufen, wenn diese wirklich benötigt wird, im gegebenen Fall also, wenn das Template gerendert wird. Und dann wird es auch komplett übersetzt angezeigt.

Weitere Informationen zu gettext_lazy sind auch in der Django Dokumentation zu finden.

Mittwoch, 22. November 2023

REST-API mit Python und hug für Pimoroni Blinkt LED Leiste erstellen

In diesen kleinen Projekt wird mit Hilfe des Python Webframeworks hug eine einfache webbasierte REST-API zum Ansteuern der Blinkt LED-Leiste von Pimoroni für den Raspbery Pi erstellt.

Das ganze hat keinen speziellen Grund, es ist für mich aber eine gute Gelegenheit, mal etwas mit hug zu erstellen. Wollte ich schon länger, es hatte sich bis dato aber keine Gelegenheit ergeben.

Ziel

Ziel des Programmierprojekt ist es, eine webbasierte API / Steuerung für die Blinkt-Leiste zu erstellen, genutzt werden dafür nur GET-Requests. Konkret bedeutet dies, das z.B. über den Aufruf der URL http://ip_adresse:port/color/red alle acht LEDs des Blinkt auf rot zu stellen. Die LEDs der Blinkt Leiste werden von Pimoroni übrigens "Pixel" genannt, dieser Begriff wird auch im weiteren hier verwendet.

Die API soll die folgenden Funktionen bieten:

  • alle Pixel auf eine vordefinierte Farbe stellen
  • Helligkeit für alle Pixel einstellen
  • für jedes der Pixel eine separate Farbe einstellen
  • für jedes Pixel eine separate Farbe einstellen, aber alle acht Farben auf einmal als Parameter übergeben
  • allen Pixeln eine zufällig gewählte Farbe zuweisen
  • Blinkt Leiste ausschalten

Die Farben sollen sich für diese Projekt nicht frei einstellen lassen, sondern es gibt sieben vordefinierte Farben: rot, orange, gelb, grün, blau, indigo, violett sowie weiß. Was den Farben das Regenbogens plus weiß entspricht.

Hardware

Als Hardware kommen eine Raspberry Pi Zero W sowie die Pimoroni Blinkt Leiste zum Einsatz. Die Blinkt Leiste wird direkt auf die GPIO Pins des Raspberry Pi gesteckt. Natürlich lässt sich das Projekt auch mit jedem anderen Raspberry umsetzen, solange dieser die 40 GPIO Pins hat Auf ganz alte Raspis mit nur 28 Pins passt die Leiste nicht.

Pi Zero im Pibow Gehäuse mit aufgesteckter Blinkt LED Leiste

 Software

Als Software kommt zum Einsatz:

  • Raspberry Pi OS. Hier wird die zum Zeitpunkt des Schreibens aktuelle Version "Bookworm" verwendet, das Projekt funktioniert aber auch mit älteren Versionen.
  • venv zum Erstellen eines virtual Environments für Python, in dem das Programm später läuft
  • innerhalb des venvs pip, der Paketmanager für Python
  • das Python Modul für hug, installiert via pip
  • das Python Modul für Blinkt, installiert aus den Paketquellen von Raspberry Pi OS
  • waitress als WSGI-Server, installiert via pip
  • systemd zum Erstellen einer Service Unit zum automatischen Starten des Server. systemd ist bei Raspberry Pi OS standardmäßig an Bord.

Anstellen von waitress kann natürlich auf jeder andere WSGI-Applikationsserver genutzt werden, wie z.B. Gunicorn. Beim Entwickeln bzw. zum Testen kann man auch den Server nutzen, den hug mitbringt - habe ich auch in der Testphase gemacht.

Programm

Vorbereitung

Als erstes wird die benötigte Software installiert und das venv angelegt sowie aktiviert.

Aus den Paketquellen von Raspberry Pi OS wird folgendes installiert:

sudo apt install python3-venv
sudo apt install python3-blinkt

Dann wird ein venv Names "hug_blinkt" angelegt. Ich habe dieses bei mir im Homeverzeichnis im Unterverzeichnis "code" angelegt, so dass der volle absolute Pfad /home/noisefloor/code/hug_blinkt lautet. Von Prinzip her geht aber natürlich auch jedes andere Verzeichnis, für das man ausreichende Rechte hat.

python3 -m venv hug_blinkt --system-site-packages

Die Option --system-site-packages ist deshalb notwendig, weil das Python-Modul für Blinkt via APT systemweit installiert ist, aber systemweite Pakete standardmäßig nicht in ein venv übernommen werden.

Jetzt ins venv wechseln, dieses aktivieren und darin die Python-Module für hug und waitress installieren:

cd hug_blinkt
source bin/activate
pip3 install hug
pip3 install waitress

Nun ist alles bereit, um das Programm zu erstellen und starten.

Quellcode

Der komplette Quellcode für das Programm sieht wie folgt aus:

import random
import hug
import blinkt


COLORS = {'red': (255, 0 , 0),
          'green': (0, 255, 0),
          'blue': (0, 0, 255),
          'yellow': (255, 255, 0),
          'orange': (255, 165, 0),
          'violet': (255, 87, 51),
          'indigo': (75, 0, 130),
          'white': (255, 255, 255),
          }

#define short name for each color, which is the first letter of the color
#as defined in COLORS
#needs to be modified / removed once two colors with the same starting letter
#are defined in COLORS, e.g. if ever purple and pink would be added.
COLORS_SHORT = {key[0]:value for (key, value) in COLORS.items()}

ALL_COLORS = COLORS | COLORS_SHORT

@hug.get('/color/{color}')
@hug.get('/color', examples='color=red')
def set_all_pixels(color: str):
    '''Sets all pixels to the the given color. The color is specified by name,
    e.g. red or green. Color can also be given by shortname, e.g. y for yellow
    or m for magenta.'''
    if not color in ALL_COLORS.keys():
        return {'error': f'{color} is not a valid color. Please choose one of \
{list(ALL_COLORS.keys())}'}
    else:
        rgb_value = ALL_COLORS[color]
        blinkt.set_all(rgb_value[0], rgb_value[1], rgb_value[2])
        blinkt.show()
        return {'success': f'set Pixels to color {color} (RBG: {rgb_value})'}

@hug.get('/color_for_pixel/{pixel}/{color}')
@hug.get('/color_for_pixel', examples='pixel=2&color=magenta')
def set_pixel_to(pixel: hug.types.in_range(1, 9), color: str):
    '''Sets the given Pixel to the given color. Pixels are numbered 1 to 8 from
    left to right. The color is specified by name, e.g. green or blue. Color can
    also be given by shortname, e.g. y for yellow or m for magenta.'''
    if not color in ALL_COLORS.keys():
        return {'error': f'{color} is not a valid color. Please choose one of \
{list(ALL_COLORS.keys())}'}
    else:
        pixel-=1
        rgb_value = ALL_COLORS[color]
        blinkt.set_pixel(pixel, rgb_value[0], rgb_value[1], rgb_value[2])
        blinkt.show()
        return {'success': f'set Pixel {pixel} to color {color} (RBG: {rgb_value})'}

@hug.get('/colors_for_pixels/{colors}')
@hug.get('/colors_for_pixels', examples='colors=green,red,yellow,magenta,white,blue,y,g')
def set_pixels_to(colors: hug.types.delimited_list(',')):
    '''Set each pixel to the given color specified for each Pixel. The colors
    are specified by name,     e.g. blue or yellow. In total eight colors needs
    to be given, one for each Pixel. Colors must     be separated by a comma , .'''
    if len(colors) != 8:
        return {'error': 'wrong length - exactly eight (8) colors separated by \
a comma have to be provided'}
    if not all(color.strip() in ALL_COLORS for color in colors):
        return {'error': f'There is at least one invalid value in {colors}. \
            Please ensure all values are one of {ALL_COLORS.keys()}'}
    _set_color_for_each_pixel(colors)
    return {'success': f'set Pixels 1 to 8 to colors {colors}'}

@hug.get('/brightness/{value}')
@hug.get('/brightness')
def set_brightness(value: hug.types.in_range(1, 100), 
                   examples='value=50'):
    '''Sets the brightness of all Pixels to the given value. Value must be an
    integer between 1 and 99'''
    value = round(value/100, 2)
    blinkt.set_brightness(value)
    blinkt.show()
    return {'success': f'set brightness to: {value}'}

@hug.get('/randomize')
def randomize_colors():
    '''Sets all eight pixels to a randomly chosen color from the color dict.'''
    colors = random.choices(list(COLORS.keys()), k=8)
    _set_color_for_each_pixel(colors)
    return {'success': 'set pixels to randomly chosen colors.'}

@hug.get('/show_state')
def show_state():
    '''Shows the current state of each pixel - current RGB value and brightness.'''
    state = {}
    for pixel in range(blinkt.NUM_PIXELS):
        value = blinkt.get_pixel(pixel)
        state[f'Pixel {pixel}'] = f'color: {value[0:3]}, brightness: {value[3]}' 
    return state

@hug.get('/off')
def leds_off():
    '''Turns off all pixels.'''
    blinkt.clear()
    blinkt.show()
    return {'success': 'turned off all Pixels'}

def _set_color_for_each_pixel(colors):
    '''Helper function to set each pixel to a given color.
    Expects one argument, which has to be an iterable holding the names of exactly
    eight colors.'''
    assert len(colors)==8
    rgb_values = [ALL_COLORS[color] for color in colors]
    for position, rgb_value in enumerate(rgb_values):
        blinkt.set_pixel(position, rgb_value[0], rgb_value[1], rgb_value[2])
    blinkt.show()

set_all_pixels(color: str) bindet die Route color an die Funktion set_all_pixels. Die Route und die daran gebundene Funktion erwarten ein Parameter Namens color, welches auf zwei Wege übergeben werden kann: color?color=red oder color/red.

Im Wörterbuch COLORS werden die erlaubten Farben definiert, was, wie weiter oben bereits erwähnt, die Regenbogenfarben plus weiß sind. Außerdem wird daraus das Wörterbuch COLORS_SHORT abgeleitet. Darin ist definiert, dass auch nur der Anfangsbuchstabe jeder Farbe als Farbangabe genutzt werden darf, also z.B. y für yellow. Alle Funktionen, denen ein oder mehrere Farben übergeben werden, prüfen, ob die Farbangabe bzw. die Farbangaben gültig sind, sprich im Wörterbuch ALL_COLORS vorkommen.

Alle Funktionen, die an eine Route gebunden sind, liefern als Antwort JSON zurück. Dies ist der Standard von hug. Das Wörterbuch nach dem return wird von hug in ein JSON-Objekt umgewandelt und mit dem passenden Content-Type Header versehen. hug kennt außer JSON aber natürlich auch noch andere Ausgabeformate.

Was meines Erachtens bei hug ganz praktisch ist sind die Type Annotations, welche man optional bei den Route angeben kann. Diese können ganz gewöhnlich sein wie bei color: str . Aber hug kennt auch weiterführende, komplexere Annotations. Hier im Programm verwendet werden zwei weitere:
value: hug.types.in_range(1, 100) prüft und stellt sicher, dass für value nur Zahlen zwischen 1 und 99 akzeptiert werden.
colors: hug.types.delimited_list(',') stellt sicher, dass für colors nur durch ein Komma separierte Liste von Werten akzeptiert wird, wie z.B. red,yellow,blue. Werte wie red;yellow;blue würden zu einem Fehler führen. Nach erfolgreicher Prüfung wandelt hug die Werte praktischerweise direkt in eine Python-Liste um.
Eine tiefer gehende Erklärung zu den Type Annotations in hug sind in der Doku zu finden.

Programm testen

Zum Testen des Programms benötigt man keinen WSGI Applikationsserver, sondern kann den in hug enthaltenen Server nutzen. Diesen ruft man mit

hug -f name_des_programms.py

auf. Danach ist das Programm bzw. die darüber bereit gestellt Web-API unter der Adresse 127.0.0.1:8000 auf dem Rechner, auf dem das Programm läuft, erreichbar.

Deployment

Das fertige Programm sollte zur produktiven / dauerhaften Nutzung - selbst wenn es nur im Heimnetzwerk ist - über einen WSGI-Server ausgeliefert werden, um einen stabilen Betrieb zu gewährleisten. Wie weiter oben bereits erwähnt nutze ich dafür waitress.

Der Befehl zum Starten von waitress innerhalb des venvs lautet:

waitress-serve --threads=2 --listen=192.168.178.82:8001 blinkt_api:__hug_wsgi__

Die Angaben für --listen müssen natürlich auf die IP-Adresse des Host-Rechners angepasst werden. Ebenso können der Port sowie die Anzahl der Threads geändert werden. Der Einstiegspunkt von hug für den WSGI-Server __hug_wsgi__ erhält man automatisch über das import hug im Skript.

Wer das Programm beim Systemstart automatisch starten will kann dies auf Basis der folgenden systemd Service Unit machen:

[Unit]
Description=Blinkt Server using hug as the backend
After=network-online.target

[Service]
User=DEIN_BENUTZERNAME
WorkingDirectory=/PFAD/ZUM/HUG/VENV
ExecStart=/PFAD/ZUM/HUG/VENV/bin/waitress-serve --listen=192.168.178.117:8001 --threads=2 NAME_DES_SKRIPTS:__hug_wsgi__
Restart=always

[Install]
WantedBy=multi-user.target

Der Pfad zum angelegten venv muss entsprechend eingetragen werden. Da hug, waitress sowie das Python-Modul für Blinkt keine speziellen Rechte brauchen kann das ganze ohne Probleme mit Nutzerrechten laufen, Root-Rechte sind nicht notwendig.

abschließende Worte

Wie schon Eingangs erwähnt habe ich das ganze Projekt primär gestartet, um mal etwas mit hug zu machen, was auch einen halbwegs produktiven Nutzen hat. Die Nutzung von hug ist ziemlich einfach und gradlinig. Ein bisschen komisch ist bei hug nur, dass die Doku auf zwei Webseiten verteilt ist, nämlich https://www.hug.rest/website/learn/ und https://hugapi.github.io/hug/ 

hug kann übrigens nicht nur webbasierte REST-APIs, sondern auch APIs für die Kommandozeile. Und hug hat noch wesentlich mehr Funktionen und Möglichkeiten als die hier gezeigten / genutzten.

Das hier gezeigt Programm ist natürlich auch nicht auf die Nutzung in Kombination mit der Blinkt LED-Leiste beschränkt. Das Prinzip lässt sich leicht auch auf andere Hardware übertragen, für die es ein Python-Modul zu Ansteuerung gibt.

Mit der Blinkt LED Leiste lässt sich übrigens sehr gut und einfach eine DIY Ambilight Beleuchtung hinter einem Monitor, Fernseher oder ähnlichem realisieren. Dazu einfach einen Raspberry Pi Zero W mit aufgesteckter Blinkt LED-Leiste hinter einem Monitor anbringen und die Pixel der Blinkt Leiste über eine Web REST-API wie hier gezeigt ansteuern.

Sonntag, 12. November 2023

GNU Nano Editor unter Windows intstallieren

Neulich war ich im Terminal von Windows unterwegs und wollte mal schnell eine einfache Änderung in einer Textdatei machen. Unter Linux wäre das ein Fall für den Nano Editor gewesen - unter Windows gibt es nach meinem Wissen im Terminal aber keinen vorinstallierten, textbasierten Editor. Also Google befragt, was es so gibt. Und, siehe da, es gibt tatsächlich Nano nicht nur für Linux, sondern auch für Windows.

Installieren lässt sich Nano auch direkt im Terminal über eine Paketverwaltung für Windows namens winget. Das ist unter Windows 11 und den neueren Versionen von Windows 10 normalerweise vorinstalliert. Wenn nicht kann man es einfach nachinstallieren, eine Anleitung und weitere Informationen sind auf der Infoseite von Microsoft zu finden.

Die Installation ist ganz einfach: einfach im Terminal den Befehl

winget install -e --id GNU.Nano 

ausführen und die aktuelle Nanio-Version wird installiert.

Über winget lässt sich auch noch andere Software aus dem GNU Projekt installieren, wie z.B. der Editor Emacs oder der textbasierte Dateimanager Midnight Commander. Eine Übersicht über die installierbare Software des GNU Projekt ist auf dieser Seite zu finden.

Leider ist es mir (bis jetzt) nicht gelungen die Stelle herauszufinden, wo man die Konfigurationsdatei .nanorc ablegen muss, um Nano Konfigurationsoptionen wie automatisches Einrücken, Zeilennummern, Leerzeichen statt Tab usw. mitzugeben.

Nano ist sicherlich nicht der beste Editor für Windows, aber für einen schnellen Edit im Terminal bzw. in der Powershell, ohne diese zu verlassen, gut genug.

Nano in der Windows Powershell




Sonntag, 29. Oktober 2023

ein snap Paket untersuchen

ein snap Paket untersuchen

snap Pakete bestehen aus einer einzelnen Datei mit der Dateiendung .snap . Diese Datei ist technisch gesehen ein SquashFS-Dateisystem, in das alle Dateien, die das snap enthält, verpackt sind.

Was in dem snap Paket enthalten ist kann man unter- / durchsuchen. Je nach dem, ob das snap ins System eingehängt ist oder nicht unterscheidet sich der Weg.

eingehängtes snap Paket untersuchen

Den Inhalt eines snap Pakets, welches ins System eingehängt ist, zu unter- / durchsuchen benötigt keine weiteren Hilfsmittel. Man navigiert einfach zum Einhängepunkt, also /snap . Dann geht man den Ordner des gewünschten snaps, z.B. firefox für das Firefox snap. In diesem Ordner befindet sich immer ein Verzeichnis current , welches immer ein Link auf die aktuelle, installiert, eingehängte Version des snaps ist. Wechselt man in dieses Verzeichnis sieht man alle Dateien und Verzeichnisse, die zum snap gehören. Durch diese Verzeichnisse kann man navigieren, sich Dateien anzeigen lassen usw. Befindet man sich z.B. im Verzeichnis /snap/firefox/current kann man sich im Terminal über

less firefox.desktop

die im snap enthaltene .desktop-Datei anzeigen lassen.

nicht eingehängtes snap Paket untersuchen

Ein snap Paket muss aber nicht zwingend eingehängt sein, um es zu untersuchen. Man kann auch heruntergeladen bzw. nicht eingehängte Pakete untersuchen. Komfortable geht dies, indem man Programme aus den squashfs-tools-ng nutzt.

Vorbereitung: squashfs-tools-ng installieren

Das Paket "squashfs-tools-ng" ist in den Paketquellen einiger Linux-Distribution enthalten. Eine Übersicht findet man auf der Projektseite.

Unter Ubuntu / Debian / Raspberry Pi OS kann man das Paket über

sudo apt install squashfs-tools-ng

installieren.

Inhalt anzeigen lassen

Nach der Installation von "squashfs-tools-ng" steht unter anderem das Programm rdsquashfs zur Verfügung. Mit dessen Hilfe kann man sich den Inhalt eines SquashFS-Dateisystems - und damit auch eines snaps - anzeigen lassen.

Zuerst wechselt man in der Verzeichnis, wo das snap Paket liegt. Unter Ubuntu werden regulär installierte snaps unter /var/lib/snapd/snaps gespeichert. Liegt dort z.B. das snap core22_858.snap kann man sich über

sudo rdsquashfs core22_858.snap -l /

das Wurzelverzeichnis diese snaps anzeigen lassen. Das sudo ist hier notwendig, weil die snaps in /var/lib/snapdsnaps zum Besitzer und zur Gruppe root gehören und die Dateirechte mit 300 restriktiv gesetzt sind. Hätte man ein snap manuell heruntergeladen und z.B. im eigenen Home-Verzeichnis gespeichert, wären das Ausführen des Befehls mit sudo nicht notwendig.

Die Option -l des Programm rdsquashfs gibt an, welches Verzeichnis ausgegeben werden sollte. Möchte man sich z.B. alle ausführbaren Programme ausgeben lassen, die in diesem snap enthalten sind, ruft man den Befehl

sudo rdsquashfs core22_858.snap -l /usr/bin

auf.

Man kann sich mit Hilfe von rdsquashfs auch Dateien aus dem snap ausgeben lasse. Dazu muss man das Programm mit der Option -c gefolgt vom vollen Pfad und Dateinamen aufrufen. Im folgenden Beispiel würde die Datei /etc/systemd/journald.conf aus dem snap aufgelistet:

sudo rdsquashfs core22_858.snap -c /etc/systemd/journald.conf

Man kann sich auch einfach alle Dateien und Verzeichnisse aus dem snap auflisten lassen:

sudo rdsquashfs core22_858.snap -d

rdsquashfs kennt noch einige weitere Optionen, unter anderem zum Entpacken von einzelnen oder allen Dateien aus dem snap Paket. Diese kann man sich über den Aufruf von

rdsquashfs -h
#oder
man rdsquashfs

anzeigen lasssen.

zwei snap Pakete vergleichen

Die "squashfs-tool-ng" beinhalten auch das Programm sqfsdiff mit dem sich der Inhalt von zwei SquashFS-Dateisystemen und damit auch von zwei snap Paketen vergleichen lässt. Im folgenden Beispiel würden zwei Version des core22 snaps, welche im aktuellen Verzeichnis gespeichert sind, vergleichen:

sudo sqfsdiff --old core22_858.snap --new core22_864.snap

Je nach dem, wie viele Dateien und Verzeichnisse im snap Paket sind bzw. sich geändert haben, kann die Ausgabe sehr lang sein. Es gibt verschiedene Arten von Unterschieden:

  • Lautet eine Ausgabezeile regular file ./pfad/zur datei/dateiname differs bedeutet dies, dass die Datei und der Dateipfad in beiden snaps identisch ist, sich aber der Inhalt der Datei verändert hat.
  • Lautet eine Ausgabezeile > /pfad/zur/datei/dateinamebedeutet dies, diese diese Datei mit diesem Dateipfad in der neuen Datei enthalten ist, aber nicht in der alten.
  • Lautet eine Ausgabezeile < /pfad/zur/datei/dateinamebedeutet dies, diese diese Datei mit diesem Dateipfad in der alten Datei enthalten ist, aber nicht in der neue.
  • Lautet eine Ausgabezeile /pfad/zur/datei/dateiname has a different link target bedeutet dies, dass Datei und Dateipfad in beiden snaps enthalten ist, aber sich die Verlinkung innerhalb des SquashFS-Dateisystem geändert hat.

sqfsdiff kennt eine Reihe von Optionen, um die Vergleich einzuschränken. Die Optionen kann man sich über den Aufruf von

sqfsdiff -h
#oder
man sqfsdiff

anzeigen lassen.

Links

snap

  • Startseite der offiziellen Dokumentation zu snaps

squashfs

squashfs-tools-ng

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