Blog

Counter, defaultdict y deque en Python

Contar cosas, agrupar cosas y mantener una cola son tres bucles que casi todo el mundo escribe a mano. El módulo collections ya tiene los tres.

Todo lo que hay aquí viene en la biblioteca estándar.

Contar a mano

La versión con dict.get es la común.

# counting the long way
words = ['apple', 'bread', 'apple', 'milk', 'bread', 'apple']

counts = {}
for w in words:
    counts[w] = counts.get(w, 0) + 1
print(counts)

Imprime:

{'apple': 3, 'bread': 2, 'milk': 1}

Counter

Counter toma cualquier iterable y lo cuenta. Las claves que faltan devuelven 0 en lugar de lanzar un error.

# Counter does it in one line
from collections import Counter, defaultdict, deque

c = Counter(words)
print(c)
print(c.most_common(2))
print(c['apple'], c['missing'])

Imprime:

Counter({'apple': 3, 'bread': 2, 'milk': 1})
[('apple', 3), ('bread', 2)]
3 0

most_common es la razón por la que la gente lo usa. Ordenar un diccionario por valor cuesta una línea y una lambda.

Aritmética de Counter

Los Counter se suman y se restan. La resta descarta todo lo que llegue a cero o menos.

# Counter arithmetic
stock = Counter(apple=5, bread=2)
sold = Counter(apple=3, bread=1, milk=1)
print(stock - sold)
print(stock + sold)

Imprime:

Counter({'apple': 2, 'bread': 1})
Counter({'apple': 8, 'bread': 3, 'milk': 1})

Práctico para niveles de inventario, diferencias entre dos conteos y trabajo simple de bolsa de palabras.

Agrupar con defaultdict

defaultdict(list) llama a list() para cualquier clave que no exista, así que puedes agregar sin comprobar antes.

# grouping with defaultdict
people = [('eng', 'ada'), ('ops', 'grace'), ('eng', 'alan')]
groups = defaultdict(list)
for team, name in people:
    groups[team].append(name)
print(dict(groups))

Imprime:

{'eng': ['ada', 'alan'], 'ops': ['grace']}

La trampa de defaultdict

Leer una clave que falta la crea. Este es el comportamiento que sorprende a la gente, normalmente cuando un diccionario tiene más claves de las esperadas después de un bucle de consultas.

# the gotcha, reading creates the key
d = defaultdict(list)
print(d['not-there'])
print(dict(d))

plain = {}
print(plain.get('not-there'))
print(plain)

Imprime:

[]
{'not-there': []}
None
{}

Un diccionario normal con .get() no hace esto. Si solo quieres una lectura segura, usa .get().

setdefault hace el mismo trabajo

Si prefieres no importar nada, setdefault te lleva al mismo lugar.

# setdefault does the same without a factory
grouped = {}
for team, name in people:
    grouped.setdefault(team, []).append(name)
print(grouped)

Imprime:

{'eng': ['ada', 'alan'], 'ops': ['grace']}

Es más lento en un bucle apretado, porque la lista vacía se construye en cada pasada, se necesite o no.

deque para colas

Sacar del frente de una lista es lento, porque todo lo que va después se corre. Un deque agrega y saca en los dos extremos en tiempo constante.

# deque for queues, because list.pop(0) is slow
q = deque(['a', 'b', 'c'])
q.appendleft('start')
print(q.popleft(), list(q))

recent = deque(maxlen=3)
for item in range(6):
    recent.append(item)
print(list(recent))

Imprime:

start ['a', 'b', 'c']
[3, 4, 5]

maxlen te da una ventana móvil que descarta el elemento más viejo, lo cual encaja muy bien para «los últimos N» y para promedios móviles.

Qué recordar

  • Counter cuenta cualquier iterable y te da most_common.

  • Los Counter soportan + y -.

  • defaultdict crea la clave al leer, no solo al escribir.

  • deque es el tipo correcto cuando sacas del frente.

El mismo módulo también tiene namedtuple y ChainMap. Leer collections de punta a punta una vez es una de las mejores horas que puedes pasar con la biblioteca estándar.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.