Guter Python-Code (Teil 6): Funktionale Programmierkonzepte

Aus LinuxUser 11/2024

Guter Python-Code (Teil 6): Funktionale Programmierkonzepte

© Przemyslaw Koch / 123RF.com

Richtungswechsel

Wenn alle Programmierschritte nacheinander linear ablaufen, artet das Umsetzen bestimmter Lösungen schnell in immensen Aufwand aus. Funktionale Programmierung eröffnet da andere Möglichkeiten.

Jeder Programmierstil orientiert sich an einem bestimmten Denkmuster und bringt entsprechende Vor- und Nachteile mit sich. Funktionale Programmierung bildet da keine Ausnahme. In der letzten Ausgabe haben wir im fünften Teil [1] unserer Artikelserie die Behauptung aufgestellt, der funktionalen Programmierung eile der Ruf voraus, ein Türöffner zu sicherer, zuverlässiger Parallelverarbeitung in Industriequalität zu sein. Dem gehen wir in dieser Folge auf den Grund.

Obwohl im Kern objektorientiert, unterstützt die Programmiersprache Python gleichermaßen sowohl imperative, prozedurale und objektorientierte als auch funktionale Konzepte. In einer Anwendung kann eine objektorientierte grafische Benutzeroberfläche mit einer funktionalen Verarbeitung koexistieren. Intern versteht Python ohnehin alles als Objekte [2], selbst Variablen und Funktionen [3].

Codeanalysen zeigen, dass sich mit funktionaler Programmierung manche Dinge leichter (also mit weniger Code) oder überhaupt erst umsetzen lassen. Doch dazu braucht es zunächst einen Lernprozess, um herauszufinden, für welche Situationen sich dieser Programmierstil eignet. Gewohnheiten und Wissen aus anderen Programmiersprachen spielen ebenso eine Rolle. In der Praxis zeigt sich letztendlich, was nicht nur in der jeweiligen Situation am besten funktioniert, sondern auch verständlich ist und sich dementsprechend unkompliziert warten lässt. Pauschale Aussagen kann man hier nicht treffen, häufig mischen sich im Alltag unterschiedliche Paradigmen.

Was dahintersteckt

Funktionale Programmierung arbeitet weniger mit ausschließlichen Zuweisungen, sondern zielt darauf ab, Programmieraufgaben mithilfe von Funktionen, deren Anwendung und deren Aneinanderreihung zu bewältigen. Eine Entsprechung findet sich im Konzept der Pipes zwischen Linux-Kommandos: Auch dort löst man komplexe Probleme durch das Zerlegen und Zusammenwirken spezialisierter Werkzeuge. Dabei liegt der Fokus nicht darauf, einen Zustand zu ändern, sondern auf dem Datenfluss zwischen den Funktionen beziehungsweise Werkzeugen.

Der Stil der funktionalen Programmierung lässt sich eher als deklarativ denn als imperativ beschreiben. Konkreter ausgedrückt, sagen Sie dem Computer nicht mehr bei jedem Schritt, was er tun soll. Stattdessen bestimmt die Funktionskette den Zustand der Anwendung. Die Funktionen besitzen einen eigenen Gültigkeitsbereich (Scope).

Speicher und Variablen bleiben lokal, dort vorgenommene Zuweisungen wirken sich lediglich innerhalb der jeweiligen Funktion aus. Das Verwenden globaler Variablen steht im sprichwörtlichen Giftschrank, gleich neben der Goto-Anweisung, und bleibt somit außen vor. Damit gibt es kaum eine Chance für Seiteneffekte und verborgene Zustände, wie sie bei imperativer Programmierung und bei Objekten vorkommen. Im Ergebnis vereinfachen sich die Tests auf die Korrektheit des Programmcodes sowie die Fehlersuche.

Bei Multithreading und beim Einsatz von Nebenläufigkeit verringern sich die Race Conditions und Deadlocks, da auf weniger Daten und weitere Ressourcen gleichzeitig gemeinsam zugegriffen wird. Die Teilung verstärkt das Nutzen von Parallelisierung und ermöglicht dementsprechend, die volle Leistungsfähigkeit von Mehrkernprozessoren auszuschöpfen [4].

Zu den mit Fokus auf funktionale Programmierung entworfenen Sprachen gehören beispielsweise Scala, Haskell, Objective Caml (OCaml) und Common Lisp. Zu Python kamen ab Version 2 vermehrt funktionale Elemente hinzu. Nur einen kurzen Blick darauf zu werfen, genügt jedoch kaum, denn es braucht eine Weile, um in die Denkweise der funktionalen Programmierung mit den genannten Sprachen hineinzufinden.

Intern

Spannend ist, wie Python Funktionen intern handhabt. Wie schon erwähnt, organisiert der Interpreter alle Elemente als Objekte. Somit übergeben Sie Argumente an Funktionen, erhalten Ergebnisse als Funktionen zurück und speichern sie in Variablen. Höherwertige und bereits mitgelieferte Funktionen wie map(), filter() oder reduce() dienen als Anker für komplexere Ausdrücke.

Weil Mengen (Sets) anstelle von Listen zum Einsatz kommen, reduziert sich der Umfang des Speichers, den Python intern für die Datenstruktur reserviert. Listing 1 und Listing 2 stellen gegenüber, wie sich identische Elemente in zwei Listen identifizieren lassen – zunächst mit einer vergleichsweise langsamen For-Schleife und danach mithilfe des Vereinigungsoperators &. Bei einer kleinen Anzahl Elemente fällt die Ausführungszeit beider Implementierungen nahezu identisch aus. Bereits bei Listen aus 1000 Elementen erhöht sich der Faktor auf 10. In unserem Test verringerte sich die Ausführungsdauer von 0,3 auf 0,01 Sekunden.

Das Konzept der List Comprehension, frei übersetzt das Verständnis von Listen, bildet eine weitere Alternative zu Schleifen. Damit erzeugen Sie elegant neue Listen aus bestehenden Listen [5]. Wie Sie das umsetzen, stellen wir nun zusammen mit Iteratoren, Generatoren und Lambda-Funktionen genauer vor.

Listing 1

Identische Elemente mit einer For-Schleife finden

# define two lists
ListeA = [1,2,3,4,5] * 1000
ListeB = [2,3,4,5,6] * 1000
# define result of identical elements
overlaps = []
# for loop to iterate over the two lists
for x in ListeA:
  if x in ListeB:
    if x not in overlaps:
      overlaps.append(x)
# output result
print(overlaps)

Listing 2

Identische Elemente per Vereinigungsoperator finden

ListeA = [1,2,3,4,5] * 1000
ListeB = [2,3,4,5,6] * 1000
# calculate the similar elements in both lists
overlaps = set(ListeA) & set(ListeB)
# output result
print(overlaps)

Iteratoren

Bei Iteratoren handelt es sich um Verweise in einer Datenstruktur, die auf ein bestimmtes Element zeigen. Grob vereinfacht verbirgt sich dahinter eine Aufzählung mit Zustandsmerker [6]. Sie kennen das Konzept womöglich von Programmiersprachen wie C#, Java, PHP, Ruby und Rust.

Als Rückgabewert erhält man stets den Wert des Elements, auf das gerade verwiesen wird. In Python ist das initial stets das erste Element. Mit dem Aufruf der Funktion next() rutschen Sie in der Aufzählung ein Element weiter, sofern Sie sich nicht bereits am Ende der Datenstruktur befinden. Listing 3 demonstriert das anhand der Liste number, die Sie in der ersten Zeile definieren. Dabei enthält number die drei Zahlenwerte 1, 2 und 3. In der zweiten Zeile erstellen Sie mithilfe der Funktion iter() aus der Liste den Iterator it. Mittels next() hüpfen Sie anschließend durch die Elemente von numbersIT (Zeile 3 bis 6).

Listing 3

Iterator benutzen

numbers = [1, 2, 3]
numbersIT = iter(numbers)
print(next(numbersIT))
print(next(numbersIT))
print(next(numbersIT))
print(next(numbersIT))

Für die Zeilen 3 bis 5 klappt das gut und Sie erhalten das erhoffte Resultat. In Zeile 6 produzieren Sie allerdings einen Laufzeitfehler: Sie greifen auf ein Element zu, das sich nach dem Listenende befindet. Diesen Ausnahmefall gilt es zu verhindern. Listing 4 zeigt deswegen anhand einer While-Schleife in Kombination mit Try-Except, wie Sie die Situation sauber abfangen. Ausgelöst wird die Ausnahme StopIteration, bei deren Eintreten Sie die Schleife abbrechen (Zeile 10 bis 12). Für die Ausgabe zu Listing 4 werfen Sie bitte einen Blick auf Listing 5.

Listing 4

Iterator, While-Schleife und Ausnahmebehandlung kombiniert

numbers = [1, 2, 3]
# transform list into an iterator
numbersIT = iter(numbers)
# loop though the items one after the next
while True:
  try:
    value = next(numbersIT)
    square = value * value
    print("the square of number", value, "is", square)
  except StopIteration:
    print("end of iterator reached")
    break

Listing 5

Ausgabe von Listing 4

$ python3 square-iterator.py
the square of number 1 is 1
the square of number 2 is 4
the square of number 3 is 9
end of iterator reached

Ein Iterator gilt zwar als nützlich, hat aber seine Grenzen. Mit dem Schlüsselwort next() geht es ausschließlich vorwärts. Ein Rückwärtsgang oder das Überspringen von Elementen sind ebenso wenig vorgesehen wie ein Reset oder das Erzeugen einer Kopie des Iterators. Für ein Durchlaufen in umgekehrter Reihenfolge liegt ein Lösungsweg darin, die Funktion reversed() zwischenzuschalten, damit die Liste umzudrehen und letztlich daraus einen Iterator zu erzeugen.

Generatoren

Bei regulären Funktionen erfolgt die Berechnung eines oder mehrerer Werte und dessen beziehungsweise deren Rückgabe über die Anweisung return. Sobald das Programm sie erreicht, geht alles innerhalb der Funktion verloren. Es kommen quasi die Heinzelmännchen der Müllabfuhr des Python-Interpreters herbei gehuscht und räumen freundlicherweise hinter Ihnen auf.

Bei Generatoren hingegen merkt sich Python den Zustand der Funktion. Alle Variablen und deren Zustände bleiben bis zum Programmende erhalten, das Aufräumen entfällt. Intern erzeugt Python beim Aufruf der Funktion ein Generator-Objekt, das das Iterator-Protokoll unterstützt. Damit Python weiß, dass es sich bei einer Funktion um einen Generator handelt und es Speicher sowie Stack nicht aufräumen muss, erfolgt die Rückkehr zum Aufrufpunkt mit dem Schlüsselwort yield anstelle von return.

In Listing 6 verarbeitet square() als Generator eine Werteliste. Bei einem Aufruf liefert square() nicht sämtliche berechneten Werte auf einmal, sondern stets nur einen einzigen. Als Ausgangspunkt dient der Zustand, den sich Python für den Generator gemerkt hat. Der Generator spielt seinen Vorteil erst bei großen Wertemengen voll aus: Nicht alle Zwischenergebnisse müssen gleichzeitig errechnet und im Speicher gehalten werden, sondern stets nur ein einziges.

Listing 6

Quadrat eines Werts als Generator

def square(valueList):
  """Return the square of a number"""
  for value in valueList:
    yield value * value
numbers = [1, 2, 3]
for value in square(numbers):
  print(value)

Kompakt

Kombinieren Sie Generator-Ausdrücke mit List Comprehensions, dann ergibt sich eine überaus kompakte Schreibweise. Listing 7 entspricht Listing 6, allerdings (Kommentare außen vor) als Vierzeiler. Zunächst definieren Sie in der ersten Zeile eine Zahlenliste numbers und daraus in Zeile 3 eine Menge aus den Quadraten der Zahlen darin. In Zeile 5 iterieren Sie mithilfe einer For-Schleife wertweise über die Menge und geben den berechneten Wert daraufhin in der letzten Zeile aus.

Listing 7

Kompaktversion zu Listing 6

numbers = [1, 2, 3]
# create the generator object as a set
squares_generator = (value * value for value in numbers)
# iterate over the generator and print the values
for value in squares_generator:
  print(value)

Funktionen und Module

In dem Python-Werkzeugkasten finden Sie etliche Schubladen, bei denen es lohnt, sie aufzuziehen. Kluge Entwickler haben Funktionen und Module beigesteuert, die Ihnen im Alltag bereits etliches an Denk- und Tipparbeit abnehmen [7]. Die Tabelle “Nützliche Funktionen für die funktionale Programmierung” stellt eine Auswahl passender Funktionen zusammen, die Tabelle “Nützliche Module für die funktionale Programmierung” zeigt die Module. Der Begriff Aufzählung entspricht der deutschen Übersetzung für Iterable. Das kommt dem Original recht nahe und passt beispielsweise sowohl auf eine Liste als auch auf eine Menge und ein Tupel.

Funktion

Bedeutung

Beispiel

map()

Wende eine Funktion auf jedes Element der Aufzählung an.

Listing 9

filter()

Erzeuge einen Iterator für jedes Element der Aufzählung, bei dem der Funktionsaufruf True ergibt.

Listing 10

enumerate()

Zähle die Elemente in der Aufzählung ab und gib ein Tupel aus Nummer und Element zurück.

sorted()

Sortiere eine Aufzählung aufsteigend.

reversed()

Drehe eine Aufzählung um.

any()

Gib True zurück, falls irgendeines der Elemente der Aufzählung einen wahren Wert besitzt.

all()

Gib True zurück, falls alle Elemente der Aufzählung einen wahren Wert besitzen.

zip()

Entnimmt jeweils das n-te Element aus den Aufzählungen und gibt ein passendes Tupel aus Index und Element zurück.

Modul

Bedeutung

Beispiel

itertools

Funktionen mit Iteratoren zum effizienten Durchlaufen von Schleifen [8]

operator

als Funktionen ausgeführte Standardoperatoren [9]

functools

Funktionen höherer Ordnung und Operationen auf aufrufbare Objekte [10]

Listing 11

Lambda-Funktionen

Oft bestehen Funktionen nur aus wenigen Anweisungen und werden lediglich ein- oder zweimal aufgerufen. Hier kommt das interessante Konzept der Lambda-Funktionen ins Spiel, das den Code auf das Wesentliche reduziert.

Als Lambda-Funktionen bezeichnet man anonyme Funktionen ohne Funktionsnamen. Der Anzahl der Parameter sind keine Grenzen gesetzt, Lambda-Funktionen liefern Rückgabewerte wie herkömmliche Funktionen. Die Begrenzung liegt im Funktionskörper, der nur aus einem Ausdruck bestehen darf und nicht aus einem mehrzeiligen Anweisungsblock. Eine Kombination mit anderen Funktionsaufrufen ist hingegen zulässig, solang sich am Ende wieder ein einziger gültiger Ausdruck ergibt.

Statt mit def wie reguläre Funktionen beginnen Lambda-Funktionen mit dem Schlüsselwort lambda. Das Beenden der Funktion erfordert weder ein return noch ein yield. Listing 8 zeigt einen einfachen Aufruf zum Berechnen des Quadrats für die beiden Zahlenwerte 5 und 10.

Listing 8

Quadrat mittels Lambda-Funktion

square = lambda x: x * x
print("the square of 5 is", square(5))
print("the square of 10 is", square(10))

Wie Sie in Listing 8 sehen, können Sie die Lambda-Funktion jederzeit mit anderen Werten aufrufen. Für eine größere Menge von Werten erweist sich das Vorgehen jedoch als unpraktisch und langsam. Es vereinfacht sich mit der bereits in der Tabelle “Nützliche Funktionen für die funktionale Programmierung” vorgestellten Funktion map(). Listing 9 berechnet das Quadrat für jeden Wert in der Liste numbers und speichert das Ergebnis im Generator squared_numbers. Wenden Sie danach list() auf squared_numbers an, erhalten Sie eine Liste mit den errechneten Quadraten.

Listing 9

Quadrat für Listenwerte mittels Lambda-Funktion

numbers = [1, 2, 3]
# calculate vie map() and anonymous lambda function
squared_numbers = map(lambda x: x * x, numbers)
# print the result: [1, 4, 9]
print("numbers:", numbers)
print("squared numbers:", list(squared_numbers))

Was mit einer explizit definierten Lambda-Funktion klappt, klappt ebenso gut auch implizit. Listing 10 veranschaulicht, wie Sie die Funktion filter() und eine Lambda-Funktion miteinander kombinieren. Zunächst legen Sie eine Liste von Zahlen an, die ages heißt und Altersangaben repräsentiert (erste Zeile). Um daraus die Werte aller Personen zu ermitteln, die älter als 18 Jahre alt sind, rufen Sie die Funktion filter() auf. Sie erwartet als Parameter die Filterfunktion und die Liste, auf der sie arbeiten soll. Erstere ist als anonyme Lambda-Funktion mit einem simplen Vergleich ausgeführt, bei der Liste handelt es sich um das zuvor definierte ages.

Das Ergebnis des Aufrufs wandeln Sie wieder mittels list() in eine Liste um (Zeile 3) und geben sie aus (Zeile 5). Effektiv umfasst Listing 10 nur drei Zeilen Programmcode. Ohne Lambda-Funktion würde das Gezeigte mithilfe einer If-Else-Schachtelung ebenfalls funktionieren, fiele aber deutlich länger aus.

Listing 10

filter() und Lambda-Funktion kombinieren

ages = [13, 90, 17, 59, 21, 60, 5]
# determine the adults from the list using filter, and a lambda function
adults = list(filter(lambda age: age > 18, ages))
# output the list of adults
print(adults)

Von den in der Tabelle “Nützliche Module für die funktionale Programmierung” genannten Modulen nehmen wir functools genauer unter die Lupe, speziell die Methode reduce() [11]. Sie erwartet zwei Parameter, eine Funktion zur Auswertung und eine Aufzählung (Iterable). reduce() wendet die Funktion zur Auswertung stets auf die ersten beiden Elemente der Aufzählung an und ersetzt sie durch den Rückgabewert der Funktion zur Auswertung. Das wiederholt sich so lange, bis die Aufzählung nur noch aus einem Element besteht.

In Listing 11 ist die Funktion zur Auswertung als Lambda-Funktion unter dem Bezeichner compare_numbers definiert (Zeile 5). Sie liefert jeweils den größeren der beiden ihr übermittelten Werte als Ergebnis. Stück für Stück verkürzt sich die Liste, bis am Ende der Wert 6 als größter Wert übrig bleibt und als Ergebnis ausgegeben wird (letzte Zeile). Das Listing enthält effektiv erneut lediglich vier Zeilen Programmcode und ist ähnlich kompakt wie Listing 10. Ohne Lambda-Funktion lässt sich dasselbe mit einer If-Else-Schachtelung oder der Funktion max() implementieren.

Listing 11

Finde das Maximum in einer Zahlenliste

# import reduce from the functools module
from functools import reduce
numbers = [1, 3, 5, 6, 2]
# define a lambda function to compare two numbers
compare_numbers = lambda a, b: a if a > b else b
# apply reduce() to numbers
print("The maximum element of the list is", reduce(compare_numbers, numbers))

Interessanterweise ergibt der Vergleich mit max() und einer unsortierten Liste aus 100 000 Elementen, dass max() etwa doppelt so schnell arbeitet wie die Kombination aus reduce() und compare_numbers. In Bezug auf den Speicherverbrauch unterscheiden sich beide jedoch nicht und liegen bei rund 50 MByte. Listing 12 zeigt die dazugehörige Ausgabe des Python Memory Profilers [12].

Listing 12

Ausgabe des Python Memory Profilers

$ python3 find-max-reduce-lambda-memory-usage.py
The maximum element of the list is 6
Filename: find-max-reduce-lambda-memory-usage.py
Line #    Mem usage    Increment    Occurrences    Line Contents
================================================================
  17       46.8 MiB     46.8 MiB              1    @profile
  18                                               def findMax():
  19                                               # define a list of numbers
  20       50.6 MiB      3.8 MiB              1    numbers = [1, 3, 5, 6, 2] * 100000
  21
  22                                               # define a lambda function to compare two numbers
  23       50.6 MiB
    0.0 MiB         999999    compare_numbers = lambda a, b: a if a > b else b
  24
  25                                               # apply reduce () to numbers
  26       50.6 MiB      0.0 MiB              1    print("The maximum element of the list is",
                                                         reduce(compare_numbers, numbers))

Im Vergleich

Abschließend interessiert uns, wie gut sich der Programmcode im Vergleich zur imperativen Programmierung warten lässt. Dazu befragten wir Radon [13], die Ergebnisse sehen Sie in Listing 13. Alle Implementierungen wurden als sehr gut eingestuft, wobei die Buchstaben A, B und C den Farbwerten rot, gelb und grün aus der Microsoft-Entwicklerdokumentation entsprechen. Weitergehende Informationen zu den Aufrufparametern von Radon sowie zur Einordnung der erzielten Ergebnisse liefert die Dokumentation [14].

Die erreichten Werte decken sich mit unseren Erwartungen. Einfache Umsetzungen wie eine For-Schleife mögen länger sein, sind aber verständlicher und einfacher zu warten. Je komplexer die Anweisungen ausfallen – und bei funktionaler Programmierung tun sie das durchaus –, umso mehr müssen Sie darüber nachdenken, was der Programmcode tatsächlich bedeutet und bewirkt. Das spiegelt sich im Wartbarkeitsindex wider, der hier niedriger liegt. Er liegt beispielsweise für einen einfachen Generator bei 94,42, für eine Lambda-Funktion dagegen lediglich bei 67,23.

Der Programmcode reduziert sich, da die Schleifenzähler nicht mehr zu sehen sind: Sie wurden in die eingebauten Funktionen der Sprache verlagert. Das verbessert die Bewertung des Codes im Vergleich zu den anderen Faktoren etwas.

Listing 13

Wartbarkeit der Lösungen im Vergleich

$ radon mi -s *.py
find-overlap-elements-for-loop-simple.py - A (94.34) # Listing 1
find-overlap-elements-set.py - A (83.04)             # Listing 2
square-for-loop.py - A (94.68)
square-function.py - A (87.73)
square-generator-lc.py - A (74.63)                   # Listing 7
square-generator.py - A (94.42)                      # Listing 6
square-iterator.py - A (98.53)                       # Listing 5
square-lambda-minimal.py - A (60.49)                 # Listing 8
square-lambda.py - A (67.23)                         # Listing 11

Fazit

Funktionale Programmierung erzeugt überaus kompakten Code, der sich oft auf verblüffend einfachen Weg erreichen lässt [15]. Im Github-Repository namens Awesome Functional Programming [16] sammelt Lucas Viola interessante Unterlagen zu dem Themengebiet, auch für andere Programmiersprachen als Python. Bisher blieb die Ausführungszeit von Programmcode außen vor. Mit diesem Aspekt werden wir uns im nächsten Teil dieser Artikelserie genauer auseinandersetzen. Dabei werten wir den Programmcode mithilfe des Moduls timeit [17] und anderer Werkzeuge aus. (csi)

Danksagung

Der Autor bedankt sich bei Gerold Rupprecht und Wolfram Eifler für deren Unterstützung und Anregungen bei der Erstellung des Artikels.

Der Autor

Frank Hofmann arbeitet zumeist von unterwegs, bevorzugt in Berlin, Genf und Kapstadt, als Entwickler, Trainer und Autor. Er ist Co-Autor des Debian-Paketmanagement-Buchs [18].

Infos

  1. Guter Python-Code (Teil 5): Frank Hofmann, “Putzkolonne”, LU 10/2024, S. 86: https://www.linux-community.de/51003

  2. “Python Internals: An Introduction.”: https://blog.sourcerer.io/python-internals-an-introduction-d14f9f70e583

  3. “Everything’s an object”: https://pythoninternal.wordpress.com/2014/08/11/everythings-an-object/

  4. “How to Use 100 percent of All CPU Cores in Python”: https://superfastpython.com/python-use-all-cpu-cores/

  5. “Python – List Comprehension”: https://www.w3schools.com/python/python_lists_comprehension.asp

  6. “Iterators and simple generators”: https://developer.ibm.com/articles/l-pycon/

  7. “Python Functional Programming HOWTO”: https://docs.python.org/3/howto/functional.html

  8. Python-Modul itertools: https://docs.python.org/3/library/itertools.html#module-itertools

  9. Python-Modul operator: https://docs.python.org/3/library/operator.html#module-operator

  10. Python-Modul functools: https://docs.python.org/3/library/functools.html#module-functools

  11. “Python Lambda Functions”: https://www.geeksforgeeks.org/python-lambda-anonymous-functions-filter-map-reduce/

  12. Python Memory Profiler: https://github.com/pythonprofilers/memory_profiler

  13. Radon: https://pypi.org/project/radon/

  14. Optionen für Radon: https://radon.readthedocs.io/en/latest/commandline.html

  15. “Python Performance Tuning: 20 Simple Tips”: https://stackify.com/20-simple-python-performance-tuning-tips/

  16. “Awesome Functional Programming”: https://github.com/lucasviola/awesome-functional-programming

  17. Python-Modul timeit: https://docs.python.org/3/library/timeit.html

  18. Debian-Paketmanagement-Buch: https://dpmb.org

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDF
LinuxUser 11/2024 KAUFEN
EINZELNE AUSGABE
ABONNEMENTS
TABLET & SMARTPHONE APPS
E-Mail Benachrichtigung
Benachrichtige mich zu:

Hinweis: Dieser Artikel ist älter als ein Jahr, enthaltene Informationen sind möglicherweise veraltet.

0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben