Python verbraucht viel Speicher oder wie kann die Größe von Objekten reduziert werden?

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:


Anzahl der ExemplareTrace-Größe
1.000.000240 Mb
10.000.0002,40 GB
100.000.00024 GB

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:


Das FeldGröße (Bytes)
PyGC_Head24
PyObject_HEAD16
__weakref__8
__dict__8
GESAMT:56

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 ):


Anzahl der ExemplareTrace-Größe
1.000.000168 Mb
10.000.0001,68 GB
100.000.00016,8 GB

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:


Das FeldGröße (Bytes)
PyGC_Head24
PyObject_HEAD16
x8
y8
z8
GESAMT:64

Die Verwendung von __slots__ in der Klassendefinition führt dazu, dass die Ablaufverfolgung einer großen Anzahl von Instanzen im Speicher erheblich reduziert wird:


Anzahl der ExemplareTrace-Größe
1.000.00064 Mb
10.000.000640 Mb
100.000.0006,4 GB

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:


Das FeldGröße (Bytes)
PyGC_Head24
PyObject_HEAD16
ob_size8
[0]8
[1]8
[2]8
GESAMT:72

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:


Anzahl der ExemplareTrace-Größe
1.000.00072 Mb
10.000.000720 Mb

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 :


Das FeldGröße (Bytes)
PyObject_HEAD16
ob_size8
x8
y8
y8
GESAMT:48

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__ :


Anzahl der ExemplareTrace-Größe
1.000.00048 Mb
10.000.000480 Mb
100.000.0004,8 GB

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:


Das FeldGröße (Bytes)
PyObject_HEAD16
x8
y8
y8
GESAMT:40

 >>> 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:


Anzahl der ExemplareTrace-Größe
1.000.00040 Mb
10.000.000400 Mb
100.000.0004,0 GB

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:


Das FeldGröße (Bytes)
PyObject_HEAD16
x4
y4
y4
ist leer4
GESAMT:32

Die Trace-Größe einer großen Anzahl von Kopien ist kleiner:


Anzahl der ExemplareTrace-Größe
1.000.00032 Mb
10.000.000320 Mb
100.000.0003,2 GB

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:


Anzahl der ExemplareTrace-Größe
1.000.00012 Mb
10.000.000120 Mb
100.000.0001,20 GB

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.

Source: https://habr.com/ru/post/de455722/


All Articles