Python Ecosystem
Python unterstützt verschiedene Paradigmen (prozedurale, funktionale, objektorientierte). Es handelt sich um eine interpretierte Sprache - somit entfällt das lästige Erstellen compilieren (man kann aber auch Just-In-Time-Compiler wie PyPy verwenden) ... einfach Editor auf und coden - was man eben von einer Skriptsprache erwartet. Auf diese Weise kann man es auch sehr praktisch mit Shellprogrammierung mixen oder die Shellskripte vielleicht sogar ablösen.
Versionen
Python gibt es in verschiedenen Versionen. Version 2 (hat am 1.1.2020 sein End-of-Life erreicht) und 3 sind zueinander inkompatibel. Dieser Code
stadt = input("In welcher Stadt wohnst Du?")
if "berg" in stadt:
  print("Du wohnst am Berg :-)")
else:
  print("Du wohnst NICHT am Berg :-)")läuft in Python 3 aber nicht in 2.
Glücklicherweise lassen sich beide Versionen parallel betreiben, da die Binaries dann python, python2 oder python3 heißen (es gibt noch weitere Binaries, die dieses Konzept dann auch verwenden). Am besten funktioniert das mit virtuellen Environments (siehe unten), so daß man unabhängig von der Version immer mit python arbeiten kann.
Linux - switch to Python 3 default
Ich hatte ein realtiv altes Ubuntu-System, das noch Python 2 als default verwendete. Python 3 war schon installiert, doch ich wollte nicht immer python3 eingeben müssen, um den "richtigen" Interpreter zu starten. Leider besteht Python nicht nur aus dem Interpreter, sondern auch aus dem Paketmanager pip und noch weiteren Tools (pipenv, ...), die dann alle zueinander passen müssen. Das kann ganz schön nerven und unter Umständen sogar das System zerschießen, wenn andere Systemtools evtl. dauaruf aufbauen.
Es gibt verschiedene Lösungen
Aus meiner Sicht sollte man IMMER virtuelle Environements verwenden.
Getting started
Python im Browser
Um mal eben schnell was auszuprobieren (während des Lernen der Sprache) hat sich ReplIt etabliert. Deutlich besser als Python in Shell Mode.
Lokale Installation - Command Line
Ubuntu via APT
Install Python sudo apt-get install python3-pip and create a file hello.py
Ubuntu tarball
Über einen tarball (z. B. Python-3.9.5.tgz) ist es häufig einfacher eine bestimmte Version oder die neuesten Versionen zu installieren. Evtl. hat die verwendete Linux-Distribution kein Paket für die relevante Version.
Über einen tarball compiliert man Python aus den Sourcen selber innerhalb weniger Sekunden. Man braucht allerdings auch ein paar Development Packages (siehe offizielle Dokumentation). Leider führen fehlende Developer Packages nicht sofort bei configure, make oder altinstall (siehe unten) zu einem Fehler. Stattdessen können die Fehler später (z. B. pip install -r requirements.txt) zu einem Abbruch führen. Das ist leider recht frustrierend :-(
DESHALB: der abschließende Test der Installation per
python -m testist absolut erforderlich, um spätere Probleme zu vermeiden, die man dann nicht so einfach einer unvollständigen/fehlerhaften Installation zuordnen kann. Nichtsdestotrotz kann man den Test natürich jederzeit durchführen.
Danach gehts per
wget https://www.python.org/ftp/python/3.8.10/Python-3.8.10.tgz
tar xvf Python-3.8.10.tar.xz
cd Python-3.8.10
./configure --enable-optimizations --with-ensurepip=install
make -j 8
sudo make altinstallACHTUNG: das Scripting scheint das Fail-Fast-Prinzip zu ignorieren ... so brach
makebei mir nicht beim ersten Fehler ab. Glücklicherweise war der Fehler auffällig in rot im Consolen-Output zu sehen ... sonst wäre mir das nie aufgefallen.
Der letzte Befehl ist entscheidend und wird unbedingt empfohlen. Hierdurch wird die neue Version parallel zu den bereits existierenden installiert und ersetzt diese nicht.
In obigem Beispiel wurde eine Version
/usr/local/bin/python3.8installiert, die sich übrigens nicht neben den per APT installierten Versionen/usr/bin/python(Python 2) und/usr/bin/python3(Python 3).
Anschließend teste ich die Version über eine virtuelle Umgebung:
cd my-project
virtualenv --python=/usr/local/bin/python3.8 venv
source ./venv/bin/activate
python --version
python -m testfür newbe's:
python -m teststartet das Modultest... das wiederum einen Test der Python-Installation durchführt.
Wenn alle Tests (dauernn schon ein paar Minuten) erfolgreich waren, dann kanns losgehen :-)
iOS Pythonista
Auf meinem iPad habe ich die App Pythoista installiert, mit der sich kleinere Programme auch unterwegs implementieren lassen. Die vorhandenen Bibliotheken sind natürlich eingeschränkt (es ist aber mehr als die Python-Standardinstallation), doch für Algorithmen reicht es allemal.
Online Entwicklungsumgebungen
...
Nutzungsmodi
Python Shell Mode
Dieser Modus wird per python gestartet ... es ist anschließend kein Editor notwendig, um Dateien zu editieren und zu speichern. Stattdessen wird der Code direkt in die Shell geschrieben und direkt ausgeführt. Das ist sehr praktischen, wenn man mal eben schnell eine Berechnung wie beispielsweise print(2**10) ausführen will. Streng genommen, kann man sich das print sparen und einfach 2**10 eingeben. Die Werte der Expressions werden in diesem Modus automatisch ausgegeben.
>>> 2**10
1024
>>> print(2**10)
1024
>>> "Pierre"
'Pierre'
>>> vorname="Pierre"
>>> nachname="Feldbusch"
>>> vorname + " " + nachname
'Pierre Feldbusch'In File-Mode
In diesem Modus muß man dem Python-Interpreter eine Datei (= Modul) vorwerfen, die er dann interpretiert/ausführt:
echo "print(42)" > main.py
python main.pywird dann die Zahl 42 ausgegeben :-)
Browser-Mode
Python läßt sich auch im Browser ausführen - insbes. für Einsteiger ist das eine angenehme Option:
Auto-Formatting
Syntax-Highlightning
Funktionsdoku
Code Intelligence
Evtl. lassen sich nicht alle Bibliotheken und Features (Multi-Threading) nutzen. Für den Anfang bzw. kleinere Skripte könnte das aber ausreichen.
AWS Serverless
Python Code läßt sich als Lambda Function in AWS ausführen.
Visual Studio Code
prepare System - see above
Starte Visual Studio Code and installiere Visual-Studio-Code Python Extension von Microsoft. Create a file hello.py:
msg = "Hello World, Pierre"
print(msg)Führe anschließend den Code per "Run Selection/Line in Python Terminal" aus :-)
In der unteren blauen Statusleiste ist die verwendete Python-Version zu erkennen. Hier kann man die Version auch umschalten ... beispielsweise auf ein Virtuelles Environment.
Nichtsdestotrotz hatte ich mit den Default-Einstellungen der Python-Extension einige Probleme:
Navigieren in importierte Module (auch meine eigenen, die sich direkt im gleichen Git-Repository befinden) funktioniert nicht. Hierzu finde ich diesen Beitrag:
der Wechsel des Python-Interpreters funktioniert nicht zuverlässig. Ich muß dann immer mal die Datei wechseln, um den neuen Interpreter auch tatsächlich in der Statusleiste angezeigt zu bekommen
nach Installation eines Moduls per
pip install module(über meine Console oder auch das VSCode Terminal) steht das Modul scheinbar noch nicht zur Verfügung. Einimport modulesorgt für einen Fehler, der erst durch einen VSCode-Neustart verschwindet
Nach Umstellen vom Microsoft Language-Server auf Pylance hat alles schon viel besser funktioniert:

Enthält ein Projekt eine Virtuelle Umgebung z. B. in PROJEKT/venv-myproject so erkennt das VSCode und verwendet den darin befindlichen Interpreter und deren Libraries. In der unteren blauen Statusleiste ist das zu erkennen - hierüber kann man au
Best Practices
Ich verwende normalerweise einen VSCode Workspace, der 30 oder mehr Projekte enthält (das funktioniert, weil VSCode wirklich super schnell ist). Für die Nutzung in einem Python-Projekt ist das allerdings nicht die beste Lösung, da relative Module (in lokalen Packages) nicht aufgelöst werden können. Das sorgt für angezeigte Fehler und reduziert VSCode von einer Python-IDE zu einem reinen Editor.
Deshalb öffne ich mein Python Projekt in einer separaten VSCode-Instanz, das nur diese eine Projekt im Root enthält.
Python Virtual Environments
ACHTUNG: virtuelle Environments erfordern die Bereitstellung von Python-Versionen auf dem System ... das virtuelle Environment bringt diese nicht mit. In dem virtuellen Environment wird dann eine der auf dem System installierten Python Versionen verwendet.
Bei diesem Ansatz werden Bibliotheken und Interpreter in einem applikationsspezifischen Verzeichnis installiert, so daß verschiedene Ausführungsumgebungen voneinander getrennt werden können. Auf diese Weise werden Konflikte vermieden und man kann sich nicht auf Bibliotheken stützen, die im Zusammenhang mit einem anderen Projekt installiert wurden ... das verbessert die Qualität in den projektspezifischen Dependencies (requirements.txt Datei).
Installation per Linux-Package
Das ist die präferierte Variante, da
python -m venvnicht alle Features mitbringt. Das Paket zum Management dieser virtuellen Umgebungen wird persudo apt install virtualenvinstalliert.
Per (z. B.) virtualenv my-venv wird ein virtuelles Environment im Ordner ./my-venv angelegt.
Installation per pip
venv ist auch ein Python Modul und kann per pip install venv installiert werden. Die Erzeugung einer virtuellen Umgebung erfolg dann per python -m venv my-venv
Best-Practice
Ich präferiere, das virtuelle Environment in einen Subfolder (z. B. ~/src/jenkins-stats/venv) des Git-Repo's zu packen, das den Source-Code enthält. Zumindest, wenn ich den Source-Code selbst unter Kontrolle habe, denn dann füge ich venv zur .gitignore des Repos hinzu. Aus meiner Sicht erhöht das die Übersichtlichkeit ... das virtuelle Environment liegt nicht mehr irgendwo, sondern direkt im Python-Projekt.
So sieht der ganze Prozess dann aus:
git clone https://github.com/HewlettPackard/Jenkins-stats.git jenkins-stats
cd jenkins-stats
mkdir venv
echo venv >> .gitignore
virtualenv venv
source venv/bin/activateAnschließend befinden sich in diesem Verzeichnis einige Shell-Skripte. Um diese Umgebung zu aktivieren wird source ~/src/jenkins-stats/venv/bin/activate ausgeführt. In meiner zsh-Shell ändert sich dadurch der Command-Prompt ... der Name der aktuell aktiven virtuellen Umgebung ist erkennbar:
╭─pfh@workbench ~/src/com.github  
╰─➤  source ~/ideWorkspaces/venv/jenkins-stats/bin/activate
(jenkins-stats) ╭─pfh@workbench ~/src/com.githubBackground Info
Die typischen Python Kommandos (python, pip, ...) sind in einem virtuellen Environment auf die Skripte in der virtuellen Umgebung (~/src/jenkins-stats/venv/bin/python) umgebogen. Die Installation von Libraries per pip install Jinja2 oder pip install -r requirements.txt führen zur Installation der Pakete im virtuellen Environment (~/src/jenkins-stats/venv/lib) ... nicht im System-Installationsverzeichnis für Python-Module.
virtualenv verwendet dabei die auf dem System verfügbare Default-Python-Version (die per python verfügbar ist)... kopiert dabei aber die Executables in die virtuelle Umgebung. Hat man noch eine andere Python-Version (z. B. python3) installiert (aber Python 2 war der Default bei python --version), so kann man diese per
virtualenv --python=/usr/bin/python3 venvIst dies die erste Python Version in dem virtuellen Environment, dann wird sie per python --version verfügbar sein ... es muß kein python3 --version verwendet werden.
ich habe lange überlegt, ob ich meinen virtuellen Environments einen kontextabhängigen Namen geben soll, damit ich in meiner ZSH-Shell immer weiß welche virtuelle Umgebung (wird dort angezeigt) aktiviert ist. Letztlich habe ich mich dagegen entschieden und verwende IMMER
venv. Erstens scheint das der Standardname zu sein und zweitens kann ich so einfachvenvin jedes meiner.gitignorepacken. Vor der Python-Nutzung initialisiere ich dann immer die virtuelle Umgebung ... hierzu verwende ich einenalias venv=./venv/bin/activate. Für die Erzeugung habe ich einen anderenvenv-create='virtualenv --python=/usr/bin/python3 venv. Das macht das Handling komfortabler ... ich könnte auch ein Shellscript als Decorator umpythonbauen, um eine automatische Aktivierung durchzuführen (aber natürlich müßte ich dann auf der Konsole immer im Python-Projekt stehen, um die passende Umgebung zu finden).
Das Umschalten auf eine neue virtuelle Umgebung ist in VSCode ein bisschen umständlich, wenn ich einen Workspace verwende, der ganz viele Projekte (auch mit unterschiedlichen virtuellen Umgebungen) enthält. Starte ich hingegen einen VSCode-Worspace mit nur einem Projekt/Verzeichnis, unter dem sich dann auch gleich der Ordner venv befindet, dann ist das super komfortabel, da meine venv-Umgebung sofort zur Auswahl steht.
Sprache
Man sollte sich an die typischen Idiome/Patterns halten und auch den Styleguide berücksichtigen. Bestenfalls unterstützt die IDE hierbei und gibt Warnungen aus, wenn sie nicht eingehalten werden (Stickwort Linting).
Ganz wichtig:
eine Python-Datei (
foo.py) repräsentiert ein Modul, das wiederum ... wild gemischt enthalten kannausführbaren Code (
print("pierre"))Funktionsdefinitionen (
def foo():)Klassendefinitionen (
class Foo:)Meta-Daten (
import random)
Besonderheiten
Variables don’t have types in Python; values do. That means that it is acceptable in Python to have a variable name refer to an integer and later have the same variable name refer to a string.
Python unterstützt viele Programmier-Paradigmen ... prozedural, objektorientiert, funktional. Für mich als Java-Entwickler war die Mischung am Anfang etwas befremdlich, d. h. man schreibt sein Hauptprogramm
main.pyund darin sind dann prozedurale Element (Initialisierung), aber auch schon gleich erste Klassen. Eine Mixtur aus Klassen und Initialisierung globaler Variablen. Geht sicher auch anders oder ist das ein gängiges Idiom? Vielleicht habe ich nur keinen Çlean-Code gesehen ...Python hat Built-In-Funktionen wie
type,len,input,range... diese können ohneimportgenutzt werdenDatentypen wie
int,string, Listen, Tupel haben eingebaute Funktionen, die man nutzen kann ... (print("Pierre".count("r"))"Pierre".split("e")
"_".join(["P", "i", "e", "r", "r". "e"])
Variablen
Global-Namespace vs. Local-Namespace:
def anyFunction(x):
  m = 8
  z = m + 6
  w = y + 1
  y = x * x + k
  return y
y = 5
k = 3
m = 1In diesem Fall ist y im globalen Namespace definiert - innerhalb der Funktion anyFunction wird y allerdings als lokale Variable behandelt, weil sie eine Zuweisung (y = x * x + k) erhält. Im lokalen Kontext hat die Variable aber keinen Wert und es wird zu einem Fehler kommen.
Im Gegensatz dazu hat die globale Variable k in anyFuntion keine Zuweisung, sodaß innerhalb anyFuntion der globale Wert k = 3 verwendet wird.
m hingegen ist sowohl im globalen Namensraum als auch im lokalen Namensraum von anyFuntion. Der lokale Namensraum hat höhere Priorität.
Absurd ist in Python, daß man
global mdefinieren könnte und dann würde innerhalbanyFuntionder Wert aus dem globalen Namensraum verwendet. Wer ist das denn für ein Quatsch???
Einrückung
Einrückungen sind bei Python wichtig. Bei Kontrollstrukturen ist das gleich offensichtlich. Bei folgendem Code nicht sofort ... dieser Code ist syntaktisch falsch wegen der Einrückung:
    fruit = "Banane"
    print(fruit)So muß es aussehen:
fruit = "Banane"
print(fruit)Standarddatentypen
int
float
str
bool (
True,False)
Man kann den Type eines Wertes per "Casting" verändern int5 = int("5").
Sequences
Sequences sind die Datentypen
String (immutable)
List (mutable) ... mit eckigen Klammern definiert
ändern
names[1] = "Pierre"einfügen
names[1:1] = ["Jonas", "Robin"]anhängen
names += ["Jonas", "Robin"]löschen
names[1:3] = []del names[1:3]
Liste clonen by Slice-Operator:
namesClone = names[:]der Operator
+=hat ein spezielles Handling bei (mutable) List: "obj = obj + object_two is different than obj += object_two ... The first version makes a new object entirely and reassigns to obj. The second version changes the original object so that the contents of object_two are added to the end of the first."Empfehlung: den
+=Operatior nicht bei Listen - oder besser - gar nicht verwenden
Tuple (immutable) ... mit runden Klammern definiert
die runden Klammern können weggelassen werden (weil dieser Sequenz-Typ der typische ist - Stichwort funktionale Programmierung) ... die beiden folgenden Tupel sind semantisch gleich
tupelA = ( "Banane", "Apfel", "Pfirsich" ) tupelB = "Banane", "Apfel", "Pfirsich"
Besonderheiten von Sequenzen
bei immutable Datenstrukturen nimmt Python eine Speicheroptimierung vor und speichert den gleichen Wert nur ein einziges mal (Aliasing) - bei mutable Datentypen ist das nicht der Fall!!!
list = ["hello", 2.0, 5, [10, 20]]ist in Sprache wie Java nicht erlaubt, weil die Werte unterschiedlichen Typs sind ... in Python ist das erlaubt wegen der Regel "Variablen haben keinen Typ - Werte haben einen Typ"mit Listen funktionieren auch typische Operatoren wie
+und*blist = alist * 2
tupel = ("hello", 2.0, 5, [10,20])ist ein Tupel ... sieht einer Listlist = ["hello", 2.0, 5, [10,20]]sehr ähnlich ist aber immutableSequenzen unterstützen Slicing ... List-Slices sind Listen, Tupel-Slices sind Tupel, String-Slices sind Strings:
name = Pierre; inBetween = name[1:len(name)-2]
Tupel:
('n', 'no', 'nop', 'nope')Elemente müssen nicht vom gleichen Datentyp sein:
((12345, 54321, 'hello!'), (1, 2, 3, 4, 5))im Gegensatz zu
listist ein Tuppel immutable und hat i. a. unterschiedliche Datentypen - Tupel werden in anderen Use-Cases verwendet
Sequence - String
Speicheroptimierung:
fruitA = "Banane"
fruitB = "Banane"
print("compare identity - expected True", fruitA is fruitB)
print("compare content - expected True:", fruitA == fruitB)mit String kann man "rechnen"
def multiply(s, mult_int): return s * mult_int print(multiply("Hello", 3)) # HelloHelloHelloimmutable Functions:
nameUpper = "Pierre".upper()nameLower = "Pierre".lower()stripped = " Das ist ein Test ".strip()stripentfernt auch Zeilenumbrüche
replaced = "Das ist ein Test".replace("a", "b")print("Hallo {}, ich bin {} Jahre alt und ich habe {} Euro in der Brieftasche".format("Pierre", 13))String-Formatierungsvarianten
hier kann man den Wert noch formatieren (z B.
print("Hallo {}, ich bin {} Jahre alt und ich habe {:.2f} Euro in der Brieftasche".format("Pierre", 13, 100)))noch schöner ist, wenn man den Platzhaltern Namen geben kann
das vereinfacht Refactorings am String, weil die Reihenfolge der Variablenwerte keine Rolle spielt:
print("Hallo {name}, ich bin {alter} Jahre alt und ich habe {betrag:.2f} Euro in der Brieftasche".format(name="Pierre", alter=13, betrag=100))
wiederholte Strings müssen nicht als Werte wiederholt angegeben werden:
print("Hallo {name}, ich bin {alter} Jahre alt und ich habe {betrag:.2f} Euro in der Brieftasche. Bis bald, {name}".format(name="Pierre", alter=13, betrag=100))
mittlerweile hat sich der f-String durchgesetzt, der noch leichter lesbar ist. Es erlaubt Formatierungen und Rechenoperationen innerhalb des Strings
print(f"Hallo {name}, ich bin {alter} Jahre alt und ich habe {betrag:.2f} Euro in der Brieftasche")print(f"a + b = {a+b}")
Interessante Built-In Functions:
l = list("Pierre")liefertl = ["P", "i", "e", "r", "r", "e"]s = set("Pierre")lieferts = {"P", "i", "e", "r"}(oder eine andere Reihenfolge)
Sequence - List
mit mutating Object-Functions (funktioniert natürlich nicht auf immutable Tuples) - es werden keine neuen Objekte erstellt, d. h. list = list + ["banana"] erstellt ein neues list Objekt ... was bei list.append("banana") nicht der Fall ist:
list.append("banana")list.count("banana")list.insert(1, "banana")list.index("banana")list.remove("banana")list.reverse()list.sort()list.sort(key=None, reverse=False)list.pop()auf diese Weise lassen sich mit
appendundpopStacks implementieren
list.popleft()auf diese Weise lassen sich Queues mit
list.append()/list.popleft()implementierennicht performant - besser
queueaus demcollections-Paket verwenden
Alternativ zu den Object-Functions (z. B. list.sort()) kann man die Built-In-Funktion sorted(list) verwenden, die immutable (= funktional) ist und somit auch auf immutable Sequences (z. B. Tupels).
Sequence - Tupel
Speicheroptimierung:
fruitsA = [ "Banane", "Apfel", "Pfirsich" ]
fruitsB = [ "Banane", "Apfel", "Pfirsich" ]
print("expected False (compare identical object)", fruitsA is fruitsB)
print("expected True (compare content!!!)", fruitsA == fruitsB)
print("expected False", id(fruitsA) == id(fruitsB))Mit Tupel-Assignment läßt sich das Tauschen von Variablenwerten sehr elegant beschreiben:
a = 3
b = 5
print(a, b)
a, b = b, a   # siehe Anmerkung
print(a, b)In
a, b = b, azeigt sich die Eleganz von Python. Im Prinzip handelt es sich um diese Zuweisung:(a, b) = (b, a). Dadurch, daß die Klammern bei Tupeln optional sind, wirkta, b = b, awie ein neues Sprachkonstrukt.
Auf Tupeln kann man per sorted sortieren (Breaking Ties Eigenschaft):
list = [(3, 5), (1,4), (1, 3)]
print(sorted(list))    # [(1, 3), (1, 4), (3, 5)]Diese Eigenschaft kann man gut verwenden, um komlexe Sortierkriterien zu definieren:
list = ["Anton", "Zorro", "Nathan", "12345", "Robin"]
print(sorted(list, key=lambda name: (len(name), name))    # ["12345", "Anton", "Robin", "Zorro", "Nathan"]Sets
geschweifte Klammern (wie in der Mathematik)
set:myset = {"Pierre", "Silke", "Jonas"}mit comprehensions:
a = {x for x in 'abracadabra' if x not in 'abc'}Operationen:
myset - { "Pierre" }liefert{"Silke", "Jonas"}
Functions:
myset.add("Pierre")myset.pop()myset.discard("Pierre")myset.remove("Pierre")myset.clear()
Auch für Dictionaries werden geschweifte Klammern verwendet. Das führt dazu, daß leere Sets und leere Dictionaries die gleiche Definition
x = {}verwenden würden. Python macht daraus ein Dictionary. Ein leeres set könnte man überx = set()definieren.
Dictionary
Dictionary (Key-Value-Maps) ... mit geschweiften Klammern:
tel = {'jack': 4098, 'sape': 4139}tel['pierre']=3006=>{'jack': 4098, 'sape': 4139, 'pierre': 3006}
ungeordnet
der Zugriff ist aber - wie bei den Squenzen - über eckige Klammern
wichtige Object-Functions
dict.keys()- liefert ein Iterable (for k in tel.keys():)dict.values()- liefert ein Iterable (for k in tel.values():dict.items()- liefert ein Tupelfor k in tel.items(): print(k[0], k[1])mit Tupel-Assignment kann das aber eleganter geschrieben werden
for (key, value) in tel.items(): print(key, value)
# Creation at initialization time
dictAlt = { "one":"eins", "two":"zwei", "three":"drei" }
print(dictAlt)
# Creation after creation
dict = {}
dict["one"] = "eins"
dict["two"] = "zwei"
dict["three"] = "drei"
dict["wasauchimmer"] = ["pierre", "feldbusch", 1972]
print(dict)
del dict["wasauchimmer"]
# dict.keys() erzeugt nur ein Iterable, aber keine list ... aber wird können es per cast transformieren
for key in dict.keys():
  print(key, ":", dict[key])
for value in dict.values():
  print(value)
# hier wird eine List erstellt und ist damit ein Iterator
for item in dict.items():
  print("key: ", item[0], "value", item[1])
# ... aber VIIIIEL eleganter <==== IDIOM
for k, v in dict.items():
  print("key: ", k, "value", v)
# dict implementiert einen Iterator ... wie list/tupel
for key in dict:
  print(key, ":", dict[key])
if "two" in dict:
  print("two ist drin")
# wenn man auf einen Key per Indexing zugreift, der nicht existiert, gibt es einen Fehler
# Wenn man also nicht genau weiß, ob der Key drin ist, dann sollte man es vorher
# prüfen (um den Runtime-Error zu vermeiden) oder die get-Methode verwenden
value = dict.get("Pierre")          # liefert None
if value is None:
  print("nicht gefunden")
else:
  print(value)
print(dict.get(value, "default"))   # liefert default
if value in dict:
  print(dict[two])
feldbusch = { "Pierre" : 48, "Pierre": 76 }
print(feldbusch["Pierre"])        # liefert 76
# dictionary sortieren nach values
dict = { "Pierre" : 48, "a": 76, "b": 14, "c": 100 }
for k in sorted(dict.keys(), key=lambda k: dict[k]):
  print(k, dict[k])
# ... oder noch kürzer ... das sieht doch schon fast wie eine DSL aus :-)
for k in sorted(dict, key=lambda k: dict[k]):
  print(k, dict[k])Type Hints
Python ist eine untypisierte Sprache ... Variablen haben keine Datentypen ... Werte haben Datentypen. Deshalb ist folgendes problemlos möglich:
var = "Pierre"
print(var)
var = 42
print(var)Die Variable ändert seinen Typ zur Laufzeit. Bei kleineren Skripten und wenigen Datentypen ist das ganz ok, doch hinsichtlich der Lesbarkeit des Code ist schwierig. Hier weiß man nicht, ob das Ergebnis ein String ist oder eine Liste oder gar ein Set:
def getServices(category):
  my_name = "Pierre"
  if category == "backend":
    return [ "a", "b", "c" ]
  else:
    return [ "d", "e", "f" ]Erst in der Implementierung erkennt man den zurückgegebenen Datentyp Liste. Bei komplexeren Methoden mit evtl. mehreren return Statements kann das problematisch werden - die Folge sind Fehler ... zudem will niemand unleserlichen Code refactorn (der Anfang vom Ende).
Aus diesem Grund hat Python aus Type Checking eingführt, so daß man dem Code mit
from typing import List
def get_services(category:str) -> List[str]:
  my_name: str = "Pierre"                   # sieht gewöhnungsbedürftig aus
  if category == "backend":
    return [ "a", "b", "c" ]
  else:
    return [ "d", "e", "f" ]
services = get_services("backend")mehr Semantik verleihen kann. Das verhindert erstens Fehler und ermöglicht zudem eine bessere Code-Navigation in der IDE mit Auto-Completion-Support (bei Eingabe von service. in der IDE werden nur die Funktionen/Methoden einer List zur Auswahl gestellt). So klappt das Tippen doch gleich viel schneller und mit weniger Fehlern. Wer schon mal versucht hat, eine unbekannte API (ich hatte das Vergnügen mit api4jenkins) ohne Type Hints zu verwenden, weiß wovon ich spreche. Das endet in frustrierendem Trial-and-Error ... wer Glück hat findet zumindest eine gute Dokumentation. Da greife ich doch lieber gleich zur REST-API und der requests-API ... da bin ich wahrscheinlich schneller - obwohl ich davon überzeugt bin, daß api4jenkins die bessere Abstraktionsebene bietet.
Type checking zur Compile-Zeit bzw. vor der Ausführung hat man damit aber noch nicht ... zumindest nicht mit python von der Console. Eine gute Python-IDE kann hier helfen.
Alias Types
Mehrfach geschachtelte Typen wie z. B. List[Tuple[str, str]] sind i. a. recht schwer zu lesen. Hier kann es helfen einen Alias zu definieren, der einen semantischen Namen verwendet, so daß man dann
Card = Tuple[str, str]
Deck = List[Card]verwendet. Aus meiner Sicht deutlich besser zu lesen.
An dieser Stelle kann man allerdings auch eine Klasse
Carddefinieren. Das hätte den Vorteil, daß man dann auch noch Methoden reinpacken kann. Aus meiner Sicht sind Alias ein guter Mittelweg zwischen nichtssagenden Datentypen und Klassen mit Daten und Methoden.
Exceptions
Exceptions dienen der Behandlung von Ausnahme-Situationen. Sie sollen verhindern, daß das Programm komplett abbricht und man stattdessen eine weitere Chance erhält (Stichwort Resilience).
Der Exception-Typ (im Beispiel ZeroDivisionError) ist optional ... läßt man ihn weg, wird jegliche Exception vom except-Block "gefangen".
vornamen = [ "pierre", "hans", "patrick" ]
try:
    x = 5/0
    vorname = vornamen[3]
    myvar = doesNotExist
except ZeroDivisionError as e:
    print("got an ZeroDivisionError")
    print(e)
except IndexError as e:
    print("got an IndexError")
    print(e)
except:
    print("got any other error")
    print(e)Man sollte Exceptions aber nicht mißbrauchen, um den Programmfluß für typische Use-Cases zu steuern:
vornamen = [ "pierre", "hans", "patrick" ]
i = 0
while True:
  try:
      print(vornamen[i])
      i += 1
  except IndexError:
      breakExceptions sind Klassen und haben eine Vererbungshierarchie. Ein except ArithmeticError: fängt alle von ArithmeticError abgeleiteten Exceptions, also ZeroDivisionError, FloatingPointError und OverflowError. Auf diese Weise kann man mit EINEM except-Statements gleich eine ganze Familie von Fehlern fangen.
Mit einem KeyboardInterrupt-Exception-Handler lassen sich beispielsweise Server-Prozesse sauber beenden:
def createServer():
  serversocket = socket(AF_INET, DOCK_STREAM)
  try:
      serversocket.bind(("localhost", 5000))
      serversocket.listen(5)
      while True:
        (clientsocket, address) = serversocket.accept()
        receivedData = clientsocket.recv(5000).decode()
        # interpret received data
        # ...
        # prepare response
        data = "HTTP/1.1 200OK\r\n"
        data += "Content-Type: text/html; charset=utf-8\r\n"
        data += "\r\n"
        data += "<html><body>hello world</body></html>\r\n\r\n"
        # send response
        clientsocket.sendall(data.encode())
        clientsocker.shutdown(SHUT_WR)
  except KeyboardInterrupt:
    print("Server shutdown initiated")
  except Exception as e:
    print("error")
    print(e)
  
  serversocket.close()
createServer()Exceptions kann man über
raise Myexception()auslösen.
Funktionen
in Python verwendet man für Funktionsnamen (und Variablennamen) Snake-Case (
get_value()) anstatt Camel-Case (getValue())Funktionen liefern IMMER genau EINEN Returnwert ... wenn nicht explizit mit
return bla, dann ist der Returnwert immerNoneund kann dann beispielsweise perif myFunc() == None:abgefragt werdenein Idiom in Python ist die Verwendung von Tupels als Rückgabewert - damit lassen sich sehr schön mehrere Rückgabewerte definieren und die Formulierung sieht dabei sehr elegant aus (ganz ähnlich wie in Golang):
def getSchwerpunkt(whatever): # Berechnung return x, y xAxis, yAxis= getSchwerpunkt(quadrat)
Parameter können auch über Tupel übergeben werden ... allerdings mit einer sehr speziellen Star-Notation:
def setMittelpunkt(x, y): print("(x, y) = (", x, ",", y, ")") x = 3 y = 5 setMittelpunkt(x, y) mittelpunkt = x, y setMittelpunkt(mittelpunkt[0], mittelpunkt[1]) setMittelpunkt(*mittelpunkt)Parameter können Default-Werte haben und sind dann optional ... aber ACHTUNG bei mutable Parameters (z. B. Lists), denn der Default-Wert bleibt erhalten!!!
mandatory Parameter müssen zuerst aufgeführt werden
def doit(value, list=[]): list.append(value) return list print(doit(1)) # [1] print(doit(2)) # [1, 2] print(doit(3)) # [1, 2, 3] print(doit(4), ["Pierre"]) # [Pierre, 4]der Default-Wert wird zum Zeitpunkt der Funktionsdefinition festgelegt - nicht zum Zeitpunkt der Asuführung:
initial = 3 def doit(value=initial): return value initial = 7 print(doit()) # 3man kann hier eine Menge Schindluder betreiben ... das sollte man vermeiden - CleanCode!!! man schreibt den Code für den Leser (Code wird 10x häufiger gelesen als geschrieben) ... irgendwelche Spitzfindigkeiten sollte man vermeiden
Keyword-Parameter machen den Code sehr lesbar und sind in Kombination mit optionalen Parametern häufig absolut notwendig
def doit(x, y=2, z=3): return x * y + z print(doit(1)) print(doit(1, z=5)) # ausgelassener Parameter "y" print(doit(1, z=5, y=7)) # Reihenfolge y, z geändert print(doit(z=5, y=7, x=1)) # Reihenfolge x, y, z geändertDokumentation einer Funktion sollte man mit einen sog. "docstring" machen, denn es gibt Tools, die daraus eine Dokumentation erzeugen
def hello(): """Gibt "Hallo" aus""" print("Hallo") hello()übrigens: die Dokumentation ist zur Laufzeit per
print(hello.__doc__)lesbarbeachte: Funktionen sind ganz normale Objekte
print(type(hello))liefert<class 'function'>
Funktionsparameter können Defaultwerte haben und sind dann optional:
def ask_ok(prompt, retries=4)mit
*argsgibt es eine spezielle Variante von Übergabeparameter: Argumentliste
def store(*args):
    ...
store("Pierre", "Silke")mit
*kvargsgibt es eine spezielle Variante von Übergabeparameter: Dictionary
def store(**kvargs):
    ...
store(name="Pierre", age=27)Als eingefleischter Nutzer typisierter Sprachen finde ich es relativ schwierig eine Funktion aus dem Kontext heraus zu verstehen, weil ich bei der Funktion
def best_key(x):
  # irgendein komplexer codegar nicht weiß, welchen Datentyp x repräsentiert. Der Code innerhalb der Funktion funktioniert aber nur basierend auf einem nicht sichtbaren Kontrakt. Ich muß also den Code der Funktion erstmal halbwegs verstehen, um dann daraus den erwarteten Input-Typ abzulesen.
Das finde ich sehr gewöhnungsbedürftig ... aber konsistent, wenn man berücksichtigt, daß Variablen keinen Typ haben, nur die Werte. Was ich aus dem Code häufig noch schwieriger rauslesen kann ist, ob es sich um eine Liste (mutable) oder ein Tupel (immutable) handelt. Das ist auch für den Entwickler schwierig, der die Methode best_key refactoren will und eigentlich nicht weiß, ob dort immer Listen oder manchmal auch Tupel reinwandern ... das bestimmt nämlich der Aufrufer???
In Python 2 hat man das in die Dokumentation geschrieben. In Python 3 verwendet man hierfür Annotationen ... das wird allerdings von Python nicht ausgewertet, sondern komplett ignoriert:
def floatToInt(x: float) -> int: return int(x)
Durch die fehlende Typisierung kann eine Funktion sogar ganz unterschiedliche Datentypen zuürckliefern:
def hello(name):
  if "Pierre" == name:
    return "Hallo Pierre"
  else:
    return 42
name = input("wie ist dein name")
print(type(hello(name)))Sicherlich kein Best-Practice, aber prinzipiell möglich. Typisierter Sprachen würden das verhindern ... um so wichtiger die losen Best-Practices zu kennen und einzuhalten. ABER: es zeigt sich, daß man damit extremen Spaghetticode schreiben kann :-(
Built-In-Funktionen
... können ohne import genutzt werden
printtypelen(list)max(a, b)inputage=int(input("How old are you? "))
rangel=range(3,6) # l=(3,4,5)
sortedlistB = sorted(listA)listB = sorted(listA, reverse = True)...reverseist ein optionaler ParameterlistB = sorted(listA, key = absolute)mit folgenderabsolute-Funktion (ACHTUNG:absoluteist ein Functionpointer!!!):def absolute(x): if x >= 0: return x else: return -xlistB = sorted(listA, key = lambda x: absolute(x))mit einer Lambda-Function
enumerateerhält eine Sequence als Parameter und liefert ein Iterable von(index, value)- IDIOMstatt
fruits = ['apple', 'pear', 'apricot', 'cherry', 'peach'] for n in range(len(fruits)): print(n, fruits[n])verwendet man
fruits = ['apple', 'pear', 'apricot', 'cherry', 'peach'] for index, fruit in enumerate(fruits): print(index, fruit)
Anonyme Funktionen - Lambda Functions
In meiner Zeit als C-Entwickler nannte man das Functionpointer. Viele Jahre später wurde daraus der Begriff Lambda-Function. Auf diese Art und Weise läßt sich ein Algorithmus als Parameter übergeben, um so das Strategy-Pattern zu implementieren und den Code sehr schön lesbar zu halten. Die Funktionsbeschreibung wird schlanker und erinnert an eine mathematische Funktionsdefinition im Stil von f(x) = x * x häufig auch per x -> x * x ausgedrückt.
Eine Funktion
def func(args):
  return valuewird ganz schematisch (und deshalb können IDEs auch eine automatische Transformation anbieten) folgendermaßen in eine Lambda-Funktion transformiert:
lambda args: valueEin Beispiel:
def square(n):
  return n * n
print(square(2))wird zu
sq = lambda n: n*n
print(sq(2))In diesem Beispiel habe ich eigentlich nicht viel gewonnen, weil ich ja doch eine benannte (Lambda-) Funktion sq erstellt habe. Doch in folgendem Beispiel wird die Mächtigkeit deutlich ... ein Sortierkriterium wird über eine Lambda-Funktion definiert:
pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
pairs.sort(key=lambda pair: pair[1])Bibliotheken / Pakete / Module
"It’s important to keep in mind that all packages are modules, but not all modules are packages. Or put another way, packages are just a special kind of module." (siehe Dokumentation)
Die Stärke einer Sprache liegt häufig in den Bibliotheken, die man verwenden kann. In Python nennt man dies Bibliotheken Packages (sie enthalten ein oder mehrere Module) - sie müssen lokal vorhanden sein. Entweder sind sie bereits in der verwendeten Python-Distribution oder müssen mit dem Python Package Manager (pip) installiert werden. Der Python-Interpreter sucht in folgender Reihenfolge nach Modulen (kann man auch zur Laufzeit über sys.path rausbekommen):
aktuelles Verzeichnis
PATHONPATHUmgebungsvariablePython Installation (hierüber werden die Python-Basis-Bibliotheken eingebunden)
Ein Paket (auf dem Filesystem ein Verzeichnis) ist eine Sammlung von Modulen (auf dem Filesystem eine einzelne
foo.pyDatei). Jedes Modul kann ausführbaren Code enthalten, der beim Import eines Moduls ausgeführt wird. Die Definition einer Funktion/Klasse (def) wird auch "ausgeführt", resultiert aber nicht Ausführung des Bodies, sondern nur in die Aufnahme der Funktionen in die Symboltabelle, so daß sie gefunden werden können.
Für den Import gibt es zwei Varianten:
import randomhiermit wird das Modul
randomgesucht (random.py), initialisiert (Ausführungrandom/__init__.pysofern vorhanden) undrandomwird in die lokale Symboltabelle aufgenommen (damit ist das Paket erst nutzbar), doch die Funktionen/Klassen des Moduls müssen mit vorangestelltem Paketnamen (diceValue = random.randrange(1, 7)) referenziert werden.
import jinja2.environmenthier wird das Package
jinja2verwendetdurch den
importeines Moduls aus diesem Package wird das Package (beim ersten mal) überjinja2/__init__pyinitialisiert - anschließend wird das Moduljinja2/environmenteingelesennun stehen Funktionionen/Klassen über
jinja2.environment.Template()zur Nutzung zur Verfügung
from random import randrangehiermit wird das Modul
random.pygesucht und - wenn noch nicht geschehen - initialisiert. Zudem wird die Funktionrandrangeaus dem Modulrandomin die lokale Symboltabelle des aktuellen Moduls aufgenommen. Beim Aufruf kann man auf den Modulnamen verzichten und die Funktion verwenden als wäre sie im aktuellen Python-File implementiert. Das sieht auf den ersten Blick einfacher aus (diceValue = randrange(1, 7)liest sich angenehmer), doch es geht die Information verloren aus welchem Paket die Komponente stammt.
from jinja2.environment import Templatehier wird das Package
jinja2verwendetdurch den
importeines Moduls aus diesem Package wird das Package (beim ersten mal) überjinja2/__init__pyinitialisiert - anschließend wird das Moduljinja2/environmenteingelesen und die KlasseTemplatewird in der lokalen Symboltabelle hinterlegt => ist also perTemplate()(ohne Paket/Modul-Prefix) nutzbar
Packages wie
jinja2vereinfachen den Import ... es reicht hier ausfrom jinja2 import Templateanzugeben. Hier fehlt die Information über das Modulenvironment.Die Erklärung warum das funktioniert findet sich im
jinja2/__init__.pyModul (zu finden untervenv/lib/python3.6/site-packages/jinja2/__init__.py), das u. a. folgendes macht:from .environment import Template as TemplateDie Klasse wird also bei der Initialisierung des
jinja2-Packages alsTemplatezur Verfügung gestellt. Und genau diesen "Alias" verwendet man dann beifrom jinja2 import Template.Es ist tatsächlich Best-Practice das Public-Interface eines Packages (das der Anbieter unbedingt stabil halten wird (Backward-Compatible)) auf diesem Weg zu definieren!!!
Es wird nicht verhindert auch Funktionen/Klassen aus den Modulen zu nutzen, die nicht public sind (z. B.
from jinja2.environment import get_spontaneous_environment), doch können die jederzeit refactored werden (u. a. gelöscht werden). Auf diesen Funktionen/Klassen sollte man den eigenen Code nicht aufbauen!!!
Funktionen, die in einem anderen Module (= Python-File) innerhalb eines Verzeichnisses liegen, müssen genauso importiert werden wie Funktionen aus Libraries, die man per pip installiert hat.
Unter VSCode hatte ich das Problem, daß meine importierten Custom-Python-Files nicht gefunden werden konnten. Es fehlte die Information über den Root, von dem aus die in Unterordnern befindlichen
foo.pyModule gefunden werden konnten. Wenn mein VSCode Workspace allerdings den Root-Folder enthielt (code ./my-project), dann war der Root vorhanden und der Import der KlasseSettingsperfrom package.helper import Settingswurde korrekt über das Verzeichnis./my-project/package/helper.pyaufgelöst.
Wenn man ein Paket importiert, so wird immer die sich darin befindliche Datei __init__py ausgeführt (ich spreche hier von "Regular Packages" ... nicht von "Namespace Packages")
Man muß Pakete aber nicht statisch importieren. Das kann auch dynamisch per __import()__-BuiltIn-Funktion geschehen. Python kann damit sehr generischen Code bereitstellen. Das Paket importlib bietet noch viel mehr Optionen.
Bibliotheken - Main
Importiert man ein Modul, so wird dieses Modul (greetings.py) ausgeführt, d. h. der gesamte ausführbare Code kommt tatsächlich zur Ausführung:
def greet(name="World"):
  print(f"Hello {name}")In diesem Fall besteht die "Ausführung" in der Definition der Funktion greet ... dadurch erfolgt keine Ausgabe.
Als Entwickler möchte man seinen Code allerdings auch selbst nutzen, beispielsweise um ihn zu testen, d. h. in greetings.py würde man gerne auch sowas machen:
def greet(name="World"):
  print(f"Hello {name}")
greet()
greet(name="Pierre")
greet("Python")so dass ein python greetings.py zur Ausgabe
Hello World
Hello Pierre
Hello Pythonführt.
Klar, man kann den Testcode in ein anderes Modul verlagern ... hält den Code aber nicht so schlank wie ein einziges File, d. h. ein Modul, das seine Tests enthält (finde ich ganz charmant).
Das hat aber den Nachteil, daß diese Ausgabe auch erscheinen würde, wenn man ein import greetings ausführt ... und das will man i. d. R. nicht.
In Python löst man das Problem i. a. mit
def greet(name="World"):
  print(f"Hello {name}")
if __name__ == "__main__"
  greet()
  greet(name="Pierre")
  greet("Python")dann erfolgt die Ausgabe nur, wenn die Ausführung als Main-Modul gestartet wurde. Bei einem import greetings ist der __name__ zur Laufzeit greetings => die Ausgabe erfolgt nicht.
Paketmanager Pip
siehe eigenes Kapitel für mehr Details
pip install jinja2pip install -r requirements.txtpip listwelche Packages sind installiert
pip checkDependency check der installierten Packages
Idiome
Werte ignorieren mit
_als Variablennametrack_medal_counts = { 'long jump': 3, '100 meters': 2, '400 meters': 1 } track_events = [] for event, _ in track_medal_counts.items(): track_events.append(event)List Comprehensions:
squares = [x**2 for x in range(10)]
String contains
Da ein String eine Sequence/List ist macht man das nicht per Functions-Call, sondern
stadt = input("In welcher Stadt wohnst Du?")
if "berg" in stadt:
  print("Du wohnst am Berg :-)")
else:
  print("Du wohnst NICHT am Berg :-)")Loops
for i in range(5):
    print(i)ACHTUNG: in erwartet ein Iterable ... eine Sequenz ist ein Iterable. range(5) erzeugt eine List (einfach mal ausprobieren: print(type(range(5)))).
Files
# open in readonly mode
fileref = open("../data/mydata.csv", "r")   # relativ zum Ausführungskontext (absolute Pfade funktionieren auch)
                                            # Absolute Pfade sind natürlich nicht portierbar
content = fileref.read()                    # fileref.read(n) liest n characters
print(contents[:100])
print(len(content))
lines = fileref.readlines()                 # fileref.readlines(2) liest x Zeilen ... oder eine: fileref.readline()
                                            #
                                            # ACHTUNG: diese Variante verwendet man aus Speichergründen besser nicht,
                                            #          denn hierbei wird die gesamte Datei in den Speicher geladen.
                                            #          Stattdessen verwendet man "fileref" als Iterator und liest
                                            #          zeilenweise.
print(len(lines))
print(lines[:4])
for line in lines:
  print(line)
for line in fileref:                        # das ist die typische Nutzung (IDIOM)
  print(line.strip())
fileref.close()
open()ist eine Built-In-Function
Achtung: es handelt sich bei dem File-Handle (z. B. fileref) um einen Iterator, d. h. nach einem content = fileref.readlines() sorgt ein erneutes content2 = fileref.readlines() für eine leere Liste.
Da man beim Filehandling gerne mal das Schließen des Handles vergißt, besitzt Python folgendes Idiom:
with open("../data/mydata.csv", "r") as fileref:
  for line in fileref:
    print(line.strip())Das ist Kontextmanager um das Filehandling, der sich auch gleich um das Schließen kümmert :-)
Garbage Collector
In Python muss man keinen Speicher freigeben ... das geschieht automatisch, indem Python die Anzahl der Referenzen auf eine Speicherstelle zählt und den Speicher bei erreichen von 0 freigibt.
Dieser Ansatz basiert allerdings auf einem Global-Interpreter-Lock (GIL), der Multithreading schwierig macht.
"The GIL has long been seen as an obstacle to better multithreaded performance in CPython (and thus Python generally). Many efforts have been made to remove it over the years, but at the cost of hurting single-threaded performance—in other words, by making the vast majority of existing Python applications slower." (infoworld.com)
Objektorientierung
Klassen
class Point():
  pass
point1 = Point()
point2 = Point()
point1.x = 5Instanzvariablen vs Klassenvariablen
Klassenvariablen sind instanzübergreifend und existieren nur ein einziges mal (entspricht static Properties in Java). Deshalb werden sie auch innerhalb der Klasse definiert.
Das paßt konzeptionell zu dem Ansatz die Instanz an alle Methoden explizit zu übergeben. Für mich als Java-Entwickler zunächst mal eine Umstellung.
Instanzvariablen haben höhere Priorität ... bei print(point1.x) wird also zunächst mal nach einer Instanzvariablen x geschaut und wenn es keine gibt, dann wird nach einer Klassenvariablen geschaut.
Nichtsdestotrotz greift man auf eine Klassenvariable auch über die Instanz zu (self.classVariable). Man könnte auf die Klassenvariable aber auch per Point.classVariable = 3 zugreifen.
class Point():
  classVariable = 0
  def methodA(self):
    if self.classVariable == 0:
      return "null"
point1 = Point()
point1.instanceVariable = 5Eine Sache, die mir hier nicht gefällt ist, daß ich als Leser der Klasse
Pointnicht weiß, daß es ein PropertyinstanceVariablehat ... das taucht irgendwann im Code auf - vielleicht sogar in einer anderen Datei. Zudem werden die Instanzvariablen erst zur Laufzeit angelegt ... nur, daß sie im Code erscheinen bedeutet nicht, daß sie auch existieren:
class Point():
  def initialization(self):
    self.x = 0
    self.y = 0
  def __str__(self):
    return "({}, {})".format(x, y)
print(Point())führt zu einem Laufzeitfehler NameError: name 'x' is not defined, da die Instanzvariable x (und auch y) zum Zeitpunkt der Ausführung der __str__-Methode noch gar nicht existiert. Erst durch expliziten Aufruf der Methode initialization() erfolgt die Anlage und danach funktioniert auch __str__ fehlerfrei. Typischerweisse sollte man diese Initialisierung im Konstruktor __init__(self) machen.
Um Spaghetti-Code zu vermeiden, sollte man im Konstruktore ALLE Instanzvariablen anlegen!!!
Jetzt wird es sehr technisch - aber es lohnt sich, darüber nachzudenken:
Methoden werden in Python als Klassenvariablen behandelt
Das bedeutet, daß ein Aufuf von point1.methodA() folgendermaßen abgearbeitet wird:
gibt es in der Instanz
point1eine InstanzvariablemethodA=> NEINgibt es in der Klasse
Pointeine KlassenvariablemethodA=> JAda nach
methodAeine()folgt wird die FunktionmethodA()mitpoint1als Parameter aufgerufen
Das Wissen hierüber ist für die tägliche Arbeit nicht entscheidend, gibt aber Einblicke wie das objektorierte Konzept in Python implementiert ist.
Methoden
Die Funktionen in Klassen werden Methoden genannt. Alle Methoden haben als ersten Parameter IMMER das Objekt self, das die Instanz repräsentiert. Python verwendet also einen Delegationsansatz, um Klassen mit Instanzen zu verknüpfen.
Da ich aus einer anderen Welt (Java, C++, Eiffel) komme, mutet das zunächst mal komisch an.
Dunderscore-Methoden
Die Lifecycle-Methoden (Konstruktor, String-Repräsentation, ...) sind in speziellen __init__(self) (den Parameternamen self zu verwenden ist ein Idiom - man kann jeden anderen verwenden ... macht aber niemand) Methoden verpackt ... können überschrieben werden, müssen aber nicht.
__init__(self, x, y)Konstruktor
__str__(self)__add__(self, other)point1 + point2
__sub__(self, other)
Vererbung
Die __init__(self) Methode kann, muß aber nicht in der erbenden Klasse überschrieben werden. Wenn keine definiert ist, wird die aus der Superklasse automatisch verwendet.
Beim erweitern des geerbten Konstruktors muß dieser explizit per super() aufgerufen werden:
class SubClass(SuperClass):
  def __init__(self, name, size):
    super().__init__(name)
    self.size = sizeBibliotheken
Module, die Python nicht mitliefert, müssen per pip install MODULE_NAME installiert werden. Für die Automatisierung dieses Prozesses ist es hilfreich, alle notwendigen Module in einer Datei (z. B. requirements.txt) zu sammeln (die i. a. unter Versionskontrolle steht) und dann per pip3 install -r requirements.txt auf einen Schlag zu installieren. Das vereinfacht die Nutzung einer (unbekannten) Anwendung ungemein.
Dies hat allerdings den Nachteil, daß dieses Paket in der zentralen gemeinsam genutzten Python-Installation installiert wird. Dabei werden verschiedene Versionen einer Bibliothek nicht unterstützt. Das führt dazu, daß evtl. andere Python Projekte nicht mehr ausführbar sind.
Hierfür gibt es verschiedene Lösungen
Python Virtual Environments
Python Docker Environment
TLDR ... Während ich als Entwickler die Verwendung von virtuellen Environments auf meiner lokalen Entwickler-Maschine präferiere, halte ich das Deployment für Enduser in Form eines Docker Images für die bessere Variante. Beide Ansätze haben also ihre Berechtigung.
Python Virtual Environments
Diese Variante wird man bei der lokalen Entwicklung verwenden, weil der Ansatz sehr schlank ist und für den Entwickler transparent ist.
Die Nutzung ist abhängig von der Python-Version ... ich beschreibe hier Python 3.8 ...
Manche Python 3 kommen bereits mit dem Modul venv vorinstalliert - bei anderen muß man es erst installieren. Zur Installation gibt es folgende Varianten:
als Linux Binary
apt install virtualenv... eine virtuelle Umgebung wird pervirtualenv --python=/usr/bin/python3 venvunter dem Verzeichnisvenvangelegtals Python-Package (in der Python-System-Installation) per
pip install virtualenv. Anschließend kann perpython -m virtualenv venveine virtuelle Umgebung unter dem Verzeichnisvenvangelegt werden.
Das wichtigste Verzeichnis einer virtuellen Umgebung ist bin (unter Windows heißt das Verzeichnis Scripts), das u. a. folgende Skripte enthält:
activatedeactvatepythonpip
Anschließend muß dieses Environment per source venv/bin/activate aktiviert werden.
ACHTUNG: unter Cygwin hat das nicht besonders gut funktioniert. Erstens mußte ich die Linebreaks von
activatevon Windows (CRLF) auf Linux (LF) umstellen. Zweitens wurden teilweise Batch Dateien statt Shell-Skripte erstellt ... es fehltedeactivate(stattdessen existiertedeactivate.bat).
Nun ist nicht mehr die zentrale Python Installation des Systems konfiguriert, sondern die des virtuellen Environments. Das ist an folgendermaßen erkennbar:
Prompt enthält den Namen des virtuellen Environments (z. B.
(myenv) ubuntu2004beta%)which pythonliefertvenv/Scripts/pythonstatt einer zentralen Python Installation (z. B./usr/bin/python3) - erreicht durch eine Anpassung derPATHUmgebungsvariableallerdings ist das nur ein symbolischer Link auf die zentrale Python-Installation
erreicht wird damit aber eine environment-spezifische Bibliothek-Konfiguration, da
"When Python is starting up, it looks at the path of its binary. In a virtual environment, it is actually just a copy of, or symlink to, your system’s Python binary. It then sets the location of sys.prefix and sys.exec_prefix based on this location, omitting the bin portion of the path. The path located in sys.prefix is then used for locating the site-packages directory by searching the relative path lib/pythonX.X/site-packages/, where X.X is the version of Python you’re using." (realpython)
Aus der Location des Executables wird auch die Location für Bibliotheken (site-packages) abgeleitet
Es sollten nicht gleichzeitig mehrere virtuelle Environments aktiviert sein (Seiteneffekte, seltsame Fehlermeldungen). Deshalb startet man besser eine neue Konsole!!!
Python Docker Environment
Python Services/Code in einen Docker Container zu packen vereinfacht die Ausführung von Python-Code, weil die Laufzeitumgebung (Python 2 vs 3, Bibliotheken) gleich mitgeliefert wird. Der Ausführende braucht nur eine Docker-Installation und kann den Code starten.
Bei diesem Ansatz würde man
Python im
Dockerfileinstalliereneine
requirements.txtdefinieren und imDockerfileinstallieren
FastAPI
Bibliotheken / Pakete
Python in Kombination mit Shell
Python executes Shell-Commands
Mit Fabric steht eine sehr brauchbare Python Library zur Verfügung, um aus Python-Code (remote) Shell-Kommandos abzusetzen und die Ergebnisse in Python weiterzuverarbeiten.
Die Installation ist mit pip schnell erledigt
pip install fabric2Danach kann man Programme wie hello-fabric.py
from fabric2 import Connection
c = Connection('localhost')
result = c.run('uname -s')
print("result: " + result.stdout.strip())per python hello-fabric.py ausführen.
In der Dokumentation findet man erste Programmier-Beispiele.
Kinder lernen programmieren
Jupyter Notebook
Interessante Variante, um interaktive Dokumentationen mit Markdown, HTML-iFrames, Python Code, JavaScript, Bash, Ruby, Shell Kommandos zu erstellen.
Im Hintergrund wird ein Webserver gestartet - die Dokumentation ist eigentlich Code, der in einer JSON-Datei gespeichert werden. Diese kann man mit anderen teilen und unter Versionskontrolle stellen. Visual-Studio-Code unterstützt das auch ... da ist die Integration noch weniger sichtbar.
Das Dateiformat ipynb wir beispielsweise auch direkt von Github unterstützt, so daß die Datei unter Versionskontrolle gestellt, geteilt und komfortabel im Browser angezeigt werden kann.
Mybinder Deployment
Über mybinder können Jupyter-Notebooks aus Github-Repositories deployed werden, um sie interaktiv zu nutzen.
Ein Beispiel:
Im Github-Repository python-course befindet sich das Jupyter-Notebook
Python Web-Development
Python Web Server Gateway Interface (WSGI)
Dieses Interface ermöglicht die standardisierte Integration von Python-Backends in Webserver wie Apache-Http, NGINX, ... und trägt damit bei, daß sich das Python-Ecosystem (z. B. weitere Python-Web-Application-Frameworks) weiterentwickeln kann.
Die Spezifikation allein ist aber nur die Grundlage, auf der sich dann Implementierungen entwickelt haben:
Fazit
TL;DR ... ich mag Python und halte es auch für eine sehr gute Sprache zum Lernen von Softwareentwicklung.
Mein Bedarf
Für meinen Bedarf ist es eine tolle Sprache, weil
Scripting möglich ... ich muß nicht erst einen Compiler anwerfen, um einen Einzeiler zu programmieren
hierzu verwendet man i. a. funktionale oder prozedurale Programmierung
komplexere Software läßt sich gut mit objektorientierten Konzepten umsetzen
Manche Dinge (z. B. der vorgesehene Variablentyp) wünsche ich mir in der Schnittstellenspezifikation. Aus meiner Sicht ist es keine gute Idee, den intendierten Parametertyp aus dem Funktionscode ableiten zu müssen. Insbesondere bei der Verwendung von Bibliotheken ist das unter Umständen nicht mal möglich. Auch hier helfen Best-Practices (Beschreibung des Parameter-Typs in der Doku).
Lern-Sprache
Das schöne an Python ist die nahtlose Integration unterschiedlicher Programmier-Paradigmen (prozedural, funktional, objektorientiert). Dadurch entsteht eine schöne Varianz, so daß man auch leicht die verschiedenen Paradigmen vergleichen kann und ihre Vor- und Nachteile kennenlernt. Konzepte wie Lambda und Comprehensions helfen, sehr eleganten und leserlichen Code zu produzieren.
Insbesondere die gute Raspberry Pi Unterstützung macht Python zu DER Lernsprache.
Ein Nachteil ist aus meiner sicht, daß durch den Interpreter-Ansatz Best-Practices schwieriger einzuhalten sind. Eine Variable kann an Zeile 23 einen String repräsentieren und an Zeile 24 einen Integer ... das wird einfach nicht verhindert. Während erfahrene Softwareentwickler das automatisch vermeiden, sind Anfänger vielleicht geneigt, solche "Abkürzungen" zu nehmen und Spaghetti-Code zu schreiben, der kaum lesebar und fehlerträchtig ist. Allerdings - wenn man es positiv sieht - lassen sich zumindest solche Ansätze direkt nebeneinanderstellen und die Vorteile schön rausarbeiten ... zum Lernen vielleicht sogar NOCH besser.
Last updated
Was this helpful?