Introduction
Dans ce laboratoire (lab), vous allez apprendre le concept d'objets de première classe en Python et explorer son modèle mémoire. Python considère les fonctions, les types et les données comme des objets de première classe, permettant des modèles de programmation puissants et flexibles.
Vous allez également créer des fonctions utilitaires réutilisables pour le traitement de données CSV. Plus précisément, vous allez créer une fonction généralisée pour lire des données CSV dans le fichier reader.py, qui peut être réutilisée dans différents projets.
Comprendre les objets de première classe en Python
En Python, tout est considéré comme un objet. Cela inclut les fonctions et les types. Que signifie cela ? Eh bien, cela signifie que vous pouvez stocker des fonctions et des types dans des structures de données, les passer en tant qu'arguments à d'autres fonctions et même les retourner depuis d'autres fonctions. C'est un concept très puissant, et nous allons l'explorer en utilisant le traitement de données CSV comme exemple.
Explorer les types de première classe
Tout d'abord, lançons l'interpréteur Python. Ouvrez un nouveau terminal dans le WebIDE et tapez la commande suivante. Cette commande lancera l'interpréteur Python, où nous allons exécuter notre code Python.
python3
Lorsque nous travaillons avec des fichiers CSV en Python, nous avons souvent besoin de convertir les chaînes de caractères que nous lisons à partir de ces fichiers en types de données appropriés. Par exemple, un nombre dans un fichier CSV peut être lu comme une chaîne de caractères, mais nous voulons l'utiliser comme un entier ou un nombre à virgule flottante dans notre code Python. Pour ce faire, nous pouvons créer une liste de fonctions de conversion.
coltypes = [str, int, float]
Remarquez que nous créons une liste qui contient les fonctions de type réelles, pas des chaînes de caractères. En Python, les types sont des objets de première classe, ce qui signifie que nous pouvons les traiter comme n'importe quel autre objet. Nous pouvons les mettre dans des listes, les passer d'un endroit à un autre et les utiliser dans notre code.
Maintenant, lisons des données à partir d'un fichier CSV de portefeuille pour voir comment nous pouvons utiliser ces fonctions de conversion.
import csv
f = open('portfolio.csv')
rows = csv.reader(f)
headers = next(rows)
row = next(rows)
print(row)
Lorsque vous exécutez ce code, vous devriez voir une sortie similaire à la suivante. Ceci est la première ligne de données du fichier CSV, représentée sous forme de liste de chaînes de caractères.
['AA', '100', '32.20']
Ensuite, nous allons utiliser la fonction zip. La fonction zip prend plusieurs itérables (comme des listes ou des tuples) et associe leurs éléments. Nous l'utiliserons pour associer chaque valeur de la ligne à sa fonction de conversion de type correspondante.
r = list(zip(coltypes, row))
print(r)
Cela produira la sortie suivante. Chaque paire contient une fonction de type et une valeur sous forme de chaîne de caractères provenant du fichier CSV.
[(<class 'str'>, 'AA'), (<class 'int'>, '100'), (<class 'float'>, '32.20')]
Maintenant que nous avons ces paires, nous pouvons appliquer chaque fonction pour convertir les valeurs en leurs types appropriés.
record = [func(val) for func, val in zip(coltypes, row)]
print(record)
La sortie montrera que les valeurs ont été converties en leurs types appropriés. La chaîne de caractères 'AA' reste une chaîne de caractères, '100' devient l'entier 100 et '32.20' devient le nombre à virgule flottante 32.2.
['AA', 100, 32.2]
Nous pouvons également combiner ces valeurs avec leurs noms de colonnes pour créer un dictionnaire. Un dictionnaire est une structure de données utile en Python qui nous permet de stocker des paires clé - valeur.
record_dict = dict(zip(headers, record))
print(record_dict)
La sortie sera un dictionnaire où les clés sont les noms de colonnes et les valeurs sont les données converties.
{'name': 'AA', 'shares': 100, 'price': 32.2}
Vous pouvez effectuer toutes ces étapes dans une seule compréhension. Une compréhension est un moyen concis de créer des listes, des dictionnaires ou des ensembles en Python.
result = {name: func(val) for name, func, val in zip(headers, coltypes, row)}
print(result)
La sortie sera le même dictionnaire que précédemment.
{'name': 'AA', 'shares': 100, 'price': 32.2}
Lorsque vous avez terminé de travailler dans l'interpréteur Python, vous pouvez le quitter en tapant la commande suivante.
exit()
Cette démonstration montre comment le traitement des fonctions en tant qu'objets de première classe en Python permet des techniques de traitement de données puissantes. En étant capable de traiter les types et les fonctions comme des objets, nous pouvons écrire un code plus flexible et concis.
Création d'une fonction utilitaire pour le traitement de fichiers CSV
Maintenant que nous comprenons comment les objets de première classe de Python peuvent nous aider dans la conversion de données, nous allons créer une fonction utilitaire réutilisable. Cette fonction lira les données d'un fichier CSV et les transformera en une liste de dictionnaires. C'est une opération très utile car les fichiers CSV sont couramment utilisés pour stocker des données tabulaires, et les convertir en une liste de dictionnaires facilite la manipulation des données en Python.
Création de l'utilitaire de lecture de fichiers CSV
Tout d'abord, ouvrez le WebIDE. Une fois ouvert, accédez au répertoire du projet et créez un nouveau fichier nommé reader.py. Dans ce fichier, nous allons définir une fonction qui lit les données d'un fichier CSV et applique des conversions de type. Les conversions de type sont importantes car les données dans un fichier CSV sont généralement lues sous forme de chaînes de caractères, mais nous pouvons avoir besoin de différents types de données comme des entiers ou des nombres à virgule flottante pour un traitement ultérieur.
Ajoutez le code suivant à reader.py :
import csv
def read_csv_as_dicts(filename, types):
"""
Read a CSV file into a list of dictionaries, converting each field according
to the types provided.
Parameters:
filename (str): Name of the CSV file to read
types (list): List of type conversion functions for each column
Returns:
list: List of dictionaries representing the CSV data
"""
records = []
with open(filename, 'r') as f:
rows = csv.reader(f)
headers = next(rows) ## Get the column headers
for row in rows:
## Apply type conversions to each value in the row
converted_row = [func(val) for func, val in zip(types, row)]
## Create a dictionary mapping headers to converted values
record = dict(zip(headers, converted_row))
records.append(record)
return records
Cette fonction ouvre d'abord le fichier CSV spécifié. Elle lit ensuite les en-têtes du fichier CSV, qui sont les noms des colonnes. Ensuite, elle parcourt chaque ligne du fichier. Pour chaque valeur dans la ligne, elle applique la fonction de conversion de type correspondante de la liste types. Enfin, elle crée un dictionnaire où les clés sont les en-têtes de colonne et les valeurs sont les données converties, et ajoute ce dictionnaire à la liste records. Une fois que toutes les lignes ont été traitées, elle retourne la liste records.
Test de la fonction utilitaire
Testons notre fonction utilitaire. Tout d'abord, ouvrez un terminal et lancez l'interpréteur Python en tapant :
python3
Maintenant que nous sommes dans l'interpréteur Python, nous pouvons utiliser notre fonction pour lire les données du portefeuille. Les données du portefeuille sont contenues dans un fichier CSV qui contient des informations sur les actions, telles que le nom de l'action, le nombre de parts et le prix.
import reader
portfolio = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
for record in portfolio[:3]: ## Show the first 3 records
print(record)
Lorsque vous exécutez ce code, vous devriez voir une sortie similaire à :
{'name': 'AA', 'shares': 100, 'price': 32.2}
{'name': 'IBM', 'shares': 50, 'price': 91.1}
{'name': 'CAT', 'shares': 150, 'price': 83.44}
Cette sortie montre les trois premiers enregistrements des données du portefeuille, avec les types de données correctement convertis.
Essayons également notre fonction avec les données des bus CTA. Les données des bus CTA sont contenues dans un autre fichier CSV qui contient des informations sur les itinéraires de bus, les dates, les types de jour et le nombre de trajets.
rows = reader.read_csv_as_dicts('ctabus.csv', [str, str, str, int])
print(f"Total rows: {len(rows)}")
print("First row:", rows[0])
La sortie devrait être quelque chose comme :
Total rows: 577563
First row: {'route': '3', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}
Cela montre que notre fonction peut gérer différents fichiers CSV et appliquer les conversions de type appropriées.
Pour quitter l'interpréteur Python, tapez :
exit()
Vous avez maintenant créé une fonction utilitaire réutilisable qui peut lire n'importe quel fichier CSV et appliquer les conversions de type appropriées. Cela démontre la puissance des objets de première classe de Python et comment ils peuvent être utilisés pour créer un code flexible et réutilisable.
Exploration du modèle mémoire de Python
Le modèle mémoire de Python joue un rôle crucial dans la détermination de la manière dont les objets sont stockés en mémoire et de la façon dont ils sont référencés. Comprendre ce modèle est essentiel, surtout lorsqu'on travaille avec de grands ensembles de données, car cela peut avoir un impact significatif sur les performances et l'utilisation de la mémoire de vos programmes Python. Dans cette étape, nous allons nous concentrer spécifiquement sur la façon dont les objets de type chaîne de caractères sont gérés en Python et explorer des moyens d'optimiser l'utilisation de la mémoire pour de grands ensembles de données.
Répétition de chaînes de caractères dans les ensembles de données
Les données des bus CTA contiennent de nombreuses valeurs répétées, telles que les noms d'itinéraires. Les valeurs répétées dans un ensemble de données peuvent entraîner une utilisation inefficace de la mémoire si elles ne sont pas gérées correctement. Pour comprendre l'ampleur de ce problème, examinons d'abord combien de chaînes d'itinéraires uniques se trouvent dans l'ensemble de données.
Tout d'abord, ouvrez un interpréteur Python. Vous pouvez le faire en exécutant la commande suivante dans votre terminal :
python3
Une fois l'interpréteur Python ouvert, nous allons charger les données des bus CTA et trouver les itinéraires uniques. Voici le code pour y parvenir :
import reader
rows = reader.read_csv_as_dicts('ctabus.csv', [str, str, str, int])
## Find unique route names
routes = {row['route'] for row in rows}
print(f"Number of unique route names: {len(routes)}")
Dans ce code, nous importons d'abord le module reader, qui contient probablement une fonction pour lire les fichiers CSV sous forme de dictionnaires. Nous utilisons ensuite la fonction read_csv_as_dicts pour charger les données du fichier ctabus.csv. Le deuxième argument [str, str, str, int] spécifie les types de données pour chaque colonne du fichier CSV. Ensuite, nous utilisons une compréhension d'ensemble pour trouver tous les noms d'itinéraires uniques dans l'ensemble de données et afficher le nombre de noms d'itinéraires uniques.
La sortie devrait être :
Number of unique route names: 181
Maintenant, vérifions combien d'objets de chaîne de caractères différents sont créés pour ces itinéraires. Même s'il n'y a que 181 noms d'itinéraires uniques, Python peut créer un nouvel objet de chaîne de caractères pour chaque occurrence d'un nom d'itinéraire dans l'ensemble de données. Pour vérifier cela, nous allons utiliser la fonction id() pour obtenir l'identifiant unique de chaque objet de chaîne de caractères.
## Count unique string object IDs
routeids = {id(row['route']) for row in rows}
print(f"Number of unique route string objects: {len(routeids)}")
La sortie peut vous surprendre :
Number of unique route string objects: 542305
Cela montre qu'il n'y a que 181 noms d'itinéraires uniques, mais plus de 500 000 objets de chaîne de caractères uniques. Cela se produit car Python crée un nouvel objet de chaîne de caractères pour chaque ligne, même si les valeurs sont les mêmes. Cela peut entraîner un gaspillage de mémoire important, surtout lorsqu'on travaille avec de grands ensembles de données.
Internement de chaînes de caractères pour économiser de la mémoire
Python propose un moyen d'"interner" (réutiliser) les chaînes de caractères à l'aide de la fonction sys.intern(). L'internement de chaînes de caractères peut économiser de la mémoire lorsque vous avez de nombreuses chaînes de caractères dupliquées dans votre ensemble de données. Lorsque vous internez une chaîne de caractères, Python vérifie si une chaîne identique existe déjà dans le pool d'internement. Si c'est le cas, il retourne une référence à l'objet de chaîne de caractères existant au lieu de créer un nouveau.
Démontrons comment fonctionne l'internement de chaînes de caractères avec un exemple simple :
import sys
## Without interning
a = 'hello world'
b = 'hello world'
print(f"a is b (without interning): {a is b}")
## With interning
a = sys.intern(a)
b = sys.intern(b)
print(f"a is b (with interning): {a is b}")
Dans ce code, nous créons d'abord deux variables de chaîne de caractères a et b avec la même valeur sans interner. L'opérateur is vérifie si deux variables font référence au même objet. Sans interner, a et b sont des objets différents, donc a is b retourne False. Ensuite, nous internons les deux chaînes de caractères à l'aide de sys.intern(). Après l'internement, a et b font référence au même objet dans le pool d'internement, donc a is b retourne True.
La sortie devrait être :
a is b (without interning): False
a is b (with interning): True
Maintenant, utilisons l'internement de chaînes de caractères lors de la lecture des données des bus CTA pour réduire l'utilisation de la mémoire. Nous allons également utiliser le module tracemalloc pour suivre l'utilisation de la mémoire avant et après l'internement.
import sys
import reader
import tracemalloc
## Start memory tracking
tracemalloc.start()
## Read data with interning for the route column
rows = reader.read_csv_as_dicts('ctabus.csv', [sys.intern, str, str, int])
## Check unique route objects again
routeids = {id(row['route']) for row in rows}
print(f"Number of unique route string objects (with interning): {len(routeids)}")
## Check 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")
Dans ce code, nous démarrons d'abord le suivi de la mémoire à l'aide de tracemalloc.start(). Ensuite, nous lisons les données des bus CTA en internant les chaînes de caractères de la colonne des itinéraires en passant sys.intern comme type de données pour la première colonne. Ensuite, nous vérifions à nouveau le nombre d'objets de chaîne de caractères d'itinéraire uniques et affichons l'utilisation actuelle et maximale de la mémoire.
La sortie devrait être quelque chose comme :
Number of unique route string objects (with interning): 181
Current memory usage: 189.56 MB
Peak memory usage: 209.32 MB
Redémarrons l'interpréteur et essayons d'interner à la fois les chaînes d'itinéraire et de date pour voir si nous pouvons réduire encore plus l'utilisation de la mémoire.
exit()
Relancez Python :
python3
import sys
import reader
import tracemalloc
## Start memory tracking
tracemalloc.start()
## Read data with interning for both route and date columns
rows = reader.read_csv_as_dicts('ctabus.csv', [sys.intern, sys.intern, str, int])
## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage (interning route and date): {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage (interning route and date): {peak / 1024 / 1024:.2f} MB")
La sortie devrait montrer une nouvelle diminution de l'utilisation de la mémoire :
Current memory usage (interning route and date): 170.23 MB
Peak memory usage (interning route and date): 190.05 MB
Cela démontre comment la compréhension du modèle mémoire de Python et l'utilisation de techniques telles que l'internement de chaînes de caractères peuvent aider à optimiser vos programmes, surtout lorsqu'on travaille avec de grands ensembles de données contenant des valeurs répétées.
Enfin, quittez l'interpréteur Python :
exit()
Stockage de données orienté colonnes
Jusqu'à présent, nous avons stocké les données CSV sous forme de liste de dictionnaires de lignes. Cela signifie que chaque ligne du fichier CSV est représentée sous forme de dictionnaire, où les clés sont les en-têtes de colonne et les valeurs sont les données correspondantes de cette ligne. Cependant, lorsqu'on travaille avec de grands ensembles de données, cette méthode peut être inefficace. Stocker les données dans un format orienté colonnes peut être un meilleur choix. Dans une approche orientée colonnes, les données de chaque colonne sont stockées dans une liste distincte. Cela peut réduire considérablement l'utilisation de la mémoire car les types de données similaires sont regroupés, et cela peut également améliorer les performances pour certaines opérations telles que l'agrégation de données par colonne.
Création d'un lecteur de données orienté colonnes
Maintenant, nous allons créer un nouveau fichier qui nous aidera à lire les données CSV dans un format orienté colonnes. Créez un nouveau fichier nommé colreader.py dans le répertoire du projet avec le code suivant :
import csv
class DataCollection:
def __init__(self, headers, columns):
"""
Initialize a column-oriented data collection.
Parameters:
headers (list): Column header names
columns (dict): Dictionary mapping header names to column data lists
"""
self.headers = headers
self.columns = columns
self._length = len(columns[headers[0]]) if headers else 0
def __len__(self):
"""Return the number of rows in the collection."""
return self._length
def __getitem__(self, index):
"""
Get a row by index, presented as a dictionary.
Parameters:
index (int): Row index
Returns:
dict: Dictionary representing the row at the given index
"""
if isinstance(index, int):
if index < 0 or index >= self._length:
raise IndexError("Index out of range")
return {header: self.columns[header][index] for header in self.headers}
else:
raise TypeError("Index must be an integer")
def read_csv_as_columns(filename, types):
"""
Read a CSV file into a column-oriented data structure, converting each field
according to the types provided.
Parameters:
filename (str): Name of the CSV file to read
types (list): List of type conversion functions for each column
Returns:
DataCollection: Column-oriented data collection representing the CSV data
"""
with open(filename, 'r') as f:
rows = csv.reader(f)
headers = next(rows) ## Get the column headers
## Initialize columns
columns = {header: [] for header in headers}
## Read data into columns
for row in rows:
## Convert values according to the specified types
converted_values = [func(val) for func, val in zip(types, row)]
## Add each value to its corresponding column
for header, value in zip(headers, converted_values):
columns[header].append(value)
return DataCollection(headers, columns)
Ce code fait deux choses importantes :
- Il définit une classe
DataCollection. Cette classe stocke les données par colonnes, mais elle nous permet d'accéder aux données comme si c'était une liste de dictionnaires de lignes. Cela est utile car il offre un moyen familier de travailler avec les données. - Il définit une fonction
read_csv_as_columns. Cette fonction lit les données CSV à partir d'un fichier et les stocke dans une structure orientée colonnes. Elle convertit également chaque champ du fichier CSV selon les types que nous fournissons.
Test du lecteur de données orienté colonnes
Testons notre lecteur de données orienté colonnes en utilisant les données des bus CTA. Tout d'abord, ouvrez un interpréteur Python. Vous pouvez le faire en exécutant la commande suivante dans votre terminal :
python3
Une fois l'interpréteur Python ouvert, exécutez le code suivant :
import colreader
import tracemalloc
from sys import intern
## Start memory tracking
tracemalloc.start()
## Read data into column-oriented structure with string interning
data = colreader.read_csv_as_columns('ctabus.csv', [intern, intern, intern, int])
## Check that we can access the data like a list of dictionaries
print(f"Number of rows: {len(data)}")
print("First 3 rows:")
for i in range(3):
print(data[i])
## Check 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")
La sortie devrait ressembler à ceci :
Number of rows: 577563
First 3 rows:
{'route': '3', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}
{'route': '4', 'date': '01/01/2001', 'daytype': 'U', 'rides': 9288}
{'route': '6', 'date': '01/01/2001', 'daytype': 'U', 'rides': 6048}
Current memory usage: 38.67 MB
Peak memory usage: 103.42 MB
Maintenant, comparons cela avec notre approche précédente orientée lignes. Exécutez le code suivant dans le même interpréteur Python :
import reader
import tracemalloc
from sys import intern
## Reset memory tracking
tracemalloc.reset_peak()
## Read data into row-oriented structure with string interning
rows = reader.read_csv_as_dicts('ctabus.csv', [intern, intern, intern, int])
## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage (row-oriented): {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage (row-oriented): {peak / 1024 / 1024:.2f} MB")
La sortie devrait être quelque chose comme ceci :
Current memory usage (row-oriented): 170.23 MB
Peak memory usage (row-oriented): 190.05 MB
Comme vous pouvez le voir, l'approche orientée colonnes utilise considérablement moins de mémoire !
Testons également que nous pouvons toujours analyser les données comme avant. Exécutez le code suivant :
## Find all unique routes in the column-oriented data
routes = {row['route'] for row in data}
print(f"Number of unique routes: {len(routes)}")
## Count rides per route (first 5)
from collections import defaultdict
route_rides = defaultdict(int)
for row in data:
route_rides[row['route']] += row['rides']
## Show the top 5 routes by total rides
top_routes = sorted(route_rides.items(), key=lambda x: x[1], reverse=True)[:5]
print("Top 5 routes by total rides:")
for route, rides in top_routes:
print(f"Route {route}: {rides:,} rides")
La sortie devrait être :
Number of unique routes: 181
Top 5 routes by total rides:
Route 9: 158,545,826 rides
Route 49: 129,872,910 rides
Route 77: 120,086,065 rides
Route 79: 109,348,708 rides
Route 4: 91,405,538 rides
Enfin, quittez l'interpréteur Python en exécutant la commande suivante :
exit()
Nous pouvons voir que l'approche orientée colonnes non seulement économise de la mémoire, mais nous permet également d'effectuer les mêmes analyses que précédemment. Cela montre comment différentes stratégies de stockage de données peuvent avoir un impact significatif sur les performances tout en offrant la même interface pour travailler avec les données.
Résumé
Dans ce laboratoire (lab), vous avez appris plusieurs concepts clés de Python. Tout d'abord, vous avez compris comment Python traite les fonctions, les types et autres entités comme des objets de première classe (first-class objects), permettant de les passer et de les stocker comme des données ordinaires. Deuxièmement, vous avez créé des fonctions utilitaires réutilisables pour le traitement de données CSV avec conversion de type automatique.
De plus, vous avez exploré le modèle mémoire de Python et utilisé l'internement de chaînes de caractères pour réduire l'utilisation de la mémoire pour les données répétitives. Vous avez également mis en œuvre une méthode de stockage orientée colonnes plus efficace pour de grands ensembles de données, offrant une interface utilisateur familière. Ces concepts montrent la flexibilité et la puissance de Python dans le traitement des données, et les techniques peuvent être appliquées à des projets d'analyse de données dans le monde réel.