Un problème de mémoire peut survenir lorsqu'un grand nombre d'objets sont actifs dans la RAM pendant l'exécution d'un programme, en particulier s'il existe des restrictions sur la quantité totale de mémoire disponible.
Vous trouverez ci-dessous un aperçu de certaines méthodes de réduction de la taille des objets, ce qui peut réduire considérablement la quantité de RAM nécessaire pour les programmes en Python pur.
Remarque: il s'agit de la version anglaise de mon message d' origine (en russe).
Pour simplifier, nous considérerons les structures en Python pour représenter des points avec les coordonnées x
, y
, z
avec accès aux valeurs de coordonnées par leur nom.
Dict
Dans les petits programmes, en particulier dans 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 en Python 3.6 avec un ensemble ordonné de clés, dict
est devenu encore plus attrayant. Cependant, regardons la taille de son empreinte 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:
Instance de classe
Pour ceux qui aiment tout habiller en classes, il est préférable de définir les structures 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:
Ici, __weakref__
est une référence à la liste des soi-disant références faibles à cet objet, le champ __dict__
est une référence au dictionnaire d'instance de classe, qui contient les valeurs des attributs d'instance (notez que la plate-forme de références 64 bits occupe 8 octets). À partir de Python 3.3, l'espace partagé est utilisé pour stocker des clés dans le dictionnaire pour toutes les instances de la classe. Cela réduit la taille de la trace d'instance dans la RAM:
>>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112
Par conséquent, un grand nombre d'instances de classe ont une plus petite empreinte en mémoire qu'un dictionnaire standard ( dict
):
Il est facile de voir que la taille de l'instance en RAM est toujours importante en raison de la taille du dictionnaire de l'instance.
Instance de classe avec __slots__
Une réduction significative de la taille d'une instance de classe en RAM est obtenue en éliminant __dict__
et __weakref__
. Ceci est possible à l'aide d'une "astuce" 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 taille de l'objet dans la RAM est devenue beaucoup plus petite:
L'utilisation de __slots__
dans la définition de classe réduit considérablement l'encombrement d'un grand nombre d'instances en mémoire:
Actuellement, il s'agit de la principale méthode de réduction substantielle de l'empreinte mémoire d'une instance d'une classe en RAM.
Cette réduction est obtenue par le fait que dans la mémoire après le titre de l'objet, les références d'objet sont stockées - les valeurs d'attribut, et leur accès est effectué à l'aide de descripteurs spéciaux qui se trouvent dans le dictionnaire de classe:
>>> pprint(Point.__dict__) mappingproxy( .................................... 'x': <member 'x' of 'Point' objects>, 'y': <member 'y' of 'Point' objects>, 'z': <member 'z' of 'Point' objects>})
Pour automatiser le processus de création d'une classe avec __slots__
, il existe une bibliothèque [namedlist] ( https://pypi.org/project/namedlist ). La fonction namedlist.namedlist
crée une classe avec __slots__
:
>>> Point = namedlist('Point', ('x', 'y', 'z'))
Un autre package [attrs] ( https://pypi.org/project/attrs ) vous permet d'automatiser le processus de création de classes avec et sans __slots__
.
Tuple
Python possède également un tuple
type intégré pour représenter les structures de données immuables. Un tuple est une structure ou un enregistrement fixe, mais sans nom de champ. Pour l'accès aux champs, l'index des champs est utilisé. Les champs de tuple sont une fois pour toutes associés aux objets valeur au moment de la création de l'instance de tuple:
>>> ob = (1,2,3) >>> x = ob[0] >>> ob[1] = y # ERROR
Les instances de tuple sont assez compactes:
>>> print(sys.getsizeof(ob)) 72
Ils occupent 8 octets en mémoire de plus que les instances de classes avec __slots__
, car la trace de tuple en mémoire contient également un certain nombre de champs:
Namedtuple
Étant donné que le tuple est très largement utilisé, un jour, il a été demandé que vous puissiez toujours accéder aux champs et par nom également. La réponse à cette demande a été le module collections.namedtuple
.
La fonction namedtuple
est 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, dans laquelle des descripteurs sont définis pour accéder aux champs par leur nom. Pour notre exemple, cela ressemblerait à ceci:
class Point(tuple): # @property def _get_x(self): return self[0] @property def _get_y(self): return self[1] @property def _get_z(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 empreinte mémoire identique à celle d'un tuple. Un grand nombre d'instances laisse une empreinte mémoire légèrement plus grande:
Recordclass: mutable nommé tuple sans GC cyclique
Étant donné que le tuple
et, par conséquent, les classes namedtuple
génèrent des objets immuables dans le sens où l'attribut ob.x
ne peut plus être associé à un autre objet de valeur, une demande pour une variante mutable namedtuple est apparue. Puisqu'il n'y a pas de type intégré en Python qui soit identique au tuple qui prend en charge les affectations, de nombreuses options ont été créées. Nous nous concentrerons sur [recordclass] ( https://pypi.org/project/recordclass ), qui a reçu une note de [stackoverflow] ( https://stackoverflow.com/questions/29290359/existence-of-mutable-named- tuple-in -python / 29419745). De plus, il peut être utilisé pour réduire la taille des objets en RAM par rapport à la taille des objets de type tuple
.
Le package recordclass introduit le type recordclass.mutabletuple
, qui est presque identique au tuple, mais prend également en charge les affectations. Sur sa base, des sous-classes sont créées qui sont presque complètement 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
, vous permet d'automatiser la création de ces classes:
>>> Point = recordclass('Point', ('x', 'y', 'z')) >>> ob = Point(1, 2, 3)
Les instances de classe ont la même structure que tuple
, mais uniquement sans PyGC_Head
:
Par défaut, la fonction recordclass
crée une classe qui ne participe pas au mécanisme de récupération de place cyclique. En règle générale, namedtuple
et recordclass
sont utilisés pour générer des classes représentant des enregistrements ou des structures de données simples (non récursives). Les utiliser correctement en Python ne génère pas de références circulaires. Pour cette raison, dans le sillage des instances de classes générées par recordclass
, par default, the
fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the
PyGC_Head fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the
structure, corresponding to the created class, in the
PyTypeObject structure, corresponding to the created class, in the
field, by default, the flag
drapeaux field, by default, the flag
Py_TPFLAGS_HAVE_GC` n'est pas défini).
La taille de l'empreinte mémoire d'un grand nombre d'instances est inférieure à celle des instances de la classe avec __slots__
:
Dataobject
Une autre solution proposée dans la bibliothèque recordclass est basée sur l'idée: utiliser la même structure de stockage en mémoire que dans les instances de classe avec __slots__
, mais ne pas participer au mécanisme cyclique de récupération de place. Ces classes sont générées à l'aide de la fonction recordclass.make_dataclass
:
>>> Point = make_dataclass('Point', ('x', 'y', 'z'))
La classe ainsi créée, par défaut, crée des instances mutables.
Une autre façon - utilisez la déclaration de classe avec l'héritage de recordclass.dataobject
:
class Point(dataobject): x:int y:int z:int
Les classes créées de cette manière créeront des instances qui ne participent pas au mécanisme de collecte de déchets cyclique. La structure de l'instance en mémoire est la même que dans le cas avec __slots__
, mais sans PyGC_Head
:
>>> 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 se trouvent 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 l'empreinte mémoire d'un grand nombre d'instances est le minimum possible pour CPython:
Cython
Il existe une approche basée sur l'utilisation de [Cython] ( https://cython.org ). Son avantage est que les champs peuvent prendre les valeurs des types atomiques du langage C. Des 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
La trace d'instance en mémoire a la structure suivante:
La taille de l'empreinte d'un grand nombre de copies est moindre:
Cependant, il faut se rappeler que lors de l'accès à partir du code Python, une conversion de int
en objet Python et vice versa sera effectuée à chaque fois.
Numpy
L'utilisation de tableaux multidimensionnels ou de tableaux d'enregistrements pour une grande quantité 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 de N
éléments, initialisé avec des zéros, est créé à l'aide de la fonction:
>>> points = numpy.zeros(N, dtype=Point)
La taille du tableau en mémoire est le minimum possible:
L'accès normal aux éléments et aux lignes du tableau nécessitera la conversion d'un objet Python en une valeur C int
et vice versa. L'extraction d'une seule ligne entraîne la création d'un tableau contenant un seul élément. Sa trace ne sera plus 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
Sur un exemple clair et simple, il a été possible de vérifier que la communauté de développeurs et d'utilisateurs du langage de programmation Python (CPython) a de réelles possibilités pour une réduction significative de la quantité de mémoire utilisée par les objets.