lange laufende Programme können ihren Ablauf in Log-Dateien dokumentieren
z.B.: lange laufende Algorithmen, Webserver
import logging
logging.basicConfig(
filename="sort.log",
level=logging.DEBUG,
filemode="w"
)
logging.debug("hello")
Übung: Hinzufügen von Logging zu einer bestehenden Funktion (z.B. zu einem Sortieralgorithmus)
warum:
pytest: Testlibrary mit einfachem Interface
doctest: Überprüft Codebeispiele in Docstrings
unittest: Testlibrary, die in der Standardlibrary beinhaltet ist
zu testender Code:
# insertion_sort.py
def insertion_sort(unsorted):
"""Return a sorted version of a list."""
sorted = []
for new_item in unsorted:
i = 0
for sorted_item in sorted:
if new_item >= sorted_item:
i += 1
else:
break
sorted.insert(i, new_item)
return sorted
assert: Keyword, das sicherstellt, dass eine bestimmte Bedingung eintrifft
assert isinstance(a, int)
assert a > 0
Bei Nichterfüllen wird ein assertion error ausgelöst
# insertion_sort_test.py
from insertion_sort import insertion_sort
assert insertion_sort([3, 2, 4, 1, 5]) == [1, 2, 3, 4, 5]
assert insertion_sort([1, 1, 1]) == [1, 1, 1]
assert insertion_sort([]) == []
Das Skript sollte ohne Fehler laufen
Test-Library mit einfachem Interface, basierend auf assert
pip install pytest
Testdatei für pytest:
# insertion_sort_test.py
from insertion_sort import insertion_sort
def test_insertion_sort():
assert insertion_sort([3, 2, 4, 1, 5]) == [1, 2, 3, 4, 5]
assert insertion_sort([1, 1, 1]) == [1, 1, 1]
assert insertion_sort([]) == []
Finden und Ausführen von Tests:
python -m pytest
=================== test session starts ===================
platform win32 -- Python 3.8.7, pytest-6.2.1, [...]
rootdir: C:\[...]
collected 1 item
insertion_sort_test.py . [100%]
==================== 1 passed in 0.19s ====================
Namensgebung für Dateien: *_test.py
(oder test_*.py
)
Namensgebung für Funktionen: test*
Bestimmen, wie viel des Codes von Tests abgedeckt ist (welcher Anteil an Statements wird ausgeführt):
pip install pytest-cov
python -m pytest -cov=.
example output:
Name Stmts Miss Cover
--------------------------------------------
insertion_sort.py 10 0 100%
insertion_sort_test.py 5 0 100%
--------------------------------------------
TOTAL 15 0 100%
import pytest
def test_no_argument_raises():
with pytest.raises(TypeError):
insertion_sort()
Gruppieren von Tests via Klassen:
class TestExceptions():
def test_no_argument_raises():
with pytest.raises(TypeError):
insertion_sort()
def test_different_types_raises():
with pytest.raises(TypeError):
insertion_sort(["a", 1])
Fixtures können bestimmte Bedingungen vor dem durchführen eines Tests herstellen
def test_foo(tmp_path):
# tmp_path is a path to a temporary directory
verfügbare Fixtures:
tmp_path
capsys
(überwacht Output in stdout und stderr)Codebeispiele können in Docstrings beinhaltet sein und zum Testen verwendet werden
einfacher Doctest:
# insertion_sort.py
def insertion_sort(unsorted):
"""Return a sorted version of a list.
>>> insertion_sort([3, 2, 4, 1, 5])
[1, 2, 3, 4, 5]
"""
# code here
Ausführen von Doctests aus pytest:
python -m pytest --doctest-modules
"""
>>> insertion_sort(range(10)) #doctest: +NORMALIZE_WHITESPACE
[0, 1, 2, 3, 4, 5,
6, 7, 8, 9]
>>> insertion_sort(range(10)) #doctest: +ELLIPSIS
[0, 1, 2, ..., 8, 9]
"""
unittest: Testpaket in der Standardlibrary
oft wird stattdessen pytest empfohlen
python -m unittest
Sucht nach dem Namensschema test_*.py*
Bemerkung: Unterordner mit Tests müssen eine datei namens __init__.py beinhalten (siehe https://bugs.python.org/issue35617)
nach einem anderen Schema suchen:
python -m unittest discover -p "*_test.py"
# insertion_sort_test.py
import unittest
import insertion_sort
class InsertionSort(unittest.TestCase):
def test_five_items(self):
input = [3, 2, 4, 1, 5]
expected = [1, 2, 3, 4, 5]
actual = insertion_sort.insertion_sort(input)
self.assertEqual(actual, expected)
def test_empty(self):
actual = insertion_sort.insertion_sort([])
self.assertEqual(actual, [])
Assertions:
.assertEqual(a, 3)
.assertTrue(b)
.assertFalse(c)
.assertIsNone(d)
.assertIn(a, [2, 3, 4])
.assertIsInstance(a, int)
.assertRaises(TypeError, len)
es gibt auch gegenteilige Assertions, z.B. .assertNotEqual(a, 3)
Definieren von Funktionen, die vor / nach jedem Test ausgeführt werden:
import unittest
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget('The widget')
def tearDown(self):
self.widget.dispose()
PIP-Paket coverage
ausführen:
python -m coverage run test_shorten.py
python -m coverage report
mögliche Ausgabe:
Name Stmts Miss Cover
-------------------------------------
shorten.py 4 0 100%
test_shorten.py 11 0 100%
-------------------------------------
TOTAL 15 0 100%
# insertion_sort_test.py
import doctest
import insertion_sort
def load_tests(loader, tests, ignore):
tests.addTests(doctest.DocTestSuite(insertion_sort))
return tests
aus der interaktiven Konsole:
help(round)
import math
help(math)
help(math.floor)
aus dem Terminal:
python -m pydoc round
python -m pydoc math
python -m pydoc math.floor
Docstring eines Moduls: Beschreibung, Liste exportierter Funktionen mit einzeiligen Zusammenfassungen
Docstring einer Klasse: Beschreibung, Liste der Methoden
Docstring einer Funktion: Beschreibung, Liste der Parameter
Linter zum Validieren von Docstrings
reStructuredText (reST) = einfache Auszeichnungssprache (ähnlich zu Markdown), die in Python Docstrings verwendet werden kann
Sphinx = Werkzeug, das aus bestehenden Docstrings Dokumentation in HTML und ähnlichen Formaten generiert
Beispiel
Heading
=======
- list item 1
- list item 2
Link to `Wikipedia`_.
.. _Wikipedia: https://www.wikipedia.org/
.. code:: python
print("hello")
Neuere Versionen von Python unterstützen optionale Typenannotationen
Typenannotationen können die IDE - z.B. durch das Bereitstellen zusätzlicher Fehlermeldungen
VS Code Extensions für beide verfügbar
Variablen:
i: int = 3
Funktionen:
def double(n: int) -> int:
return 2 * n
from typing import List, Set, Dict, Tuple
names: List[str] = ['Anna', 'Bernd', 'Caro']
person: Tuple[str, str, int] = ('Anna', 'Berger', 1990)
roman_numerals: Dict[int, str] = {1: 'I', 2: 'II', 3: 'III', 4: 'IV'}
from typing import Iterable
names: Iterable[str] = ...
Beispiel: Klasse Length
a = Length(130, "cm")
a.value # 130
a.unit # cm
a.unit = "in"
a.value # 51.18
str(a) # 51.18in
b = Length.from_string("12cm")
2 * b # 24cm
b + a # 142cm
Getter & Setter (in Python unüblich, in anderen Sprachen verbreitet):
r = Rectangle(length=3, width=4)
print(r.get_area()) # 12
r.set_length(4)
print(r.get_area()) # 16
Mit Properties in Python:
r = Rectangle(length=3, width=4)
print(r.area) # 12
r.length = 4
print(r.area) # 16
Ãœbung: Umsetzen einer Klasse Rectangle_gs
mit Gettern und Settern
class Rectangle_gs:
def __init__(self, length, width):
self._length = length
self._width = width
def get_length(self):
return self._length
def set_length(self, new_length):
self._length = new_length
def get_width(self):
return self._width
def set_width(self, new_width):
self._width = new_width
def get_area(self):
return self._length * self._width
Mit Properties können wir das Auslesen oder Setzen von Attributen über eine Funktion "umleiten" - es kann also der Zugriff auf r.area
im Hintergrund zum Ausführen einer Getter- oder Setterfunktion führen.
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def _get_area(self):
return self.length * self.width
area = property(_get_area)
property
ist ein built-in, also ähnlich wie print
immer verfügbar.
Erweiterung: Setter für area
class Rectangle:
...
def _set_area(self, new_area):
# adjust the length
self.length = new_area / self.width
area = property(_get_area, _set_area)
Alternative Schreibweise mit Decorators:
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
@property
def area(self):
return self.length * self.width
@area.setter
def area(self, new_area):
self.length = new_area / self.width
Statische Attribute und Statische Methoden sind mit einer Klasse assoziiert, jedoch nicht mit einer spezifischen Instanz davon
Beispiel anhand der datetime-Klasse:
datetime.today()
datetime.fromisoformat()
datetime.resolution
Klassenattribute sind Attribute, die nur auf der Klasse (nicht auf jeder Instanz) definiert sind - alle Instanzen teilen sich die Attribute.
Beispiel Money
-Klasse: _currency_data
kann von allen Instanzen verwendet werden.
class Money:
_currency_data = [
{"code": "USD", "symbol": "$", "rate": 1.0},
{"code": "EUR", "symbol": "€", "rate": 1.1},
{"code": "GBP", "symbol": "£", "rate": 1.25},
{"code": "JPY", "symbol": "Â¥", "rate": 0.01},
]
def __init__(self, ...):
...
Muss eine Methode nicht auf die Daten einer bestimmten Instanz zugreifen, so kann sie als statische Methode deklariert werden.
class Money:
...
@staticmethod
def _get_currency_data(code):
for currency in Money._currency_data:
if code == currency["code"]:
return currency
raise ValueError(f"unknown currency: {code}")
Beachte: Bei einer statischen Methode wird als erster Parameter nicht self
übergeben - die Referenz auf die Instanz ist nicht vorhanden.
Statische Methoden haben zwei wichtige Anwendungsbereiche:
Money.from_string("23.40EUR")
_get_currency_data
Um den folgenden Code für Vererbung portabler zu machen, wäre es sinnvoll, den Klassennamen (Money
) nicht in der Methodendefinition zu erwähnen:
class Money:
...
@staticmethod
def _get_currency_data(code):
for currency in Money._currency_data:
if code == currency["code"]:
return currency
raise ValueError(f"unknown currency: {code}")
Klassenmethoden sind besondere statische Methoden, die die Möglichkeit bieten, unter einem allgemeinen Namen (üblicherweise cls
) auf die Klasse zu verweisen:
class Money:
...
@classmethod
def _get_currency_data(cls, code):
for currency in cls._currency_data:
if code == currency["code"]:
return currency
raise ValueError(f"unknown currency: {code}")
Besondere Methoden, die das Verhalten einer Klasse beeinflussen
Beginnen und enden mit zwei Unterstrichen, z.B. __init__
Liste von magic Methods: https://docs.python.org/3/reference/datamodel.html#special-method-names
Methoden zur Umwandlung in Strings:
__repr__
: möglichst vollständige Informationen zum Objekt, idealerweise von Python interpretierbar__str__
: "schön" zu lesenBeispiel:
from datetime import time
a = time(23, 45)
repr(a) # 'datetime.time(23, 45)'
str(a) # '23:45:00'
Methoden für mathematische Operatoren:
__add__
__mul__
__rmul__
__call__
__getitem__
Proxy zu den Elternklassen
ohne super:
class Child(A, B):
def __init__(self, x, y):
A.__init__(self, x, y)
B.__init__(self, x, y)
Mit super:
class Child(A, B):
def __init__(self, x, y):
super().__init__(x, y)
@logattraccess
class Foo():
def __init__(self):
self.a = 3
f = Foo()
f.a # prints: "get property 'a'"
f.b = 3 # prints: "set propery 'b'"
Generell können beliebige Attribute festgesetzt werden:
a.value = 3
Um den Speicherverbrauch zu verringern, können in einer Klasse sogenannte Slots definiert werden:
class Money():
__slots__ = ['currency', 'amount']
Iterable: ein Objekt, über das mittels for element in my_iterable
iteriert werden kann
Hierarchie von Iterables:
Vorteile von "dynamischen Iterables" / Iterators:
Beispiele für "statische Iterables":
Beispiele für "dynamische Iterables":
Aufrufe, die Iterators zurückgeben:
enumerate()
reversed()
open()
os.walk()
os.scandir()
map()
filter()
Beispiel: open()
gibt einen Iterator von Zeilen einer Datei zurück
with open("./foo.txt", encoding="utf-8") as f:
for line in f:
print line
Die Datei könnte mehrere GB groß sein und dieser Code würde problemlos laufen
Beispielfunktionen:
Lädt alle Textdateien in ./foo/ gleichzeitig in eine Liste, iteriert dann über sie:
for text in read_textfiles_as_list("./foo/"):
print(text[:5])
Lädt Textdateien nacheinander, wodurch der Speicherverbrauch niedrig gehalten wird:
for text in read_textfiles_as_iterator("./foo/"):
print(text[:5])
itertools: Modul zum erstellen von Iterators
itertools.count
itertools.repeat
itertools.product
from itertools import count
for i in count():
print(i)
if i >= 5:
break
# 0 1 2 3 4 5
Generatorfunktionen und Generator Expressions sind zwei Möglichkeiten, um selbst Iterators zu erstellen
Eine Funktion kann ein yield
-Statement enthalten und wird dadurch zum Generator
def count():
i = 0
while True:
yield i
i += i
Eine Generatorfunktion kann wiederholt verlassen werden (via yield
) und wieder betreten werden (durch anfragen des nächsten Werts)
Wir erstellen einen Iterator, der die String-Inhalte aller UTF-8-Textdateien in einem Ordner zurückgibt
Verwendung:
for content in read_textfiles("."):
print(content[:10])
Lösung:
def read_textfiles(path="."):
for file in os.listdir(path):
try:
with open(path + "/" + file) as fobj:
yield fobj.read()
except:
pass
Generator Expressions sind ähnlich zu List Comprehensions
List comprehension:
mylist = [i*i for i in range(3)]
Generator Expression:
mygenerator = (i*i for i in range(3))
Aufsummieren aller Zahlen von 1 bis 10 Millionen:
mittels List Comprehension - dies wird hunderte von Megabyte an RAM verbrauchen (siehe Task Manager):
sum([a for a in range(1, 10_000_001)])
mittels Generator Expression:
sum((a for a in range(1, 10_000_001)))
In Python wird jede for-Schleife über einen Iterator durchlaufen.
Wenn eine Iteration über ein iterierbares Objekt ausgeführt wird, wird für diese Iteration ein Iterator erstellt.
Jedes Iterable hat eine __iter__
-Methode, die einen Iterator zurückgibt.
Ein Iterator besitzt eine __next__
-Methode
__next__()
gibt entweder das nächste Objekt der Iteration zurück oder wirft eine StopIteration
-Exception
Ein Iterator is tatsächlich auch immer ein Iterable (hat eine __iter__
-Methode, die den Iterator zurückgibt)
Iterator einer Liste:
numbers = [1, 2, 3, 4]
numbers_iterator = numbers.__iter__()
Iterators haben eine __next__
-Methode, die das nächste Objekt in der Iteration zurückgibt.
Beispiel:
numbers = [1, 2, 3]
numbers_iterator = numbers.__iter__()
print(numbers_iterator.__next__()) # 1
print(numbers_iterator.__next__()) # 2
Wenn ein Iterator "verbraucht" ist, wird eine StopIteration
-Exception ausgelöst:
print(numbers_iterator.__next__()) # 1
print(numbers_iterator.__next__()) # 2
print(numbers_iterator.__next__()) # 3
print(numbers_iterator.__next__()) # StopIteration
Die globale Funktion next()
ist äquivalent zum Aufruf von .__next__()
next(numbers_iterator)
numbers_iterator.__next__()
Ãœbung: Wir erstellen ein selbstdefiniertes Iterable durch das Implementieren einer klasse mit __iter__
und __next__
for i in random():
...
oder
for number in roulette():
print(number, end=" ")
4 0 29 7 13 19
Einer for-Schleife kann eine optionale else-Klausel hinzugefügt werden - diese wird ausgeführt, wenn die Schleife ganz durchläuft - wenn also Python während des Ausführens nicht auf ein break
(oder return
oder ähnliches) stößt.
Diese Funktionalität gibt es bei keiner anderen verbreiteten Programmiersprache; viele Python-Entwickler kennen sie auch nicht - Zitat vom Erfinder von Python:
I would not have the feature at all if I had to do it over.
is_prime()
mit Schleifen und for ... else
Definieren einer Lambda-Funktion (anonymen Funktion):
multiply = lambda a, b: a * b
Verwenden eines Lambdas zum Sortieren:
pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
pairs.sort(key=lambda pair: pair[1])
Funktion höherer Ordnung (higher-order function): eine Funktion, die andere Funktionen als Parameter erhalten kann und/oder eine Funktion zurückgeben kann
wir erinnern uns: "alles ist eine Objekt" in Python - so auch Funktionen
Modul functools: Sammlung von Funktionen höherer Ordnung
Beispiele:
functools.lru_cache
functools.cache
(Python 3.9)functools.partial
functools.reduce
from functools import partial
open_utf8 = partial(open, encoding='utf-8')
Memoisierung: Strategie zur Performanceoptimierung:
Die Rückgabewerte bisheriger Funktionsaufrufe werden gespeichert und bei erneutem Aufruf mit den gleichen Parameterwerten wiederverwendet
def fibonacci(n):
if n in [0, 1]:
return n
return fibonacci(n-1) + fibonacci(n-2)
# make faster by caching
fibonacci = lru_cache(fibonacci)
Decorator-Syntax: einfache Möglichkeit, Funktionen höherer Ordnung auf Funktionsdefinitionen anzuwenden
@lru_cache # Python >= 3.8
def fibonacci(n):
...
äquivalent zu:
def fibonacci(n):
...
fibonacci = lru_cache(fibonacci)
Set: ungeordnete Menge von Elementen (ohne Duplikate)
Frozenset: unveränderliches set
ingredients = {"flour", "water", "salt", "yeast"}
ingredients = set(["flour", "water", "salt", "yeast"])
ingredients = frozenset(["flour", "water", "salt", "yeast"])
Sets können insbesondere Listen Ersetzen, wenn die Reihenfolge nicht von Bedeutung sein soll.
ingredients1 = {"flour", "water", "salt", "yeast"}
ingredients2 = {"water", "salt", "flour", "yeast"}
ingredients1 == ingredients2 # True
Achtung: Ein leeres set erstellen wir immer mittels set()
.
Warum ergibt der Ausdruck {}
kein leeres set?
x = set('abc')
y = set('aeiou')
x | y
x & y
x - y
x <= y
countries = {
"Canada", "USA", "Mexico", "Guatemala", "Belize",
"El Salvador", "Honduras", "Nicaragua", "Costa Rica",
"Panama"}
neighbors = [
{"Canada", "USA"},
{"USA", "Mexico"},
{"Mexico", "Guatemala"},
{"Mexico", "Belize"},
{"Guatemala", "Belize"},
{"Guatemala", "El Salvador"},
{"Guatemala", "Honduras"},
{"Honduras", "Nicaragua"},
{"Nicaragua", "Costa Rica"},
{"Costa Rica", "Panama"}
]
Beispiel:
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(11, y=22)
print(p[0])
print(p.x)
Enum = eine Sammlung Symbolischer Namen, die z.B. anstatt vorgegebener Strings verwendet werden kann.
Verwendung eines Strings:
a = Button(position="left")
Verwendung eines Enums namens Position:
a = Button(position=Position.LEFT)
Enums können Tippfehler vermeiden und Autovervollständigung erleichtern.
Definieren eines Enums:
from enum import Enum
class Position(Enum):
UP = 1
RIGHT = 2
DOWN = 3
LEFT = 4
Threads:
Multiprocessing:
Vorteil von Threading: einfacher, Variablen können direkt verändert werden
generella Arbeitsweise: Wir beauftragen Python damit, einzelne Funktionen in separaten Threads / Prozessen auszuführen, z.B.:
Führe download_xkcd_comic(i)
in parallelen Threads für i = 100 - 120 aus
Führe is_prime(i)
in parallelen Prozessen für mehrere Zahlen aus und sammle die booleschen Ergebnisse in einer Liste
Threads: Python schaltet wiederholt zwischen parallel laufenden Threads um, sodass diese scheinbar parallel laufen; in Wahrheit ist aber zu jedem Zeitpunkt nur ein Thread aktiv (Global Interpreter Lock - GIL)
Multiprocessing: Python startet mehrere Prozesse (sichtbar auch im Taskmanager); Teilen von Werten zwischen Prozessen kann schwerer sein
high-level:
concurrent.futures.ThreadPoolExecutor
concurrent.futures.ProcessPoolExecutor
low-level:
threading.Thread
multiprocessing.Process
from concurrent.futures import ThreadPoolExecutor
def print_multiple(text, n):
for i in range(n):
print(text, end="")
with ThreadPoolExecutor() as executor:
executor.submit(print_multiple, ".", 200)
executor.submit(print_multiple, "o", 200)
print("completed all tasks")
Wir schreiben ein Programm, das zwei Threads (a und b) ausführt. Die zwei Threads enthalten Schleifen, welche mitzählen, wie oft sie aufgerufen wurden.
Beispielausgabe:
797 iterations in thread a
799 iterations in thread b
1750 iterations in thread a
20254 iterations in thread b
829 iterations in thread a
Übung: Lade parallel Python Dokumentationsseiten für die folgenden Themen herunter:
["os", "sys", "urllib", "pprint", "math", "time"]
Beispiel-URL: https://docs.python.org/3/library/os.html
Die Downloads sollten in einem separaten downloads-Ordner gespeichert werden
Programm muss eine Python-Datei sein, die den "Hauptteil" nur dann ausführt, wenn sie selbst direkt ausgeführt - und nicht importiert - wurde (via __name__ == "__main__"
)
from concurrent.futures.process import ProcessPoolExecutor
def print_multiple(text, n):
for i in range(n):
print(text, end="")
if __name__ == "__main__":
with ProcessPoolExecutor() as executor:
executor.submit(print_multiple, ".", 200)
executor.submit(print_multiple, "o", 200)
Verwendung für die parallele Verarbeitung mehrerer Eingangsdaten zu Ausgangsdaten
Beispiel: Verarbeitung jedes Eintrages in [2, 3, 4, 5, 6]
, um zu bestimmen, ob die Zahl eine Primzahl ist → [True, True, False, True, False]
with ProcessPoolExecutor() as executor:
prime_indicators = executor.map(is_prime, [2, 3, 4, 5, 6])
Ãœbung: Schreibe eine Funktion, die eine Liste von Primzahlen in einem bestimmten Bereich erstellt:
prime_range(100_000_000_000_000, 100_000_000_000_100)
# [100000000000031, 100000000000067,
# 100000000000097, 100000000000099]
Verwende einen ProcessPoolExecutor
und diese Funktion:
def is_prime(n):
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True