Python consomme beaucoup de mémoire ou comment réduire la taille des objets?

Un problème de mémoire peut survenir lorsque vous devez avoir un grand nombre d'objets pendant l'exécution du programme, surtout s'il existe des restrictions sur la taille totale de la RAM disponible.


Ce qui suit est un aperçu de certaines méthodes pour réduire la taille des objets, ce qui peut réduire considérablement la quantité de RAM requise pour les programmes en Python pur.


Pour plus de simplicité, nous considérerons les structures en Python pour représenter des points avec des x , y , z avec accès aux valeurs de coordonnées par leur nom.


Dict


Dans les petits programmes, en particulier les scripts, il est assez simple et pratique d'utiliser le dict intégré pour représenter les informations structurelles:


 >>> ob = {'x':1, 'y':2, 'z':3} >>> x = ob['x'] >>> ob['y'] = y 

Avec l'avènement d'une implémentation plus «compacte» dans Python 3.6 avec un ensemble ordonné de clés, dict devenu encore plus attrayant. Cependant, regardez la taille de sa trace en RAM:


 >>> print(sys.getsizeof(ob)) 240 

Cela prend beaucoup de mémoire, surtout si vous devez soudainement créer un grand nombre d'instances:


Nombre d'exemplairesTaille de trace
1 000 000240 Mo
10 000 0002,40 Go
100 000 00024 Go

Instance de classe


Pour ceux qui aiment tout habiller en classe, il est préférable de le définir comme une classe avec accès par nom d'attribut:


 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 

La structure de l'instance de classe est intéressante:


Le terrainTaille (octets)
PyGC_Head24
PyObject_HEAD16
__weakref__8
__dict__8
TOTAL:56

Ici, __weakref__ est un lien vers une liste de soi-disant références faibles à cet objet, le champ __dict__ est un lien vers le dictionnaire d'instances de la classe qui contient les valeurs des attributs de l'instance (notez que les liens sur une plate-forme 64 bits occupent 8 octets). À partir de Python 3.3, un espace clé de dictionnaire partagé est utilisé pour toutes les instances de la classe. Cela réduit la taille de la trace d'instance en mémoire:


 >>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112 

Par conséquent, un grand nombre d'instances de classe laissent une plus petite empreinte en mémoire qu'un dictionnaire standard ( dict ):


Nombre d'exemplairesTaille de trace
1 000 000168 Mo
10 000 0001,68 Go
100 000 00016,8 Go

Il est facile de voir que la trace de l'instance en mémoire est toujours importante en raison de la taille du dictionnaire d'instances.


Instance de classe avec __slots__


Une réduction significative de la trace d'une instance en mémoire est obtenue en éliminant __dict__ et __weakref__ . C'est possible avec le "truc" avec __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 

La trace en mémoire est devenue beaucoup plus compacte:


Le terrainTaille (octets)
PyGC_Head24
PyObject_HEAD16
x8
y8
z8
TOTAL:64

L'utilisation de __slots__ dans la définition de classe conduit au fait que la trace d'un grand nombre d'instances en mémoire est considérablement réduite:


Nombre d'exemplairesTaille de trace
1 000 00064 Mo
10 000 000640 Mo
100 000 0006,4 Go

Actuellement, il s'agit de la principale méthode pour réduire considérablement la trace d'une instance de classe dans la mémoire du programme.


Cette réduction est obtenue par le fait que, après l'en-tête d'objet, les références aux objets sont stockées dans la mémoire et que l'accès à celles-ci est effectué à l'aide de descripteurs spéciaux qui se trouvent dans le dictionnaire de classes:


 >>> pprint(Point.__dict__) mappingproxy( .................................... 'x': <member 'x' of 'Point' objects>, 'y': <member 'y' of 'Point' objects>, 'z': <member 'z' of 'Point' objects>}) 

Il existe une bibliothèque de __slots__ pour automatiser le processus de création d'une classe avec __slots__ . La fonction namedlist.namedlist crée une structure de classe identique à la classe avec __slots__ :


 >>> Point = namedlist('Point', ('x', 'y', 'z')) 

Un autre package attrs vous permet d'automatiser le processus de création de classes avec et sans __slots__ .


Tuple


Python a également un type de tuple intégré pour représenter les ensembles de données. Le tuple est une structure ou un enregistrement fixe, mais sans nom de champ. Pour accéder au champ, l'index du champ est utilisé. Les champs de tuple sont une fois pour toutes associés aux objets valeur au moment où le tuple est instancié:


 >>> ob = (1,2,3) >>> x = ob[0] >>> ob[1] = y #  

Les instances de tuple sont assez compactes:


 >>> print(sys.getsizeof(ob)) 72 

Ils occupent 8 octets de plus en mémoire que les instances de classe avec __slots__ , car la trace de tuple en mémoire contient également le nombre de champs:


Le terrainTaille (octets)
PyGC_Head24
PyObject_HEAD16
ob_size8
[0]8
[1]8
[2]8
TOTAL:72

Namedtuple


Le tuple étant très largement utilisé, un jour, il a été demandé de pouvoir également accéder aux champs par leur nom. La réponse à cette demande a été le module collections.namedtuple .


La fonction namedtuple conçue pour automatiser le processus de génération de ces classes:


 >>> Point = namedtuple('Point', ('x', 'y', 'z')) 

Il crée une sous-classe de tuple, qui définit les poignées pour accéder aux champs par leur nom. Pour notre exemple, cela ressemblera à ceci:


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

Toutes les instances de ces classes ont une trace en mémoire identique à tuple. Un grand nombre d'instances laisse une empreinte mémoire légèrement plus grande:


Nombre d'exemplairesTaille de trace
1 000 00072 Mo
10 000 000720 Mo

Recordclass: muté nommé tuple sans GC


Étant namedtuple classes tuple et, par conséquent, namedtuple génèrent des objets non modifiables dans le sens où un objet valeur ob.x ne ob.x plus être associé à un autre objet valeur, une demande de variante de mutable namedtuple mutée est apparue. Étant donné que Python n'a pas de type intégré identique à un tuple qui prend en charge les affectations, de nombreuses variantes ont été créées. Nous nous concentrerons sur la classe d'enregistrement , qui a reçu une note de stackoverflow . De plus, avec son aide, il est possible de réduire la taille de la trace d'un objet en mémoire par rapport à la taille de la trace d'objets de type tuple .


Dans le package recordclass , le type recordclass.mutabletuple est recordclass.mutabletuple , qui est presque identique à tuple mais prend également en charge les affectations. Sur sa base, des sous-classes sont créées qui sont presque identiques aux couples nommés, mais prennent également en charge l'affectation de nouvelles valeurs aux champs (sans créer de nouvelles instances). La fonction recordclass , comme la fonction namedtuple , automatise la création de telles classes:


  >>> Point = recordclass('Point', ('x', 'y', 'z')) >>> ob = Point(1, 2, 3) 

Les instances de la classe ont la même structure que tuple , mais uniquement sans PyGC_Head :


Le terrainTaille (octets)
PyObject_HEAD16
ob_size8
x8
y8
y8
TOTAL:48

Par défaut, la fonction recordclass génère une classe qui n'est pas impliquée dans le mécanisme de récupération de place circulaire. En règle générale, namedtuple et recordclass sont utilisés pour générer des classes qui représentent des enregistrements ou des structures de données simples (non récursives). Leur utilisation correcte en Python ne génère pas de références circulaires. Pour cette raison, la trace des instances de classes générées par la PyGC_Head par défaut PyGC_Head fragment PyGC_Head , qui est nécessaire pour les classes qui prennent en charge le mécanisme de récupération de place cyclique (plus précisément: l'indicateur PyTypeObject n'est pas défini dans le champ flags de la structure PyTypeObject correspondant à la classe en cours de création).


La taille de trace d'un grand nombre d'instances est plus petite que celle des instances d'une classe avec __slots__ :


Nombre d'exemplairesTaille de trace
1 000 00048 Mo
10 000 000480 Mo
100 000 0004,8 Go

Dataobject


Une autre solution proposée dans la bibliothèque recordclass repose sur l'idée: utiliser la structure de stockage en mémoire, comme dans les instances de classes avec __slots__ , mais ne pas participer au mécanisme de ramassage cyclique des ordures. La classe est recordclass.make_dataclass à l'aide de la fonction recordclass.make_dataclass :


  >>> Point = make_dataclass('Point', ('x', 'y', 'z')) 

La classe par défaut créée de cette manière crée des instances mutées.


Une autre façon consiste à utiliser la déclaration de classe en héritant de recordclass.dataobject :


 class Point(dataobject): x:int y:int z:int 

Les classes créées de cette manière généreront des instances qui ne participent pas au mécanisme de collecte de déchets circulaire. La structure de l'instance en mémoire est la même qu'avec __slots__ , mais sans l'en PyGC_Head tête PyGC_Head :


Le terrainTaille (octets)
PyObject_HEAD16
x8
y8
y8
TOTAL:40

 >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 40 

Pour accéder aux champs, des descripteurs spéciaux sont également utilisés pour accéder au champ par son décalage par rapport au début de l'objet, qui sont placés dans le dictionnaire de classe:


 mappingproxy({'__new__': <staticmethod at 0x7f203c4e6be0>, ....................................... 'x': <recordclass.dataobject.dataslotgetset at 0x7f203c55c690>, 'y': <recordclass.dataobject.dataslotgetset at 0x7f203c55c670>, 'z': <recordclass.dataobject.dataslotgetset at 0x7f203c55c410>}) 

La taille de trace d'un grand nombre d'instances est la plus petite possible pour CPython:


Nombre d'exemplairesTaille de trace
1 000 00040 Mo
10 000 000400 Mo
100 000 0004,0 Go

Cython


Il existe une approche basée sur l'utilisation de Cython . Son avantage est que les champs peuvent prendre des valeurs de types de langage C. Les descripteurs pour accéder aux champs à partir de Python pur sont créés automatiquement. Par exemple:


 cdef class Python: cdef public int x, y, z def __init__(self, x, y, z): self.x = x self.y = y self.z = z 

Dans ce cas, les instances ont une taille de mémoire encore plus petite:


 >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 32 

Une trace d'instance en mémoire a la structure suivante:


Le terrainTaille (octets)
PyObject_HEAD16
x4
y4
y4
est vide4
TOTAL:32

La taille de trace d'un grand nombre de copies est plus petite:


Nombre d'exemplairesTaille de trace
1 000 00032 Mo
10 000 000320 Mo
100 000 0003,2 Go

Cependant, il faut se rappeler que lors de l'accès à partir du code Python, la conversion de int en objet Python et vice versa sera effectuée à chaque fois.


Numpy


L'utilisation de tableaux multidimensionnels ou d'enregistrement pour de grandes quantités de données donne un gain de mémoire. Cependant, pour un traitement efficace en Python pur, vous devez utiliser des méthodes de traitement qui se concentrent sur l'utilisation des fonctions du package numpy .


 >>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)]) 

Un tableau et N éléments initialisés avec des zéros sont créés à l'aide de la fonction:


  >>> points = numpy.zeros(N, dtype=Point) 

La taille du tableau est la plus petite possible:


Nombre d'exemplairesTaille de trace
1 000 00012 Mo
10 000 000120 Mo
100 000 0001,20 Go

L'accès régulier aux éléments du tableau et aux chaînes nécessitera une conversion d'objet Python
dans la valeur de C int et vice versa. L'extraction d'une seule ligne donne un tableau contenant un seul élément. Sa piste ne sera pas aussi compacte:


  >>> sys.getsizeof(points[0]) 68 

Par conséquent, comme indiqué ci-dessus, dans le code Python, il est nécessaire de traiter les tableaux à l'aide des fonctions du package numpy .


Conclusion


À l'aide d'un exemple clair et simple, il a été possible de vérifier que la communauté des développeurs et des utilisateurs du langage de programmation Python (CPython) a de réelles opportunités de réduire considérablement la quantité de mémoire utilisée par les objets.

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


All Articles