Python Ecosystem
Last updated
Was this helpful?
Last updated
Was this helpful?
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 verwenden) ... einfach Editor auf und coden - was man eben von einer Skriptsprache erwartet. Auf diese Weise kann man es auch sehr praktisch mit mixen oder die .
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
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.
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.
Install Python sudo apt-get install python3-pip
and create a file hello.py
Ü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 test
ist 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
ACHTUNG: das Scripting scheint das Fail-Fast-Prinzip zu ignorieren ... so brach
make
bei 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.8
installiert, 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:
für newbe's:
python -m test
startet das Modultest
... das wiederum einen Test der Python-Installation durchführt.
Wenn alle Tests (dauernn schon ein paar Minuten) erfolgreich waren, dann kanns losgehen :-)
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.
...
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.
In diesem Modus muß man dem Python-Interpreter eine Datei (= Modul) vorwerfen, die er dann interpretiert/ausführt:
wird dann die Zahl 42
ausgegeben :-)
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.
Python Code läßt sich als Lambda Function in AWS ausführen.
prepare System - see above
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. Ein import module
sorgt 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
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.
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).
Per (z. B.) virtualenv my-venv
wird ein virtuelles Environment im Ordner ./my-venv
angelegt.
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
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:
Anschließ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:
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
Ist 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 einfachvenv
in jedes meiner.gitignore
packen. 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 umpython
bauen, 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.
Ganz wichtig:
eine Python-Datei (foo.py
) repräsentiert ein Modul, das wiederum ... wild gemischt enthalten kann
ausführbaren Code (print("pierre")
)
Funktionsdefinitionen (def foo():
)
Klassendefinitionen (class Foo:
)
Meta-Daten (import random
)
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.py
und 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 ohne import
genutzt werden
Datentypen 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"])
Global-Namespace vs. Local-Namespace:
In 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 m
definieren könnte und dann würde innerhalbanyFuntion
der Wert aus dem globalen Namensraum verwendet. Wer ist das denn für ein Quatsch???
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:
So muß es aussehen:
int
float
str
bool (True
, False
)
Man kann den Type eines Wertes per "Casting" verändern int5 = int("5")
.
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
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 List list = ["hello", 2.0, 5, [10,20]]
sehr ähnlich ist aber immutable
Sequenzen 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 list
ist ein Tuppel immutable und hat i. a. unterschiedliche Datentypen - Tupel werden in anderen Use-Cases verwendet
Speicheroptimierung:
mit String kann man "rechnen"
immutable Functions:
nameUpper = "Pierre".upper()
nameLower = "Pierre".lower()
stripped = " Das ist ein Test ".strip()
strip
entfernt 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")
liefert l = ["P", "i", "e", "r", "r", "e"]
s = set("Pierre")
liefert s = {"P", "i", "e", "r"}
(oder eine andere Reihenfolge)
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 append
und pop
Stacks implementieren
list.popleft()
auf diese Weise lassen sich Queues mit list.append()
/list.popleft()
implementieren
nicht performant - besser queue
aus dem collections
-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).
Speicheroptimierung:
In
a, b = b, a
zeigt 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, a
wie ein neues Sprachkonstrukt.
Auf Tupeln kann man per sorted
sortieren (Breaking Ties Eigenschaft):
Diese Eigenschaft kann man gut verwenden, um komlexe Sortierkriterien zu definieren:
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 (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 Tupel
for 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)
Python ist eine untypisierte Sprache ... Variablen haben keine Datentypen ... Werte haben Datentypen. Deshalb ist folgendes problemlos möglich:
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:
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
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.
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
verwendet. Aus meiner Sicht deutlich besser zu lesen.
An dieser Stelle kann man allerdings auch eine Klasse
Card
definieren. 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 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".
Man sollte Exceptions aber nicht mißbrauchen, um den Programmfluß für typische Use-Cases zu steuern:
Exceptions 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:
Exceptions kann man über
auslösen.
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 immer None
und kann dann beispielsweise per if myFunc() == None:
abgefragt werden
Parameter können auch über Tupel übergeben werden ... allerdings mit einer sehr speziellen Star-Notation:
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
der Default-Wert wird zum Zeitpunkt der Funktionsdefinition festgelegt - nicht zum Zeitpunkt der Asuführung:
man 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
Dokumentation einer Funktion sollte man mit einen sog. "docstring" machen, denn es gibt Tools, die daraus eine Dokumentation erzeugen
übrigens: die Dokumentation ist zur Laufzeit per print(hello.__doc__)
lesbar
beachte: 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 *args
gibt es eine spezielle Variante von Übergabeparameter: Argumentliste
mit *kvargs
gibt es eine spezielle Variante von Übergabeparameter: Dictionary
Als eingefleischter Nutzer typisierter Sprachen finde ich es relativ schwierig eine Funktion aus dem Kontext heraus zu verstehen, weil ich bei der Funktion
gar 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:
Durch die fehlende Typisierung kann eine Funktion sogar ganz unterschiedliche Datentypen zuürckliefern:
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 :-(
... können ohne import
genutzt werden
print
type
len(list)
max(a, b)
input
age=int(input("How old are you? "))
range
l=range(3,6) # l=(3,4,5)
sorted
listB = sorted(listA)
listB = sorted(listA, reverse = True)
... reverse
ist ein optionaler Parameter
listB = sorted(listA, key = absolute)
mit folgender absolute
-Funktion (ACHTUNG: absolute
ist ein Functionpointer!!!):
listB = sorted(listA, key = lambda x: absolute(x))
mit einer Lambda-Function
enumerate
erhält eine Sequence als Parameter und liefert ein Iterable von (index, value)
- IDIOM
statt
verwendet man
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
wird ganz schematisch (und deshalb können IDEs auch eine automatische Transformation anbieten) folgendermaßen in eine Lambda-Funktion transformiert:
Ein Beispiel:
wird zu
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:
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
PATHONPATH
Umgebungsvariable
Python 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.py
Datei). 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 random
hiermit wird das Modul random
gesucht (random.py
), initialisiert (Ausführung random/__init__.py
sofern vorhanden) und random
wird 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.environment
hier wird das Package jinja2
verwendet
durch den import
eines Moduls aus diesem Package wird das Package (beim ersten mal) über jinja2/__init__py
initialisiert - anschließend wird das Modul jinja2/environment
eingelesen
nun stehen Funktionionen/Klassen über jinja2.environment.Template()
zur Nutzung zur Verfügung
from random import randrange
hiermit wird das Modul random.py
gesucht und - wenn noch nicht geschehen - initialisiert. Zudem wird die Funktion randrange
aus dem Modul random
in 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 Template
hier wird das Package jinja2
verwendet
durch den import
eines Moduls aus diesem Package wird das Package (beim ersten mal) über jinja2/__init__py
initialisiert - anschließend wird das Modul jinja2/environment
eingelesen und die Klasse Template
wird in der lokalen Symboltabelle hinterlegt => ist also per Template()
(ohne Paket/Modul-Prefix) nutzbar
Packages wie
jinja2
vereinfachen den Import ... es reicht hier ausfrom jinja2 import Template
anzugeben. Hier fehlt die Information über das Modulenvironment
.Die Erklärung warum das funktioniert findet sich im
jinja2/__init__.py
Modul (zu finden untervenv/lib/python3.6/site-packages/jinja2/__init__.py
), das u. a. folgendes macht:Die Klasse wird also bei der Initialisierung des
jinja2
-Packages alsTemplate
zur 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.py
Module gefunden werden konnten. Wenn mein VSCode Workspace allerdings den Root-Folder enthielt (code ./my-project
), dann war der Root vorhanden und der Import der KlasseSettings
perfrom package.helper import Settings
wurde korrekt über das Verzeichnis./my-project/package/helper.py
aufgelö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")
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:
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:
so dass ein python greetings.py
zur Ausgabe
fü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
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.
pip install jinja2
pip install -r requirements.txt
pip list
welche Packages sind installiert
pip check
Dependency check der installierten Packages
Werte ignorieren mit _
als Variablenname
List Comprehensions: squares = [x**2 for x in range(10)]
Da ein String eine Sequence/List ist macht man das nicht per Functions-Call, sondern
ACHTUNG: in erwartet ein Iterable ... eine Sequenz ist ein Iterable. range(5)
erzeugt eine List (einfach mal ausprobieren: print(type(range(5)))
).
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:
Das ist Kontextmanager um das Filehandling, der sich auch gleich um das Schließen kümmert :-)
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.
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.
Eine Sache, die mir hier nicht gefällt ist, daß ich als Leser der Klasse
Point
nicht weiß, daß es ein PropertyinstanceVariable
hat ... 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:
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 point1
eine Instanzvariable methodA
=> NEIN
gibt es in der Klasse Point
eine Klassenvariable methodA
=> JA
da nach methodA
eine ()
folgt wird die Funktion methodA()
mit point1
als 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.
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.
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)
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:
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.
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 per virtualenv --python=/usr/bin/python3 venv
unter dem Verzeichnis venv
angelegt
als Python-Package (in der Python-System-Installation) per pip install virtualenv
. Anschließend kann per python -m virtualenv venv
eine virtuelle Umgebung unter dem Verzeichnis venv
angelegt werden.
Das wichtigste Verzeichnis einer virtuellen Umgebung ist bin
(unter Windows heißt das Verzeichnis Scripts
), das u. a. folgende Skripte enthält:
activate
deactvate
python
pip
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
activate
von 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 python
liefert venv/Scripts/python
statt einer zentralen Python Installation (z. B. /usr/bin/python3
) - erreicht durch eine Anpassung der PATH
Umgebungsvariable
allerdings ist das nur ein symbolischer Link auf die zentrale Python-Installation
erreicht wird damit aber eine environment-spezifische Bibliothek-Konfiguration, da
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 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 Dockerfile
installieren
eine requirements.txt
definieren und im Dockerfile
installieren
Die Installation ist mit pip
schnell erledigt
Danach kann man Programme wie hello-fabric.py
per python hello-fabric.py
ausführen.
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.
Ein Beispiel:
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:
TL;DR ... ich mag Python und halte es auch für eine sehr gute Sprache zum Lernen von Softwareentwicklung.
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).
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.
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.
Um mal eben schnell was auszuprobieren (während des Lernen der Sprache) hat sich etabliert. Deutlich besser als Python in Shell Mode.
Starte Visual Studio Code and installiere . Create a file hello.py
:
Das ist die präferierte Variante, da python -m venv
. Das Paket zum Management dieser virtuellen Umgebungen wird per sudo apt install virtualenv
installiert.
Man sollte sich an die typischen Idiome/Patterns halten und auch den berücksichtigen. Bestenfalls unterstützt die IDE hierbei und gibt Warnungen aus, wenn sie nicht eingehalten werden (Stickwort Linting).
Mit läßt sich das Tauschen von Variablenwerten sehr elegant beschreiben:
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 ) 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.
ein 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 ):
"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." ()
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 bietet noch viel mehr Optionen.
Dieser Ansatz basiert allerdings auf einem , 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." ()
"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." ()
Mit steht eine sehr brauchbare Python Library zur Verfügung, um aus Python-Code (remote) Shell-Kommandos abzusetzen und die Ergebnisse in Python weiterzuverarbeiten.
In der findet man erste Programmier-Beispiele.
Über können Jupyter-Notebooks aus Github-Repositories deployed werden, um sie interaktiv zu nutzen.
Im Github-Repository befindet sich das Jupyter-Notebook
Insbesondere die gute Unterstützung macht Python zu DER Lernsprache.