Explorando el modelo de memoria de los objetos de primera clase en Python

Intermediate

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

Introducción

En este laboratorio, aprenderás sobre el concepto de objetos de primera clase en Python y explorarás su modelo de memoria. Python trata a las funciones, tipos y datos como objetos de primera clase, lo que permite patrones de programación poderosos y flexibles.

También crearás funciones de utilidad reutilizables para el procesamiento de datos CSV. Específicamente, crearás una función generalizada para leer datos CSV en el archivo reader.py, que se puede reutilizar en diferentes proyectos.

Este es un Guided Lab, que proporciona instrucciones paso a paso para ayudarte a aprender y practicar. Sigue las instrucciones cuidadosamente para completar cada paso y obtener experiencia práctica. Los datos históricos muestran que este es un laboratorio de nivel principiante con una tasa de finalización del 85%. Ha recibido una tasa de reseñas positivas del 100% por parte de los estudiantes.

Comprendiendo los Objetos de Primera Clase en Python

En Python, todo se trata como un objeto. Esto incluye funciones y tipos. ¿Qué significa esto? Bueno, significa que puedes almacenar funciones y tipos en estructuras de datos, pasarlos como argumentos a otras funciones e incluso devolverlos desde otras funciones. Este es un concepto muy poderoso, y lo vamos a explorar utilizando el procesamiento de datos CSV como ejemplo.

Explorando Tipos de Primera Clase

Primero, comencemos el intérprete de Python. Abre una nueva terminal en el WebIDE y escribe el siguiente comando. Este comando iniciará el intérprete de Python, donde ejecutaremos nuestro código Python.

python3

Cuando trabajamos con archivos CSV en Python, a menudo necesitamos convertir las cadenas que leemos de estos archivos en los tipos de datos adecuados. Por ejemplo, un número en un archivo CSV podría leerse como una cadena, pero queremos usarlo como un entero o un flotante en nuestro código Python. Para hacer esto, podemos crear una lista de funciones de conversión.

coltypes = [str, int, float]

Observa que estamos creando una lista que contiene las funciones de tipo reales, no cadenas. En Python, los tipos son objetos de primera clase, lo que significa que podemos tratarlos como cualquier otro objeto. Podemos ponerlos en listas, pasarlos de un lugar a otro y usarlos en nuestro código.

Ahora, leamos algunos datos de un archivo CSV de cartera para ver cómo podemos usar estas funciones de conversión.

import csv
f = open('portfolio.csv')
rows = csv.reader(f)
headers = next(rows)
row = next(rows)
print(row)

Cuando ejecutes este código, deberías ver una salida similar a la siguiente. Esta es la primera fila de datos del archivo CSV, representada como una lista de cadenas.

['AA', '100', '32.20']

A continuación, usaremos la función zip. La función zip toma múltiples iterables (como listas o tuplas) y empareja sus elementos. La usaremos para emparejar cada valor de la fila con su función de conversión de tipo correspondiente.

r = list(zip(coltypes, row))
print(r)

Esto producirá la siguiente salida. Cada par contiene una función de tipo y un valor de cadena del archivo CSV.

[(<class 'str'>, 'AA'), (<class 'int'>, '100'), (<class 'float'>, '32.20')]

Ahora que tenemos estos pares, podemos aplicar cada función para convertir los valores a sus tipos adecuados.

record = [func(val) for func, val in zip(coltypes, row)]
print(record)

La salida mostrará que los valores se han convertido a sus tipos adecuados. La cadena 'AA' sigue siendo una cadena, '100' se convierte en el entero 100 y '32.20' se convierte en el flotante 32.2.

['AA', 100, 32.2]

También podemos combinar estos valores con sus nombres de columna para crear un diccionario. Un diccionario es una estructura de datos útil en Python que nos permite almacenar pares clave - valor.

record_dict = dict(zip(headers, record))
print(record_dict)

La salida será un diccionario donde las claves son los nombres de las columnas y los valores son los datos convertidos.

{'name': 'AA', 'shares': 100, 'price': 32.2}

Puedes realizar todos estos pasos en una comprensión única. Una comprensión es una forma concisa de crear listas, diccionarios o conjuntos en Python.

result = {name: func(val) for name, func, val in zip(headers, coltypes, row)}
print(result)

La salida será el mismo diccionario que antes.

{'name': 'AA', 'shares': 100, 'price': 32.2}

Cuando hayas terminado de trabajar en el intérprete de Python, puedes salir de él escribiendo el siguiente comando.

exit()

Esta demostración muestra cómo el tratamiento de las funciones como objetos de primera clase en Python permite técnicas de procesamiento de datos poderosas. Al poder tratar tipos y funciones como objetos, podemos escribir código más flexible y conciso.

Creando una Función de Utilidad para el Procesamiento de CSV

Ahora que entendemos cómo los objetos de primera clase de Python pueden ayudarnos con la conversión de datos, vamos a crear una función de utilidad reutilizable. Esta función leerá datos CSV y los transformará en una lista de diccionarios. Esta es una operación muy útil porque los archivos CSV se utilizan comúnmente para almacenar datos tabulares, y convertir estos archivos en una lista de diccionarios facilita el trabajo con los datos en Python.

Creando la Utilidad de Lectura de CSV

Primero, abre el WebIDE. Una vez abierto, navega hasta el directorio del proyecto y crea un nuevo archivo llamado reader.py. En este archivo, definiremos una función que lee datos CSV y aplica conversiones de tipo. Las conversiones de tipo son importantes porque los datos en un archivo CSV generalmente se leen como cadenas, pero es posible que necesitemos diferentes tipos de datos, como enteros o números de punto flotante, para un procesamiento posterior.

Agrega el siguiente código a 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

Esta función primero abre el archivo CSV especificado. Luego lee los encabezados del archivo CSV, que son los nombres de las columnas. Después de eso, recorre cada fila del archivo. Para cada valor en la fila, aplica la función de conversión de tipo correspondiente de la lista types. Finalmente, crea un diccionario donde las claves son los encabezados de las columnas y los valores son los datos convertidos, y agrega este diccionario a la lista records. Una vez que se han procesado todas las filas, devuelve la lista records.

Probando la Función de Utilidad

Probemos nuestra función de utilidad. Primero, abre una terminal y inicia un intérprete de Python escribiendo:

python3

Ahora que estamos en el intérprete de Python, podemos usar nuestra función para leer los datos de la cartera. Los datos de la cartera son un archivo CSV que contiene información sobre acciones, como el nombre de la acción, el número de acciones y el precio.

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)

Cuando ejecutes este código, deberías ver una salida similar a:

{'name': 'AA', 'shares': 100, 'price': 32.2}
{'name': 'IBM', 'shares': 50, 'price': 91.1}
{'name': 'CAT', 'shares': 150, 'price': 83.44}

Esta salida muestra los tres primeros registros de los datos de la cartera, con los tipos de datos convertidos correctamente.

Probemos también nuestra función con los datos de autobuses del CTA. Los datos de autobuses del CTA son otro archivo CSV que contiene información sobre rutas de autobús, fechas, tipos de día y el número de viajes.

rows = reader.read_csv_as_dicts('ctabus.csv', [str, str, str, int])
print(f"Total rows: {len(rows)}")
print("First row:", rows[0])

La salida debería ser algo como:

Total rows: 577563
First row: {'route': '3', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}

Esto muestra que nuestra función puede manejar diferentes archivos CSV y aplicar las conversiones de tipo adecuadas.

Para salir del intérprete de Python, escribe:

exit()

Ahora has creado una función de utilidad reutilizable que puede leer cualquier archivo CSV y aplicar las conversiones de tipo adecuadas. Esto demuestra el poder de los objetos de primera clase de Python y cómo se pueden utilizar para crear código flexible y reutilizable.

Explorando el Modelo de Memoria de Python

El modelo de memoria de Python juega un papel crucial en la determinación de cómo se almacenan los objetos en memoria y cómo se les hace referencia. Comprender este modelo es esencial, especialmente cuando se trabaja con grandes conjuntos de datos, ya que puede afectar significativamente el rendimiento y el uso de memoria de tus programas en Python. En este paso, nos centraremos específicamente en cómo se manejan los objetos de cadena en Python y exploraremos formas de optimizar el uso de memoria para grandes conjuntos de datos.

Repetición de Cadenas en Conjuntos de Datos

Los datos de autobuses del CTA contienen muchos valores repetidos, como los nombres de las rutas. Los valores repetidos en un conjunto de datos pueden provocar un uso ineficiente de la memoria si no se manejan adecuadamente. Para entender la magnitud de este problema, primero examinemos cuántas cadenas de ruta únicas hay en el conjunto de datos.

Primero, abre un intérprete de Python. Puedes hacer esto ejecutando el siguiente comando en tu terminal:

python3

Una vez que el intérprete de Python esté abierto, cargaremos los datos de autobuses del CTA y encontraremos las rutas únicas. Aquí está el código para lograr esto:

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

En este código, primero importamos el módulo reader, que presumiblemente contiene una función para leer archivos CSV como diccionarios. Luego usamos la función read_csv_as_dicts para cargar los datos del archivo ctabus.csv. El segundo argumento [str, str, str, int] especifica los tipos de datos para cada columna en el archivo CSV. Después de eso, usamos una comprensión de conjunto para encontrar todos los nombres de ruta únicos en el conjunto de datos y mostramos la cantidad de nombres de ruta únicos.

La salida debería ser:

Number of unique route names: 181

Ahora, veamos cuántos objetos de cadena diferentes se crean para estas rutas. Aunque solo hay 181 nombres de ruta únicos, Python podría crear un nuevo objeto de cadena para cada aparición de un nombre de ruta en el conjunto de datos. Para verificar esto, usaremos la función id() para obtener el identificador único de cada objeto de cadena.

## Count unique string object IDs
routeids = {id(row['route']) for row in rows}
print(f"Number of unique route string objects: {len(routeids)}")

La salida puede sorprenderte:

Number of unique route string objects: 542305

Esto muestra que solo hay 181 nombres de ruta únicos, pero más de 500,000 objetos de cadena únicos. Esto sucede porque Python crea un nuevo objeto de cadena para cada fila, incluso si los valores son los mismos. Esto puede provocar un desperdicio significativo de memoria, especialmente cuando se trabaja con grandes conjuntos de datos.

Internamiento de Cadenas para Ahorrar Memoria

Python proporciona una forma de "internar" (reutilizar) cadenas utilizando la función sys.intern(). El internamiento de cadenas puede ahorrar memoria cuando tienes muchas cadenas duplicadas en tu conjunto de datos. Cuando internas una cadena, Python comprueba si una cadena idéntica ya existe en el grupo de cadenas internadas. Si es así, devuelve una referencia al objeto de cadena existente en lugar de crear uno nuevo.

Demostremos cómo funciona el internamiento de cadenas con un ejemplo sencillo:

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

En este código, primero creamos dos variables de cadena a y b con el mismo valor sin internamiento. El operador is comprueba si dos variables se refieren al mismo objeto. Sin internamiento, a y b son objetos diferentes, por lo que a is b devuelve False. Luego, internamos ambas cadenas utilizando sys.intern(). Después del internamiento, a y b se refieren al mismo objeto en el grupo de cadenas internadas, por lo que a is b devuelve True.

La salida debería ser:

a is b (without interning): False
a is b (with interning): True

Ahora, usemos el internamiento de cadenas al leer los datos de autobuses del CTA para reducir el uso de memoria. También usaremos el módulo tracemalloc para seguir el uso de memoria antes y después del internamiento.

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

En este código, primero comenzamos a seguir el uso de memoria utilizando tracemalloc.start(). Luego, leemos los datos de autobuses del CTA con internamiento para la columna de ruta pasando sys.intern como el tipo de datos para la primera columna. Después de eso, comprobamos la cantidad de objetos de cadena de ruta únicos nuevamente y mostramos el uso de memoria actual y máximo.

La salida debería ser algo como:

Number of unique route string objects (with interning): 181
Current memory usage: 189.56 MB
Peak memory usage: 209.32 MB

Reiniciemos el intérprete e intentemos internar tanto las cadenas de ruta como las de fecha para ver si podemos reducir aún más el uso de memoria.

exit()

Inicia Python nuevamente:

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 salida debería mostrar una disminución adicional en el uso de memoria:

Current memory usage (interning route and date): 170.23 MB
Peak memory usage (interning route and date): 190.05 MB

Esto demuestra cómo comprender el modelo de memoria de Python y utilizar técnicas como el internamiento de cadenas puede ayudar a optimizar tus programas, especialmente cuando se trabaja con grandes conjuntos de datos que contienen valores repetidos.

Finalmente, sal del intérprete de Python:

exit()

Almacenamiento de Datos Orientado a Columnas

Hasta ahora, hemos estado almacenando los datos CSV como una lista de diccionarios de filas. Esto significa que cada fila en el archivo CSV se representa como un diccionario, donde las claves son los encabezados de las columnas y los valores son los datos correspondientes en esa fila. Sin embargo, cuando se trabaja con grandes conjuntos de datos, este método puede ser ineficiente. Almacenar los datos en un formato orientado a columnas puede ser una mejor opción. En un enfoque orientado a columnas, los datos de cada columna se almacenan en una lista separada. Esto puede reducir significativamente el uso de memoria porque los tipos de datos similares se agrupan juntos, y también puede mejorar el rendimiento para ciertas operaciones, como la agregación de datos por columna.

Creando un Lector de Datos Orientado a Columnas

Ahora, vamos a crear un nuevo archivo que nos ayudará a leer los datos CSV en un formato orientado a columnas. Crea un nuevo archivo llamado colreader.py en el directorio del proyecto con el siguiente código:

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)

Este código hace dos cosas importantes:

  1. Define una clase DataCollection. Esta clase almacena los datos en columnas, pero nos permite acceder a los datos como si fuera una lista de diccionarios de filas. Esto es útil porque proporciona una forma familiar de trabajar con los datos.
  2. Define una función read_csv_as_columns. Esta función lee los datos CSV de un archivo y los almacena en una estructura orientada a columnas. También convierte cada campo en el archivo CSV de acuerdo con los tipos que proporcionamos.

Probando el Lector Orientado a Columnas

Probemos nuestro lector orientado a columnas utilizando los datos de autobuses del CTA. Primero, abre un intérprete de Python. Puedes hacer esto ejecutando el siguiente comando en tu terminal:

python3

Una vez que el intérprete de Python esté abierto, ejecuta el siguiente código:

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 salida debería ser así:

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

Ahora, comparemos esto con nuestro enfoque anterior orientado a filas. Ejecuta el siguiente código en el mismo intérprete de 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 salida debería ser algo como esto:

Current memory usage (row-oriented): 170.23 MB
Peak memory usage (row-oriented): 190.05 MB

Como se puede ver, el enfoque orientado a columnas utiliza significativamente menos memoria.

Probemos también que todavía podemos analizar los datos como antes. Ejecuta el siguiente código:

## 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 salida debería ser:

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

Finalmente, sal del intérprete de Python ejecutando el siguiente comando:

exit()

Podemos ver que el enfoque orientado a columnas no solo ahorra memoria, sino que también nos permite realizar los mismos análisis que antes. Esto muestra cómo diferentes estrategias de almacenamiento de datos pueden tener un impacto significativo en el rendimiento mientras todavía proporcionan la misma interfaz para trabajar con los datos.

Resumen

En este laboratorio, has aprendido varios conceptos clave de Python. En primer lugar, has comprendido cómo Python trata a las funciones, tipos y otras entidades como objetos de primera clase, lo que les permite ser pasados y almacenados como datos normales. En segundo lugar, has creado funciones de utilidad reutilizables para el procesamiento de datos CSV con conversión de tipos automática.

Además, has explorado el modelo de memoria de Python y has utilizado el internamiento de cadenas para reducir el uso de memoria en datos repetitivos. También has implementado un método de almacenamiento orientado a columnas más eficiente para grandes conjuntos de datos, proporcionando una interfaz de usuario familiar. Estos conceptos demuestran la flexibilidad y el poder de Python en el procesamiento de datos, y las técnicas se pueden aplicar a proyectos de análisis de datos del mundo real.