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 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
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 altinstall
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:
cd my-project
virtualenv --python=/usr/local/bin/python3.8 venv
source ./venv/bin/activate
python --version
python -m test
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 :-)
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.py
wird 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 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
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 venv
nicht alle Features mitbringt. Das Paket zum Management dieser virtuellen Umgebungen wird persudo apt install virtualenv
installiert.
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/activate
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:
ââpfh@workbench ~/src/com.github
â°â⤠source ~/ideWorkspaces/venv/jenkins-stats/bin/activate
(jenkins-stats) ââpfh@workbench ~/src/com.github
Background 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 venv
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.
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.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 ohneimport
genutzt 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 = 1
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Ăź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
list
ist 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)) # HelloHelloHello
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")
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
append
undpop
Stacks implementieren
list.popleft()
auf diese Weise lassen sich Queues mit
list.append()
/list.popleft()
implementierennicht performant - besser
queue
aus 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, 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):
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
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
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:
break
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:
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 immerNone
und 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()) # 3
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
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ändert
Dokumentation 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
*args
gibt es eine spezielle Variante von Ăbergabeparameter: Argumentliste
def store(*args):
...
store("Pierre", "Silke")
mit
*kvargs
gibt 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 code
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:
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
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 ParameterlistB = sorted(listA, key = absolute)
mit folgenderabsolute
-Funktion (ACHTUNG:absolute
ist ein Functionpointer!!!):def absolute(x): if x >= 0: return x else: return -x
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)
- 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 value
wird ganz schematisch (und deshalb kĂśnnen IDEs auch eine automatische Transformation anbieten) folgendermaĂen in eine Lambda-Funktion transformiert:
lambda args: value
Ein 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
PATHONPATH
UmgebungsvariablePython 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Ăźhrungrandom/__init__.py
sofern vorhanden) undrandom
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
verwendetdurch den
import
eines Moduls aus diesem Package wird das Package (beim ersten mal) Ăźberjinja2/__init__py
initialisiert - anschlieĂend wird das Moduljinja2/environment
eingelesennun 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 Funktionrandrange
aus dem Modulrandom
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
verwendetdurch den
import
eines Moduls aus diesem Package wird das Package (beim ersten mal) Ăźberjinja2/__init__py
initialisiert - anschlieĂend wird das Moduljinja2/environment
eingelesen und die KlasseTemplate
wird in der lokalen Symboltabelle hinterlegt => ist also perTemplate()
(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:from .environment import Template as Template
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")
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 Python
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
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 jinja2
pip install -r requirements.txt
pip list
welche Packages sind installiert
pip check
Dependency 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 = 5
Instanzvariablen 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 = 5
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:
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
point1
eine InstanzvariablemethodA
=> NEINgibt es in der Klasse
Point
eine KlassenvariablemethodA
=> JAda nach
methodA
eine()
folgt wird die FunktionmethodA()
mitpoint1
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.
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 = size
Bibliotheken
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 venv
unter dem Verzeichnisvenv
angelegtals Python-Package (in der Python-System-Installation) per
pip install virtualenv
. AnschlieĂend kann perpython -m virtualenv venv
eine virtuelle Umgebung unter dem Verzeichnisvenv
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
liefertvenv/Scripts/python
statt einer zentralen Python Installation (z. B./usr/bin/python3
) - erreicht durch eine Anpassung derPATH
Umgebungsvariableallerdings 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
Dockerfile
installiereneine
requirements.txt
definieren und imDockerfile
installieren
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 fabric2
Danach 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?