Ein Speicherproblem kann auftreten, wenn Sie während der Programmausführung eine große Anzahl von Objekten benötigen, insbesondere wenn die Gesamtgröße des verfügbaren Arbeitsspeichers eingeschränkt ist.
Im Folgenden finden Sie eine Übersicht über einige Methoden zum Reduzieren der Größe von Objekten, wodurch der für Programme in reinem Python erforderliche RAM-Speicher erheblich reduziert werden kann.
Der Einfachheit halber werden wir Strukturen in Python betrachten, um Punkte mit x
, y
und z
Koordinaten mit Zugriff auf Koordinatenwerte nach Namen darzustellen.
Dikt
In kleinen Programmen, insbesondere in Skripten, ist es recht einfach und bequem, das integrierte dict
zur Darstellung von Strukturinformationen zu verwenden:
>>> ob = {'x':1, 'y':2, 'z':3} >>> x = ob['x'] >>> ob['y'] = y
Mit dem Aufkommen einer „kompakteren“ Implementierung in Python 3.6 mit einem geordneten Schlüsselsatz ist das dict
noch attraktiver geworden. Sehen Sie sich jedoch die Größe der Ablaufverfolgung im RAM an:
>>> print(sys.getsizeof(ob)) 240
Es nimmt viel Speicherplatz in Anspruch, insbesondere wenn Sie plötzlich eine große Anzahl von Instanzen erstellen müssen:
Klasseninstanz
Für diejenigen, die alles in Klassen kleiden möchten, ist es vorzuziehen, es als Klasse mit Zugriff über den Attributnamen zu definieren:
class Point: # def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> x = ob.x >>> ob.y = y
Die Struktur der Klasseninstanz ist interessant:
Hier ist __weakref__
ein Link zu einer Liste sogenannter schwacher Verweise auf dieses Objekt. __dict__
Feld __dict__
ist ein Link zu einem Wörterbuch einer Instanz einer Klasse, die Werte von __dict__
enthält (beachten Sie, dass Links auf einer 64-Bit-Plattform 8 Byte belegen). Ab Python 3.3 wird für alle Instanzen der Klasse ein gemeinsam genutzter Wörterbuchschlüssel verwendet. Dies reduziert die Größe der Instanzablaufverfolgung im Speicher:
>>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112
Infolgedessen hinterlässt eine große Anzahl von Klasseninstanzen einen geringeren Speicherbedarf als ein reguläres Wörterbuch ( dict
):
Es ist leicht zu erkennen, dass die Ablaufverfolgung der Instanz im Speicher aufgrund der Größe des Instanzwörterbuchs immer noch groß ist.
Instanz der Klasse mit __slots__
Eine signifikante Reduzierung der Ablaufverfolgung einer Instanz im Speicher wird durch Eliminieren von __dict__
und __weakref__
. Dies ist mit dem "Trick" mit __slots__
:
class Point: __slots__ = 'x', 'y', 'z' def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 64
Die Spur im Speicher ist viel kompakter geworden:
Die Verwendung von __slots__
in der Klassendefinition führt dazu, dass die Ablaufverfolgung einer großen Anzahl von Instanzen im Speicher erheblich reduziert wird:
Derzeit ist dies die Hauptmethode, um die Ablaufverfolgung einer Klasseninstanz im Programmspeicher erheblich zu reduzieren.
Diese Reduzierung wird durch die Tatsache erreicht, dass im Speicher nach dem Speichern des Titels des Objekts Verweise auf Objekte gespeichert werden und der Zugriff auf diese mithilfe spezieller Deskriptoren erfolgt, die sich im Klassenwörterbuch befinden:
>>> pprint(Point.__dict__) mappingproxy( .................................... 'x': <member 'x' of 'Point' objects>, 'y': <member 'y' of 'Point' objects>, 'z': <member 'z' of 'Point' objects>})
Es gibt eine __slots__
Bibliothek, um das Erstellen einer Klasse mit __slots__ zu automatisieren. Die Funktion namedlist.namedlist
erstellt eine Klassenstruktur, die mit der Klasse mit __slots__
identisch ist:
>>> Point = namedlist('Point', ('x', 'y', 'z'))
Mit einem weiteren attrs- Paket können Sie das Erstellen von Klassen mit und ohne __slots__
.
Tupel
Python verfügt außerdem über einen integrierten tuple
zur Darstellung von Datasets. Tupel ist eine feste Struktur oder ein fester Datensatz, jedoch ohne Feldnamen. Für den Zugriff auf das Feld wird der Feldindex verwendet. Die Tupelfelder sind zum Zeitpunkt der Instanziierung des Tupels ein für alle Mal Wertobjekten zugeordnet:
>>> ob = (1,2,3) >>> x = ob[0] >>> ob[1] = y #
Tupelinstanzen sind recht kompakt:
>>> print(sys.getsizeof(ob)) 72
Sie belegen 8 Byte mehr im Speicher als Klasseninstanzen mit __slots__
, da der Tupel-Trace im Speicher auch die Anzahl der Felder enthält:
Namedtuple
Da Tupel sehr häufig verwendet wird, gab es eines Tages die Anfrage, weiterhin auch namentlich auf die Felder zugreifen zu können. Die Antwort auf diese Anfrage war das Modul collections.namedtuple
.
Die Funktion namedtuple
den Prozess der Generierung dieser Klassen automatisieren:
>>> Point = namedtuple('Point', ('x', 'y', 'z'))
Es wird eine Unterklasse von Tupeln erstellt, die Handles für den Zugriff auf Felder nach Namen definiert. In unserem Beispiel sieht es ungefähr so aus:
class Point(tuple): # @property def _get_x(self): return self[0] @property def _get_y(self): return self[1] @property def _get_y(self): return self[2] # def __new__(cls, x, y, z): return tuple.__new__(cls, (x, y, z))
Alle Instanzen solcher Klassen haben eine Ablaufverfolgung im Speicher, die mit Tupel identisch ist. Eine große Anzahl von Instanzen hinterlässt einen etwas größeren Speicherbedarf:
Recordclass: mutiertes Namedtupel ohne GC
Da namedtuple
und entsprechend benannte namedtuple
nicht veränderbare Objekte in dem Sinne erzeugen, dass ein ob.x
Wertobjekt keinem anderen Wertobjekt mehr zugeordnet werden ob.x
, ist eine Anforderung für eine mutierte benannte Tupelvariante aufgetreten. Da Python keinen integrierten Typ hat, der mit einem Tupel identisch ist, das Zuweisungen unterstützt, wurden viele Variationen erstellt. Wir werden uns auf die Rekordklasse konzentrieren , die eine Stackoverflow- Bewertung erhalten hat. Darüber hinaus kann damit die Größe der Ablaufverfolgung eines Objekts im Speicher im Vergleich zur Größe einer Ablaufverfolgung von Objekten des tuple
verringert werden.
Im Paket recordclass wird der Typ recordclass.mutabletuple recordclass.mutabletuple
, der fast identisch mit tuple ist, aber auch Zuweisungen unterstützt. Auf dieser Basis werden Unterklassen erstellt, die fast identisch mit Namedtuples sind, aber auch die Zuweisung neuer Werte zu Feldern unterstützen (ohne neue Instanzen zu erstellen). Die Funktion namedtuple
automatisiert wie die Funktion namedtuple
die Erstellung solcher Klassen:
>>> Point = recordclass('Point', ('x', 'y', 'z')) >>> ob = Point(1, 2, 3)
Instanzen der Klasse haben dieselbe Struktur wie tuple
, jedoch nur ohne PyGC_Head
:
Standardmäßig recordclass
Funktion recordclass
eine Klasse, die nicht am Mechanismus der zirkulären recordclass
beteiligt ist. In der Regel werden namedtuple
und recordclass
verwendet, um Klassen zu recordclass
, die Datensätze oder einfache (nicht rekursive) Datenstrukturen darstellen. Ihre korrekte Verwendung in Python generiert keine Zirkelverweise. Aus diesem Grund schließt die Ablaufverfolgung von Instanzen von Klassen, die von der Standardaufzeichnungsklasse PyGC_Head
Fragment aus, das für Klassen erforderlich ist, die den zyklischen Garbage Collection-Mechanismus unterstützen (genauer gesagt: PyTypeObject
Flag PyTypeObject
wird im Feld flags
in der PyTypeObject
Struktur, die der erstellten Klasse entspricht, nicht gesetzt).
Die Ablaufverfolgungsgröße einer großen Anzahl von Instanzen ist kleiner als die von Instanzen einer Klasse mit __slots__
:
Datenobjekt
Eine andere in der recordclass
vorgeschlagene Lösung basiert auf der Idee: die Speicherstruktur im Speicher wie in Instanzen von Klassen mit __slots__
, aber nicht am Mechanismus der zyklischen Speicherbereinigung __slots__
. Die Klasse wird mit der Funktion recordclass.make_dataclass
:
>>> Point = make_dataclass('Point', ('x', 'y', 'z'))
Die auf diese Weise erstellte Standardklasse erstellt mutierte Instanzen.
Eine andere Möglichkeit besteht darin, die Klassendeklaration zu verwenden, indem Sie von recordclass.dataobject
erben:
class Point(dataobject): x:int y:int z:int
Auf diese Weise erstellte Klassen generieren Instanzen, die nicht am Mechanismus der zirkulären Speicherbereinigung teilnehmen. Die Struktur der Instanz im Speicher ist dieselbe wie bei __slots__
, jedoch ohne den PyGC_Head
Header:
>>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 40
Um auf die Felder zuzugreifen, werden spezielle Deskriptoren verwendet, um auf das Feld um seinen Versatz relativ zum Anfang des Objekts zuzugreifen, die im Klassenwörterbuch platziert werden:
mappingproxy({'__new__': <staticmethod at 0x7f203c4e6be0>, ....................................... 'x': <recordclass.dataobject.dataslotgetset at 0x7f203c55c690>, 'y': <recordclass.dataobject.dataslotgetset at 0x7f203c55c670>, 'z': <recordclass.dataobject.dataslotgetset at 0x7f203c55c410>})
Die Trace-Größe einer großen Anzahl von Instanzen ist für CPython so klein wie möglich:
Cython
Es gibt einen Ansatz, der auf der Verwendung von Cython basiert. Der Vorteil ist, dass Felder Werte von C-Sprachtypen annehmen können. Deskriptoren für den Zugriff auf Felder aus reinem Python werden automatisch erstellt. Zum Beispiel:
cdef class Python: cdef public int x, y, z def __init__(self, x, y, z): self.x = x self.y = y self.z = z
In diesem Fall haben die Instanzen eine noch kleinere Speichergröße:
>>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 32
Ein Instanz-Trace im Speicher hat die folgende Struktur:
Die Trace-Größe einer großen Anzahl von Kopien ist kleiner:
Es ist jedoch zu beachten, dass beim Zugriff von Python-Code jedes Mal die Konvertierung von int
in Python-Objekt und umgekehrt durchgeführt wird.
Numpy
Die Verwendung von mehrdimensionalen oder Datensatz-Arrays für große Datenmengen führt zu einem Speichergewinn. Für eine effiziente Verarbeitung in reinem Python sollten Sie jedoch Verarbeitungsmethoden verwenden, die sich auf die Verwendung von Funktionen aus dem numpy
Paket konzentrieren.
>>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)])
Ein Array und N
mit Nullen initialisierte Elemente werden mit der folgenden Funktion erstellt:
>>> points = numpy.zeros(N, dtype=Point)
Die Größe des Arrays ist so klein wie möglich:
Der regelmäßige Zugriff auf Array-Elemente und Zeichenfolgen erfordert eine Python-Objektkonvertierung
in den Wert von C int
und umgekehrt. Das Extrahieren einer einzelnen Zeile führt zu einem Array, das ein einzelnes Element enthält. Sein Track wird nicht so kompakt sein:
>>> sys.getsizeof(points[0]) 68
Wie oben erwähnt, ist es daher im Python-Code erforderlich, Arrays mithilfe von Funktionen aus dem numpy
Paket zu verarbeiten.
Fazit
Anhand eines klaren und einfachen Beispiels konnte überprüft werden, ob die Community von Entwicklern und Benutzern der Python-Programmiersprache (CPython) echte Möglichkeiten hat, den von Objekten verwendeten Speicher erheblich zu reduzieren.