2010-05-20 16 views
46

Hay muchas formas de escribir un programa de Python que calcule un histograma.python histogram one-liner

Por histograma, me refiero a una función que cuenta la ocurrencia de objetos en un iterable y emite los conteos en un diccionario. Por ejemplo:

>>> L = 'abracadabra' 
>>> histogram(L) 
{'a': 5, 'b': 2, 'c': 1, 'd': 1, 'r': 2} 

Una manera de escribir esta función es:

def histogram(L): 
    d = {} 
    for x in L: 
     if x in d: 
      d[x] += 1 
     else: 
      d[x] = 1 
    return d 

¿Hay formas más concisas de escribir esta función?

Si tuviéramos comprensiones del diccionario en Python, podríamos escribir:

>>> { x: L.count(x) for x in set(L) } 

pero desde Python 2.6 no los tiene, tenemos que escribir:

>>> dict([(x, L.count(x)) for x in set(L)]) 

Aunque este enfoque puede ser legible, no es eficiente: L es caminado varias veces. Además, esto no funcionará para generadores de vida única; la función debería funcionar igual de bien para generadores de iterador como:

def gen(L): 
    for x in L: 
     yield x 

podríamos tratar de utilizar la función reduce (RIP):

>>> reduce(lambda d,x: dict(d, x=d.get(x,0)+1), L, {}) # wrong! 

Vaya, esto no funciona: el nombre clave es 'x' , no x. :(

que terminó con:

>>> reduce(lambda d,x: dict(d.items() + [(x, d.get(x, 0)+1)]), L, {}) 

(En Python 3, que tendría que escribir list(d.items()) en lugar de d.items(), pero es hypothethical, ya que no hay reduce allí.)

favor batir ¡Yo con un one-liner mejor y más legible!;)

+9

"un trazador de líneas" y "más legible" no son mutuamente excluyentes, pero están cerca – msw

+3

No es una respuesta, solo algunos comentarios: Primero, dict ((x, L.count (x)) para x en el conjunto (L)) funciona perfectamente bien (al menos en 2.6 o más, posiblemente versiones anteriores también), por lo que no es necesario introducir la lista adicional en el ejemplo anterior. En segundo lugar, si no te importan los one-liners, entonces este es un trabajo hecho a medida para el default del módulo de colecciones. Reemplace d = {} con d = collections.defaultdict (int) en su función de histograma original, y luego puede omitir el si x en d: bit. –

+0

Peter Milley: ¡y la comprensión de casi dict funciona incluso en Python 2.5.2! gracias, no estaba al tanto de esta sintaxis – mykhal

Respuesta

76

Python 3.x tiene reduce, solo tienes que hacer un from functools import reduce. También tiene "comprensión dict", que tiene exactamente la sintaxis en su ejemplo.

Python 2.7 y 3.x también tienen una clase Counter que hace exactamente lo que quiere:

from collections import Counter 
cnt = Counter("abracadabra") 

En Python 2.6 o anterior, que haría uso de un personal defaultdict y hacerlo en 2 líneas:

d = defaultdict(int) 
for x in xs: d[x] += 1 

Eso es limpio, eficiente, pitónico, y mucho más fácil de entender para la mayoría de la gente que nada relacionado con reduce.

+4

Python 2.7 también tiene comprensión dict. –

1

Por un tiempo, cualquier cosa que use itertools fue por definición Pythonic. Aún así, esto es un poco en el lado opaco:

>>> from itertools import groupby 
>>> grouplen = lambda grp : sum(1 for i in grp) 
>>> hist = dict((a[0], grouplen(a[1])) for a in groupby(sorted("ABRACADABRA"))) 
>>> print hist 
{'A': 5, 'R': 2, 'C': 1, 'B': 2, 'D': 1} 

Actualmente estoy ejecutando Python 2.5.4.

+3

Esta solución es O (n log n). Hay varias soluciones lineales más simples proporcionadas aquí. –

+0

@Mike - ¿estás seguro? Cuidado con las complejidades al acecho. Iterar sobre la lista es obviamente O (n), pero ¿cuál es la complejidad de la búsqueda repetida de cada tecla en el diccionario de síntesis? No es O (1). – PaulMcG

+2

Buscar las claves dict es O (1). –

7

Es un poco cheaty importar módulos de oneliners, así que aquí tiene un oneliner que es O (n) y funciona al menos tan atrás como python2.4

>>> f=lambda s,d={}:([d.__setitem__(i,d.get(i,0)+1) for i in s],d)[-1] 
>>> f("ABRACADABRA") 
{'A': 5, 'R': 2, 'B': 2, 'C': 1, 'D': 1} 

Y si usted piensa __ métodos son hacky, siempre se puede hacer esto

>>> f=lambda s,d=lambda:0:vars(([setattr(d,i,getattr(d,i,0)+1) for i in s],d)[-1]) 
>>> f("ABRACADABRA") 
{'A': 5, 'R': 2, 'B': 2, 'C': 1, 'D': 1} 

:)

+3

genial, nunca he visto argumentos predeterminados en lambda antes ... – mykhal

+1

Realmente genial, pero tengo que aceptar el comentario de @msw sobre la legibilidad. Si hubiera visto a alguien empujar esto a nuestra repro, tendría una discusión seria con él ... – RickyA

1

Su sola línea usando reduce era casi bien, sólo es necesario que ajustar un poco:

>>> reduce(lambda d, x: dict(d, **{x: d.get(x, 0) + 1}), L, {}) 
{'a': 5, 'b': 2, 'c': 1, 'd': 1, 'r': 2} 

Por supuesto, esto no va a superar en el lugar de soluciones (ni en la velocidad, ni en pythonicity), pero a cambio usted se tiene un buen fragmento puramente funcional. Por cierto, esto sería algo más lindo si Python tuviera un método dict.merge().

+0

tokland, no es 'dict.update()' lo mismo que lo que quiere decir con 'dict.merge()' – sblom

+0

@sblom: mata a un gato funcional ;-) dict.update() funciona en el lugar mientras que dict.merge() no lo haría (compruebe la fusión de Hash # de Ruby, la actualización de Hash #). Incluso si no nos importara la pureza, como dict.update() no devuelve el dict actualizado, no podría ser utilizado en un lambda de una sola línea. – tokland

6
$d{$_} += 1 for split //, 'abracadabra'; 
+8

genial, perl. pero es perl – mykhal

+2

@perl Creo que deberías aprovechar esta novedosa cuenta más –

+8

¡Oh, Perl! Siempre tan legible ... :-) – JJC

1

necesitaba una aplicación histograma para trabajar en Python 2.2 hasta 2.7, y se le ocurrió esto:

>>> L = 'abracadabra' 
>>> hist = {} 
>>> for x in L: hist[x] = hist.setdefault(x,0)+1 
>>> print hist 
{'a': 5, 'r': 2, 'b': 2, 'c': 1, 'd': 1} 

Me he inspirado en un puesto de defaultdict de Eli Courtwright. Estos fueron introducidos en Python 2.5 por lo que no pueden ser utilizados. Pero se pueden emular con dict.setdefault (clave, por defecto).

Esto es básicamente lo mismo que gnibbler, pero tuve que escribir esto antes de poder entender completamente su función lambda.

4

uno que funcione de nuevo a 2.3 (ligeramente más corto que Timmerman de, creo que más legible):

L = 'abracadabra' 
hist = {} 
for x in L: hist[x] = hist.pop(x,0) + 1 
print hist 
{'a': 5, 'r': 2, 'b': 2, 'c': 1, 'd': 1} 
+0

¡Esto me ayudó! ¡gracias! –

5

para Python 2.7, puede utilizar esta lista pequeña de comprensión:

v = list('abracadabra') 
print {x: v.count(x) for x in set(v)} 
+0

Creo que esta es la solución más elegante. ¡Bonito! – Ohumeronen

6
import pandas as pd 

pd.Series(list(L)).value_counts()