Créer un conteneur personnalisé

PythonPythonBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Dans ce laboratoire, vous allez apprendre à connaître les conteneurs Python et la gestion de la mémoire. Vous explorerez la manière dont Python gère la mémoire pour les structures de données intégrées et découvrirez comment créer une classe de conteneur personnalisée économisant de la mémoire.

Les objectifs de ce laboratoire sont d'examiner le comportement d'allocation de mémoire des listes et des dictionnaires Python, de créer une classe de conteneur personnalisée pour optimiser l'utilisation de la mémoire et de comprendre les avantages du stockage de données orienté colonne.

Comprendre l'allocation de mémoire des listes

En Python, les listes sont une structure de données très utile, notamment lorsque vous avez besoin d'ajouter des éléments. Les listes Python sont conçues pour être efficaces pour les opérations d'ajout. Au lieu d'allouer exactement la quantité de mémoire nécessaire, Python alloue de la mémoire supplémentaire en prévision d'ajouts futurs. Cette stratégie minimise le nombre de réallocations de mémoire nécessaires lorsque la liste grandit.

Comprenons mieux ce concept en utilisant la fonction sys.getsizeof(). Cette fonction renvoie la taille d'un objet en octets, ce qui nous permet de voir combien de mémoire une liste utilise à différents stades.

  1. Tout d'abord, vous devez ouvrir un shell interactif Python dans votre terminal. C'est comme un terrain de jeu où vous pouvez exécuter immédiatement du code Python. Pour l'ouvrir, tapez la commande suivante dans votre terminal et appuyez sur Entrée :
python3
  1. Une fois que vous êtes dans le shell interactif Python, vous devez importer le module sys. Les modules en Python sont comme des boîtes à outils qui contiennent des fonctions utiles. Le module sys possède la fonction getsizeof() dont nous avons besoin. Après avoir importé le module, créez une liste vide nommée items. Voici le code pour cela :
import sys
items = []
  1. Maintenant, vérifions la taille initiale de la liste vide. Nous allons utiliser la fonction sys.getsizeof() avec la liste items comme argument. Tapez le code suivant dans le shell interactif Python et appuyez sur Entrée :
sys.getsizeof(items)

Vous devriez voir une valeur comme 64 octets. Cette valeur représente la surcharge pour une liste vide. La surcharge est la quantité de mémoire de base que Python utilise pour gérer la liste, même lorsqu'elle n'a aucun élément.

  1. Ensuite, nous allons commencer à ajouter des éléments à la liste un par un et observer comment l'allocation de mémoire change. Nous allons utiliser la méthode append() pour ajouter un élément à la liste, puis vérifier à nouveau la taille. Voici le code :
items.append(1)
sys.getsizeof(items)

Vous devriez voir une valeur plus grande, environ 96 octets. Cette augmentation de taille montre que Python a alloué plus de mémoire pour accueillir le nouvel élément.

  1. Continuons d'ajouter plus d'éléments à la liste et vérifions la taille après chaque ajout. Voici le code pour cela :
items.append(2)
sys.getsizeof(items)  ## La taille reste la même

items.append(3)
sys.getsizeof(items)  ## La taille reste inchangée

items.append(4)
sys.getsizeof(items)  ## La taille reste inchangée

items.append(5)
sys.getsizeof(items)  ## La taille saute à une valeur plus grande

Vous remarquerez que la taille n'augmente pas avec chaque opération d'ajout. Au lieu de cela, elle augmente périodiquement. Cela démontre que Python alloue de la mémoire par blocs plutôt que pour chaque nouvel élément individuellement.

Ce comportement est intentionnel. Python alloue plus de mémoire que nécessaire au départ pour éviter des réallocations fréquentes à mesure que la liste grandit. Chaque fois que la liste dépasse sa capacité actuelle, Python alloue un bloc de mémoire plus grand.

N'oubliez pas qu'une liste stocke des références aux objets, pas les objets eux - mêmes. Sur un système 64 bits, chaque référence nécessite généralement 8 octets de mémoire. Il est important de comprendre cela car cela affecte la quantité de mémoire qu'une liste utilise réellement lorsqu'elle contient différents types d'objets.

Allocation de mémoire des dictionnaires

En Python, tout comme les listes, les dictionnaires sont une structure de données fondamentale. Un aspect important à comprendre à leur sujet est la manière dont ils allouent de la mémoire. L'allocation de mémoire fait référence à la façon dont Python réserve de l'espace dans la mémoire de l'ordinateur pour stocker les données de votre dictionnaire. À l'instar des listes, les dictionnaires Python allouent également de la mémoire par blocs. Explorons comment fonctionne l'allocation de mémoire pour les dictionnaires.

  1. Tout d'abord, nous devons créer un dictionnaire avec lequel travailler. Dans le même shell Python (ou ouvrez-en un nouveau si vous l'avez fermé), nous allons créer un dictionnaire représentant un enregistrement de données. Un dictionnaire en Python est une collection de paires clé - valeur, où chaque clé est unique et est utilisée pour accéder à sa valeur correspondante.
import sys  ## Importez sys si vous commencez une nouvelle session
row = {'route': '22', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}

Ici, nous avons importé le module sys qui donne accès à certaines variables utilisées ou maintenues par l'interpréteur Python et à des fonctions qui interagissent fortement avec l'interpréteur. Nous avons ensuite créé un dictionnaire nommé row avec quatre paires clé - valeur.

  1. Maintenant que nous avons notre dictionnaire, nous voulons vérifier sa taille initiale. La taille d'un dictionnaire fait référence à la quantité de mémoire qu'il occupe dans l'ordinateur.
sys.getsizeof(row)

La fonction sys.getsizeof() renvoie la taille d'un objet en octets. Lorsque vous exécutez ce code, vous devriez voir une valeur d'environ 240 octets. Cela vous donne une idée de la quantité de mémoire que le dictionnaire occupe initialement.

  1. Ensuite, nous allons ajouter de nouvelles paires clé - valeur au dictionnaire et observer comment l'allocation de mémoire change. Ajouter des éléments à un dictionnaire est une opération courante, et il est crucial de comprendre comment cela affecte la mémoire.
row['a'] = 1
sys.getsizeof(row)  ## La taille peut rester la même

row['b'] = 2
sys.getsizeof(row)  ## La taille peut augmenter

Lorsque vous ajoutez la première paire clé - valeur ('a': 1), la taille du dictionnaire peut rester la même. C'est parce que Python a déjà alloué un certain bloc de mémoire, et il peut y avoir assez d'espace dans ce bloc pour accueillir le nouvel élément. Cependant, lorsque vous ajoutez la deuxième paire clé - valeur ('b': 2), la taille peut augmenter. Vous remarquerez qu'après avoir ajouté un certain nombre d'éléments, la taille du dictionnaire augmente soudainement. C'est parce que les dictionnaires, comme les listes, allouent de la mémoire par blocs pour optimiser les performances. Allouer de la mémoire par blocs réduit le nombre de fois où Python doit demander plus de mémoire au système, ce qui accélère le processus d'ajout de nouveaux éléments.

  1. Essayons de supprimer un élément du dictionnaire pour voir si l'utilisation de la mémoire diminue. Supprimer des éléments d'un dictionnaire est également une opération courante, et il est intéressant de voir comment cela affecte la mémoire.
del row['b']
sys.getsizeof(row)

Fait intéressant, supprimer un élément ne réduit généralement pas l'allocation de mémoire. C'est parce que Python conserve la mémoire allouée pour éviter de réallouer si des éléments sont ajoutés à nouveau. Réallouer de la mémoire est une opération relativement coûteuse en termes de performances, donc Python essaie de l'éviter autant que possible.

Considérations sur l'efficacité mémoire :

Lorsque vous travaillez avec de grands ensembles de données où vous devez créer de nombreux enregistrements, utiliser des dictionnaires pour chaque enregistrement peut ne pas être la méthode la plus efficace en termes de mémoire. Les dictionnaires sont très flexibles et faciles à utiliser, mais ils peuvent consommer une quantité importante de mémoire, notamment lorsqu'il s'agit d'un grand nombre d'enregistrements. Voici quelques alternatives qui consomment moins de mémoire :

  • Tuples : Séquences immuables simples. Un tuple est une collection de valeurs qui ne peut pas être modifiée après sa création. Il utilise moins de mémoire qu'un dictionnaire car il n'a pas besoin de stocker les clés et de gérer la correspondance clé - valeur associée.
  • Tuples nommés : Tuples avec des noms de champs. Les tuples nommés sont similaires aux tuples ordinaires, mais ils vous permettent d'accéder aux valeurs par nom, ce qui peut rendre le code plus lisible. Ils utilisent également moins de mémoire que les dictionnaires.
  • Classes avec __slots__ : Classes qui définissent explicitement les attributs pour éviter d'utiliser un dictionnaire pour les variables d'instance. Lorsque vous utilisez __slots__ dans une classe, Python ne crée pas de dictionnaire pour stocker les variables d'instance, ce qui réduit l'utilisation de la mémoire.

Ces alternatives peuvent réduire considérablement l'utilisation de la mémoire lors de la manipulation de nombreux enregistrements.

Optimisation de la mémoire avec des données organisées par colonnes

Dans le stockage de données traditionnel, nous stockons souvent chaque enregistrement sous forme de dictionnaire distinct, ce qui est appelé une approche orientée ligne. Cependant, cette méthode peut consommer une quantité importante de mémoire. Une autre façon de procéder consiste à stocker les données par colonnes. Dans l'approche orientée colonne, nous créons des listes distinctes pour chaque attribut, et chaque liste contient toutes les valeurs de cet attribut spécifique. Cela peut nous aider à économiser de la mémoire.

  1. Tout d'abord, vous devez créer un nouveau fichier Python dans votre répertoire de projet. Ce fichier contiendra le code pour lire les données de manière orientée colonne. Nommez le fichier readrides.py. Vous pouvez utiliser les commandes suivantes dans le terminal pour y parvenir :
cd ~/project
touch readrides.py

La commande cd ~/project change le répertoire courant en votre répertoire de projet, et la commande touch readrides.py crée un nouveau fichier vide nommé readrides.py.

  1. Ensuite, ouvrez le fichier readrides.py dans l'éditeur WebIDE. Ensuite, ajoutez le code Python suivant au fichier. Ce code définit une fonction read_rides_as_columns qui lit les données de trajets en bus à partir d'un fichier CSV et les stocke dans quatre listes distinctes, chacune représentant une colonne de données.
## readrides.py
import csv
import sys
import tracemalloc

def read_rides_as_columns(filename):
    '''
    Read the bus ride data into 4 lists, representing columns
    '''
    routes = []
    dates = []
    daytypes = []
    numrides = []
    with open(filename) as f:
        rows = csv.reader(f)
        headings = next(rows)     ## Skip headers
        for row in rows:
            routes.append(row[0])
            dates.append(row[1])
            daytypes.append(row[2])
            numrides.append(int(row[3]))
    return dict(routes=routes, dates=dates, daytypes=daytypes, numrides=numrides)

Dans ce code, nous importons d'abord les modules nécessaires csv, sys et tracemalloc. Le module csv est utilisé pour lire les fichiers CSV, sys peut être utilisé pour des opérations liées au système (bien qu'il ne soit pas utilisé dans cette fonction), et tracemalloc est utilisé pour le profilage de mémoire. À l'intérieur de la fonction, nous initialisons quatre listes vides pour stocker les différentes colonnes de données. Ensuite, nous ouvrons le fichier, sautons la ligne d'en-tête et parcourons chaque ligne du fichier, ajoutant les valeurs correspondantes aux listes appropriées. Enfin, nous renvoyons un dictionnaire contenant ces quatre listes.

  1. Maintenant, analysons pourquoi l'approche orientée colonne peut économiser de la mémoire. Nous le ferons dans le shell Python. Exécutez le code suivant :
import readrides
import tracemalloc

## Estimate memory for row-oriented approach
nrows = 577563     ## Number of rows in original file
dict_overhead = 240  ## Approximate dictionary overhead in bytes
row_memory = nrows * dict_overhead
print(f"Estimated memory for row-oriented data: {row_memory} bytes ({row_memory/1024/1024:.2f} MB)")

## Estimate memory for column-oriented approach
pointer_size = 8   ## Size of a pointer in bytes on 64-bit systems
column_memory = nrows * 4 * pointer_size  ## 4 columns with one pointer per entry
print(f"Estimated memory for column-oriented data: {column_memory} bytes ({column_memory/1024/1024:.2f} MB)")

## Estimate savings
savings = row_memory - column_memory
print(f"Estimated memory savings: {savings} bytes ({savings/1024/1024:.2f} MB)")

Dans ce code, nous importons d'abord le module readrides que nous venons de créer et le module tracemalloc. Ensuite, nous estimons l'utilisation de mémoire pour l'approche orientée ligne. Nous supposons que chaque dictionnaire a une surcharge de 240 octets, et nous multiplions cela par le nombre de lignes dans le fichier original pour obtenir l'utilisation totale de mémoire pour les données orientées ligne. Pour l'approche orientée colonne, nous supposons que sur un système 64 bits, chaque pointeur prend 8 octets. Étant donné que nous avons 4 colonnes et un pointeur par entrée, nous calculons l'utilisation totale de mémoire pour les données orientées colonne. Enfin, nous calculons les économies de mémoire en soustrayant l'utilisation de mémoire orientée colonne de l'utilisation de mémoire orientée ligne.

Ce calcul montre que l'approche orientée colonne devrait économiser environ 120 Mo de mémoire par rapport à l'approche orientée ligne avec des dictionnaires.

  1. Vérifions cela en mesurant l'utilisation réelle de mémoire avec le module tracemalloc. Exécutez le code suivant :
## Start tracking memory
tracemalloc.start()

## Read the data
columns = readrides.read_rides_as_columns('ctabus.csv')

## Get current and peak memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current/1024/1024:.2f} MB")
print(f"Peak memory usage: {peak/1024/1024:.2f} MB")

## Stop tracking memory
tracemalloc.stop()

Dans ce code, nous commençons d'abord à suivre l'utilisation de mémoire en utilisant tracemalloc.start(). Ensuite, nous appelons la fonction read_rides_as_columns pour lire les données à partir du fichier ctabus.csv. Après cela, nous utilisons tracemalloc.get_traced_memory() pour obtenir l'utilisation actuelle et maximale de mémoire. Enfin, nous arrêtons de suivre l'utilisation de mémoire en utilisant tracemalloc.stop().

La sortie vous montrera l'utilisation réelle de mémoire de votre structure de données orientée colonne. Cela devrait être nettement inférieur à notre estimation théorique pour l'approche orientée ligne.

Les économies de mémoire significatives proviennent de l'élimination de la surcharge de milliers d'objets dictionnaire. Chaque dictionnaire en Python a une surcharge fixe, quelle que soit le nombre d'éléments qu'il contient. En utilisant un stockage orienté colonne, nous n'avons besoin que de quelques listes au lieu de milliers de dictionnaires.

Création d'une classe de conteneur personnalisée

Dans le traitement des données, l'approche orientée colonne est excellente pour économiser de la mémoire. Cependant, cela peut poser des problèmes lorsque votre code existant s'attend à ce que les données soient sous la forme d'une liste de dictionnaires. Pour résoudre ce problème, nous allons créer une classe de conteneur personnalisée. Cette classe présentera une interface orientée ligne, ce qui signifie qu'elle ressemblera et se comportera comme une liste de dictionnaires pour votre code. Mais en interne, elle stockera les données au format orienté colonne, nous aidant ainsi à économiser de la mémoire.

  1. Tout d'abord, ouvrez le fichier readrides.py dans l'éditeur WebIDE. Nous allons ajouter une nouvelle classe à ce fichier. Cette classe sera la base de notre conteneur personnalisé.
## Add this to readrides.py
from collections.abc import Sequence

class RideData(Sequence):
    def __init__(self):
        ## Each value is a list with all of the values (a column)
        self.routes = []
        self.dates = []
        self.daytypes = []
        self.numrides = []

    def __len__(self):
        ## All lists assumed to have the same length
        return len(self.routes)

    def __getitem__(self, index):
        return {'route': self.routes[index],
                'date': self.dates[index],
                'daytype': self.daytypes[index],
                'rides': self.numrides[index]}

    def append(self, d):
        self.routes.append(d['route'])
        self.dates.append(d['date'])
        self.daytypes.append(d['daytype'])
        self.numrides.append(d['rides'])

Dans ce code, nous définissons une classe nommée RideData qui hérite de Sequence. La méthode __init__ initialise quatre listes vides, chacune représentant une colonne de données. La méthode __len__ renvoie la longueur du conteneur, qui est la même que la longueur de la liste routes. La méthode __getitem__ nous permet d'accéder à un enregistrement spécifique par son index, en le renvoyant sous forme de dictionnaire. La méthode append ajoute un nouvel enregistrement au conteneur en ajoutant des valeurs à chaque liste de colonne.

  1. Maintenant, nous avons besoin d'une fonction pour lire les données de trajets en bus dans notre conteneur personnalisé. Ajoutez la fonction suivante au fichier readrides.py.
## Add this to readrides.py
def read_rides_as_dicts(filename):
    '''
    Read the bus ride data as a list of dicts, but use our custom container
    '''
    records = RideData()
    with open(filename) as f:
        rows = csv.reader(f)
        headings = next(rows)     ## Skip headers
        for row in rows:
            route = row[0]
            date = row[1]
            daytype = row[2]
            rides = int(row[3])
            record = {
                'route': route,
                'date': date,
                'daytype': daytype,
                'rides': rides
            }
            records.append(record)
    return records

Cette fonction crée une instance de la classe RideData et la remplit avec les données du fichier CSV. Elle lit chaque ligne du fichier, extrait les informations pertinentes, crée un dictionnaire pour chaque enregistrement, puis l'ajoute au conteneur RideData. L'important est qu'elle conserve la même interface qu'une liste de dictionnaires, mais stocke les données en colonnes en interne.

  1. Testons notre conteneur personnalisé dans le shell Python. Cela nous aidera à vérifier qu'il fonctionne comme prévu.
import readrides

## Read the data using our custom container
rows = readrides.read_rides_as_dicts('ctabus.csv')

## Check the type of the returned object
type(rows)  ## Should be readrides.RideData

## Check the length
len(rows)   ## Should be 577563

## Access individual records
rows[0]     ## Should return a dictionary for the first record
rows[1]     ## Should return a dictionary for the second record
rows[2]     ## Should return a dictionary for the third record

Notre conteneur personnalisé implémente avec succès l'interface Sequence, ce qui signifie qu'il se comporte comme une liste. Vous pouvez utiliser la fonction len() pour obtenir le nombre d'enregistrements dans le conteneur, et vous pouvez utiliser l'indexation pour accéder à des enregistrements individuels. Chaque enregistrement semble être un dictionnaire, même si les données sont stockées en colonnes en interne. C'est excellent car le code existant qui s'attend à une liste de dictionnaires continuera de fonctionner avec notre conteneur personnalisé sans aucune modification.

  1. Enfin, mesurons l'utilisation de mémoire de notre conteneur personnalisé. Cela nous montrera combien de mémoire nous économisons par rapport à une liste de dictionnaires.
import tracemalloc

tracemalloc.start()
rows = readrides.read_rides_as_dicts('ctabus.csv')
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current/1024/1024:.2f} MB")
print(f"Peak memory usage: {peak/1024/1024:.2f} MB")
tracemalloc.stop()

Lorsque vous exécutez ce code, vous devriez voir que l'utilisation de mémoire est similaire à celle de l'approche orientée colonne, ce qui est beaucoup moins que ce qu'une liste de dictionnaires utiliserait. Cela démontre l'avantage de notre conteneur personnalisé en termes d'efficacité mémoire.

✨ Vérifier la solution et pratiquer

Amélioration du conteneur personnalisé pour le découpage (slicing)

Notre conteneur personnalisé est excellent pour accéder à des enregistrements individuels. Cependant, il y a un problème lorsqu'il s'agit du découpage (slicing). Lorsque vous essayez de prendre une tranche de notre conteneur, le résultat n'est pas celui que vous attendez normalement.

Comprenons pourquoi cela se produit. En Python, le découpage (slicing) est une opération courante utilisée pour extraire une partie d'une séquence. Mais pour notre conteneur personnalisé, Python ne sait pas comment créer un nouvel objet RideData avec seulement les données découpées. Au lieu de cela, il crée une liste contenant les résultats de l'appel de __getitem__ pour chaque index dans la tranche.

  1. Testons le découpage (slicing) dans le shell Python :
import readrides

rows = readrides.read_rides_as_dicts('ctabus.csv')
r = rows[0:10]  ## Take a slice of the first 10 records
type(r)  ## This will likely be a list, not a RideData object
print(r)  ## This might look like a list of numbers, not dictionaries

Dans ce code, nous importons d'abord le module readrides. Ensuite, nous lisons les données du fichier ctabus.csv dans une variable rows. Lorsque nous essayons de prendre une tranche des 10 premiers enregistrements et vérifions le type du résultat, nous constatons qu'il s'agit d'une liste au lieu d'un objet RideData. L'impression du résultat peut montrer quelque chose d'inattendu, comme une liste de nombres au lieu de dictionnaires.

  1. Modifions notre classe RideData pour gérer correctement le découpage (slicing). Ouvrez readrides.py et mettez à jour la méthode __getitem__ :
def __getitem__(self, index):
    if isinstance(index, slice):
        ## Handle slice
        result = RideData()
        result.routes = self.routes[index]
        result.dates = self.dates[index]
        result.daytypes = self.daytypes[index]
        result.numrides = self.numrides[index]
        return result
    else:
        ## Handle single index
        return {'route': self.routes[index],
                'date': self.dates[index],
                'daytype': self.daytypes[index],
                'rides': self.numrides[index]}

Dans cette méthode __getitem__ mise à jour, nous vérifions d'abord si l'index est une tranche (slice). Si c'est le cas, nous créons un nouvel objet RideData appelé result. Ensuite, nous remplissons cet nouvel objet avec des tranches des colonnes de données originales (routes, dates, daytypes et numrides). Cela garantit que lorsque nous découpons notre conteneur personnalisé, nous obtenons un autre objet RideData au lieu d'une liste. Si l'index n'est pas une tranche (c'est-à-dire qu'il s'agit d'un index unique), nous renvoyons un dictionnaire contenant l'enregistrement pertinent.

  1. Testons notre capacité améliorée de découpage (slicing) :
import readrides

rows = readrides.read_rides_as_dicts('ctabus.csv')
r = rows[0:10]  ## Take a slice of the first 10 records
type(r)  ## Should now be readrides.RideData
len(r)   ## Should be 10
r[0]     ## Should be the same as rows[0]
r[1]     ## Should be the same as rows[1]

Après avoir mis à jour la méthode __getitem__, nous pouvons tester à nouveau le découpage (slicing). Lorsque nous prenons une tranche des 10 premiers enregistrements, le type du résultat devrait maintenant être readrides.RideData. La longueur de la tranche devrait être de 10, et l'accès aux éléments individuels de la tranche devrait nous donner les mêmes résultats que l'accès aux éléments correspondants dans le conteneur original.

  1. Vous pouvez également tester avec différents motifs de découpage (slicing) :
## Get every other record from the first 20
r2 = rows[0:20:2]
len(r2)  ## Should be 10

## Get the last 10 records
r3 = rows[-10:]
len(r3)  ## Should be 10

Ici, nous testons différents motifs de découpage (slicing). La première tranche rows[0:20:2] prend tous les autres enregistrements parmi les 20 premiers enregistrements, et la longueur de la tranche résultante devrait être de 10. La deuxième tranche rows[-10:] prend les 10 derniers enregistrements, et sa longueur devrait également être de 10.

En implémentant correctement le découpage (slicing), notre conteneur personnalisé se comporte maintenant encore plus comme une liste Python standard, tout en conservant l'efficacité mémoire du stockage orienté colonne.

Cette approche de création de classes de conteneurs personnalisés qui imitent les conteneurs intégrés de Python mais avec des représentations internes différentes est une technique puissante pour optimiser l'utilisation de la mémoire sans changer l'interface que votre code présente aux utilisateurs.

Résumé

Dans ce laboratoire (lab), vous avez appris plusieurs compétences importantes. Tout d'abord, vous avez exploré le comportement d'allocation mémoire dans les listes et les dictionnaires Python et avez appris à optimiser l'utilisation de la mémoire en passant du stockage de données orienté ligne à un stockage orienté colonne. Deuxièmement, vous avez créé une classe de conteneur personnalisée qui conserve l'interface originale tout en utilisant moins de mémoire et l'avez améliorée pour gérer correctement les opérations de découpage (slicing).

Ces techniques sont très précieuses pour travailler avec de grands ensembles de données, car elles peuvent réduire considérablement l'utilisation de la mémoire sans modifier l'interface de votre code. La capacité à créer des classes de conteneurs personnalisés imitant les conteneurs intégrés de Python avec des représentations internes différentes est un outil d'optimisation puissant pour les applications Python. Vous pouvez appliquer ces concepts à d'autres projets critiques en termes de mémoire, en particulier ceux impliquant de grands ensembles de données à structure régulière.