List Comprehension richtig verstehen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo Leute,

ich setze mich wieder nach längerer Zeit an Python ran, und mir fiel gleich auf, dass ich die Listen-Abstraktion (List Comprehension) nicht richtig verstehe. Dazu habe ich mir mal ein Beispiel mitgebracht. Beim Anblick dieses Quelltextes habe ich versucht diesen Quelltext gedanklich zu verstehen. Mein Erklärungsversuch seht ihr unten.

Code: Alles auswählen

import re

def password_check(password_prompt):
    password_scores = {0:'Horrible', 1:'Weak', 2:'Medium', 3:'Strong', 4:'More Strong', 5:'Very Strong'}
    print "password_scores", password_scores 
    password_strength = dict.fromkeys(['has_upper', 'has_lower', 'has_num', 'has_digit', 'has_symbolic'], False)
    print "password_strength",password_strength
    if re.search(r'[A-Z]', password_prompt):
        has_upper = password_strength['has_upper ='] = True
        print "has_upper =", has_upper
    if re.search(r'[a-z]', password_prompt):
        has_lower = password_strength['has_lower'] = True
        print "has_lower =", has_lower
    if re.search(r'[0-9]', password_prompt):
        has_num = password_strength['has_num'] = True
        print "has_num =", has_num
    if re.search(r"\d", password_prompt):
        has_digit = password_strength['has_digit'] = True
        print "has_digit =", has_digit
    if re.search(r"[ !#$%&'()*+,-./[\\\]^_`{|}~"+r'"]', password_prompt):
        has_symbolic = password_strength['has_symbolic'] = True
        print "has_symbolic =", has_symbolic

    score = len([b for b in password_strength.values() if b])
    print "Score", score

    print ('Password is %s' % password_scores[score])
    return (password_scores[score])

password = raw_input('Password: ... ')
pwd_chk = password_check(password)
print pwd_chk
Ich konzentriere mich auf Zeile 24.

Soweit ich weiß, besteht eine List Comprehension grundlegend aus einem Ausdruck, gefolgt von beliebig vielen for/in-Bereichen und von eventuellen Bedingungen. Die Bedingungen sind aber optional. Allgemein weiß ich, dass sich der for/in-Bereich einer List Comprehension der for-Schleife syntaktisch ähnelt. Soweit die Theorie.

Nun versuche ich folgenden Ausdruck in Worten zu fassen:

Code: Alles auswählen

    score = len([b for b in password_strength.values() if b])
Beginne die List Comprehension mit einem Ausdruck b. Iteriere anschließend im for/in-Bereich jedes Element b über die Dictionarie password_strength. Anschließend erzeuge mit der Methode values() aus diesem Dictionarie eine Liste, die nur aus den Werten - ohne Schlüssel - besteht, aber nur sofern die Bedingung b True ergibt, und fügte das Ergebnis in die neue Liste. Die List Comprehension ist hier zum Inhalt der Methode len(). Berechne die Länge vom neuen Ergebnis und speichere diese in das Attribut score.

Habe ich das richtig formuliert oder etwas übersehen oder gar falsch formuliert?
Benutzeravatar
/me
User
Beiträge: 3555
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

Sophus hat geschrieben:

Code: Alles auswählen

score = len([b for b in password_strength.values() if b])
Bei deinem Beispielcode wird über password_strength.values() iteriert.

Ohne list comprehension könnte das so aussehen.

Code: Alles auswählen

data = []
for b in password_strength.values():
    if b:
        data.append(b)
score = len(data)
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Jede LC kann man herunterbrechen:

Code: Alles auswählen

[dostuff(x) for x in xs if condition(x)]
~

Code: Alles auswählen

result = []
for x in xs:
    if condition(x):
        result.append(dostuff(x))
Insofern ist "Abstraktion" fuer eine LC IMO etwas zu viel, das ist nicht mehr als syntaktischer Zucker.

Abgesehen davon laesst sich die LC in deinem Beispiel als

Code: Alles auswählen

score = sum(password_strength.values())
schreiben
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Danke euch. Also, die For-Schleife verstehe ich problemlos, aber bei LC fange ich immer wieder an von vorne zu denken. Rein von der Geschwindigkeit macht es keinen Unterschied, ob ich über die LC oder For-Schleife laufen lasse? Wobei cofi in meinem Fall aufgezeigt hat, dass man sowohl auf die For-Schleife als auch auf LC verzichten kann.
Sirius3
User
Beiträge: 17747
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: in diesem Fall würde man erst gar keine Liste erzeugen, sondern entweder einen Generatorausdruck verwenden:

Code: Alles auswählen

score = sum(b for b in password_strength.values())
und dann sieht man gleich, dass gar kein for nötig ist:

Code: Alles auswählen

score = sum(password_strength.values())
Ohne den Umweg über ein Wörterbuch macht das for dann wieder Sinn:

Code: Alles auswählen

PASSWORD_SCORES = ['Horrible', 'Weak', 'Medium', 'Strong', 'Very Strong']
PASSWORD_STRENGTHS = ['[A-Z]', '[a-z]', '[0-9]', r'''[ !#$%&'()*+,-./[\\\]^_`{|}~"]''']

def password_check(password_prompt):
    score = sum(bool(re.search(exp, password_prompt)) for exp in PASSWORD_STRENGTHS)
    return PASSWORD_SCORES[score]
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Puh, so viele Informationen auf einmal. Das muss ich erst einmal verdauen, Sirius3.

Aber eine Frage habe ich noch.

Sowohl cofi als auch du (Sirius3) bringt als Beispiel:

Code: Alles auswählen

score = sum(password_strength.values())
an.

Zunächst habe ich aus reiner Neugier folgendes gemacht:

Code: Alles auswählen

    score = (password_strength.values())
    print "Score", score
Nachdem ich ein Passwort eingegeben habe, wurde mir folgendes über die print-Anweisung ausgegeben:
Score [True, False, True, False, False, False]
Und bei der sum()-Methode müsste das Ergebnis ja 5 sein (schließlich beginnt Python bei der Zählung immer mit 0).

Verwende ich aber eure Vorschläge:

Code: Alles auswählen

    score = sum(password_strength.values())
    print "Score", score
Wird mir über die Print-Anweisung folgendes ausgegeben:
Score 2
Werden hier nur die True-Elemente gezählt?
Benutzeravatar
/me
User
Beiträge: 3555
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

Sophus hat geschrieben:Werden hier nur die True-Elemente gezählt?
Gezählt wird gar nichts sondern aufsummiert.

Code: Alles auswählen

>>> print(int(True))
1
>>> print(int(False))
0
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Wenn du die `for` Schleife verstehst, dann ist die LC nur ein kleiner Schritt davon weg: Es gibt eine direkte Entsprechung.

Die Ausfuehrungsgeschwindigkeit kann man vernachlaessigen, aber die _Lese_geschwindigkeit ist bei einer LC definitiv hoeher (wenn man sie gewohnt ist), da der Ausdruck direkt widerspiegelt was der Autor im Sinn hatte, waehrend die `for`-Schleife das auf verschiedene, kleine Schritte verteilt, die u.U. auch noch viel weiter verteilt sind als in dem Beispiel.

Wuerde der `sum` Ausdruck 5 ergeben, wuerde unser Vorschlag zum Ersatz keinen SInn machen ;)
Warum es funktioniert:

Code: Alles auswählen

In [1]: int(True)
Out[1]: 1

In [2]: int(False)
Out[2]: 0
Benutzeravatar
pillmuncher
User
Beiträge: 1484
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

@Sophus: sum() zählt nicht, sondern summiert, und jedes bool ist ein int:

Code: Alles auswählen

In [1]: sum([3, 5, 9])
Out[1]: 17

In [2]: isinstance(True, int)
Out[2]: True

In [3]: int(True)
Out[3]: 1

In [4]: int(False)
Out[4]: 0
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Ich hatte das irgendwie falsch verstanden. Ich dachte eher so:

Bei diesem Ausdruck

Code: Alles auswählen

score = (password_strength.values())
    print "Score", score
sehe ich
Score [True, False, True, False, False, False]
fünf Elemente (gezählt mit 0).

Und mit der Methode sum() soll dann die Summe aller Elemente einer Liste zusammen gezählt werden.

So habe ich das gelesen 8)
Benutzeravatar
pillmuncher
User
Beiträge: 1484
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

@Sophus: Du verwechselst Kardinalzahlen und Ordinalzahlen.

Außerdem: In len(x) ist x nicht der Inhalt der Funktion len(), sondern ein sog. Argument.

List Comprehensions kommen aus Haskell, wurden aber der Mengenkomprehension von Gottlob Frege, einem der zwei Erfinder der modernen Prädikatenlogik, nachempfunden. Den Ausdruck List Comprehension hat Philip Wadler erfunden.

List Comprehensions sind Anwendungen von Applikativen Funktoren. Ein Funktor ist etwas, worüber man ein Mapping ausführen kann, so wie das die Python-Funktion map() macht:

Code: Alles auswählen

In [5]: list(map(lambda x: x * 2, [1, 2, 3, 4, 5]))
Out[5]: [2, 4, 6, 8, 10]
Hier ist die Liste [1, 2, 3, 4, 5] der Funktor, da diese List dasjenige ist, worüber das Mapping ausgeführt wird. Dazu gibt es eine äquivalente List Comprehension:

Code: Alles auswählen

In [6]: [(lambda x: x * 2)(y) for y in [1, 2, 3, 4, 5]]
Out[6]: [2, 4, 6, 8, 10]
Oder kürzer:

Code: Alles auswählen

In [7]: [y * 2 for y in [1, 2, 3, 4, 5]]
Out[7]: [2, 4, 6, 8, 10]
Wenn man dagegen mehrere Funktoren hat (zB. zwei Listen), über die man gleichzeitig ein Mapping ausführen will, funktioniert map() nicht mehr. Eine List Comprehension dagegen schon:

Code: Alles auswählen

In [8]: [x + y for x in [1, 2, 3, 4, 5] for y in [6, 7, 8]]
Out[8]: [7, 8, 9, 8, 9, 10, 9, 10, 11, 10, 11, 12, 11, 12, 13]
In Haskell gibt es neben der List Comprehension eine Funktion namens <*>, die das ohne List Comprehension ermöglicht. Ein Funktor, auf dem diese Funktion definiert ist, ist ein sog. Applikativer Funktor. In Python gibt es kein solches Konstrukt, aber dafür die List Comprehension, die dazu äquivalent ist.
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Besten Dank, pillmuncher. Ich werde das alles verdauen müssen. Wobei ich zugeben muss, dass ich mich mit Haskell gar nicht auskenne.
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Man muß sich mit Haskell nicht auskennen um List Comprehensions (LCs) zu verstehen, aber es ist sicher schön zu wissen, dass dieses Konstrukt in Python aus Haskell entlehnt ist. In Ergänzung zu Cofis Anmerkung haben LCs Geschwindigkeitsvorteile gegenüber dem Aufbau einer Liste per for-Loop und append. Sie sind auch schneller als entsprechende Generator-Expressions. In der Praxis machen sich diese Unterschiede aber erst bei häufiger Iteration über diese Konstrukte bemerkbar. Bei großen Listen sind dann wieder Generatoren von Vorteil. Wann die jeweiligen Grenzen erreicht sind, lässt sich im Bedarfsfall durch Profiling ermitteln. Bei Listen mit 5 Elementen (und auch etwas größere), die zudem nur gelegentlich erzeugt werden, machen sich diese Unterschiede nicht bemerkbar. Aber wenn man LCs einmal verstanden hat, sind sie leicht zu lesen, schnell zu schreiben und man hat zudem das gute Gefühl auch flotten Code erstellt zu haben ... :wink:
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Vielen Dank, kbr. Im Grunde möchte ich als Anfänger alles verstehen. Ich möchte jeden kleinen Schritt verstehen. Aber eine kleine, und vielleicht peinliche, Frage habe ich noch. Wenn ich mir folgende LC anschaue:

Code: Alles auswählen

attribut = [b for b in list1 if b]
Verstehe ich den For/In-Bereich (for b in list1) und den If-Statement (if b). Und ich weiß auch, dass das erste b ein Ausdruck ist, mit dem die LC immer beginnt. Was macht das erste b in diesem Fall? Es ist schon mal nicht im For/In-Bereich, also iteriert wird dieser Ausdruck nicht, und in der Bedingung ist er auch nicht mit eingebunden. Und ich glaube auch nicht, dass das erste b nur als Dekoration dient.
Benutzeravatar
pillmuncher
User
Beiträge: 1484
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

@Sophus: Wenn man deine List Comprehension auf Deutsch reformuloeren würde, käme ungefähr sowa raus:

Die Liste derjenigen bs, so dass b ein Element aus list1 ist und bool(b) wahr ist. Die Reihenfolge bleibt erhalten.

Die Teile einer einfachen List Comprehension sind: Ein Ausdruck, der eine Funktion (im mathematischen Sinne, nicht im Python-Sinne) darstellt, dessen formale Parameter über den nachfolgenden Iterationsausdruck gebunden wird, und optional folgend ein Prädikat (ebenfalls im mathematischen Sinne), dessen formale Parameter ebenfalls durch den vorhergehenden Iterationsausdruck gebunden werden und das dazu dient, die Folge der Elemente, auf denen der erste Ausdruck operiert, zu beschränken/filtern. Iterations- und Prädikatausdrücke können wiederholt werden. LCs haben also diese Struktur:

Code: Alles auswählen

"[" fx [ "for" x "in" xs [ "if" px ]? ]+ "]"
x, xs, fx und px sind in diesem Kontext nicht als Python-Namen zu verstehen, sondern als metasyntaktische Variablen und Formeln. Auch " ist hier ein metasyntaktisches Symbol. fx ist irgendein Ausdruck der auf x basiert, ebenso px. Ersteres liefert das, was Element der neuen Liste werden soll, und letzteres filtert die Ausgangselemente.
? bedeutet: das in eckigen Klammern stehende vorausgehende Konstrukt kann 0 oder 1 mal vorkommen.
+ bedeutet: das in eckigen Klammern stehende vorausgehende Konstrukt kann 1 mal oder öfter vorkommen.
In specifications, Murphy's Law supersedes Ohm's.
stefanbunde
User
Beiträge: 29
Registriert: Dienstag 20. Oktober 2015, 12:59

die LC gibt dir ja eine liste mit elementen zurück.
und der erste wert dort gibt an, wie die elemente aussehen sollen.
in deinem fall wird einfach nur b zurück gegeben

Code: Alles auswählen

attribut = [b * b for b in list1 if b]
dieses beispiel würde eine liste mit den quadratzahlen von b zurückgeben, sofern b weder 0 noch None oder False ist
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo pillmuncher und Stefanbunde,

wenn ich es also richtig verstehe, dann dient der Ausdruck (b) für die neue Liste, die vom LC erzeugt wurde? Das heißt, all die neuen Elemente werden über diesen Ausdruck (b) in die neue Liste hinzugefügt? Im Umkehrschluss: Würde ich rein hypothetisch diesen Ausdruck weglassen, hätte die LC keine Möglichkeit eine neue Liste mit neuen Elementen zu erstellen, da der Ausdruck fehlt?
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

@Sophus: Um es umgangssprachlich auszudrücken: das erste 'b' in der LC ist das, "was in die Liste kommt" - das, was sonst in einer for-Loop per append hinzugefügt würde.
Zuletzt geändert von kbr am Donnerstag 29. Oktober 2015, 13:16, insgesamt 1-mal geändert.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Danke dir, kbr. Jetzt geht ein Licht bei mir auf.
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Oder nochmal anders geschrieben:
[Tätigkeit-mit-Element for Element in Quell-Liste]

Und alternativ mit einer Bedingung als Zusatz:
[Tätigkeit-mit-Element for Element in Quell-Liste if Bedingung-für-Element]

Bei der zweiten Form landen nur die Elemente im Ergebnis, für welche die Bedingung zutrifft. Das ist also ein Filter, um das Ergebnis einzuschränken.

Tätigkeit-mit-Element kann auch das Element selbst sein, ohne dass damit eine Operation ausgeführt wird. Das kann sinnvoll sein, wnen man ein if im Ausdruck hat. Also in dieser Art:

Code: Alles auswählen

[elem for elem in mylist if elem.startswith('x')]
EDIT: Und mit "Quell-Liste" ist allgemein ein iterierbares Objekt gemeint, wie z.B. auch ein Tupel oder ein String. Ich wollte es sprachlich hier etwas einfacher halten, weil der Fragesteller da meiner Meinung nach mehr von hat.
Zuletzt geändert von snafu am Donnerstag 29. Oktober 2015, 14:23, insgesamt 1-mal geändert.
Antworten