Kapitel 4. Verstehen von Datentypen in Python

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

Effektive datengesteuerte Wissenschaft und Berechnungen erfordern ein Verständnis dafür, wie Daten gespeichert und bearbeitet werden. Dieses Kapitel beschreibt und vergleicht, wie Arrays in der Sprache Python selbst gehandhabt werden und wie NumPy dies verbessert. Das Verständnis dieses Unterschieds ist grundlegend für das Verständnis des restlichen Buches.

Die Benutzer von Python werden oft durch die Benutzerfreundlichkeit angelockt, zu der auch die dynamische Typisierung gehört. Während in einer statisch typisierten Sprache wie C oder Java jede Variable explizit deklariert werden muss, entfällt diese Angabe in einer dynamisch typisierten Sprache wie Python. In C könntest du zum Beispiel eine bestimmte Operation wie folgt angeben:

/* C code */
int result = 0;
for(int i=0; i<100; i++){
    result += i;
}

In Python könnte die entsprechende Operation folgendermaßen geschrieben werden:

# Python code
result = 0
for i in range(100):
    result += i

Beachte einen wesentlichen Unterschied: In C werden die Datentypen jeder Variablen explizit deklariert, während in Python die Typen dynamisch abgeleitet werden. Das bedeutet zum Beispiel, dass wir jeder Variablen jede Art von Daten zuweisen können:

# Python code
x = 4
x = "four"

Hier haben wir den Inhalt von x von einer Ganzzahl in einen String umgewandelt. Das Gleiche würde in C (je nach Compiler-Einstellungen) zu einem Kompilierungsfehler oder anderen unbeabsichtigten Folgen führen:

/* C code */
int x = 4;
x = "four";  // FAILS

Diese Art von Flexibilität ist ein Element, das Python und andere dynamisch typisierte Sprachen bequem und einfach zu benutzen macht. Zu verstehen,wie das funktioniert, ist wichtig, um zu lernen, wie man Daten mit Python effizient und effektiv analysiert. Diese Typflexibilität weist aber auch darauf hin, dass Python-Variablen mehr sind als nur ihre Werte; sie enthalten auch zusätzliche Informationen über den Typ des Wertes. Wir werden das in den folgenden Abschnitten genauer untersuchen.

Eine Python-Ganzzahl ist mehr als nur eine Ganzzahl

Die Standardimplementierung von Python ist in C geschrieben. Das bedeutet, dass jedes Python-Objekt einfach eine geschickt getarnte C-Struktur ist, die nicht nur ihren Wert, sondern auch andere Informationen enthält. Wenn wir zum Beispiel eine ganze Zahl in Python definieren, wie x = 10000, ist x nicht nur eine "rohe" ganze Zahl. Es handelt sich vielmehr um einen Zeiger auf eine zusammengesetzte C-Struktur, die mehrere Werte enthält. Wenn wir uns den Quellcode von Python 3.10 ansehen, sehen wir, dass die Definition des Typs Integer (Long) in etwa so aussieht (nachdem die C-Makros erweitert wurden):

struct _longobject {
    long ob_refcnt;
    PyTypeObject *ob_type;
    size_t ob_size;
    long ob_digit[1];
};

Eine einzelne Ganzzahl in Python 3.10 enthält eigentlich vier Teile:

  • ob_refcnteine Referenzzählung, die Python hilft, Speicherzuweisungen und -freigaben geräuschlos zu handhaben

  • ob_typedie den Typ der Variablen kodiert

  • ob_size, die die Größe der folgenden Datenelemente angibt

  • ob_digit, die den tatsächlichen ganzzahligen Wert enthält, den die Python-Variable darstellen soll

Das bedeutet, dass das Speichern einer ganzen Zahl in Python im Vergleich zu einer kompilierten Sprache wie C mit einem gewissen Overhead verbunden ist, wie in Abbildung 4-1 dargestellt.

pdsh2 0401
Abbildung 4-1. Der Unterschied zwischen C- und Python-Ganzzahlen

Hier ist PyObject_HEAD der Teil der Struktur, der die Anzahl der Verweise, den Typcode und andere bereits erwähnte Elemente enthält.

Beachte den Unterschied: Eine C-Ganzzahl ist im Wesentlichen eine Bezeichnung für eine Position im Speicher, deren Bytes einen Ganzzahlwert kodieren. Eine Python-Ganzzahl ist ein Zeiger auf eine Position im Speicher, die alle Python-Objektinformationen enthält, einschließlich der Bytes, die den Ganzzahlwert enthalten. Diese zusätzlichen Informationen in der Python-Ganzzahlstruktur ermöglichen es, Python so frei und dynamisch zu kodieren. All diese zusätzlichen Informationen in den Python-Typen haben jedoch ihren Preis, was besonders in Strukturen deutlich wird, die viele dieser Objekte kombinieren.

Eine Python-Liste ist mehr als nur eine Liste

Betrachten wir nun, was passiert, wenn wir eine Datenstruktur in Python verwenden, die viele Python-Objekte enthält. Der Standardcontainer in Python, der mehrere Elemente enthält, ist die Liste. Wir können eine Liste mit ganzen Zahlen wie folgt erstellen:

In [1]: L = list(range(10))
        L
Out[1]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [2]: type(L[0])
Out[2]: int

Oder, ähnlich, eine Liste von Strings:

In [3]: L2 = [str(c) for c in L]
        L2
Out[3]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
In [4]: type(L2[0])
Out[4]: str

Dank der dynamischen Typisierung in Python können wir sogar heterogene Listen erstellen:

In [5]: L3 = [True, "2", 3.0, 4]
        [type(item) for item in L3]
Out[5]: [bool, str, float, int]

Diese Flexibilität hat jedoch ihren Preis: Um diese flexiblen Typen zu ermöglichen, muss jedes Element in der Liste seinen eigenen Typ, seine eigene Referenzanzahl und andere Informationen enthalten. Das heißt, jeder Eintrag ist ein vollständiges Python-Objekt. In dem speziellen Fall, dass alle Variablen denselben Typ haben, sind viele dieser Informationen überflüssig, so dass es viel effizienter sein kann, die Daten in einem Array vom festen Typ zu speichern. Der Unterschied zwischen einer Liste vom dynamischen Typ und einem Array vom festen Typ (im Stil von NumPy) ist in Abbildung 4-2 dargestellt.

pdsh2 0402
Abbildung 4-2. Der Unterschied zwischen C- und Python-Listen

Auf der Implementierungsebene enthält das Array im Wesentlichen einen einzelnen Zeiger auf einen zusammenhängenden Datenblock. Die Python-Liste hingegen enthält einen Zeiger auf einen Block von Zeigern, von denen jeder wiederum auf ein vollständiges Python-Objekt verweist, wie die Python-Ganzzahl, die wir vorhin gesehen haben. Auch hier liegt der Vorteil der Liste in der Flexibilität: Da jedes Listenelement eine vollständige Struktur ist, die sowohl Daten als auch Typinformationen enthält, kann die Liste mit Daten jedes gewünschten Typs gefüllt werden. Bei Arrays vom Typ NumPy fehlt diese Flexibilität, aber sie sind viel effizienter für die Speicherung und Bearbeitung von Daten.

Arrays festen Typs in Python

Python bietet verschiedene Möglichkeiten, Daten in effizienten Datenpuffern festen Typs zu speichern. Mit dem eingebauten Modul array (verfügbar seit Python 3.3) kannst du dichte Arrays eines einheitlichen Typs erstellen:

In [6]: import array
        L = list(range(10))
        A = array.array('i', L)
        A
Out[6]: array('i', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

Hier ist 'i' ein Typcode, der angibt, dass der Inhalt eine ganze Zahl ist.

Viel nützlicher ist jedoch das ndarray Objekt des NumPy Pakets. Während das array Objekt von Python eine effiziente Speicherung von Array-basierten Daten ermöglicht, bietet NumPy zusätzlich effiziente Operationen mit diesen Daten. Wir werden diese Operationen in späteren Kapiteln erkunden; als Nächstes zeige ich dir ein paar verschiedene Möglichkeiten, ein NumPy-Array zu erstellen.

Arrays aus Python-Listen erstellen

Wir beginnen mit dem Standard-Import von NumPy unter dem Aliasnp:

In [7]: import numpy as np

Jetzt können wir np.array verwenden, um Arrays aus Python-Listen zu erstellen:

In [8]: # Integer array
        np.array([1, 4, 2, 5, 3])
Out[8]: array([1, 4, 2, 5, 3])

Erinnere dich daran, dass NumPy-Arrays, anders als Python-Listen, nur Daten desselben Typs enthalten können. Wenn die Typen nicht übereinstimmen, werden sie von NumPy gemäß den Regeln für die Typumwandlung umgewandelt; hier werden Ganzzahlen in Fließkommazahlen umgewandelt:

In [9]: np.array([3.14, 4, 2, 3])
Out[9]: array([3.14, 4.  , 2.  , 3.  ])

Wenn wir den Datentyp des resultierenden Arrays explizit festlegen wollen, können wir das Schlüsselwort dtype verwenden:

In [10]: np.array([1, 2, 3, 4], dtype=np.float32)
Out[10]: array([1., 2., 3., 4.], dtype=float32)

Im Gegensatz zu Python-Listen, die immer eindimensionale Sequenzen sind, können NumPy-Arrays mehrdimensional sein. Hier ist eine Möglichkeit, ein mehrdimensionales Array mit einer Liste von Listen zu initialisieren:

In [11]: # Nested lists result in multidimensional arrays
         np.array([range(i, i + 3) for i in [2, 4, 6]])
Out[11]: array([[2, 3, 4],
                [4, 5, 6],
                [6, 7, 8]])

Die inneren Listen werden als Zeilen des resultierenden zweidimensionalen Arrays behandelt.

Arrays von Grund auf neu erstellen

Vor allem bei größeren Arrays ist es effizienter, Arrays mit den in NumPy integrierten Routinen von Grund auf neu zu erstellen. Hier sind einige Beispiele:

In [12]: # Create a length-10 integer array filled with 0s
         np.zeros(10, dtype=int)
Out[12]: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
In [13]: # Create a 3x5 floating-point array filled with 1s
         np.ones((3, 5), dtype=float)
Out[13]: array([[1., 1., 1., 1., 1.],
                [1., 1., 1., 1., 1.],
                [1., 1., 1., 1., 1.]])
In [14]: # Create a 3x5 array filled with 3.14
         np.full((3, 5), 3.14)
Out[14]: array([[3.14, 3.14, 3.14, 3.14, 3.14],
                [3.14, 3.14, 3.14, 3.14, 3.14],
                [3.14, 3.14, 3.14, 3.14, 3.14]])
In [15]: # Create an array filled with a linear sequence
         # starting at 0, ending at 20, stepping by 2
         # (this is similar to the built-in range function)
         np.arange(0, 20, 2)
Out[15]: array([ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18])
In [16]: # Create an array of five values evenly spaced between 0 and 1
         np.linspace(0, 1, 5)
Out[16]: array([0.  , 0.25, 0.5 , 0.75, 1.  ])
In [17]: # Create a 3x3 array of uniformly distributed
         # pseudorandom values between 0 and 1
         np.random.random((3, 3))
Out[17]: array([[0.09610171, 0.88193001, 0.70548015],
                [0.35885395, 0.91670468, 0.8721031 ],
                [0.73237865, 0.09708562, 0.52506779]])
In [18]: # Create a 3x3 array of normally distributed pseudorandom
         # values with mean 0 and standard deviation 1
         np.random.normal(0, 1, (3, 3))
Out[18]: array([[-0.46652655, -0.59158776, -1.05392451],
                [-1.72634268,  0.03194069, -0.51048869],
                [ 1.41240208,  1.77734462, -0.43820037]])
In [19]: # Create a 3x3 array of pseudorandom integers in the interval [0, 10)
         np.random.randint(0, 10, (3, 3))
Out[19]: array([[4, 3, 8],
                [6, 5, 0],
                [1, 1, 4]])
In [20]: # Create a 3x3 identity matrix
         np.eye(3)
Out[20]: array([[1., 0., 0.],
                [0., 1., 0.],
                [0., 0., 1.]])
In [21]: # Create an uninitialized array of three integers; the values will be
         # whatever happens to already exist at that memory location
         np.empty(3)
Out[21]: array([1., 1., 1.])

NumPy Standard Datentypen

NumPy-Arrays enthalten Werte eines einzigen Typs, daher ist es wichtig, diese Typen und ihre Grenzen genau zu kennen. Da NumPy in C entwickelt wurde, sind die Typen den Benutzern von C, Fortran und anderen verwandten Sprachen vertraut.

Die Standard-NumPy-Datentypen sind in Tabelle 4-1 aufgeführt. Beachte, dass sie beim Aufbau eines Arrays mit einer Zeichenkette angegeben werden können:

np.zeros(10, dtype='int16')

Oder du verwendest das zugehörige NumPy-Objekt:

np.zeros(10, dtype=np.int16)

Es ist möglich, weitere Typen zu spezifizieren, z. B. Big- oder Little-Endian-Zahlen; weitere Informationen findest du in derNumPy-Dokumentation. NumPy unterstützt auch zusammengesetzte Datentypen, die inKapitel 12 behandelt werden.

Tabelle 4-1. Standard NumPy-Datentypen
Datentyp Beschreibung

bool_

Boolescher Wert (Wahr oder Falsch), gespeichert als Byte

int_

Standard-Integer-Typ (wie C long; normalerweise entweder int64oder int32)

intc

Identisch mit C int (normalerweise int32 oder int64)

intp

Ganzzahl, die für die Indexierung verwendet wird (wie in C ssize_t; normalerweise entwederint32 oder int64)

int8

Byte (-128 bis 127)

int16

Ganzzahl (-32768 bis 32767)

int32

Integer (-2147483648 bis 2147483647)

int64

Integer (-9223372036854775808 bis 9223372036854775807)

uint8

Ganzzahl ohne Vorzeichen (0 bis 255)

uint16

Ganzzahl ohne Vorzeichen (0 bis 65535)

uint32

Ganzzahl ohne Vorzeichen (0 bis 4294967295)

uint64

Ganzzahl ohne Vorzeichen (0 bis 18446744073709551615)

float_

Kurzform für float64

float16

Halbgenaue Fließkommazahl: Vorzeichenbit, 5 Bit Exponent, 10 Bit Mantisse

float32

Gleitkommazahl mit einfacher Genauigkeit: Vorzeichenbit, 8 Bit Exponent, 23 Bit Mantisse

float64

Doppelt genaue Fließkommazahl: Vorzeichenbit, 11 Bit Exponent, 52 Bit Mantisse

complex_

Kurzform für complex128

complex64

Komplexe Zahl, dargestellt durch zwei 32-Bit-Gleitkommazahlen

complex128

Komplexe Zahl, dargestellt durch zwei 64-Bit-Fließkommazahlen

Get Python Data Science Handbook, 2. Auflage 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.