Explorando o Modelo de Memória e Objetos de Primeira Classe do Python

Intermediate

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

Introdução

Neste laboratório, você aprenderá sobre o conceito de objeto de primeira classe (first-class object) do Python e explorará seu modelo de memória. O Python trata funções, tipos e dados como objetos de primeira classe, permitindo padrões de programação poderosos e flexíveis.

Você também criará funções utilitárias reutilizáveis para processamento de dados CSV. Especificamente, você criará uma função generalizada para ler dados CSV no arquivo reader.py, que pode ser reutilizada em diferentes projetos.

Este é um Lab Guiado, que fornece instruções passo a passo para ajudá-lo a aprender e praticar. Siga as instruções cuidadosamente para completar cada etapa e ganhar experiência prática. Dados históricos mostram que este é um laboratório de nível iniciante com uma taxa de conclusão de 85%. Recebeu uma taxa de avaliações positivas de 100% dos estudantes.

Compreendendo Objetos de Primeira Classe em Python

Em Python, tudo é tratado como um objeto. Isso inclui funções e tipos. O que isso significa? Bem, significa que você pode armazenar funções e tipos em estruturas de dados, passá-los como argumentos para outras funções e até mesmo retorná-los de outras funções. Este é um conceito muito poderoso, e vamos explorá-lo usando o processamento de dados CSV como exemplo.

Explorando Tipos de Primeira Classe

Primeiro, vamos iniciar o interpretador Python. Abra um novo terminal no WebIDE e digite o seguinte comando. Este comando iniciará o interpretador Python, que é onde executaremos nosso código Python.

python3

Ao trabalhar com arquivos CSV em Python, frequentemente precisamos converter as strings que lemos desses arquivos em tipos de dados apropriados. Por exemplo, um número em um arquivo CSV pode ser lido como uma string, mas queremos usá-lo como um inteiro ou um float em nosso código Python. Para fazer isso, podemos criar uma lista de funções de conversão.

coltypes = [str, int, float]

Observe que estamos criando uma lista que contém as funções de tipo reais, não strings. Em Python, os tipos são objetos de primeira classe, o que significa que podemos tratá-los como qualquer outro objeto. Podemos colocá-los em listas, passá-los e usá-los em nosso código.

Agora, vamos ler alguns dados de um arquivo CSV de portfólio para ver como podemos usar essas funções de conversão.

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

Quando você executar este código, deverá ver uma saída semelhante à seguinte. Esta é a primeira linha de dados do arquivo CSV, representada como uma lista de strings.

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

Em seguida, usaremos a função zip. A função zip recebe múltiplos iteráveis (como listas ou tuplas) e emparelha seus elementos. Usaremos para emparelhar cada valor da linha com sua função de conversão de tipo correspondente.

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

Isso produzirá a seguinte saída. Cada par contém uma função de tipo e um valor de string do arquivo CSV.

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

Agora que temos esses pares, podemos aplicar cada função para converter os valores em seus tipos apropriados.

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

A saída mostrará que os valores foram convertidos para seus tipos apropriados. A string 'AA' permanece uma string, '100' se torna o inteiro 100, e '32.20' se torna o float 32.2.

['AA', 100, 32.2]

Também podemos combinar esses valores com seus nomes de coluna para criar um dicionário. Um dicionário é uma estrutura de dados útil em Python que nos permite armazenar pares chave-valor.

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

A saída será um dicionário onde as chaves são os nomes das colunas e os valores são os dados convertidos.

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

Você pode executar todas essas etapas em uma única compreensão. Uma compreensão é uma maneira concisa de criar listas, dicionários ou conjuntos em Python.

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

A saída será o mesmo dicionário de antes.

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

Quando terminar de trabalhar no interpretador Python, você pode sair digitando o seguinte comando.

exit()

Esta demonstração mostra como o tratamento de funções do Python como objetos de primeira classe permite técnicas poderosas de processamento de dados. Ao poder tratar tipos e funções como objetos, podemos escrever um código mais flexível e conciso.

Criando uma Função Utilitária para Processamento CSV

Agora que entendemos como os objetos de primeira classe (first-class objects) do Python podem nos ajudar com a conversão de dados, vamos criar uma função utilitária reutilizável. Esta função lerá dados CSV e os transformará em uma lista de dicionários. Esta é uma operação muito útil porque os arquivos CSV são comumente usados para armazenar dados tabulares, e convertê-los em uma lista de dicionários facilita o trabalho com os dados em Python.

Criando o Utilitário de Leitura CSV

Primeiro, abra o WebIDE. Depois de aberto, navegue até o diretório do projeto e crie um novo arquivo chamado reader.py. Neste arquivo, definiremos uma função que lê dados CSV e aplica conversões de tipo. As conversões de tipo são importantes porque os dados em um arquivo CSV geralmente são lidos como strings, mas podemos precisar de diferentes tipos de dados, como inteiros ou números de ponto flutuante, para processamento posterior.

Adicione o seguinte código a reader.py:

import csv

def read_csv_as_dicts(filename, types):
    """
    Leia um arquivo CSV em uma lista de dicionários, convertendo cada campo de acordo
    com os tipos fornecidos.

    Parâmetros:
    filename (str): Nome do arquivo CSV a ser lido
    types (list): Lista de funções de conversão de tipo para cada coluna

    Retorna:
    list: Lista de dicionários representando os dados CSV
    """
    records = []
    with open(filename, 'r') as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Obtenha os cabeçalhos das colunas

        for row in rows:
            ## Aplique conversões de tipo a cada valor na linha
            converted_row = [func(val) for func, val in zip(types, row)]

            ## Crie um dicionário mapeando os cabeçalhos para os valores convertidos
            record = dict(zip(headers, converted_row))
            records.append(record)

    return records

Esta função primeiro abre o arquivo CSV especificado. Em seguida, lê os cabeçalhos do arquivo CSV, que são os nomes das colunas. Depois disso, ela percorre cada linha no arquivo. Para cada valor na linha, ela aplica a função de conversão de tipo correspondente da lista types. Finalmente, ela cria um dicionário onde as chaves são os cabeçalhos das colunas e os valores são os dados convertidos, e adiciona este dicionário à lista records. Depois que todas as linhas são processadas, ela retorna a lista records.

Testando a Função Utilitária

Vamos testar nossa função utilitária. Primeiro, abra um terminal e inicie um interpretador Python digitando:

python3

Agora que estamos no interpretador Python, podemos usar nossa função para ler os dados do portfólio. Os dados do portfólio são um arquivo CSV que contém informações sobre ações, como o nome da ação, o número de ações e o preço.

import reader
portfolio = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
for record in portfolio[:3]:  ## Mostre os primeiros 3 registros
    print(record)

Quando você executar este código, deverá ver uma saída semelhante a:

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

Esta saída mostra os três primeiros registros dos dados do portfólio, com os tipos de dados corretamente convertidos.

Vamos também testar nossa função com os dados do ônibus CTA. Os dados do ônibus CTA são outro arquivo CSV que contém informações sobre rotas de ônibus, datas, tipos de dia e o número de viagens.

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

A saída deve ser algo como:

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

Isso mostra que nossa função pode lidar com diferentes arquivos CSV e aplicar as conversões de tipo apropriadas.

Para sair do interpretador Python, digite:

exit()

Você agora criou uma função utilitária reutilizável que pode ler qualquer arquivo CSV e aplicar as conversões de tipo apropriadas. Isso demonstra o poder dos objetos de primeira classe do Python e como eles podem ser usados para criar um código flexível e reutilizável.

Explorando o Modelo de Memória do Python

O modelo de memória do Python desempenha um papel crucial na determinação de como os objetos são armazenados na memória e como eles são referenciados. Compreender este modelo é essencial, especialmente ao lidar com grandes conjuntos de dados, pois pode impactar significativamente o desempenho e o uso de memória de seus programas Python. Nesta etapa, focaremos especificamente em como os objetos string são tratados em Python e exploraremos maneiras de otimizar o uso de memória para grandes conjuntos de dados.

Repetição de Strings em Conjuntos de Dados

Os dados do ônibus CTA contêm muitos valores repetidos, como nomes de rotas. Valores repetidos em um conjunto de dados podem levar ao uso ineficiente de memória se não forem tratados corretamente. Para entender a extensão desse problema, vamos primeiro examinar quantos strings de rota exclusivos existem no conjunto de dados.

Primeiro, abra um interpretador Python. Você pode fazer isso executando o seguinte comando em seu terminal:

python3

Depois que o interpretador Python estiver aberto, carregaremos os dados do ônibus CTA e encontraremos as rotas exclusivas. Aqui está o código para conseguir isso:

import reader
rows = reader.read_csv_as_dicts('ctabus.csv', [str, str, str, int])

## Encontre nomes de rotas exclusivos
routes = {row['route'] for row in rows}
print(f"Número de nomes de rotas exclusivos: {len(routes)}")

Neste código, primeiro importamos o módulo reader, que presumivelmente contém uma função para ler arquivos CSV como dicionários. Em seguida, usamos a função read_csv_as_dicts para carregar os dados do arquivo ctabus.csv. O segundo argumento [str, str, str, int] especifica os tipos de dados para cada coluna no arquivo CSV. Depois disso, usamos uma compreensão de conjunto para encontrar todos os nomes de rotas exclusivos no conjunto de dados e imprimir o número de nomes de rotas exclusivos.

A saída deve ser:

Número de nomes de rotas exclusivos: 181

Agora, vamos verificar quantos objetos string diferentes são criados para essas rotas. Embora existam apenas 181 nomes de rotas exclusivos, o Python pode criar um novo objeto string para cada ocorrência de um nome de rota no conjunto de dados. Para verificar isso, usaremos a função id() para obter o identificador exclusivo de cada objeto string.

## Contar IDs de objetos string exclusivos
routeids = {id(row['route']) for row in rows}
print(f"Número de objetos string de rota exclusivos: {len(routeids)}")

A saída pode surpreendê-lo:

Número de objetos string de rota exclusivos: 542305

Isso mostra que existem apenas 181 nomes de rotas exclusivos, mas mais de 500.000 objetos string exclusivos. Isso acontece porque o Python cria um novo objeto string para cada linha, mesmo que os valores sejam os mesmos. Isso pode levar a um desperdício significativo de memória, especialmente ao lidar com grandes conjuntos de dados.

String Interning para Economizar Memória

O Python fornece uma maneira de "internar" (reutilizar) strings usando a função sys.intern(). O string interning pode economizar memória quando você tem muitas strings duplicadas em seu conjunto de dados. Quando você interna uma string, o Python verifica se uma string idêntica já existe no pool de internamento. Se existir, ele retorna uma referência ao objeto string existente em vez de criar um novo.

Vamos demonstrar como o string interning funciona com um exemplo simples:

import sys

## Sem interning
a = 'hello world'
b = 'hello world'
print(f"a é b (sem interning): {a is b}")

## Com interning
a = sys.intern(a)
b = sys.intern(b)
print(f"a é b (com interning): {a is b}")

Neste código, primeiro criamos duas variáveis string a e b com o mesmo valor sem interning. O operador is verifica se duas variáveis se referem ao mesmo objeto. Sem interning, a e b são objetos diferentes, então a is b retorna False. Em seguida, internamos ambas as strings usando sys.intern(). Após o interning, a e b se referem ao mesmo objeto no pool de internamento, então a is b retorna True.

A saída deve ser:

a é b (sem interning): False
a é b (com interning): True

Agora, vamos usar o string interning ao ler os dados do ônibus CTA para reduzir o uso de memória. Também usaremos o módulo tracemalloc para rastrear o uso de memória antes e depois do interning.

import sys
import reader
import tracemalloc

## Iniciar o rastreamento de memória
tracemalloc.start()

## Ler dados com interning para a coluna de rota
rows = reader.read_csv_as_dicts('ctabus.csv', [sys.intern, str, str, int])

## Verificar objetos de rota exclusivos novamente
routeids = {id(row['route']) for row in rows}
print(f"Número de objetos string de rota exclusivos (com interning): {len(routeids)}")

## Verificar o uso de memória
current, peak = tracemalloc.get_traced_memory()
print(f"Uso de memória atual: {current / 1024 / 1024:.2f} MB")
print(f"Uso de memória de pico: {peak / 1024 / 1024:.2f} MB")

Neste código, primeiro iniciamos o rastreamento de memória usando tracemalloc.start(). Em seguida, lemos os dados do ônibus CTA com interning para a coluna de rota, passando sys.intern como o tipo de dados para a primeira coluna. Depois disso, verificamos o número de objetos string de rota exclusivos novamente e imprimimos o uso de memória atual e de pico.

A saída deve ser algo como:

Número de objetos string de rota exclusivos (com interning): 181
Uso de memória atual: 189.56 MB
Uso de memória de pico: 209.32 MB

Vamos reiniciar o interpretador e tentar internar as strings de rota e data para ver se podemos reduzir ainda mais o uso de memória.

exit()

Inicie o Python novamente:

python3
import sys
import reader
import tracemalloc

## Iniciar o rastreamento de memória
tracemalloc.start()

## Ler dados com interning para as colunas de rota e data
rows = reader.read_csv_as_dicts('ctabus.csv', [sys.intern, sys.intern, str, int])

## Verificar o uso de memória
current, peak = tracemalloc.get_traced_memory()
print(f"Uso de memória atual (interning rota e data): {current / 1024 / 1024:.2f} MB")
print(f"Uso de memória de pico (interning rota e data): {peak / 1024 / 1024:.2f} MB")

A saída deve mostrar uma diminuição adicional no uso de memória:

Uso de memória atual (interning rota e data): 170.23 MB
Uso de memória de pico (interning rota e data): 190.05 MB

Isso demonstra como a compreensão do modelo de memória do Python e o uso de técnicas como string interning podem ajudar a otimizar seus programas, especialmente ao lidar com grandes conjuntos de dados contendo valores repetidos.

Finalmente, saia do interpretador Python:

exit()

Armazenamento de Dados Orientado a Colunas

Até agora, temos armazenado dados CSV como uma lista de dicionários de linhas. Isso significa que cada linha no arquivo CSV é representada como um dicionário, onde as chaves são os cabeçalhos das colunas e os valores são os dados correspondentes nessa linha. No entanto, ao lidar com grandes conjuntos de dados, esse método pode ser ineficiente. Armazenar dados em um formato orientado a colunas pode ser uma escolha melhor. Em uma abordagem orientada a colunas, os dados de cada coluna são armazenados em uma lista separada. Isso pode reduzir significativamente o uso de memória porque tipos de dados semelhantes são agrupados e também pode melhorar o desempenho para certas operações, como agregar dados por coluna.

Criando um Leitor de Dados Orientado a Colunas

Agora, vamos criar um novo arquivo que nos ajudará a ler dados CSV em um formato orientado a colunas. Crie um novo arquivo chamado colreader.py no diretório do projeto com o seguinte código:

import csv

class DataCollection:
    def __init__(self, headers, columns):
        """
        Inicializa uma coleção de dados orientada a colunas.

        Parâmetros:
        headers (list): Nomes dos cabeçalhos das colunas
        columns (dict): Dicionário mapeando nomes de cabeçalhos para listas de dados de colunas
        """
        self.headers = headers
        self.columns = columns
        self._length = len(columns[headers[0]]) if headers else 0

    def __len__(self):
        """Retorna o número de linhas na coleção."""
        return self._length

    def __getitem__(self, index):
        """
        Obtém uma linha por índice, apresentada como um dicionário.

        Parâmetros:
        index (int): Índice da linha

        Retorna:
        dict: Dicionário representando a linha no índice fornecido
        """
        if isinstance(index, int):
            if index < 0 or index >= self._length:
                raise IndexError("Índice fora do intervalo")

            return {header: self.columns[header][index] for header in self.headers}
        else:
            raise TypeError("Índice deve ser um inteiro")

def read_csv_as_columns(filename, types):
    """
    Lê um arquivo CSV em uma estrutura de dados orientada a colunas, convertendo cada campo
    de acordo com os tipos fornecidos.

    Parâmetros:
    filename (str): Nome do arquivo CSV a ser lido
    types (list): Lista de funções de conversão de tipo para cada coluna

    Retorna:
    DataCollection: Coleção de dados orientada a colunas representando os dados CSV
    """
    with open(filename, 'r') as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Obtenha os cabeçalhos das colunas

        ## Inicialize as colunas
        columns = {header: [] for header in headers}

        ## Leia os dados nas colunas
        for row in rows:
            ## Converta os valores de acordo com os tipos especificados
            converted_values = [func(val) for func, val in zip(types, row)]

            ## Adicione cada valor à sua coluna correspondente
            for header, value in zip(headers, converted_values):
                columns[header].append(value)

    return DataCollection(headers, columns)

Este código faz duas coisas importantes:

  1. Ele define uma classe DataCollection. Esta classe armazena dados em colunas, mas nos permite acessar os dados como se fossem uma lista de dicionários de linhas. Isso é útil porque fornece uma maneira familiar de trabalhar com os dados.
  2. Ele define uma função read_csv_as_columns. Esta função lê dados CSV de um arquivo e os armazena em uma estrutura orientada a colunas. Ele também converte cada campo no arquivo CSV de acordo com os tipos que fornecemos.

Testando o Leitor Orientado a Colunas

Vamos testar nosso leitor orientado a colunas usando os dados do ônibus CTA. Primeiro, abra um interpretador Python. Você pode fazer isso executando o seguinte comando em seu terminal:

python3

Depois que o interpretador Python estiver aberto, execute o seguinte código:

import colreader
import tracemalloc
from sys import intern

## Iniciar o rastreamento de memória
tracemalloc.start()

## Ler dados em estrutura orientada a colunas com string interning
data = colreader.read_csv_as_columns('ctabus.csv', [intern, intern, intern, int])

## Verifique se podemos acessar os dados como uma lista de dicionários
print(f"Número de linhas: {len(data)}")
print("Primeiras 3 linhas:")
for i in range(3):
    print(data[i])

## Verificar o uso de memória
current, peak = tracemalloc.get_traced_memory()
print(f"Uso de memória atual: {current / 1024 / 1024:.2f} MB")
print(f"Uso de memória de pico: {peak / 1024 / 1024:.2f} MB")

A saída deve ser semelhante a esta:

Número de linhas: 577563
Primeiras 3 linhas:
{'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}
Uso de memória atual: 38.67 MB
Uso de memória de pico: 103.42 MB

Agora, vamos comparar isso com nossa abordagem anterior orientada a linhas. Execute o seguinte código no mesmo interpretador Python:

import reader
import tracemalloc
from sys import intern

## Redefinir o rastreamento de memória
tracemalloc.reset_peak()

## Ler dados em estrutura orientada a linhas com string interning
rows = reader.read_csv_as_dicts('ctabus.csv', [intern, intern, intern, int])

## Verificar o uso de memória
current, peak = tracemalloc.get_traced_memory()
print(f"Uso de memória atual (orientado a linhas): {current / 1024 / 1024:.2f} MB")
print(f"Uso de memória de pico (orientado a linhas): {peak / 1024 / 1024:.2f} MB")

A saída deve ser algo como:

Uso de memória atual (orientado a linhas): 170.23 MB
Uso de memória de pico (orientado a linhas): 190.05 MB

Como você pode ver, a abordagem orientada a colunas usa significativamente menos memória!

Vamos também testar se ainda podemos analisar os dados como antes. Execute o seguinte código:

## Encontrar todas as rotas exclusivas nos dados orientados a colunas
routes = {row['route'] for row in data}
print(f"Número de rotas exclusivas: {len(routes)}")

## Contar viagens por rota (primeiros 5)
from collections import defaultdict
route_rides = defaultdict(int)
for row in data:
    route_rides[row['route']] += row['rides']

## Mostrar as 5 principais rotas por total de viagens
top_routes = sorted(route_rides.items(), key=lambda x: x[1], reverse=True)[:5]
print("5 principais rotas por total de viagens:")
for route, rides in top_routes:
    print(f"Rota {route}: {rides:,} viagens")

A saída deve ser:

Número de rotas exclusivas: 181
5 principais rotas por total de viagens:
Rota 9: 158,545,826 viagens
Rota 49: 129,872,910 viagens
Rota 77: 120,086,065 viagens
Rota 79: 109,348,708 viagens
Rota 4: 91,405,538 viagens

Finalmente, saia do interpretador Python executando o seguinte comando:

exit()

Podemos ver que a abordagem orientada a colunas não apenas economiza memória, mas também nos permite realizar as mesmas análises de antes. Isso mostra como diferentes estratégias de armazenamento de dados podem ter um impacto significativo no desempenho, mantendo a mesma interface para trabalharmos com os dados.

Resumo

Neste laboratório, você aprendeu vários conceitos-chave do Python. Primeiro, você entendeu como o Python trata funções, tipos e outras entidades como objetos de primeira classe (first-class objects), permitindo que sejam passados e armazenados como dados regulares. Segundo, você criou funções utilitárias reutilizáveis para processamento de dados CSV com conversão automática de tipos.

Além disso, você explorou o modelo de memória do Python e usou o string interning para reduzir o uso de memória para dados repetitivos. Você também implementou um método de armazenamento orientado a colunas mais eficiente para grandes conjuntos de dados, fornecendo uma interface de usuário familiar. Esses conceitos demonstram a flexibilidade e o poder do Python no processamento de dados, e as técnicas podem ser aplicadas a projetos de análise de dados do mundo real.