Itera como un profesional

Beginner

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

Introducción

En este laboratorio, aprenderás sobre el concepto fundamental de iteración en la programación de Python. La iteración te permite procesar de manera eficiente los elementos en secuencias como listas, tuplas y diccionarios. Dominar las técnicas de iteración puede mejorar significativamente tus habilidades de codificación en Python.

Explorarás varias poderosas técnicas de iteración en Python, incluyendo la iteración básica con el bucle for, el desempaquetado de secuencias, el uso de funciones integradas como enumerate() y zip(), y aprovechar las expresiones generadoras para una mejor eficiencia de memoria.

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 96%. Ha recibido una tasa de reseñas positivas del 100% por parte de los estudiantes.

Iteración básica y desempaquetado de secuencias

En este paso, exploraremos la iteración básica utilizando bucles for y el desempaquetado de secuencias en Python. La iteración es un concepto fundamental en la programación, que te permite recorrer cada elemento de una secuencia uno por uno. El desempaquetado de secuencias, por otro lado, te permite asignar los elementos individuales de una secuencia a variables de manera conveniente.

Cargar datos desde un archivo CSV

Comencemos cargando algunos datos desde un archivo CSV. CSV (Comma-Separated Values, Valores Separados por Comas) es un formato de archivo común utilizado para almacenar datos tabulares. Para comenzar, necesitamos abrir un terminal en el WebIDE y iniciar el intérprete de Python. Esto nos permitirá ejecutar código Python de forma interactiva.

cd ~/project
python3

Ahora que estamos en el intérprete de Python, podemos ejecutar el siguiente código Python para leer datos del archivo portfolio.csv. Primero, importamos el módulo csv, que proporciona funcionalidades para trabajar con archivos CSV. Luego, abrimos el archivo y creamos un objeto csv.reader para leer los datos. Utilizamos la función next para obtener los encabezados de las columnas y convertimos el resto de los datos en una lista. Finalmente, utilizamos la función pprint del módulo pprint para imprimir las filas en un formato más legible.

import csv

f = open('portfolio.csv')
f_csv = csv.reader(f)
headers = next(f_csv)    ## Get the column headers
rows = list(f_csv)       ## Convert the remaining data to a list
from pprint import pprint
pprint(rows)             ## Pretty print the rows

Deberías ver una salida similar a esta:

[['AA', '100', '32.20'],
 ['IBM', '50', '91.10'],
 ['CAT', '150', '83.44'],
 ['MSFT', '200', '51.23'],
 ['GE', '95', '40.37'],
 ['MSFT', '50', '65.10'],
 ['IBM', '100', '70.44']]

Iteración básica con bucles for

La declaración for en Python se utiliza para iterar sobre cualquier secuencia de datos, como una lista, tupla o cadena. En nuestro caso, la utilizaremos para iterar sobre las filas de datos que cargamos desde el archivo CSV.

for row in rows:
    print(row)

Este código recorrerá cada fila de la lista rows e imprimirá cada una. Verás cada fila de datos de nuestro archivo CSV impresa una por una.

['AA', '100', '32.20']
['IBM', '50', '91.10']
['CAT', '150', '83.44']
['MSFT', '200', '51.23']
['GE', '95', '40.37']
['MSFT', '50', '65.10']
['IBM', '100', '70.44']

Desempaquetado de secuencias en bucles

Python te permite desempaquetar secuencias directamente en un bucle for. Esto es muy útil cuando conoces la estructura de cada elemento de la secuencia. En nuestro caso, cada fila de la lista rows contiene tres elementos: un nombre, el número de acciones y el precio. Podemos desempaquetar estos elementos directamente en el bucle for.

for name, shares, price in rows:
    print(name, shares, price)

Este código desempaquetará cada fila en las variables name, shares y price, y luego las imprimirá. Verás los datos impresos en un formato más legible.

AA 100 32.20
IBM 50 91.10
CAT 150 83.44
MSFT 200 51.23
GE 95 40.37
MSFT 50 65.10
IBM 100 70.44

Si no necesitas algunos valores, puedes utilizar _ como un marcador de posición para indicar que no te importan esos valores. Por ejemplo, si solo quieres imprimir el nombre y el precio, puedes utilizar el siguiente código:

for name, _, price in rows:
    print(name, price)

Este código ignorará el segundo elemento de cada fila e imprimirá solo el nombre y el precio.

AA 32.20
IBM 91.10
CAT 83.44
MSFT 51.23
GE 40.37
MSFT 65.10
IBM 70.44

Desempaquetado extendido con el operador *

Para un desempaquetado más avanzado, puedes utilizar el operador * como un comodín. Esto te permite recopilar múltiples elementos en una lista. Agrupemos nuestros datos por nombre utilizando esta técnica.

from collections import defaultdict

byname = defaultdict(list)
for name, *data in rows:
    byname[name].append(data)

## Print the data for IBM
print(byname['IBM'])

## Iterate through IBM's data
for shares, price in byname['IBM']:
    print(shares, price)

En este código, primero importamos la clase defaultdict del módulo collections. Un defaultdict es un diccionario que crea automáticamente un nuevo valor (en este caso, una lista vacía) si la clave no existe. Luego, utilizamos el operador * para recopilar todos los elementos excepto el primero en una lista llamada data. Almacenamos esta lista en el diccionario byname, agrupada por el nombre. Finalmente, imprimimos los datos de IBM y los recorremos para imprimir el número de acciones y el precio.

Salida:

[['50', '91.10'], ['100', '70.44']]
50 91.10
100 00.44

En este ejemplo, *data recopila todos los elementos excepto el primero en una lista, que luego almacenamos en un diccionario agrupado por nombre. Esta es una técnica poderosa para manejar datos con secuencias de longitud variable.

Uso de las funciones enumerate() y zip()

En este paso, exploraremos dos funciones integradas (built - in) en Python increíblemente útiles que son esenciales para la iteración: enumerate() y zip(). Estas funciones pueden simplificar significativamente tu código cuando trabajas con secuencias.

Contando con enumerate()

Cuando estás iterando sobre una secuencia, a menudo es necesario llevar un registro del índice o posición de cada elemento. Ahí es donde la función enumerate() resulta útil. La función enumerate() toma una secuencia como entrada y devuelve pares de (índice, valor) para cada elemento de esa secuencia.

Si has estado siguiendo en el intérprete de Python del paso anterior, puedes continuar usando el mismo. De lo contrario, inicia una nueva sesión. Aquí te mostramos cómo configurar los datos si estás comenzando desde cero:

## If you're starting a new session, reload the data first:
## import csv
## f = open('portfolio.csv')
## f_csv = csv.reader(f)
## headers = next(f_csv)
## rows = list(f_csv)

## Use enumerate to get row numbers
for rowno, row in enumerate(rows):
    print(rowno, row)

Cuando ejecutas el código anterior, la función enumerate(rows) generará pares de un índice (comenzando desde 0) y la fila correspondiente de la secuencia rows. Luego, el bucle for desempaqueta estos pares en las variables rowno y row, y los imprimimos.

Salida:

0 ['AA', '100', '32.20']
1 ['IBM', '50', '91.10']
2 ['CAT', '150', '83.44']
3 ['MSFT', '200', '51.23']
4 ['GE', '95', '40.37']
5 ['MSFT', '50', '65.10']
6 ['IBM', '100', '70.44']

Podemos hacer que el código sea aún más legible combinando enumerate() con el desempaquetado. El desempaquetado nos permite asignar directamente los elementos de una secuencia a variables individuales.

for rowno, (name, shares, price) in enumerate(rows):
    print(rowno, name, shares, price)

En este código, estamos usando un par adicional de paréntesis alrededor de (name, shares, price) para desempaquetar correctamente los datos de la fila. enumerate(rows) sigue dándonos el índice y la fila, pero ahora estamos desempaquetando la fila en las variables name, shares y price.

Salida:

0 AA 100 32.20
1 IBM 50 91.10
2 CAT 150 83.44
3 MSFT 200 51.23
4 GE 95 40.37
5 MSFT 50 65.10
6 IBM 100 70.44

Emparejando datos con zip()

La función zip() es otra herramienta poderosa en Python. Se utiliza para combinar elementos correspondientes de múltiples secuencias. Cuando pasas múltiples secuencias a zip(), crea un iterador que produce tuplas, donde cada tupla contiene elementos de cada una de las secuencias de entrada en la misma posición.

Veamos cómo podemos usar zip() con los datos de headers y row con los que hemos estado trabajando.

## Recall the headers variable from earlier
print(headers)  ## Should show ['name', 'shares', 'price']

## Get the first row
row = rows[0]
print(row)      ## Should show ['AA', '100', '32.20']

## Use zip to pair column names with values
for col, val in zip(headers, row):
    print(col, val)

En este código, zip(headers, row) toma la secuencia headers y la secuencia row y empareja sus elementos correspondientes. Luego, el bucle for desempaqueta estos pares en col (para el nombre de la columna de headers) y val (para el valor de row), y los imprimimos.

Salida:

['name', 'shares', 'price']
['AA', '100', '32.20']
name AA
shares 100
price 32.20

Un uso muy común de zip() es crear diccionarios a partir de pares clave - valor. En Python, un diccionario es una colección de pares clave - valor.

## Create a dictionary from headers and row values
record = dict(zip(headers, row))
print(record)

Aquí, zip(headers, row) crea pares de nombres de columnas y valores, y la función dict() toma estos pares y los convierte en un diccionario.

Salida:

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

Podemos extender esta idea para convertir todas las filas de nuestra secuencia rows en diccionarios.

## Convert all rows to dictionaries
for row in rows:
    record = dict(zip(headers, row))
    print(record)

En este bucle, para cada fila en rows, usamos zip(headers, row) para crear pares clave - valor y luego dict() para convertir esos pares en un diccionario. Esta técnica es muy común en aplicaciones de procesamiento de datos, especialmente cuando se trabaja con archivos CSV donde la primera fila contiene encabezados.

Salida:

{'name': 'AA', 'shares': '100', 'price': '32.20'}
{'name': 'IBM', 'shares': '50', 'price': '91.10'}
{'name': 'CAT', 'shares': '150', 'price': '83.44'}
{'name': 'MSFT', 'shares': '200', 'price': '51.23'}
{'name': 'GE', 'shares': '95', 'price': '40.37'}
{'name': 'MSFT', 'shares': '50', 'price': '65.10'}
{'name': 'IBM', 'shares': '100', 'price': '70.44'}

Expresiones generadoras y eficiencia de memoria

En este paso, exploraremos las expresiones generadoras. Estas son increíblemente útiles cuando se trabaja con grandes conjuntos de datos en Python. Pueden hacer que tu código sea mucho más eficiente en términos de memoria, lo cual es crucial cuando se maneja una gran cantidad de datos.

Comprendiendo las expresiones generadoras

Una expresión generadora es similar a una comprensión de lista, pero hay una diferencia clave. Cuando se utiliza una comprensión de lista, Python crea una lista con todos los resultados de una vez. Esto puede ocupar mucha memoria, especialmente si se está trabajando con un gran conjunto de datos. Por otro lado, una expresión generadora produce resultados uno a la vez según se necesitan. Esto significa que no necesita almacenar todos los resultados en memoria de una vez, lo que puede ahorrar una cantidad significativa de memoria.

Veamos un ejemplo sencillo para ver cómo funciona esto:

## Start a new Python session if needed
## python3

## List comprehension (creates a list in memory)
nums = [1, 2, 3, 4, 5]
squares_list = [x*x for x in nums]
print(squares_list)

## Generator expression (creates a generator object)
squares_gen = (x*x for x in nums)
print(squares_gen)  ## This doesn't print the values, just the generator object

## Iterate through the generator to get values
for n in squares_gen:
    print(n)

Cuando se ejecuta este código, se verá la siguiente salida:

[1, 4, 9, 16, 25]
<generator object <genexpr> at 0x7f...>
1
4
9
16
25

Una cosa importante a tener en cuenta sobre los generadores es que solo se pueden iterar una vez. Una vez que se han recorrido todos los valores de un generador, se agota y no se pueden obtener los valores de nuevo.

## Try to iterate again over the same generator
for n in squares_gen:
    print(n)  ## Nothing will be printed, as the generator is already exhausted

También se pueden obtener valores manualmente de un generador uno a la vez utilizando la función next().

## Create a fresh generator
squares_gen = (x*x for x in nums)

## Get values one by one
print(next(squares_gen))  ## 1
print(next(squares_gen))  ## 4
print(next(squares_gen))  ## 9

Cuando no hay más valores en el generador, llamar a next() generará una excepción StopIteration.

Funciones generadoras con yield

Para una lógica de generador más compleja, se pueden escribir funciones generadoras utilizando la declaración yield. Una función generadora es un tipo especial de función que utiliza yield para devolver valores uno a la vez en lugar de devolver un solo resultado de una vez.

def squares(nums):
    for x in nums:
        yield x*x

## Use the generator function
for n in squares(nums):
    print(n)

Cuando se ejecuta este código, se verá la siguiente salida:

1
4
9
16
25

Reduciendo el uso de memoria con expresiones generadoras

Ahora, veamos cómo las expresiones generadoras pueden ahorrar memoria cuando se trabaja con grandes conjuntos de datos. Utilizaremos el archivo de datos de autobuses CTA, que es bastante grande.

Primero, probemos un enfoque que consume mucha memoria:

import tracemalloc
tracemalloc.start()

import readrides
rows = readrides.read_rides_as_dicts('ctabus.csv')
rt22 = [row for row in rows if row['route'] == '22']
max_row = max(rt22, key=lambda row: int(row['rides']))
print(max_row)

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

Ahora, salga de Python y reinícielo para comparar con un enfoque basado en generadores:

exit() python3
import tracemalloc
tracemalloc.start()

import csv
f = open('ctabus.csv')
f_csv = csv.reader(f)
headers = next(f_csv)

## Use generator expressions for all operations
rows = (dict(zip(headers, row)) for row in f_csv)
rt22 = (row for row in rows if row['route'] == '22')
max_row = max(rt22, key=lambda row: int(row['rides']))
print(max_row)

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

Debería notar una diferencia significativa en el uso de memoria entre estos dos enfoques. El enfoque basado en generadores procesa los datos de forma incremental sin cargar todo en memoria de una vez, lo que es mucho más eficiente en términos de memoria.

Expresiones generadoras con funciones de reducción

Las expresiones generadoras son particularmente útiles cuando se combinan con funciones como sum(), min(), max(), any() y all() que procesan una secuencia completa y producen un solo resultado.

Veamos algunos ejemplos:

from readport import read_portfolio
portfolio = read_portfolio('portfolio.csv')

## Calculate the total value of the portfolio
total_value = sum(s['shares']*s['price'] for s in portfolio)
print(f"Total portfolio value: {total_value}")

## Find the minimum number of shares in any holding
min_shares = min(s['shares'] for s in portfolio)
print(f"Minimum shares in any holding: {min_shares}")

## Check if any stock is IBM
has_ibm = any(s['name'] == 'IBM' for s in portfolio)
print(f"Portfolio contains IBM: {has_ibm}")

## Check if all stocks are IBM
all_ibm = all(s['name'] == 'IBM' for s in portfolio)
print(f"All stocks are IBM: {all_ibm}")

## Count IBM shares
ibm_shares = sum(s['shares'] for s in portfolio if s['name'] == 'IBM')
print(f"Total IBM shares: {ibm_shares}")

Las expresiones generadoras también son útiles para operaciones de cadenas. Aquí está cómo unir valores en una tupla:

s = ('GOOG', 100, 490.10)
## This would fail: ','.join(s)
## Use a generator expression to convert all items to strings
joined = ','.join(str(x) for x in s)
print(joined)  ## Output: 'GOOG,100,490.1'

La principal ventaja de utilizar expresiones generadoras en estos ejemplos es que no se crean listas intermedias, lo que resulta en un código más eficiente en términos de memoria.

Resumen

En este laboratorio, has aprendido varias técnicas de iteración en Python muy poderosas. En primer lugar, dominaste la iteración básica y el desempaquetado de secuencias, utilizando bucles for para iterar sobre secuencias y desempaquetarlas en variables individuales. En segundo lugar, exploraste funciones integradas (built - in) como enumerate() para realizar un seguimiento de los índices durante la iteración y zip() para emparejar elementos de diferentes secuencias.

Estas técnicas son fundamentales para la programación eficiente en Python. Te permiten escribir código más conciso, legible y eficiente en términos de memoria. Al dominar estos patrones de iteración, puedes manejar tareas de procesamiento de datos de manera más efectiva en tus proyectos de Python.