2010-05-20 7 views
46

Es gibt viele Möglichkeiten, ein Python-Programm zu schreiben, das ein Histogramm berechnet.Python-Histogramm Einstrich

Mit Histogramm meine ich eine Funktion, die das Auftreten von Objekten in einem iterable zählt und die Zählungen in einem Wörterbuch ausgibt. Zum Beispiel:

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

Eine Möglichkeit, diese Funktion zu schreiben ist:

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

Gibt es prägnante Möglichkeiten, diese Funktion zu schreiben?

Wenn wir Wörterbuch Comprehensions in Python hätten, könnten wir schreiben:

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

aber da Python 2.6 ist sie nicht haben, haben wir schreiben:

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

Obwohl dieser Ansatz sein kann, lesbar, es ist nicht effizient: L wird mehrfach durchlaufen. Darüber hinaus wird dies nicht für Single-Life-Generatoren funktionieren; die Funktion sollte für Iterator Generatoren wie gleich gut funktionieren:

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

Wir könnten versuchen, die reduce Funktion (RIP) zu verwenden:

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

Hoppla, das nicht funktioniert: der Schlüssel Name ist 'x' , nicht x. :(

I endete mit:

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

(In Python 3, würden wir list(d.items()) statt d.items() zu schreiben, aber es ist hypothethical, da es keine reduce da ist.)

Bitte schlagen ich mit einem besseren, besser lesbaren One-Liner!;)

+9

"Ein Liner" und "lesbarer" schließen sich nicht gegenseitig aus, aber sie sind nahe – msw

+3

Keine Antwort, nur einige Kommentare: Zuerst dict ((x, L.count (x)) für x im Satz (L)) funktioniert sehr gut (zumindest in 2.6 oder so, möglicherweise auch frühere Versionen), so dass es nicht notwendig ist, die Extraliste in Ihrem obigen Beispiel einzuführen. Zweitens, wenn Sie sich nicht für Einzeiler interessieren, dann ist dies ein Job, der für das defaultdict aus dem Collections-Modul maßgeschneidert ist. Ersetzen Sie d = {} durch d = collections.defaultdict (int) in Ihrer ursprünglichen Histogrammfunktion, und dann können Sie das if x in d: Bit überspringen. –

+0

Peter Milley: yor fast dict Verständnis funktioniert sogar in Python 2.5.2! danke, ich war mir dieser syntax nicht bewusst – mykhal

Antwort

76

Python 3.x hat reduce, Sie müssen nur eine from functools import reduce tun. Es hat auch "dict comprehensions", die genau die Syntax in Ihrem Beispiel haben.

Python 2.7 und 3.x hat auch eine Counter Klasse, die genau das tut, was Sie wollen:

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

In Python 2.6 oder früher, ich persönlich ein defaultdict verwenden würde, und tut es in zwei Linien:

Das ist sauber, effizient, Pythonic und viel einfacher für die meisten Menschen zu verstehen als alles mit reduce.

+4

Python 2.7 hat auch dict verständnisse. –

1

Für eine Weile war alles mit itertools per Definition Pythonic. Dennoch ist dies ein bisschen auf der undurchsichtigen Seite:

>>> 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} 

Ich führe gerade Python 2.5.4.

+3

Diese Lösung ist O (n log n). Es gibt mehrere einfachere lineare Lösungen, die hier bereitgestellt werden. –

+0

@Mike - bist du sicher? Hüte dich vor lauernden Komplexitäten. Das Durchlaufen der Liste ist offensichtlich O (n), aber was ist die Komplexität des wiederholten Nachschlagens jedes Schlüssels in dem zusammenfassenden Diktat? Es ist nicht O (1). – PaulMcG

+2

Dict Schlüssel suchen ist O (1). –

7

Es ist irgendwie cheaty Module für oneliners zu importieren, also hier ein Einzeiler, die O (n) und arbeitet zumindest so weit zurück wie 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} 

Und wenn Sie denken __ Methoden Hacky sind, ist, Sie können dies immer tun

>>> 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

cool, ich habe noch nie Standardargumente in Lambda gesehen. – mykhal

+1

Cool in der Tat, aber ich muss @smsw Kommentar zur Lesbarkeit zustimmen. Wenn ich jemanden sehen würde, der dies auf unsere Repro drückt, würde ich eine ernsthafte Diskussion mit ihm führen ... – RickyA

1

Ihre Einzeiler reduce mit fast ok, nur brauchte es ein wenig zu zwicken:

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

Natürlich wird dies nicht schlagen in-Place-Lösungen (noch in der Geschwindigkeit, noch in pythonicity), aber im Gegenzug Sie haben ihnen einen schönen rein funktionalen Schnipsel. Übrigens wäre das etwas hübscher, wenn Python eine Methode dict.merge() hätte.

+0

tokland, ist nicht 'dict.update()' das gleiche wie das, was du mit 'dict.merge()' – sblom

+0

@sblom meinst: du hast eine funktionierende Katze umgebracht ;-) dict.update() funktioniert in-place während dict.merge() würde nicht (überprüfen Ruby Hash # merge, Hash # update). Auch wenn uns die Reinheit egal war, da dict.update() das aktualisierte Diktat nicht zurückgibt, konnte es nicht in einem Einzellinien-Lambdas verwendet werden. – tokland

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

cool, perl. aber es ist perl. – mykhal

+2

@perl Ich denke du solltest diesen Neuigkeitskonto weiter machen –

+8

Oh Perl! Immer so lesbar ... :-) – JJC

1

brauchte ich eine Histogramm-Implementierung in Python bis zu 2,7 2.2 zu arbeiten, und kam mit dieser:

>>> 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} 

ich von Eli Courtwright den Posten eines defaultdict inspiriert wurde. Diese wurden in Python 2.5 eingeführt und können nicht verwendet werden. Aber sie können mit dem dict.setdefault (key, default) emuliert werden.

Das ist im Grunde das Gleiche, was Gnibbler macht, aber ich musste dies zuerst schreiben, bevor ich seine Lambda-Funktion vollständig verstehen konnte.

4

One, die zurück bis 2,3 arbeitet (etwas kürzer als Timmerman ist, glaube ich, besser lesbar):

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

Das hat mir geholfen! Danke! –

5

für Python 2.7, können Sie diese kleine Liste Verständnis verwenden:

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

Ich finde dies die eleganteste Lösung. Nett! – Ohumeronen

6
import pandas as pd 

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