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:
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:
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
):
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:
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:
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:
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:
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
:
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__
:
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
:
>>> 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:
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:
La taille de trace d'un grand nombre de copies est plus petite:
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:
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.