Was ist NumPy?

28.02.2024
Von 
Serdar Yegulalp schreibt für unsere US-Schwesterpublikation Infoworld.
NumPy ist die richtige Wahl, wenn Sie mathematische Berechnungen in großem Umfang beschleunigen wollen. Das sollten Sie über die Python-Bibliothek wissen.
NumPy kann mathematische Berechnungen mit Python erheblich beschleunigen - insbesondere in Kombination mit Cython und Numba.
NumPy kann mathematische Berechnungen mit Python erheblich beschleunigen - insbesondere in Kombination mit Cython und Numba.
Foto: Koltukovs | shutterstock.com

Python ist zwar praktisch und flexibel, aber auch deutlich langsamer als andere Programmiersprachen. Glücklicherweise kann das Python-Ökosystem diese Einschränkungen mit diversen Tools kompensieren.

Eines der gebräuchlichsten Werkzeuge, das Developer und Data Scientists dabei unterstützt, Berechnungen in großem Maßstab zu realisieren, ist NumPy. Das Open-Source-Tool ermöglicht es, mit Arrays und Matrizen zu arbeiten, die von Code angetrieben werden, der in Hochgeschwindigkeitssprachen wie C, C++ und Fortran geschrieben ist. Sämtliche NumPy-Operationen finden dabei außerhalb der Python Runtime statt, um Beeinträchtigungen durch dessen Limitationen zu verhindern.

NumPy für Python-Tasks verwenden

Speziell im Bereich Machine Learning und Datenwissenschaft beinhalten mathematische Rechenoperationen die Arbeit mit Matrizen respektive Zahlenlisten. Um das (auf primitive Art und Weise) mit Python zu erledigen, werden die Zahlen in einer Struktur (für gewöhnlich eine list) gespeichert und anschließend über die Struktur geloopt, woraufhin eine Rechenoperation für jedes einzelne, enthaltene Element folgt. Weil jedes Element von einem Python-Objekt in eine maschinennahe Zahl hin und her übersetzt werden muss, ist diese Methode sowohl langsam als auch ineffizient.

NumPy bietet an dieser Stelle einen spezialisierten Array-Typ, der darauf optimiert ist, mit maschinennativen numerischen Typen wie Integers oder Floats zu arbeiten. Die Arrays können dabei eine beliebige Anzahl von Dimensionen aufweisen - allerdings verwendet jedes einen einheitlichen Datentyp (dtype), um die zugrundeliegenden Informationen zu repräsentieren. Ein einfaches Beispiel:

import numpy as np

np.array([0, 1, 2, 3, 4, 5, 6])

Dieser Befehl erzeugt ein eindimensionales NumPy-Array aus der angegebenen Liste von Zahlen. Weil für dieses Array kein dtype spezifiziert wurde, wird automatisch aus den übergebenen Daten gefolgert, dass es sich - je nach Plattform - um eine 32- oder 64-Bit-Ganzzahl mit Vorzeichen handelt. Folgendermaßen würden wir vorgehen, um den dtype explizit anzugeben:

np.array([0, 1, 2, 3, 4, 5, 6], dtype=np.uint32)

Bei np.uint32 handelt es sich um den dtype für eine unsignierte 32-Bit-Integer. Es ist auch möglich, generische Python-Objekte als dtype für ein NumPy-Array zu verwenden. Das führt allerdings im Vergleich zu Python im Allgemeinen, nicht zu einer besser Performance. Am besten funktioniert NumPy mit maschinennativen, numerischen Typen. Diese tragen das Gros zu den Geschwindigkeitsvorteilen von NumPy bei.

Python-Arrays mit NumPy beschleunigen

Mit Arrays zu arbeiten, ohne dabei jedes Element einzeln adressieren zu müssen, ist ein weiteres Speed-förderndes Feature. NumPy-Arrays weisen bezüglich ihres Verhaltens diverse Ähnlichkeiten zu konventionellen Python-Objekten auf. Deshalb ist es verlockend, gängige Pytohn-Metaphern zu nutzen, wenn man mit ihnen arbeitet. Ein NumPy-Array mit den Zahlen 0 bis 1000 zu erstellen, funktioniert theoretisch so:

x = np.array([_ for _ in range(1000)])

Das würde zwar funktionieren, allerdings würde die Performance durch die Zeit geschmälert, die Python benötigt, um die Liste zu erstellen und die, die NumPy braucht, um diese in ein Array umzuwandeln. Wesentlich effizienter funktioniert derselbe Task direkt in NumPy:

x = np.arange(1000)

Um neue Arrays ohne Looping zu erstellen, können Sie viele andere in NumPy integrierte Operationen nutzen - etwa Arrays mit Nullen (oder einem anderen Initialwert), vorhandene Datensätze, Puffer oder andere Quellen.

Wie bereits erwähnt, verhalten sich NumPy-Arrays der Einfachheit halber wie andere Python-Objekte. Sie können zum Beispiel wie Listen indiziert werden - arr[0] greift auf das erste Element eines NumPy-Arrays zu. So können Sie einzelne Elemente in einem Array setzen oder lesen.

Wenn Sie alle Elemente eines Arrays ändern möchten, empfehlen sich die Broadcasting-Funktionen von NumPy, mit denen Sie Operationen über ein ganzes Array oder ein Slice ausführen können - ohne Looping in Python. Auch das lässt sich vollständig in NumPy erledigen:

x1 = np.array(

[np.arange(0, 10),

np.arange(10,20)]

)

Das Ergebnis ist ein zweidimensionales NumPy-Array, wobei jede Dimension aus einem Zahlenbereich besteht. Arrays mit einer beliebigen Anzahl von Dimensionen lassen sich mit Hilfe verschachtelter Listen im Konstruktor erstellen.

[[ 0 1 2 3 4 5 6 7 8 9]

[10 11 12 13 14 15 16 17 18 19]]

Wenn wir die Achsen dieses Arrays in Python transponieren wollten, müssten wir eigentlich eine Art Loop schreiben. NumPy erlaubt allerdings, diese Art von Operation mit nur einem Befehl auszuführen:

x2 = np.transpose(x1)

Der Output:

[[ 0 10]

[ 1 11]

[ 2 12]

[ 3 13]

[ 4 14]

[ 5 15]

[ 6 16]

[ 7 17]

[ 8 18]

[ 9 19]]

Operationen wie diese sind der Schlüssel, um NumPy korrekt zu nutzen. Die Python-Bibliothek bietet einen breitgefächerten Katalog integrierter Routinen, um Array-Daten zu bearbeiten: Eingebaute Routinen für lineare Algebra, diskrete Fourier-Transformationen und Pseudozufallszahlengeneratoren ersparen Ihnen die Mühe, diese Dinge selbst entwickeln zu müssen. In den meisten Fällen können Sie das, was Sie brauchen, mit einem oder mehreren Built-Ins erreichen, ganz ohne Python-Operationen verwenden zu müssen.

Universelle NumPy-Funktionen (ufuncs)

Fortgeschrittene Berechnungen ohne Python-Loops können Sie in NumPy auch über Universal Functions - kurz ufuncs - erledigen. Eine ufunc nimmt ein Array auf, führt eine Rechenoperation für jedes Array-Element durch und sendet die Ergebnisse entweder an ein anderes Array oder führt die Operation an Ort und Stelle aus. Ein Beispiel:

x1 = np.arange(1, 9, 3)

x2 = np.arange(2, 18, 6)

x3 = np.add(x1, x2)

In diesem Beispiel addiert np.add jedes Element von x1 zu x2, wobei die Ergebnisse in einem neu erstellten Array - x3 - gespeichert werden. Das Ergebnis ist [ 3 12 21] - alle Berechnungen werden in NumPy selbst durchgeführt.

Universal Functions verfügen zudem über Attributmethoden, mit denen sie flexibler angewendet werden können. Das reduziert den Bedarf an manuellen Loops und Python-seitiger Logik. Wenn wir np.add verwenden möchten um das Array x1 zu summieren, könnten wir die .add-Methode verwenden (np.add.accumulate(x1)), statt über jedes Element im Array zu loopen.

Ähnlich verhält es sich im Fall einer Reduktionsfunktion, also der Anwendung von .add entlang der Achse eines mehrdimensionalen Arrays, wobei das Ergebnis ein neues Array mit einer Dimension weniger ist. Wir könnten loopen und ein neues Array erstellen, das wäre allerdings langsam. Um das gleiche Ergebnis ohne Loop zu erreichen, verwenden wir np.add.reduce:

x1 = np.array([[0,1,2],[3,4,5]])

# [[0 1 2] [3 4 5]]

x2 = np.add.reduce(x1)

# [3 5 7]

Auch bedingte Reduktionen sind möglich - mit Hilfe des where-Arguments:

x2 = np.add.reduce(x1, where=np.greater(x1, 1))

Das würde - in Fällen, in denen die Elemente in der ersten Achse von x1 größer als 1 sind - x1+x2 zurückgeben. Anderenfalls wird nur der Wert der Elemente in der zweiten Achse zurückgegeben. Das erspart wiederum die manuelle Iteration über das Array in Python. NumPy bietet solche Mechanismen, im Daten nach bestimmten Kriterien zu filtern und zu sortieren, um keine Loops schreiben zu müssen - oder es zumindest auf das Minimum zu reduzieren.

NumPy mit Cython nutzen

Die Cython-Bibliothek ermöglicht Ihnen, Python-Code zu schreiben und ihn zu Beschleunigungszwecken in C zu konvertieren - wobei C-Typen als Variablen verwendet werden. Diese Variablen können auch NumPy-Arrays enthalten. Cython-Code ist also in der Lage, direkt mit NumPy-Arrays zu arbeiten. Cython kann in Kombination mit NumPy einige, leistungsstarke Funktionen realisieren. Zum Beispiel, wenn es darum geht:

  • Manuelle Loops zu beschleunigen. Manchmal kommen sie nicht darum herum, Loops für NumPy-Arrays zu schreiben. Wenn Sie die Schleifenoperation in ein Cython-Modul schreiben, können Sie diese in C ausführen. Das führt zu erheblichen Geschwindigkeitssteigerungen. Zu beachten ist dabei: Das ist nur möglich, wenn es sich bei den Typen aller fraglicher Variablen entweder um NumPy-Arrays oder maschinennative C-Types handelt.

  • NumPy-Arrays mit C-Bibliotheken zu verwenden. Ein häufiger Anwendungsfall für Cython ist es, praktische Python-Wrapper für C-Bibliotheken zu schreiben. Dabei kann Cython-Code als Brücke zwischen einer bestehenden C-Bibliothek und NumPy-Arrays fungieren.

Cython bietet zwei Möglichkeiten, mit NumPy-Arrays zu arbeiten: Zum einen über eine Typed Memoryview (Cython-Konstrukt für den schnellen und begrenzungssicheren Zugriff auf ein NumPy-Array). Die andere Möglichkeit: Direkt mit einem Raw Pointer zu arbeiten, der auf die zugrundeliegenden Daten verweist. Diese Methode ist jedoch potenziell unsicher und erfordrt, das Memory Layout des Objekts im Vorfeld zu kennen.

NumPy mit Numba nutzen

Den Python-JIT-Compiler Numba zu verwenden, bietet eine weitere Möglichkeit, Python auf performante Weise mit NumPy-Arrays zu nutzen. Numba übersetzt Python-interpretierten Code in maschinennativen - mit Spezialisierungen für Tools wie NumPy. Loops in Python über NumPy-Arrays lassen sich so automatisch optimieren.

Diese Optimierungen funktionieren jedoch nur bis zu einem gewissen Grad automatisch und führen möglicherweise nicht bei allen Programmen zu drastischen Leistungssteigerungen. (fm)

Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation Infoworld.