Python: cuenta las ocurrencias en una lista usando dict comprensión / generador

Quiero escribir algunas pruebas para analizar la eficacia de diferentes operaciones en python, a saber, una comparación de las comprensiones de diccionarios y los generadores de dictados.

Para probar esto, pensé en probar un ejemplo simple: contar el número de palabras en una lista usando diccionarios.

Ahora sé que puede hacerlo utilizando collections.Counter . collections.Counter (según una respuesta aquí: ¿Cómo puedo contar las ocurrencias de un elemento de lista en Python? ), Pero mi objective era probar la memoria y el rendimiento.

Una forma de “mano larga” es hacerlo en un bucle básico.

 from pprint import pprint # Read in some text to create example data with open('text.txt') as f: words = f.read().split() dict1 = {} for w in words: if not dict1.get(w): dict1[w] = 1 else: dict1[w] += 1 pprint(dict1) 

El resultado:

 {'a': 62, 'aback': 1, 'able': 1, 'abolished': 2, 'about': 6, 'accept': 1, 'accepted': 1, 'accord': 1, 'according': 1, 'across': 1, ... 

Luego me quedé un poco atascado tratando de hacer lo mismo en una comprensión de diccionario:

 dict2 = { w: 1 if not dict2.get(w) else dict2.get(w) + 1 for w in words } 

Tengo un error:

 NameError: global name 'dict2' is not defined 

Intenté definir el dictado por adelantado:

 dict2 = {} dict2 = { w: 1 if not dict2.get(w) else dict2.get(w) + 1 for w in words } pprint(dict2) 

Pero, por supuesto, todos los conteos están establecidos en 1:

 {'a': 1, 'aback': 1, 'able': 1, 'abolished': 1, 'about': 1, 'accept': 1, 'accepted': 1, 'accord': 1, 'according': 1, 'across': 1, ... 

Tuve un problema similar con la comprensión del dict:

 dict3 = dict( (w, 1 if not dict2.get(w) else dict2.get(w) + 1) for w in words) 

Entonces, mi pregunta es: ¿cómo puedo usar un generador / comprensión de diccionarios de manera más eficiente para contar el número de ocurrencias en una lista?

Actualización : @Rawing sugirió un enfoque alternativo {word:words.count(word) for word in set(words)} pero eso evitaría el mecanismo que estoy intentando probar.

No puede hacer esto de manera eficiente (al menos en términos de memoria) usando una comprensión de dictado, porque entonces tendrá que hacer un seguimiento del conteo actual en otro diccionario, es decir, más consumo de memoria. Aquí le mostramos cómo puede hacerlo utilizando un dict-comprensión (no recomendado en absoluto :-)):

 >>> words = list('asdsadDASDFASCSAASAS') >>> dct = {} >>> {w: 1 if w not in dct and not dct.update({w: 1}) else dct[w] + 1 if not dct.update({w: dct[w] + 1}) else 1 for w in words} >>> dct {'a': 2, 'A': 5, 's': 2, 'd': 2, 'F': 1, 'C': 1, 'S': 5, 'D': 2} 

Otra forma será ordenar primero la lista de palabras y luego agruparlas usando itertools.groupby y luego contar la longitud de cada grupo. Aquí, la comprensión del dictado se puede convertir en un generador si lo desea, pero sí, esto requerirá leer todas las palabras en la memoria primero:

 from itertools import groupby words.sort() dct = {k: sum(1 for _ in g) for k, g in groupby(words)} 

Tenga en cuenta que el más rápido del lote es collections.defaultdict :

 d = defaultdict(int) for w in words: d[w] += 1 

Comparaciones de tiempo:

 >>> from string import ascii_letters, digits >>> %timeit words = list(ascii_letters+digits)*10**4; words.sort(); {k: sum(1 for _ in g) for k, g in groupby(words)} 10 loops, best of 3: 131 ms per loop >>> %timeit words = list(ascii_letters+digits)*10**4; Counter(words) 10 loops, best of 3: 169 ms per loop >>> %timeit words = list(ascii_letters+digits)*10**4; dct = {}; {w: 1 if w not in dct and not dct.update({w: 1}) else dct[w] + 1 if not dct.update({w: dct[w] + 1}) else 1 for w in words} 1 loops, best of 3: 315 ms per loop >>> %%timeit ... words = list(ascii_letters+digits)*10**4 ... d = defaultdict(int) ... for w in words: d[w] += 1 ... 10 loops, best of 3: 57.1 ms per loop >>> %%timeit words = list(ascii_letters+digits)*10**4 d = {} for w in words: d[w] = d.get(w, 0) + 1 ... 10 loops, best of 3: 108 ms per loop #Increase input size >>> %timeit words = list(ascii_letters+digits)*10**5; words.sort(); {k: sum(1 for _ in g) for k, g in groupby(words)} 1 loops, best of 3: 1.44 s per loop >>> %timeit words = list(ascii_letters+digits)*10**5; Counter(words) 1 loops, best of 3: 1.7 s per loop >>> %timeit words = list(ascii_letters+digits)*10**5; dct = {}; {w: 1 if w not in dct and not dct.update({w: 1}) else dct[w] + 1 if not dct.update({w: dct[w] + 1}) else 1 for w in words} 1 loops, best of 3: 3.19 s per loop >>> %%timeit words = list(ascii_letters+digits)*10**5 d = defaultdict(int) for w in words: d[w] += 1 ... 1 loops, best of 3: 571 ms per loop >>> %%timeit words = list(ascii_letters+digits)*10**5 d = {} for w in words: d[w] = d.get(w, 0) + 1 ... 1 loops, best of 3: 1.1 s per loop 

Puedes hacerlo de esta manera:

 >>> words=['this','that','is','if','that','is','if','this','that'] >>> {i:words.count(i) for i in words} {'this': 2, 'is': 2, 'if': 2, 'that': 3} 

Es un caso de uso donde la comprensión no es adecuada / eficiente.

La comprensión es buena cuando puede comstackr la colección en una sola operación. No es realmente el caso aquí, ya que:

  • o bien toma las palabras como vienen y cambia los valores en el dictado en consecuencia
  • o primero debe calcular el conjunto de claves (solución Rawing), pero luego navega por la lista una vez para obtener el conjunto de claves y una vez por clave

En mi humilde opinión, la forma más eficiente es la iterativa.