Weißes Rauschen

die meisten Blogs sind trauriges Geschwätz

Perl foreach Schleifen

,

Diese Woche gehe ich auf foreach-Schleifen in Perl ein. Ein Konstrukt, das ich in den Programmiersprachen, die ich beruflich nutze (Fortran, C, C++) oft vermisse.

Perl kennt ein foreach-Konstrukt. Etwas, das sogar im aktuellen C++-Standard fehlt (for_each ist nicht wirklich gleichwertig.)

foreach $Name (sort keys %Telefonbuch)
{
    print $Name . ": " . $Telefonbuch{$Name}{"Mail"} . " " . $Telefonbuch{$Name}{"Tel"} . "\n";
}
#Ausgabe:
# Karl: 111111 (Fehlermeldung)
# Knut: 654321 Knut@gmx.de
# Peter: 123456 Peter@gmx.de
# Sepp: 112233 (Fehlermeldung)
Da sind jede Menge neue Techniken drin.
%Telefonbuch ist das Hash mit Namen Telefonbuch. $ Leitet eine (Skalar-)Variable ein, @ ein Array, % ein Hash. Warum haben wir das Hash eben mit $Telefonbuch{"Peter"}{"Mail"} angesprochen?! Weil wir da nicht das ganze Hash gemeint haben, sondern eben nur einen Eintrag (nämlich Peters Mail). Merksatz: wenn es nur ein Teil zurückliefert (sei es Zahl oder Text), wird es als einfache Variable ($Variable) angesprochen. Auch wenn dieses eine Teil in einem Array oder Hash abgelegt ist.
%Telefonbuch;   # meint das ganze Telefonbuch-Hash
$Telefonbuch{$Name};  # meint nur einen Eintrag aus dem Telefonbuch-Hash
@NamensListe;         # meint die ganze Liste
$NamensListe[1];      # meint nur den 2. Eintrag in der Liste
keys ist ein spezielles Wort für die Verwendung mit Hashes. In einem Hash ist der Teil in den Geschweiften Klammern der "Schlüssel" also key. Wenn ich also keys %Telefonbuch schreibe, meine ich die Schlüssel. In diesem Fall sind die Einträge über die Namen geschlüsselt. Also liefert mir foreach in jedem Durchgang einen anderen Namen, der in dem Hash verwendet wurde (Peter, Knut, Karl, Sepp). Die Variable hinter forach (hier: $Name) bekommt diesen Schlüssel für jeweils einen Durchlauf zugewiesen.

sort kann man sich schon fast denken. Es sagt Perl, man möchte die Schlüssel sortiert haben. Also werden die Namen in alphabetischer Folge (Karl, Knut, Peter, Sepp) geliefert. sort funktioniert übrigens auch mit Arrays, keys logischerweise nicht, weil Arrays keine Schlüssel haben.

Außerdem verwenden wir wieder Punkte (.). Diesmal bedeuten sie nicht irgendwas, oder einfach nur das Satzzeichen, sondern der . verbindet in diesem Zusammenhang Texte (oder Variablen, die als Texte betrachtet werden).
$MeinText = "Ein" . "Text";
# inhalt von $MeinText: "EinText"
$MeinText = $MeinText . " und noch ein Text";
# inhalt von $MeinText: "EinText und noch ein Text"
Der Punkt hat in Perl je nach Kontext verschiedene Bedeutungen. Dieser Modus-Wechsel wird von einigen Software-Designern als Design-Todsünde betrachtet. Trotzdem funktioniert es in Perl erstaunlich gut. Obwohl man sich durchaus an die Empfehlungen der erfahrenen Designer halten sollte, zählt letztlich, was beim Benutzer gut funktioniert.

"\n" auch dieses Konstrukt ist neu. Es bedeutet nichts anderes als einen Zeilenumbruch. Fast jede Programmiersprache nutzt dieses \n zu diesem Zweck. VisualBasic nutzt lustigerweise vbCrLf (VisualBasic Carriage Return Line Feed). Obwohl vbCrLf geringfügig mehr Hinweise auf den Zweck gibt, dürften die meisten Menschen \n angenehmer finden. Denn auswendig lernen muss man die Bedeutung sowieso, aber \n ist schneller geschrieben als vbCrLf.

(Fehlermeldung) habe ich nur geschrieben, weil Perl hier meckern würde, dass eine Variable auf die man zugreifen wollte, nicht existiert oder noch keinen Wert hat. Der Programmier-Neuling wird mit den Schultern zucken, der C-Veteran ungläubig den Kopf schütteln. Viele Programme (C, C++, Fortran, usw.) stürzen nämlich bei einem Zugriff auf eine nicht initialisierte Variable bestenfalls ab. Oft geben sie irgendeinen Müll aus, manchmal schreddern sie auch wichtige Daten und tun so als wäre nichts gewesen. Java initialisiert deshalb automatisch Variablen, die nicht vom Programmierer ausdrücklich initialisiert wurden und wo es das nicht kann, warnt der Compiler. C# spart sich die Initialisierung aber verhindert, dass ein Programm kompiliert wird. Perl geht einen vierten Weg. Es erkennt den Fehler während das Programm läuft und macht weiter, sofern das möglich ist. Das ermöglicht einem Perl-Programmierer die Fehlermeldung zu sehen und selbst einzuschätzen, wie schwerwiegend er ist. Ich schreibe oft Perl-Programme, die nur eine Seite lang sind, nur eine Aufgabe erledigen müssen und danach im Mülleimer landen. Ich weiß oft schon beim Schreiben, dass es einen Haufen Fehlermeldungen gibt. Ich weiß, dass ich sie ordentlich abfangen könnte, aber jetzt zu faul bin. In diesem Fall sehe ich: Der Typ hatte halt keine E-Mail Adresse angegeben, ja und?

Die elegantere Variante wäre:
print $Name . ": ";
print $Telefonbuch{$Name}{"Mail"} . " " if (defined $Telefonbuch{$Name}{"Mail"});
print $Telefonbuch{$Name}{"Tel"} if (defined $Telefonbuch{$Name}{"Tel"});
print "\n";
Im Grunde schreibe ich: drucke $blabla wenn $blabla definiert ist. print $blabla if (defined $blabla);

Doppelte Entfernen

@Array = ("Peter", "Klaus", "Dieter", "Klaus", "Peter", "Harry", "Toto");

foreach $Element (@Array)
{
    $OhneDoppelte{$Element} = "egal";
}

foreach $EinzigartigesElement (keys %OhneDoppelte)
{
    push (@ArrayOhneDoppelte, $EinzigartigesElement);
}
#@Array enthält ("Peter", "Klaus", "Dieter", "Klaus", "Peter", "Harry", "Toto")
#@ArrayOhneDoppelte enthält ("Peter", "Klaus", "Dieter", "Harry", "Toto")
Eigentlich ist nur die Funktion push neu. Sie sagt nichts anderes als dass $EinzigartigesElement in das @ArrayOhneDoppelte hineingedrückt werden soll (zu den anderen Elementen, die vielleicht schon drin sind).

Wenn man jetzt noch mal resümiert: Ich habe die Array-Elemente einzeln in ein Hash kopiert. Der Text in den Array-Elementen diente dabei als Schlüssel. Beim ersten Aufruf von $OhneDoppelte{"Klaus"} = "egal"; wurde das Element "Klaus" im Hash erstellt und mit "egal" gefüllt. Beim Zweiten Aufruf passierte ... nix. Das Element "Klaus" gab es schon und es stand auch schon "egal" drin.

In der 2. foreach-Schleife wurden alle Schlüssel abgerufen und in ein @ArrayOhneDoppelte kopiert. Da es keine Doppelten Schlüssel gibt, ist jeder Name nur noch genau ein Mal vorhanden. (Es gibt auch kürzere Varianten, aber die sind nicht besser lesbar.)

Wenn man die beiden foreach-Schleifen vergleicht, sieht man, dass eigentlich immer eine Liste von Elementen jedes Element einzeln nacheinander bearbeitet wird. Oben ist es die Liste @Array unten ist es die Liste der Schlüssel des Hashes %OhneDoppelte. Beim Wort Liste könnte man auf die Idee kommen, die eine Liste einfach komplett in die andere zu Kopieren;
@ArrayOhneDoppelte = keys %OhneDoppelte;
Und wie üblich bei Perl. Wenn man denkt, es müsste gehen, dann geht es meist auch (irgendwie).

Noch mal langsam:
foreach $EinzigartigesElement (keys %OhneDoppelte)
{
    push (@ArrayOhneDoppelte, $EinzigartigesElement);
}
ist das gleiche wie:
@NochEinArray = keys %OhneDoppelte;
foreach $EinzigartigesElement (@NochEinArray)
{
    push (@ArrayOhneDoppelte, $EinzigartigesElement);
}
ist das gleiche wie:
@NochEinArray = keys %OhneDoppelte;
@ArrayOhneDoppelte = @NochEinArray;
ist das gleiche wie:
@ArrayOhneDoppelte = keys %OhneDoppelte;


Textelemente Zählen
Angenommen, ich möchte meinen Programmquelltext darauf untersuchen, welche Funktion besonders oft aufgerufen wird. Ich habe das Problem, dass ich vielleicht gar nicht weiß, wie alle Funktionen im einzelnen heißen. Mit Hashes kann ich einfach Dinge zählen, deren genauen Namen ich gar nicht kenne.

/*ein mini-Programm in C*/
main()
{
	print("test");
	print("noch ein test");
	return (1);
}


open ($FHI, "meinQuelltext.c") or die "geht nicht $!";

while (<$FHI>)
{
# Folgendes soll funktionsaufrufe im Stile von funktion(para1, para, bla); finden:
    if (/(\w+)\s{0,1}\([^\)]*\);/g) 
    {
        $Zaehler{$1}++;
    }
}

foreach $Schluessel (sort keys %Zaehler)
{
    print "$Schluessel wurde " . $Zaehler{$Schluessel} . " Mal gefunden.\n";
}
# Ausgabe: print wurde 2 Mal gefunden.
# Ausgabe: return wurde 1 Mal gefunden.


Man öffnet die Datei und bearbeitet jede Zeile in der while (<$FHI>) schleife.

Wenn man eine Funktion findet, nimmt man den Wort-Teil, d.h. den teil vor der ersten Runden Klammer und ruft damit $Zaehler{$1}++; auf. Bei print("test"); wäre der Wort-Teil "print" und es würde $Zaehler{"print"}++; ausgeführt. Die Anweisung ++ ist weit verbreitet. Sie bedeutet erhöhe die Variable um 1. Eine Besonderheit von Perl: Nicht-Existierende Variablen kann man auch um eins hoch zählen und erhält dann eins. Quasi nix + 1 = 1. In diesem Fall existiert $Zaehler{"print"} nicht, aber $Zaehler{"print"}++; setzt $Zaehler{"print"} auf 1. Sollte im weiteren Verlauf print("test Nummer 2"); gefunden werden, würde wieder "print" als Funktionsname extrahiert und $Zaehler{"print"}++; ausgeführt. Am Ende erhält man dann für jeden Funktionsnamen eine Zeile: "Funktionsname wurde x Mal gefunden." In diesem Beispiel: print wurde 2 Mal gefunden.

Wie so oft bei Perl ist es manchmal hinderlich zu viel über das Innenleben von Computerprogrammen zu wissen. Das Arbeiten mit uninitialisierten Variablen ($Zaehler{"print"}++, obwohl $Zaehler{"print"} noch keinen Wert hat) ist in den meisten Programmiersprachen ein schwerer Fehler. In Perl ist es total praktisch.

Fazit

Ich hoffe, ich konnte einen kleinen Einblick in die Möglichkeiten mit Perl Hashes bieten. Mit Hashes und Perl kann man Dinge machen, die einem in anderen Programmiersprachen eine Menge graue Haare verursachen würde.

Volker Pispers - Korrekturen und Anmerkungen.Dateien lesen und schreiben in Perl

Comments

Unregistered user Thursday, February 18, 2010 8:09:40 AM

Renee Bäcker writes: Ein guter Post! Ein paar Anmerkungen habe ich dennoch: * Was meinst Du damit, dass der Punkt in verschiedenem Kontext etwas anderes bedeutet? Wäre vielleicht einen kurzen Post Wert ;-) * Ich weiß, dass es hier um "foreach" ging. Dennoch möchte ich etwas zu dem Abschnitt "Doppelte entfernen" sagen. Die Schleifen sind für die Aufgabe nicht notwendig. Um die Einträge im Hash zu machen, kann man Hashslices (siehe auch http://reneeb-perlblog.blogspot.com/2009/09/daten-scheibchenweise.html) benutzen: @OhneDoppelte{ @Array } = (1) x @Array; Und dann die Schlüssel in einer Schleife in ein Array zu "push"en ist unnötig Arbeit - @ArrayOhneDoppelte = keys %OhneDoppelte. * open() sollte man unbedingt in der 3-Arg-Form benutzen. Siehe auch http://reneeb-perlblog.blogspot.com/2009/09/verschiedenes-zu-open.html Abschließend möchte ich aber nochmal sagen, dass mir Deine Reihe über Perl-Elemente gut gefällt. Weiter so!

SaschaEinSascha Thursday, February 18, 2010 9:22:08 PM

$LangesWort = "super" . "kali" . "fragi" . "listig";
$LangesWort =~ /(kali.*)$/;
print "$1 ist Teil von $LangesWort." if $1;
s/././g;

1. Zeile: . als Textverknüpfungs-Operator.
2. Zeile: . als Regular Expression.
3. Zeile: . als Satzzeichen.
4. Zeile: linker Punkt ist Regex, rechter Punkt ist Satzzeichen.

Die perlvar $. zähle ich schon gar nicht dazu.

Die Variante @ArrayOhneDoppelte = keys %OhneDoppelte gefällt mir gut. Das erklärt sehr anschaulich, dass keys %Hash ein Array zurück liefert. Ich werde das mit hinein nehmen. Danke.

@OhneDoppelte{ @Array } = (1) x @Array; finde ich hingegen unleserlich. Ich denke es ist für Anfänger (und mich) besser, sie schreiben ein paar Zeilen zu viel, als eine Zeile zu kopieren, die sie nicht verstehen. Oder wie es so schön heißt: "Schreibe Code in erster Linie für Menschen erst danach für Maschinen."

Zum open(): zuerst gehen, dann laufen lernen. Natürlich wäre es guter Stil, open auch mit 3 Argumenten zu nutzen, obwohl man den Dateinamen wörtlich als Funktionsparameter schreibt. Aber "use strict;" wäre auch guter Stil, trotzdem verzichte ich hier darauf (vorerst). Spätestens wenn es um ernsthaft sicheres Perl geht, werde auf andere Quellen verweisen müssen, weil ich da zu wenig Erfahrung habe.

Unregistered user Friday, February 26, 2010 6:43:28 AM

Renee Bäcker writes: Danke für die Erläuterungen mit dem Punkt... > @OhneDoppelte{ @Array } = (1) x @Array; finde ich hingegen unleserlich. > Ich denke es ist für Anfänger (und mich) besser, sie schreiben ein paar > Zeilen zu viel, als eine Zeile zu kopieren, die sie nicht verstehen. Das auf jeden Fall.

How to use Quote function:

  1. Select some text
  2. Click on the Quote link

Write a comment

Comment
(BBcode and HTML is turned off for anonymous user comments.)

If you can't read the words, press the small reload icon.


Smilies