Kapitel 4. Objektorientierte Programmierung und funktionale Programmierung

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

In diesem Kapitel möchte ich dir zwei Programmierstile vorstellen, denen du in deiner Data Science-Karriere wahrscheinlich begegnen wirst: objektorientierte Programmierung (OOP) und funktionale Programmierung (FP). Es ist äußerst hilfreich, beide zu kennen. Selbst wenn du nie Code in einem dieser beiden Stile schreibst, wirst du auf Pakete stoßen, die den einen oder anderen Stil ausgiebig nutzen. Dazu gehören Standard-Python-Pakete für die Datenwissenschaft wie pandas und Matplotlib. Ich möchte dir ein Verständnis für OOP und FP vermitteln, damit du den Code, der dir begegnet, besser nutzen kannst.

OOP und FP sind Programmierparadigmen, die auf grundlegenden Prinzipien der Informatik basieren. Manche Programmiersprachen unterstützen nur eines von beiden oder bevorzugen das eine gegenüber dem anderen. Java ist zum Beispiel eine objektorientierte Sprache. Python unterstützt beide. OOP ist in Python der beliebteste Programmierstil, aber du wirst auch gelegentlich FP verwenden.

Diese Stile geben dir auch einen Rahmen für die Aufteilung deines Codes. Wenn du Code schreibst, könntest du alles, was du tun willst, als ein einziges langes Skript schreiben. Das würde zwar immer noch gut funktionieren, ist aber schwer zu pflegen und zu debuggen. Wie in Kapitel 1 beschrieben, ist es wichtig, den Code in kleinere Teile zu zerlegen, und sowohl OOP als auch FP bieten gute Möglichkeiten, dies zu tun.

In meinem Code halte ich mich weder streng an die Prinzipien der funktionalen noch der objektorientierten Programmierung. Manchmal definiere ich meine eigenen Klassen nach OOP-Prinzipien, und gelegentlich schreibe ich Funktionen, die den FP-Prinzipien entsprechen. Die meisten modernen Python-Programme bewegen sich auf einem Mittelweg zwischen den beiden Paradigmen. In diesem Kapitel gebe ich dir einen Überblick über beide Stile, damit du ein Verständnis für die Grundlagen beider Stile bekommst.

Objektorientierte Programmierung

Die objektorientierte Programmierung ist in Python sehr verbreitet. Aber was ist ein "Objekt" in diesem Zusammenhang? Du kannst dir ein Objekt als ein "Ding" vorstellen, das durch ein Substantiv beschrieben werden kann. Im Data-Science-Code sind gängige Objekte z. B. ein Pandas DataFrame, ein NumPy-Array, eine Matplotlib-Figur oder ein Scikit-Learn-Schätzer.

Ein Objekt kann Daten enthalten, es ist mit bestimmten Aktionen verbunden und es kann mit anderen Objekten interagieren. Ein DataFrame-Objekt von Pandas enthält zum Beispiel eine Liste von Spaltennamen. Eine Aktion, die mit einem DataFrame-Objekt verbunden ist, ist das Umbenennen der Spalten. Das DataFrame-Objekt kann mit einem Pandas Series-Objekt interagieren, indem es diese Reihe als neueSpalte hinzufügt.

Du kannst dir ein Objekt auch als eine eigene Datenstruktur vorstellen. Du entwirfst es so, dass es die gewünschten Daten enthält, damit du später etwas mit ihnen anfangen kannst. Nehmen wir noch einmal den Pandas DataFrame als Beispiel: Die Entwickler von Pandas haben eine Struktur entwickelt, die Daten in einem Tabellenformat speichern kann. Du kannst dann auf die Daten in Zeilen und Spalten zugreifen und mit den Daten in diesen Formen arbeiten.

Im nächsten Abschnitt stelle ich dir die wichtigsten Begriffe der OOP vor und zeige dir einige Beispiele, wie du sie vielleicht schon verwendest.

Klassen, Methoden und Attribute

Klassen, Methoden und Attribute sind wichtige Begriffe, die dir in der OOP begegnen werden. Hier findest du einen Überblick über jeden Begriff:

  • Eine Klasse definiert ein Objekt, und du kannst sie als Blaupause für die Erstellung weiterer Objekte dieser Sorte betrachten. Ein einzelnes Objekt ist eine Instanz dieser Klasse, und jedes Objekt ist ein individuelles "Ding".

  • Methoden sind etwas, das du mit Objekten dieser Klasse machen kannst. Sie legen das Verhalten des Objekts fest und können seine Eigenschaften verändern.

  • Attribute sind Variablen, die eine Eigenschaft dieser Klasse sind, und jedes Objekt kann unterschiedliche Daten in diesen Attributen gespeichert haben.

Das ist alles sehr abstrakt, also werde ich dir ein konkreteres Beispiel geben. Hier ist eine Möglichkeit, wie die objektorientierte Terminologie an die reale Welt angepasst werden kann. Das Buch, das du gerade liest, Softwareentwicklung für Datenwissenschaftler, ist ein Objekt der Klasse "Buch". Eines der Attribute dieses Objekts ist die Anzahl der Seiten und ein weiteres der Name des Autors. Eine Methode, die du für dieses Objekt aufrufen kannst, ist es zu "lesen". Es gibt viele Instanzen der Klasse "Book", aber sie haben alle eine bestimmte Anzahl von Seiten und können alle gelesen werden.

In Python wird eine Klasse normalerweise mit CamelCase benannt. Du würdest also eine Klasse MyClass und nicht my_class nennen. Diese Konvention hilft dir, Klassen leichter zu identifizieren. Du kannst ein Attribut mit dem Format class_instance.attribute nachschlagen. Eine Methode rufst du mit class_instance.method() auf (beachte, dass dies Klammern einschließt). Methoden können Argumente annehmen, Attribute jedoch nicht.

Betrachten wir zum Beispiel einen Pandas Datenrahmen (DataFrame). Wahrscheinlich bist du mit der Syntax zur Erstellung eines neuen Datenrahmens vertraut:

import pandas as pd

my_dict = {"column_1": [1, 2], "column_2": ["a", "b"]}

df = pd.DataFrame(data=my_dict)

Aus einer objektorientierten Perspektive betrachtet, hast du beim Ausführen der Zeile df = pd.DataFrame(data=my_dict) ein neues Objekt vom Typ Datenrahmen initialisiert und einige Daten übergeben, die zum Einrichten der Attribute diesesDatenrahmens verwendet werden.

Du kannst einige der Attribute dieses Datenrahmens nachschlagen, zum Beispiel so:

df.columns

df.shape

.columns und .shape sind Attribute des df Objekts.

Und du kannst viele Methoden für dieses DataFrame-Objekt aufrufen, zum Beispiel:

df.to_csv("file_path", index=False)

.to_csv() ist die Methode in diesem Beispiel.

Ein weiteres bekanntes Beispiel für die Erstellung eines neuen Objekts und den Aufruf einer Methode stammt aus scikit-learn. Wenn du ein maschinelles Lernmodell auf zwei Arrays trainierst, wobei X_train die Trainingsmerkmale und y_train die Trainingsbeschriftungen enthält, würdest du einen Code wie diesen schreiben:

from sklearn.linear_model import LogisticRegression

clf = LogisticRegression()
clf.fit(X_train, y_train)

In diesem Beispiel initialisierst du ein neues LogisticRegression Klassifizierungsobjekt und rufst die Methode .fit() auf.

Hier ist ein weiteres Beispiel. Dies ist der Code, der Abbildung 2-3 in Kapitel 2 erstellt. Hier werden zwei Objekte erstellt: ein Matplotlib-Figur-Objekt und ein Matplotlib-Achsen-Objekt. Anschließend werden mehrere Methoden aufgerufen, um verschiedene Operationen mit diesen Objekten durchzuführen, wie ich in den Code-Anmerkungen erläutern werde:

import matplotlib.pyplot as plt
import numpy as np

n = np.linspace(1, 10, 1000)
line_names = [
    "Constant",
    "Linear",
    "Quadratic",
    "Exponential",
    "Logarithmic",
    "n log n",
]
big_o = [np.ones(n.shape), n, n**2, 2**n, np.log(n), n * (np.log(n))]

fig, ax = plt.subplots() 1
fig.set_facecolor("white") 2

ax.set_ylim(0, 50) 3
for i in range(len(big_o)):
    ax.plot(n, big_o[i], label=line_names[i])
ax.set_ylabel("Relative Runtime")
ax.set_xlabel("Input Size")
ax.legend()

fig.savefig(save_path, bbox_inches="tight") 4
1

Initialisiere die Objekte figure und axes.

2

Rufe die Methode set_facecolor auf dem fig Objekt mit einem Argument white auf.

3

Alle Methoden in den nächsten Zeilen arbeiten mit dem ax Objekt.

4

Das Speichern der Figur ist eine Methode, die auf dem fig Objekt aufgerufen wird.

Die Objekte figure und axes haben viele Methoden, die du aufrufen kannst, um diese Objekte zu aktualisieren.

Hinweis

Matplotlib wirkt manchmal verwirrend, weil es zwei Arten von Schnittstellen hat. Die eine ist objektorientiert und die andere soll das Plotten in MATLAB imitieren. Matplotlib wurde 2003 zum ersten Mal veröffentlicht. Die Entwickler wollten, dass die Software denjenigen vertraut ist, die mit MATLAB vertraut sind. Heutzutage ist es viel üblicher, die objektorientierte Schnittstelle zu verwenden, wie ich im vorherigen Codebeispiel gezeigt habe. Aber da der Code von Menschen von beiden Arten von Schnittstellen abhängt, müssen beide weiterhin existieren. Der Artikel "Warum du Matplotlib hasst" enthält mehr Details zu diesem Thema.

Auch wenn dir die Terminologie rund um OOP nicht geläufig ist, wirst du sie in vielen gängigen Data-Science-Paketen bereits häufig verwenden. Der nächste Schritt besteht darin, deine eigenen Klassen zu definieren, damit du einen objektorientierten Ansatz in deinem eigenen Code verwenden kannst.

Eigene Klassen definieren

Wenn du deinen eigenen Code im objektorientierten Stil schreiben willst, musst du deine eigenen Klassen definieren. Ich zeige dir ein paar einfache Beispiele, wie du das machen kannst. Das erste Beispiel wiederholt einen Text eine bestimmte Anzahl von Malen. Das zweite Beispiel verwendet die Daten der UN-Ziele für nachhaltige Entwicklung, die ich schon in anderen Beispielen in diesem Buch verwendet habe. Weitere Details zu diesen Daten findest du unter "Daten in diesem Buch".

In Python definierst du eine neue Klasse mit der Anweisung class:

class RepeatText():

Es ist sehr üblich, einige Attribute zu speichern, wenn eine neue Instanz eines Objekts initialisiert wird. Dazu verwendet Python eine spezielle Methode namens __init__, die wie folgt definiert ist:

    def __init__(self, n_repeats):
        self.n_repeats = n_repeats

Das erste Argument in der Methode __init__ bezieht sich auf die neue Instanz des Objekts, die erstellt wird. In der Regel wird es self genannt. In diesem Beispiel benötigt die Methode __init__ ein weiteres Argument: n_repeats. Die Zeile self.n_repeats = n_repeats bedeutet, dass jede neue Instanz eines RepeatText Objekts ein n_repeats Attribut hat, das jedes Mal angegeben werden muss, wenn ein neues Objekt initialisiert wird.

Du kannst ein neues RepeatText Objekt wie folgt erstellen:

repeat_twice = RepeatText(n_repeats=2)

Dann kannst du das Attribut n_repeats mit der folgenden Syntax aufrufen:

>>> print(repeat_twice.n_repeats)
... 2

Das Definieren einer anderen Methode sieht ähnlich aus wie das Definieren der Methode __init__, aber du kannst ihr einen beliebigen Namen geben, so als wäre sie eine normale Funktion. Wie du weiter unten sehen wirst, brauchst du noch das Argument self, wenn du willst, dass jede Instanz deines Objekts diesesVerhalten hat:

    def multiply_text(self, some_text):
        print((some_text + " ") * self.n_repeats)

Diese Methode sucht nach dem n_repeats Attribut der Instanz der Klasse, auf die sie wirkt. Das bedeutet, dass du eine Instanz eines RepeatText Objekts erstellen musst, bevor du die Methode anwenden kannst.

Hinweis

In Python gibt es spezielle Methoden, die den Parameter self nicht als Argument benötigen: classmethods und staticmethods. Classmethods gelten für eine ganze Klasse, nicht nur für eine Instanz einer Klasse, und staticmethods können aufgerufen werden, ohne eine Instanz der Klasse zu erzeugen. Mehr über diese Methoden erfährst du in Introducing Python von Bill Lubanovic (O'Reilly, 2019).

Du kannst deine neu erstellte Methode wie folgt aufrufen:

>>> repeat_twice.multiply_text("hello")
... 'hello hello'

Hier ist die vollständige Definition der neuen Klasse:

class RepeatText():

    def __init__(self, n_repeats):
        self.n_repeats = n_repeats

    def multiply_text(self, some_text):
        print((some_text + " ") * self.n_repeats)

Schauen wir uns ein weiteres Beispiel an. Diesmal verwenden wir die Daten der UN-Ziele für nachhaltige Entwicklung, die in Kapitel 1 vorgestellt wurden. Im folgenden Beispiel erstelle ich ein Goal5Data Objekt, das Daten zu Ziel 5, "Gleichstellung der Geschlechter und Stärkung der Rolle aller Frauen und Mädchen", enthält. Dieses Objekt enthält Daten zu einem der Ziele, die mit diesem Ziel verbunden sind, nämlich Ziel 5.5: "Die volle und wirksame Teilhabe von Frauen und ihre Chancengleichheit in Führungspositionen auf allen Entscheidungsebenen des politischen, wirtschaftlichen und öffentlichen Lebens sicherstellen.

Ich möchte ein Objekt erstellen können, das die Daten für jedes Land speichert, damit ich sie auf dieselbe Weise bearbeiten kann. Hier ist der Code, um die neue Klasse zu erstellen und die Daten zu speichern:

class Goal5Data():
    def __init__(self, name, population, women_in_parliament):
        self.name = name
        self.population = population
        self.women_in_parliament = women_in_parliament 1
1

Dieses Attribut enthält eine Auflistung des prozentualen Anteils von Frauen an den Sitzen in der Regierung des Landes, aufgeschlüsselt nach Jahren.

Hier ist eine Methode, die eine Zusammenfassung dieser Daten ausgibt:

    def print_summary(self):
        null_women_in_parliament = len(self.women_in_parliament) -
        np.count_nonzero(self.women_in_parliament)
        print(f"There are {len(self.women_in_parliament)} data points for
        Indicator 5.5.1, 'Proportion of seats held by women in national
        parliaments'.")
        print(f"{null_women_in_parliament} are nulls.")

Auf die gleiche Weise wie im vorherigen Beispiel kannst du eine neue Instanz dieser Klasse erstellen, etwa so:

usa = CountryData(name="USA",
                  population=336262544,
                  women_in_parliament=[13.33, 14.02, 14.02, ...])

Wenn du die Methode print_summary aufrufst, erhältst du das folgende Ergebnis:

>>> usa.print_summary()
... "There are 24 data points for Indicator 5.5.1,
    'Proportion of seats held by women in national parliaments'.
    0 are nulls."

Wenn du den Code als Methode schreibst, ist er modular, gut organisiert und lässt sich leicht wiederverwenden. Es ist auch sehr klar, was er tut, was jedem hilft, der deinen Code verwenden möchte.

Ich werde diese Klasse im nächsten Abschnitt verwenden, um dir ein weiteres Prinzip von Klassen zu zeigen: Vererbung.

OOP-Prinzipien

Diese Begriffe werden dir in der OOP häufig begegnen: Kapselung, Abstraktion, Vererbung und Polymorphismus. In diesem Abschnitt definiere ich sie alle und zeige dir einige Beispiele, wie Vererbung für dich nützlich sein kann.

Vererbung

Vererbung bedeutet, dass du eine Klasse erweitern kannst, indem du eine andere Klasse erstellst, die auf ihr aufbaut. Das hilft, Wiederholungen zu vermeiden, denn wenn du eine neue Klasse brauchst, die eng mit einer bereits geschriebenen verwandt ist, musst du diese Klasse nicht duplizieren, um eine kleine Änderung vorzunehmen.

Wenn du deine eigenen Klassen definierst, brauchst du die Vererbung vielleicht nicht zu verwenden, aber bei Klassen aus einer externen Bibliothek könnte es nötig sein. Einige Beispiele für die Vererbung bei der Datenvalidierung wirst du später im Buch sehen, in "Datenvalidierung mit Pydantic" und in "Hinzufügen von Funktionen zu deiner API". In diesem Abschnitt möchte ich dir helfen, Vererbung zu erkennen und zu verstehen, wenn du ihr begegnest.

Eine Klasse, die Vererbung nutzt, erkennst du an der folgenden Syntax:

class NewClass(OriginalClass):
    ...

Die Klasse NewClass kann alle Attribute und Methoden der Klasse OriginalClass nutzen, aber du kannst jede davon, die du ändern möchtest, außer Kraft setzen. Der Begriff "Elternteil" wird oft verwendet, um sich auf die ursprüngliche Klasse zu beziehen, und die neue Klasse, die von ihr erbt, wird oft als "Kind" bezeichnet.

Hier ist ein Beispiel für eine neue Klasse, Goal5TimeSeries, die von der Klasse Goal5Data aus dem vorherigen Abschnitt erbt und sie zu einer Klasse macht, die mit Zeitreihendaten arbeiten kann:

class Goal5TimeSeries(Goal5Data):
    def __init__(self, name, population, women_in_parliament, timestamps):
        super().__init__(name, population, women_in_parliament)
        self.timestamps = timestamps

Die Methode __init__ sieht dieses Mal ein wenig anders aus. Die Verwendung von super() bedeutet, dass die Methode __init__ der Elternklasse aufgerufen wird, die die Attribute name, population und women_in_parliament initialisiert.

Du kannst ein neues Goal5TimeSeries Objekt wie folgt erstellen:

india = Goal5TimeSeries(name="India", population=1417242151,
                        women_in_parliament=[9.02, 9.01, 8.84, ...],
                        timestamps=[2000, 2001, 2002, ...])

Und du kannst die Methode immer noch von der Klasse Goal5Data aus aufrufen:

>>> india.print_summary()
... "There are 24 data points for Indicator 5.5.1,
   'Proportion of seats held by women in national parliaments'. 0 are nulls."

Du kannst auch eine neue Methode hinzufügen, die für die Kindklasse relevant ist. Diese neue Methode fit_trendline() passt zum Beispiel eine Regressionslinie an die Daten an, um den Trend zu ermitteln:

from scipy.stats import linregress

class Goal5TimeSeries(Goal5Data):
    def __init__(self, name, population, women_in_parliament, timestamps):
        super().__init__(name, population, women_in_parliament)
        self.timestamps = timestamps

    def fit_trendline(self):
        result = linregress(self.timestamps, self.women_in_parliament) 1
        slope = round(result.slope, 3)
        r_squared = round(result.rvalue**2, 3) 2
        return slope, r_squared
1

Verwende die Funktion linregress von scipy, um mit Hilfe der linearen Regression eine gerade Linie durch die Daten zu ziehen.

2

Berechne das Bestimmtheitsmaß (R-Quadrat), um die Anpassungsgüte der Linie zu bestimmen.

Der Aufruf der neuen Methode liefert die Steigung der Trendlinie und den normierten mittleren quadratischen Fehler der Anpassung der Linie an die Daten:

>>> india.fit_trendline()
... (0.292, 0.869)

Wenn du die Vererbung in deinen eigenen Klassen verwendest, kannst du damit die Fähigkeiten der von dir erstellten Klassen erweitern. Das bedeutet weniger doppelten Code und hilft dir, deinen Code modular zu halten. Es ist auch sehr hilfreich, von Klassen in einer externen Bibliothek zu erben. Auch hier bedeutet das, dass du ihre Funktionalität nicht duplizierst, sondern zusätzliche Funktionen hinzufügen kannst.

Verkapselung

Verkapselung bedeutet, dass deine Klasse ihre Details nach außen hin verbirgt. Du kannst nur die Schnittstelle der Klasse sehen, nicht aber die internen Details, die in ihr vor sich gehen. Die Schnittstelle besteht aus den Methoden und Attributen, die du entwirfst. Das ist in Python nicht so üblich, aber in anderen Programmiersprachen werden Klassen oft mit versteckten oder privaten Methoden oder Attributen entworfen, die von außen nicht geändert werden können.

Dennoch wird das Konzept der Kapselung auch in Python angewandt, und viele Bibliotheken und Anwendungen nutzen es. pandas ist ein gutes Beispiel dafür. pandas nutzt die Kapselung, indem es Methoden und Attribute bereitstellt, mit denen du mit Daten interagieren kannst, während die zugrunde liegenden Implementierungsdetails verborgen bleiben. Ein DataFrame-Objekt kapselt Daten und bietet verschiedene Methoden für den Zugriff, die Filterung und die Umwandlung der Daten. Wie in Kapitel 3 erwähnt, verwenden Pandas DataFrames unter der Haube NumPy, aber das musst du nicht wissen, um sie zu nutzen. Du kannst die DataFrame-Schnittstelle von Pandas nutzen, um deine Aufgaben zu erfüllen, aber wenn du tiefer einsteigen willst, kannst du auch NumPy-Methoden verwenden.

Hinweis

Schnittstellen sind extrem wichtig, weil andere Codes oder Klassen oft von der Existenz eines Attributs oder einer Methode abhängen. Wenn du also die Schnittstelle änderst, kann ein anderer Code kaputt gehen. Es ist in Ordnung, die interne Funktionsweise deiner Klasse zu ändern, z. B. um die Berechnungen innerhalb einer Methode effizienter zu machen. Aber du solltest dafür sorgen, dass die Schnittstelle von Anfang an einfach zu benutzen ist, und versuchen, sie nicht zu verändern. In Kapitel 8 werde ich ausführlicher auf Schnittstellen eingehen.

Abstraktion

Die Abstraktion ist eng mit der Kapselung verbunden. Sie bedeutet, dass du dich mit einer Klasse auf der entsprechenden Detailebene beschäftigen solltest. Du könntest dich also dafür entscheiden, die Details einer Berechnung in einer Methode festzuhalten, oder du könntest den Zugriff darauf über die Schnittstelle erlauben. Auch dies ist in anderen Programmiersprachen üblicher.

Polymorphismus

Polymorphismus bedeutet, dass du die gleiche Schnittstelle für verschiedene Klassen haben kannst, was deinen Code vereinfacht und Wiederholungen reduziert. Das heißt, zwei Klassen können eine Methode mit demselben Namen haben, die ein ähnliches Ergebnis liefert, aber intern unterschiedlich funktioniert. Bei den beiden Klassen kann es sich um eine Eltern- und eine Kindklasse handeln, oder sie sind nicht miteinander verwandt.

scikit-learn enthält ein großartiges Beispiel für Polymorphismus. Jeder Klassifikator hat die gleiche fit Methode, um den Klassifikator auf Daten zu trainieren, auch wenn diese als unterschiedliche Klassen definiert sind. Hier ist ein Beispiel für das Trainieren von zwei verschiedenen Klassifikatoren auf bestimmte Daten:

from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier

lr_clf = LogisticRegression()
lr_clf.fit(X_train, y_train)

rf_clf = RandomForestClassifier()
rf_clf.fit(X_train, y_train)

Auch wenn LogisticRegression und RandomForestClassifier unterschiedliche Klassen sind, haben beide eine Methode .fit(), die die Trainingsdaten und die Trainingskennungen als Argumente benötigt. Der gemeinsame Name der Methode macht es dir leicht, den Klassifikator zu ändern, ohne viel von deinem Code zu verändern.

Dies war ein kurzer Überblick über die wichtigsten Merkmale der objektorientierten Programmierung. Es ist ein riesiges Thema und ich empfehle Introducing Python von Bill Lubanovic (O'Reilly, 2019), wenn du mehr erfahren möchtest.

Funktionale Programmierung

Python unterstützt zwar das funktionale Programmierparadigma, aber es ist nicht üblich, Python in einem reinen FP-Stil zu schreiben. Viele Softwareentwicklerinnen und -entwickler sind der Meinung, dass andere Sprachen für FP besser geeignet sind, z. B. Scala. Dennoch gibt es nützliche FP-Funktionen in Python, die ich im Folgenden erläutern werde.

Bei der funktionalen Programmierung geht es, wie der Name schon sagt, um Funktionen, die sich nicht verändern. Diese Funktionen sollten keine Daten ändern, die außerhalb der Funktion existieren, oder globale Variablen verändern. Um die korrekte Terminologie zu verwenden, sind die Funktionen unveränderlich, "rein" und frei von Nebeneffekten. Sie haben keine Auswirkungen auf etwas, das nicht in der Rückgabe der Funktion enthalten ist. Wenn du zum Beispiel eine Funktion hast, die ein Element zu einer Liste hinzufügt, sollte diese Funktion eine neue Kopie der Liste zurückgeben, anstatt die bestehende Liste zu verändern. Im strengen FP besteht ein Programm nur aus der Auswertung von Funktionen. Diese können verschachtelt sein (wobei eine Funktion innerhalb einer anderen definiert ist) oder Funktionen können als Argumente an andere Funktionen übergeben werden.

Einige Vorteile von FP sind:

  • Es ist einfach zu testen, weil eine Funktion für eine bestimmte Eingabe immer die gleiche Ausgabe liefert. Außerhalb der Funktion wird nichts verändert.

  • Es ist leicht zu parallelisieren, weil die Daten nicht verändert werden.

  • Es erzwingt das Schreiben von modularem Code.

  • Es kann prägnanter und effizienter sein.

Zu den gängigen Python-Konzepten im funktionalen Stil gehören Lambda-Funktionen und die eingebauten Funktionen map und filter. Außerdem werden Generatoren oft in diesem Stil geschrieben, und auch List Comprehensions können als eine Form von FP betrachtet werden. Weitere wissenswerte Bibliotheken für FP sind itertools und more-itertools. Im nächsten Abschnitt werde ich mir Lambda-Funktionen und map() genauer ansehen.

Lambda-Funktionen und map()

Lambda-Funktionen sind kleine, anonyme Python-Funktionen, die du für schnelle, einmalige Aufgaben verwenden kannst. Sie werden als "anonym" bezeichnet, weil sie nicht wie eine normale Python-Funktion mit einem Namen definiert sind.

Eine Lambda-Funktion hat die folgende Syntax:

lambda arguments: expression

Eine Lambda-Funktion kann beliebig viele Argumente annehmen, aber sie kann nur einen Ausdruck haben. Lambda-Funktionen werden häufig mit eingebauten Funktionen wie map und filter verwendet. Diese nehmen Funktionen als Argumente an und können die Funktion dann auf jedes Element in einer Iterablen (z. B. einer Liste) anwenden.

Hier ist ein einfaches Beispiel. Mit den Daten von Ziel 5 aus "Eigene Klassen definieren" kannst du eine Liste mit dem prozentualen Anteil von Frauen in Regierungspositionen mit der folgenden Funktion in eine Liste mit Anteilen von 0 bis 1 umwandeln:

usa_govt_percentages = [13.33, 14.02, 14.02, 14.25, ...]

usa_govt_proportions = list(map(lambda x: x / 100, usa_govt_percentages))

Hier passiert eine Menge in einer Zeile. Die Lambda-Funktion ist in diesem Fall lambda x: x/100. In dieser Funktion ist x eine temporäre Variable, die außerhalb der Funktion nicht verwendet wird. map() wendet die Lambda-Funktion auf jedes Element der Liste an. Und schließlich erstellt list() eine neue Liste auf der Grundlage der Map.

Daraus ergibt sich das folgende Ergebnis:

>>> print(usa_govt_proportions)
... [0.1333, 0.1402, 0.1402, 0.1425, ...]

Beachte, dass die ursprünglichen Daten durch die Anwendung dieser Funktion nicht verändert wurden. Es wurde eine neue Liste mit den geänderten Daten erstellt.

Anwendung von Funktionen auf Datenrahmen

Ähnlich wie bei der oben beschriebenen eingebauten Funktion map() kannst du auch Funktionen auf Datenrahmen anwenden. Das kann besonders nützlich sein, wenn du eine neue Spalte auf der Grundlage einer bestehenden Spalte erstellen willst. Auch hier kannst du eine Funktion verwenden, die eine andere Funktion als Eingabe nimmt. In Pandas ist das apply().

Hier ist ein Beispiel für die Anwendung einer Lambda-Funktion auf eine Spalte in einem Datenrahmen (DataFrame):

df["USA_processed"] = df["United States of America"].apply(lambda x:
                                                           "Mostly male"
                                                           if x < 50
                                                           else "Mostly female")

In diesem Beispiel sind die Daten über Frauen in Regierungspositionen, die ich im gesamten Kapitel verwendet habe, in der Spalte United States of America zu finden. Die Lambda-Funktion nimmt den Prozentsatz der Frauen in Regierungspositionen und gibt "Mostly male" zurück, wenn dieser Wert unter 50% liegt, oder "Mostly female", wenn er 50% oder mehr beträgt.

Du kannst df.apply() auch mit einer benannten Funktion verwenden, die an anderer Stelle definiert wurde. Hier ist die gleiche Funktion wie zuvor, aber als benannte Funktion:

def binary_labels(women_in_govt):
    if women_in_govt < 50:
        return "Mostly male"
    else:
        return "Mostly female"

Du kannst diese Funktion für jede Zeile in einer Spalte aufrufen, indem du den Funktionsnamen als Argument an die Funktion apply übergibst:

df["USA_processed"] = df["United States of America"].apply(binary_labels)

Dies ist eine bessere Lösung als das Schreiben einer Lambda-Funktion, da du die Funktion in Zukunft vielleicht wiederverwenden möchtest und du sie außerdem separat testen und debuggen kannst. Außerdem kannst du komplexere Funktionen als in einer Lambda-Funktion einbinden.

Warnung

Die Funktion apply in Pandas ist langsamer als die eingebauten vektorisierten Funktionen, weil sie jede Zeile eines Datenrahmens durchläuft. Die bewährte Methode ist also, apply nur für Dinge zu verwenden, die nicht bereits implementiert sind. Einfache numerische Operationen wie das Ermitteln des Maximums einer Liste oder einfache String-Optionen wie das Ersetzen eines Strings durch einen anderen sind bereits als schnellere vektorisierte Funktionen verfügbar.

Welches Paradigma soll ich verwenden?

Um ehrlich zu sein, wenn du nur ein kleines Skript schreibst oder alleine an einem kurzen Projekt arbeitest, musst du dich keinem dieser Paradigmen vollständig anschließen. Halte dich einfach an modulare Skripte, die funktionieren.

Bei größeren Projekten ist es jedoch ratsam, über die Art des Problems nachzudenken, mit dem du dich befasst, und zu überlegen, ob eines dieser Paradigmen gut geeignet ist. Du könntest dich für OOP entscheiden, wenn du über eine Reihe von Dingen nachdenkst, mit denen etwas gemacht werden muss. Du kannst deinen Problemraum in Instanzen verwandeln, die ein ähnliches Verhalten, aber unterschiedliche Attribute oder Daten haben müssen. Ein wichtiger Punkt dabei ist, dass du viele Instanzen einer Klasse haben solltest. Es lohnt sich nicht, eine neue Klasse zu schreiben, wenn du nur eine Instanz davon hast; das bringt nur zusätzliche Komplexität, die du nicht brauchst.

Wenn du neue Dinge mit Daten machen willst, die nicht verändert werden sollen, könnte FP eine gute Wahl für dich sein. Es lohnt sich auch, FP in Betracht zu ziehen, wenn du eine große Datenmenge hast und die Operationen, die du damit durchführst, parallelisieren willst.

Allerdings gibt es hier kein Richtig oder Falsch. Wenn du alleine arbeitest, kannst du dich nach deinen persönlichen Vorlieben richten, oder du kannst dich an das halten, was in deinem Team am häufigsten verwendet wird, um die Dinge zu vereinheitlichen. Es ist gut zu erkennen, wann diese Paradigmen verwendet werden, sie im Code anderer Leute zu nutzen und zu entscheiden, was für dein spezielles Problem am besten geeignet ist.

Die wichtigsten Erkenntnisse

OOP und FP sind Programmierparadigmen, die dir in dem Code, den du liest, begegnen werden. Bei OOP geht es um Objekte, also benutzerdefinierte Datenstrukturen, und bei FP um Funktionen, die die zugrunde liegenden Daten nicht verändern.

In der OOP definiert eine Klasse neue Objekte, die Attribute und Methoden haben können. Du kannst deine eigenen Klassen definieren, um zugehörige Methoden und Daten zusammenzuhalten. Das ist ein guter Ansatz, wenn du viele Instanzen ähnlicher Objekte hast. Du kannst Vererbung einsetzen, um die Wiederholung von Code zu vermeiden, und du kannst Polymorphismus verwenden, um deine Schnittstellen zu vereinheitlichen.

In FP findet idealerweise alles innerhalb der Funktion statt. Das ist nützlich, wenn du Daten hast, die sich nicht ändern, und du viele Dinge mit ihnen machen willst, oder wenn du die Daten parallelisieren willst. Lambda-Funktionen sind das am häufigsten verwendete Beispiel für FP in Python.

Welches Paradigma du wählst, hängt von dem Problem ab, an dem du arbeitest, aber es ist nützlich, wenn du beide kennst.

Get Software Engineering für Datenwissenschaftler now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.