itertools — Funciones de iterador

Propósito:El módulo itertools incluye un conjunto de funciones para trabajar con conjuntos de datos de secuencia.

Las funciones proporcionadas por itertools están inspiradas en características de lenguajes de programación funcional como Clojure, Haskell, APL y SML. Están destinadas a ser rápidas y usar la memoria de manera eficiente, y también para ser enganchadas juntas para expresar algoritmos más complicados basados en iteración.

El código basado en iteradores ofrece mejores características de mejor consumo de memoria que el código que usa listas. Dado que los datos no se producen a partir del iterador hasta que sea necesario, no es necesario que todos los datos sean almacenados en la memoria al mismo tiempo. Este modelo de procesamiento «perezoso» puede reducir el intercambio y otros efectos secundarios de grandes conjuntos de datos, mejorando el rendimiento.

Además de las funciones definidas en itertools, los ejemplos en esta sección también se basan en algunas de las funciones incorporadas para iteración.

Fusionando y dividiendo iteradores

La función chain() toma varios iteradores como argumentos y devuelve un único iterador que produce el contenido de todas las entradas como si vinieran de un solo iterador.

itertools_chain.py
from itertools import *

for i in chain([1, 2, 3], ['a', 'b', 'c']):
    print(i, end=' ')
print()

chain() facilita el procesamiento de varias secuencias sin construir una lista grande.

$ python3 itertools_chain.py

1 2 3 a b c

Si los elementos que se van a combinar no son conocidos por adelantado o no son necesarios para ser evaluado perezosamente, se puede chain.from_iterable() usar para construir la cadena en su lugar.

itertools_chain_from_iterable.py
from itertools import *


def make_iterables_to_chain():
    yield [1, 2, 3]
    yield ['a', 'b', 'c']


for i in chain.from_iterable(make_iterables_to_chain()):
    print(i, end=' ')
print()
$ python3 itertools_chain_from_iterable.py

1 2 3 a b c

La función incorporada zip() devuelve un iterador que combina los elementos de varios iteradores en tuplas.

itertools_zip.py
for i in zip([1, 2, 3], ['a', 'b', 'c']):
    print(i)

Al igual que con las otras funciones de este módulo, el valor de retorno es un objeto iterable que produce valores uno a la vez.

$ python3 itertools_zip.py

(1, 'a')
(2, 'b')
(3, 'c')

zip() se detiene cuando el primer iterador de entrada se agota. Para procesar todas las entradas, incluso si los iteradores producen diferentes números de valores, usa zip_longest().

itertools_zip_longest.py
from itertools import *

r1 = range(3)
r2 = range(2)

print('zip stops early:')
print(list(zip(r1, r2)))

r1 = range(3)
r2 = range(2)

print('\nzip_longest processes all of the values:')
print(list(zip_longest(r1, r2)))

Por defecto, zip_longest() sustituye valores faltantes por None. Usa el argumento fillvalue para usar un valor sustituto diferente.

$ python3 itertools_zip_longest.py

zip stops early:
[(0, 0), (1, 1)]

zip_longest processes all of the values:
[(0, 0), (1, 1), (2, None)]

La función islice() devuelve un iterador que devuelve elementos seleccionados del iterador de entrada, por índice.

itertools_islice.py
from itertools import *

print('Stop at 5:')
for i in islice(range(100), 5):
    print(i, end=' ')
print('\n')

print('Start at 5, Stop at 10:')
for i in islice(range(100), 5, 10):
    print(i, end=' ')
print('\n')

print('By tens to 100:')
for i in islice(range(100), 0, 100, 10):
    print(i, end=' ')
print('\n')

islice() toma los mismos argumentos que el operador de división para listas: start, stop, y step. Los argumentos de inicio y paso son opcionales.

$ python3 itertools_islice.py

Stop at 5:
0 1 2 3 4

Start at 5, Stop at 10:
5 6 7 8 9

By tens to 100:
0 10 20 30 40 50 60 70 80 90

La función tee() devuelve varios iteradores independientes (por defecto 2) basados en una sola entrada original.

itertools_tee.py
from itertools import *

r = islice(count(), 5)
i1, i2 = tee(r)

print('i1:', list(i1))
print('i2:', list(i2))

tee() tiene una semántica similar a la utilidad tee de Unix, que repite los valores que lee de su entrada y los escribe en un archivo nombrado y la salida estándar. Los iteradores devueltos por tee() se puede utilizar para alimentar el mismo conjunto de datos en múltiples algoritmos para ser procesados en paralelo.

$ python3 itertools_tee.py

i1: [0, 1, 2, 3, 4]
i2: [0, 1, 2, 3, 4]

Los nuevos iteradores creados por tee() comparten su entrada, por lo que el iterador original no debe usarse después de que se creen los nuevos.

itertools_tee_error.py
from itertools import *

r = islice(count(), 5)
i1, i2 = tee(r)

print('r:', end=' ')
for i in r:
    print(i, end=' ')
    if i > 1:
        break
print()

print('i1:', list(i1))
print('i2:', list(i2))

Si los valores se consumen desde la entrada original, los nuevos iteradores no producirán estos valores:

$ python3 itertools_tee_error.py

r: 0 1 2
i1: [3, 4]
i2: [3, 4]

Conversión de entradas

La función incorporada map() devuelve un iterador que invoca una función con los valores en los iteradores de entrada, y devuelve los resultados. Se detiene cuando se agota cualquier iterador de entrada.

itertools_map.py

def times_two(x):
    return 2 * x


def multiply(x, y):
    return (x, y, x * y)


print('Doubles:')
for i in map(times_two, range(5)):
    print(i)

print('\nMultiples:')
r1 = range(5)
r2 = range(5, 10)
for i in map(multiply, r1, r2):
    print('{:d} * {:d} = {:d}'.format(*i))

print('\nStopping:')
r1 = range(5)
r2 = range(2)
for i in map(multiply, r1, r2):
    print(i)

En el primer ejemplo, la función lambda multiplica los valores de entrada por 2. En un segundo ejemplo, la función lambda multiplica dos argumentos, tomados de iteradores separados, y devuelve una tupla con los argumentos originales y el valor calculado. El tercer ejemplo se detiene después de producir dos tuplas porque el segundo rango está agotado.

$ python3 itertools_map.py

Doubles:
0
2
4
6
8

Multiples:
0 * 5 = 0
1 * 6 = 6
2 * 7 = 14
3 * 8 = 24
4 * 9 = 36

Stopping:
(0, 0, 0)
(1, 1, 1)

La función starmap() es similar a map(), pero en lugar de contruir una tupla de múltiples iteradores, divide los elementos en un solo iterador como argumentos a la función de mapeo usando la sintaxis *.

itertools_starmap.py
from itertools import *

values = [(0, 5), (1, 6), (2, 7), (3, 8), (4, 9)]

for i in starmap(lambda x, y: (x, y, x * y), values):
    print('{} * {} = {}'.format(*i))

Donde la función de mapeo a map() se llama f(i1, i2), la función de mapeo pasada a starmap() se llama f(*i).

$ python3 itertools_starmap.py

0 * 5 = 0
1 * 6 = 6
2 * 7 = 14
3 * 8 = 24
4 * 9 = 36

Produciendo nuevos valores

La función count() devuelve un iterador que produce enteros consecutivos, indefinidamente. El primer número se puede pasar como un argumento (el valor predeterminado es cero). No hay un argumento de límite superior (Consulta la función incorporada range() para obtener más control sobre el conjunto de resultados).

itertools_count.py
from itertools import *

for i in zip(count(1), ['a', 'b', 'c']):
    print(i)

Este ejemplo se detiene porque el argumento de la lista se consume.

$ python3 itertools_count.py

(1, 'a')
(2, 'b')
(3, 'c')

Los argumentos de inicio y paso a count() pueden ser valores numéricos que se pueden agregar juntos.

itertools_count_step.py
import fractions
from itertools import *

start = fractions.Fraction(1, 3)
step = fractions.Fraction(1, 3)

for i in zip(count(start, step), ['a', 'b', 'c']):
    print('{}: {}'.format(*i))

En este ejemplo, el punto de inicio y los pasos son objetos Fraction del módulo fraction.

$ python3 itertools_count_step.py

1/3: a
2/3: b
1: c

La función cycle() devuelve un iterador que repite el contenido de los argumentos recibidos indefinidamente. Como tiene que recordar todo el contenido del iterador de entrada, puede consumir bastante memoria si el iterador es largo.

itertools_cycle.py
from itertools import *

for i in zip(range(7), cycle(['a', 'b', 'c'])):
    print(i)

Se usa una variable de contador para salir del ciclo después de algunos ciclos en este ejemplo.

$ python3 itertools_cycle.py

(0, 'a')
(1, 'b')
(2, 'c')
(3, 'a')
(4, 'b')
(5, 'c')
(6, 'a')

La función repeat() devuelve un iterador que produce el mismo valor cada vez que se accede.

itertools_repeat.py
from itertools import *

for i in repeat('over-and-over', 5):
    print(i)

El iterador devuelto por repeat() se devuelve datos para siempre a menos que se proporcione el argumento opcional times para limitarlo.

$ python3 itertools_repeat.py

over-and-over
over-and-over
over-and-over
over-and-over
over-and-over

Es útil combinar repeat() con zip() o map() cuando los valores invariantes deben ser incluidos con los valores de otros iteradores.

itertools_repeat_zip.py
from itertools import *

for i, s in zip(count(), repeat('over-and-over', 5)):
    print(i, s)

Un valor de contador se combina con la constante devuelta por repeat() en este ejemplo.

$ python3 itertools_repeat_zip.py

0 over-and-over
1 over-and-over
2 over-and-over
3 over-and-over
4 over-and-over

Este ejemplo usa map() para multiplicar por dos los números en el rango 0 a 4.

itertools_repeat_map.py
from itertools import *

for i in map(lambda x, y: (x, y, x * y), repeat(2), range(5)):
    print('{:d} * {:d} = {:d}'.format(*i))

El iterador repeat() no necesita ser explícitamente limitado, ya que map() detiene el procesamiento cuando termina cualquiera de sus entradas, y el range() devuelve solo cinco elementos.

$ python3 itertools_repeat_map.py

2 * 0 = 0
2 * 1 = 2
2 * 2 = 4
2 * 3 = 6
2 * 4 = 8

Filtrando

La función dropwhile() devuelve un iterador que produce elementos del iterador de entrada después de que la condición se vuelva falsa por primera vez.

itertools_dropwhile.py
from itertools import *


def should_drop(x):
    print('Testing:', x)
    return x < 1


for i in dropwhile(should_drop, [-1, 0, 1, 2, -2]):
    print('Yielding:', i)

dropwhile() no filtra todos los elementos de la entrada; después de que la condición es falsa la primera vez, todos los elementos restantes en la entrada son devueltos.

$ python3 itertools_dropwhile.py

Testing: -1
Testing: 0
Testing: 1
Yielding: 1
Yielding: 2
Yielding: -2

Lo contrario de dropwhile() es takewhile(). Devuelve un iterador que devuelve elementos del iterador de entrada, siempre que la función de prueba devuelve verdadero.

itertools_takewhile.py
from itertools import *


def should_take(x):
    print('Testing:', x)
    return x < 2


for i in takewhile(should_take, [-1, 0, 1, 2, -2]):
    print('Yielding:', i)

Tan pronto como should_take() devuelve False, takewhile() deja de procesar la entrada.

$ python3 itertools_takewhile.py

Testing: -1
Yielding: -1
Testing: 0
Yielding: 0
Testing: 1
Yielding: 1
Testing: 2

La función incorporada filter() devuelve un iterador que incluye solo elementos para los que la función de prueba devuelve verdadero.

itertools_filter.py
from itertools import *


def check_item(x):
    print('Testing:', x)
    return x < 1


for i in filter(check_item, [-1, 0, 1, 2, -2]):
    print('Yielding:', i)

filter() es diferente a dropwhile() y takewhile() en que cada elemento es probado antes de ser devuelto.

$ python3 itertools_filter.py

Testing: -1
Yielding: -1
Testing: 0
Yielding: 0
Testing: 1
Testing: 2
Testing: -2
Yielding: -2

filterfalse() devuelve un iterador que incluye solo elementos donde la función de prueba devuelve falso.

itertools_filterfalse.py
from itertools import *


def check_item(x):
    print('Testing:', x)
    return x < 1


for i in filterfalse(check_item, [-1, 0, 1, 2, -2]):
    print('Yielding:', i)

La expresión de prueba en check_item() es la misma, por lo que los resultados en este ejemplo con filterfalse() son lo opuesto a los resultados del ejemplo anterior.

$ python3 itertools_filterfalse.py

Testing: -1
Testing: 0
Testing: 1
Yielding: 1
Testing: 2
Yielding: 2
Testing: -2

compress() ofrece otra forma de filtrar el contenido de un iterable. En lugar de llamar a una función, usa los valores en otro iterable para indicar cuándo aceptar un valor y cuándo ignorarlo.

itertools_compress.py
from itertools import *

every_third = cycle([False, False, True])
data = range(1, 10)

for i in compress(data, every_third):
    print(i, end=' ')
print()

El primer argumento son los datos iterables para procesar y el segundo es un selector iterable produciendo valores booleanos que indican qué elementos tomar de la entrada de datos (un valor verdadero hace que el valor sea producido, un valor falso hace que se ignore).

$ python3 itertools_compress.py

3 6 9

Agrupación de datos

La función groupby() devuelve un iterador que produce conjuntos de valores organizados por una clave común. Este ejemplo ilustra la agrupación de valores relacionados basados en un atributo.

itertools_groupby_seq.py
import functools
from itertools import *
import operator
import pprint


@functools.total_ordering
class Point:

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return '({}, {})'.format(self.x, self.y)

    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

    def __gt__(self, other):
        return (self.x, self.y) > (other.x, other.y)


# Create a dataset of Point instances
data = list(map(Point,
                cycle(islice(count(), 3)),
                islice(count(), 7)))
print('Data:')
pprint.pprint(data, width=35)
print()

# Try to group the unsorted data based on X values
print('Grouped, unsorted:')
for k, g in groupby(data, operator.attrgetter('x')):
    print(k, list(g))
print()

# Sort the data
data.sort()
print('Sorted:')
pprint.pprint(data, width=35)
print()

# Group the sorted data based on X values
print('Grouped, sorted:')
for k, g in groupby(data, operator.attrgetter('x')):
    print(k, list(g))
print()

La secuencia de entrada debe estar ordenarda por el valor clave para que las agrupaciones para funcionen como se espera.

$ python3 itertools_groupby_seq.py

Data:
[(0, 0),
 (1, 1),
 (2, 2),
 (0, 3),
 (1, 4),
 (2, 5),
 (0, 6)]

Grouped, unsorted:
0 [(0, 0)]
1 [(1, 1)]
2 [(2, 2)]
0 [(0, 3)]
1 [(1, 4)]
2 [(2, 5)]
0 [(0, 6)]

Sorted:
[(0, 0),
 (0, 3),
 (0, 6),
 (1, 1),
 (1, 4),
 (2, 2),
 (2, 5)]

Grouped, sorted:
0 [(0, 0), (0, 3), (0, 6)]
1 [(1, 1), (1, 4)]
2 [(2, 2), (2, 5)]

Combinando entradas

La función accumulate() procesa el iterable de entrada, pasando el enésimo y el elemento n+1 a una función y produciendo el valor de retorno en lugar de cualquiera de las entradas. La función predeterminada es usada para combinar los dos valores los agrega, por lo que accumulate() se puede usar para generar la suma acumulativa de una serie de entradas numéricas.

itertools_accumulate.py
from itertools import *

print(list(accumulate(range(5))))
print(list(accumulate('abcde')))

Cuando se usa con una secuencia de valores no enteros, los resultados dependen de lo que significa «agregar» dos elementos juntos. El segundo ejemplo en este script muestra que cuando accumulate() recibe una cadena de entrada cada respuesta es un prefijo progresivamente más largo de esa cadena.

$ python3 itertools_accumulate.py

[0, 1, 3, 6, 10]
['a', 'ab', 'abc', 'abcd', 'abcde']

Es posible combinar accumulate() con cualquier otra función que toma dos valores de entrada para lograr resultados diferentes.

itertools_accumulate_custom.py
from itertools import *


def f(a, b):
    print(a, b)
    return b + a + b


print(list(accumulate('abcde', f)))

Este ejemplo combina los valores de cadena de una manera que hace una serie de palíndromos (sin sentido). Cada paso del camino cuando f() es invocada, imprime los valores de entrada que le pasa accumulate().

$ python3 itertools_accumulate_custom.py

a b
bab c
cbabc d
dcbabcd e
['a', 'bab', 'cbabc', 'dcbabcd', 'edcbabcde']

Los bucles anidados for que iteran sobre múltiples secuencias a menudo pueden ser reemplazados por product(), que produce un único iterable cuyo valores son el producto cartesiano del conjunto de valores de entrada.

itertools_product.py
from itertools import *
import pprint

FACE_CARDS = ('J', 'Q', 'K', 'A')
SUITS = ('H', 'D', 'C', 'S')

DECK = list(
    product(
        chain(range(2, 11), FACE_CARDS),
        SUITS,
    )
)

for card in DECK:
    print('{:>2}{}'.format(*card), end=' ')
    if card[1] == SUITS[-1]:
        print()

Los valores producidos por product() son tuplas, con los miembros tomados de cada uno de los iterables pasados como argumentos en el orden en que son pasados. La primera tupla devuelta incluye el primer valor de cada iterable. El último iterable pasado a product() es procesado primero, seguido de el penúltimo, y así sucesivamente. El resultado es que los valores de retorno están en orden basados en el primer iterable, luego el siguiente iterable, etc.

En este ejemplo, las tarjetas se ordenan por valor y luego por palo.

$ python3 itertools_product.py

 2H  2D  2C  2S
 3H  3D  3C  3S
 4H  4D  4C  4S
 5H  5D  5C  5S
 6H  6D  6C  6S
 7H  7D  7C  7S
 8H  8D  8C  8S
 9H  9D  9C  9S
10H 10D 10C 10S
 JH  JD  JC  JS
 QH  QD  QC  QS
 KH  KD  KC  KS
 AH  AD  AC  AS

Para cambiar el orden de las tarjetas, cambia el orden de los argumentos para product().

itertools_product_ordering.py
from itertools import *
import pprint

FACE_CARDS = ('J', 'Q', 'K', 'A')
SUITS = ('H', 'D', 'C', 'S')

DECK = list(
    product(
        SUITS,
        chain(range(2, 11), FACE_CARDS),
    )
)

for card in DECK:
    print('{:>2}{}'.format(card[1], card[0]), end=' ')
    if card[1] == FACE_CARDS[-1]:
        print()

El bucle de impresión en este ejemplo busca una carta as, en lugar de juego de espadas, y luego agrega una nueva línea para dividir la salida.

$ python3 itertools_product_ordering.py

 2H  3H  4H  5H  6H  7H  8H  9H 10H  JH  QH  KH  AH
 2D  3D  4D  5D  6D  7D  8D  9D 10D  JD  QD  KD  AD
 2C  3C  4C  5C  6C  7C  8C  9C 10C  JC  QC  KC  AC
 2S  3S  4S  5S  6S  7S  8S  9S 10S  JS  QS  KS  AS

Para calcular el producto de una secuencia consigo mismo, especifica cuántas veces la entrada debe ser repetida.

itertools_product_repeat.py
from itertools import *


def show(iterable):
    for i, item in enumerate(iterable, 1):
        print(item, end=' ')
        if (i % 3) == 0:
            print()
    print()


print('Repeat 2:\n')
show(list(product(range(3), repeat=2)))

print('Repeat 3:\n')
show(list(product(range(3), repeat=3)))

Como repetir un solo iterable es como pasar el mismo iterable varias veces, cada tupla producida por product() contendrá un número de elementos igual al contador de repetición.

$ python3 itertools_product_repeat.py

Repeat 2:

(0, 0) (0, 1) (0, 2)
(1, 0) (1, 1) (1, 2)
(2, 0) (2, 1) (2, 2)

Repeat 3:

(0, 0, 0) (0, 0, 1) (0, 0, 2)
(0, 1, 0) (0, 1, 1) (0, 1, 2)
(0, 2, 0) (0, 2, 1) (0, 2, 2)
(1, 0, 0) (1, 0, 1) (1, 0, 2)
(1, 1, 0) (1, 1, 1) (1, 1, 2)
(1, 2, 0) (1, 2, 1) (1, 2, 2)
(2, 0, 0) (2, 0, 1) (2, 0, 2)
(2, 1, 0) (2, 1, 1) (2, 1, 2)
(2, 2, 0) (2, 2, 1) (2, 2, 2)

La función permutations() produce elementos a partir de el iterable de entrada combinado en las posibles permutaciones de la longitud dada. Eso se predetermina a producir el conjunto completo de todas las permutaciones.

itertools_permutations.py
from itertools import *


def show(iterable):
    first = None
    for i, item in enumerate(iterable, 1):
        if first != item[0]:
            if first is not None:
                print()
            first = item[0]
        print(''.join(item), end=' ')
    print()


print('All permutations:\n')
show(permutations('abcd'))

print('\nPairs:\n')
show(permutations('abcd', r=2))

Usa el argumento r para limitar la longitud y el número de permutaciones individuales devueltas.

$ python3 itertools_permutations.py

All permutations:

abcd abdc acbd acdb adbc adcb
bacd badc bcad bcda bdac bdca
cabd cadb cbad cbda cdab cdba
dabc dacb dbac dbca dcab dcba

Pairs:

ab ac ad
ba bc bd
ca cb cd
da db dc

Para limitar los valores a combinaciones únicas en lugar de permutaciones, usa combinations(). Siempre y cuando los miembros de la entrada sean únicos, el resultado no incluirá ningún valor repetido.

itertools_combinations.py
from itertools import *


def show(iterable):
    first = None
    for i, item in enumerate(iterable, 1):
        if first != item[0]:
            if first is not None:
                print()
            first = item[0]
        print(''.join(item), end=' ')
    print()


print('Unique pairs:\n')
show(combinations('abcd', r=2))

A diferencia de las permutaciones, el argumento r para combinations() es requerido.

$ python3 itertools_combinations.py

Unique pairs:

ab ac ad
bc bd
cd

Mientras que combinations() no repite elementos de entrada individuales, a veces es útil considerar combinaciones que sí incluyen elementos repetidos. Para esos casos, usa combinations_with_replacement().

itertools_combinations_with_replacement.py
from itertools import *


def show(iterable):
    first = None
    for i, item in enumerate(iterable, 1):
        if first != item[0]:
            if first is not None:
                print()
            first = item[0]
        print(''.join(item), end=' ')
    print()


print('Unique pairs:\n')
show(combinations_with_replacement('abcd', r=2))

En esta salida, cada elemento de entrada está emparejado consigo mismo y con todos los otros miembros de la secuencia de entrada.

$ python3 itertools_combinations_with_replacement.py

Unique pairs:

aa ab ac ad
bb bc bd
cc cd
dd

Ver también