freeX: Datenstrukturen in Perl, Teil 6

Die ausschließlichen Vertriebsrechte an diesem Artikel liegen beim Computer- & Literaturverlag (C&L). Der Artikel darf nicht kopiert oder gar erneut in einer Zeitschrift oder einem Buch veröffentlicht werden ohne vorherige Erlaubnis von C&L. Der Verlag gestattet freundlicherweise die Veröffentlichung auf diesen Seiten. Wer öfter auf diesen Hinweis trifft, sollte sich überlegen, die Zeitschrift freeX zu abonnieren.

Perl hier, Perl da, Perl ist immer da!

 
Bisher wurden Grundlagen für die erfolgreiche Programmierung mit Perl geschaffen. In dieser Folge sollen sie angewendet und erweitert werden. Es werden komplexe Datenstrukturen gebildet und mit ihnen gearbeitet.

 

1. Parameterübergabe
2. Normale und Assoziative Arrays
3. Arrays übergeben
4. Referenzen auf Variablen
5. Auflösung von Referenzen
6. Parameterübergabe
7. Kurzschreibweise
8. Arrays von Hashes
9. Anonyme Datenstrukturen
10. Data-Dumper
11. Ressourcen

Wichtig für die Programmierung mit Perl ist die Behandlung von Datentypen und Datenstrukturen. Als Programmierer muß man sich prinzipiell nicht darum kümmern, wie Variablen verwendet werden, da Perl sie automatisch in die benötigten Datentypen konvertieren wird. Komplexe Datenstrukturen lassen sich mit Perl auch erzeugen, jedoch verliert man ein bißchen dieser Flexibilität, gewinnt jedoch gleichzeitig ein erhebliches Maß an neuer Flexibilität hinzu.

Parameterübergabe

Wenn Parameter an eine Funktion übergeben werden, erstellt Perl ein normales Array bestehend aus den Argumenten. Daraus resultiert die Art, wie in einer Unterfunktion Parameter in der Funktion gelesen werden. Perl stellt die Variable »@_« zur Verfügung. Auf die einzelnen Elemente wird in der Funktion mit »$_[0]«, »$_[1]« etc. zugegriffen oder über die Perl-Funktion »shift«, die Stück für Stück Elemente vom Array liest.

Zur Verdeutlichung dient folgende Funktion:

   sub print_args
   {
       my $arg_0 = shift;
       my $arg_1 = shift;
       my $arg_2 = shift;

       print "Argument 0 = $arg_0\n";
       print "Argument 1 = $arg_1\n";
       print "Argument 2 = $arg_2\n";
   }

Drei Argumente werden gelesen und anschließend der Reihe nach ausgegeben. Für eine Routine, der immer drei Argumente übergeben werden, ist das akzeptabel. Wenn die Routine jedoch allgemeiner gehalten werden und eine beliebige Anzahl Parameter unterstützen soll, stellt sie sich als sehr unflexibel heraus.

Das muß jedoch nicht sein, denn Perl stellt die Argumente direkt in einem Array zur Verfügung, das auch direkt übernommen werden kann. Die folgende Routine bietet daher höhere Flexibilität:

   sub print_args
   {
       my @arg = @_;
       my $elem;
       my $i = 0;

       foreach $elem (@arg) {
           $i++;
           print "Argument $i: $elem\n";
       }
   }

Alle Parameter, die an die Routine »print_args« übergebenen wurden, befinden sich im Array »@_«. Dieses wird zu Anfang der lokalen Variablen »@arg« zugewiesen. Im Prinzip ist diese Anweisung nicht nötig, sie macht den Programmtext jedoch etwas übersichtlicher. Natürlich steht die Variable »@_« in der gesamten Subroutine zur Verfügung und könnte auch direkt verwendet werden. Ein Aufruf der Routine erfolgt z.B. mit folgender Zeile.

   print_args ("eins", "zwei", 3);

Sie erzeugt nun die folgende Ausgabe:

   Argument 1: eins
   Argument 2: zwei
   Argument 3: 3

Normale und Assoziative Arrays

Im vierten Teil dieser Serie wurde beschrieben, daß assoziative Arrays spezielle Formen von Arrays darstellen. Sie lassen sich dementsprechend einfach in normale Arrays überführen. Rufen Sie die Routine von oben einmal wie folgt auf:

   print_args ('eins' => 1, 'zwei' => 2);

Die Ausgabe wird Sie vielleicht verwundern. Alternativ können Sie natürlich auch ein Array (oder Hash) übergeben anstatt dessen Komponenten direkt als Parameter zu schreiben. Die obigen Aufrufe haben die gleichen Effekte wie folgende:

   @array = ("eins", "zwei", 3);
   print_args (@array);

  %hash = ('eins' => 1, 'zwei' => 2);
  print_args (%hash);

Das Resultat des zweiten Aufrufs ist jedoch nur selten im Sinne des Programmierers. Wenn als Parameter ein assoziatives Array übergeben wird, dann wird meistens erwartet, daß in der Subroutine die Vorteile des assoziativen Arrays ausgenutzt werden und dieses nicht einfach in ein herkömmliches Array umgewandelt wird. Im vierten Teil dieser Artikelserie wurde folgender Aufruf verwendet, um eine HTTP-Verbindung zum Server »www.debian.org« aufzubauen.

   $connection = new IO::Socket::INET (Proto => "tcp",
                                       PeerAddr => "www.debian.org",
                                       PeerPort => "www");

Offensichtlich untersucht der Konstruktor die Argumente, um die Verbindung aufzubauen. Diese Routine unterstützt laut Dokumentation erheblich mehr Argumente, die jedoch auch weggelassen werden dürfen. In der Tat ist es möglich, die Argumentenliste in Perl nicht nur als normales Array sondern auch als assoziatives Array aufzufassen. In dem Fall wird das Array »@_« einfach in einen Hash-Kontext gebracht, z.B. mit der Anweisung:

   %hash = @_;

Besser sieht das Beispiel von oben daher aus, wenn assoziative Arrays auch beachtet werden:

   sub print_args
   {
       my %arg = @_;

       my $elem;
       my $i = 0;

       foreach $elem (keys %arg) {
            $i++;
            printf "Argument $i: %s => %s\n", $elem, $arg{$elem};
       }
   }

   print_args ('eins' => 1, 'zwei' => 2, 'drei' => "3");

Perl verbindet sogar zwei Arrays, wenn mehrere als Parameter angegeben sind. Überprüfen Sie das mit der folgenden Anweisung:

   @array = ("eins", "zwei", 3);
   @mehr = ("vier", "fuenf", 6);
   print_args (@array, @mehr);

Wenn dieses nicht gewünscht ist, müssen entsprechende Vorsichtsmaßnahmen getroffen werden. Es muß verhindert werden, daß Perl die angegebenen Arrays zu einem neuen zusammenfaßt.

Arrays übergeben

Dieses Verhalten sieht zwar recht praktisch aus, kann jedoch auch zu Problemen führen. Wenn Sie eine Routine benötigen, die überprüfen soll, ob ein einziges Element in einem Array enthalten ist, möchten Sie die Routine vielleicht so aufrufen:

   if (ist_element (@array, $element)) {
       ...
   }

Perl wandelt beide Argumente in ein einziges Array »@_« um. Damit wird es etwas schwieriger herauszufinden, nach welchem Element gesucht werden soll. In diesem Fall könnte man die Information mit Hilfe der Länge des Arrays berechnen, sofern nur ein Element überprüft werden soll und nicht mehrere.

Doch wie soll vorgegangen werden, wenn Sie eine Routine benötigen, die die Unterschiede zweier Arrays berechnet und zurückliefert. Ein Aufruf würde dann wie folgt aussehen:

   @diff = array_diff (@array1, @array2);

Hier stehen die Chancen extrem schlecht, mit den bisher bekannten Möglichkeiten herauszufinden, wie lang die einzelnen Arrays sind. An dieser Stelle besteht noch ein weiteres Problem. Die Arrays werden kopiert. Je größer sie sind, desto mehr Speicher muß kopiert werden und desto langsamer wird der einfache Aufruf einer Subroutine.

Referenzen auf Variablen

Das muß jedoch nicht sein. Perl unterstützt nicht nur normale skalare Variablen, Arrays, Hashes und Filehandles, sondern auch Referenzen darauf. Eine Referenz in Perl ist vergleichbar mit einem Zeiger auf Daten. Referenzen sind damit vergleichbar mit Zeiger (Pointer) auf Datenstrukturen in der Programmiersprache C. Für Perl sind Referenzen seinerseits wieder skalare Variablen, so daß ihr Inhalt anderen Variablen zugewiesen und an Funktionen übergeben werden kann. Skalaren Variablen in Perl wird ein Dollarzeichen »$« vorangestellt, es sind üblicherweise Zahlen (Ganzzahlen oder Fließkommazahlen) oder Zeichenketten.

[Referenzen]

Abbildung 1: Referenz auf ein Array

Eine Referenz wird in Perl durch Voranstellen des Backslashes »\« erzeugt. Die folgenden Zeilen erzeugen jeweils eine Referenz auf eine skalere Variable, ein Array und ein Hash und weisen die erzeugte Referenz einer neuen skalaren Variablen zu:

   $ref_skalar = \$element;
   $ref_array  = \@array;
   $ref_hash   = \%hash;

Perl erlaubt es sogar, Referenzen auf Konstanten zu erzeugen und zu verwenden. Als Beispiel sollen folgende Zeilen dienen, die Referenzen auf eine Ganzzahl, eine Fließkommazahl und eine Zeichenkette erzeugen:

   $ref_int    = \4711;
   $ref_float  = \0.815;
   $ref_string = \"Konstanter Text";

Da normale und assoziative Arrays aus Skalaren bestehen, können auch Referenzen auf Elemente von normalen und assoziativen Arrays gebildet werden. Das folgende Code-Fragment soll dieses demonstrieren:

   @array = ("eins", "zwei");
   %hash  = ("eins" => 1);

   $ref_array_elem = \$array[0];
   $ref_hash_elem  = \$hash{"eins"};

Auflösung von Referenzen

Wenn über die Referenzen einfach wieder auf die ursprünglichen Variablen zugegriffen werden kann, ist diese Technik dazu geeignet, die Probleme von oben zu lösen. Dann können auch mehrere Arrays oder Hashes an eine Funktion übergeben werden ohne daß die Inhalte in ein neues Array übertragen werden - und ohne daß das gesamte normale oder assoziative Array kopiert werden muß.

Das ist in der Tat möglich und Sinne der Verwendung von Referenzen. Auf die referenzierten Daten wird in Perl zugegriffen, indem der typ-spezifische Präfix der Referenz vorangestellt wird. Für skalare Variablen ist dieser Präfix das Dollarzeichen »$«, fÜr Arrays der Klammeraffe »@« und für Hashes das Prozentzeichen »%«. Aus den erzeugten Referenzen »$ref_skalar« u.s.w. erhält man demnach mit folgenden Anweisungen die Variablen zurück:

   $var_skalar = $$ref_skalar;
   @var_array  = @$ref_array;
   %var_hash   = %$ref_hash;

Wenn nur ein Element aus einem Hash oder Array nachgeschlagen werden soll, das als Referenz vorhanden ist, oder wenn ein Element hinzugefügt werden soll, ist es recht umständlich, wenn jedesmal eine lokale Variable angelegt werden muß, der die dereferenzierte Variable (wie oben geschehen) zugewiesen wird und mit der anschließend gearbeitet werden kann.

Auch dieses ist nicht zwingend erforderlich, kann jedoch die Lesbarkeit des Quellcodes positiv beeinflussen. Perl unterstützt für Referenzen eine verkürzte Schreibweise. Bei dieser darf man sich jedoch nicht durch die Verwendung der doppelten Dollarzeichen verwirren lassen.

   printf "%s, %s\n", $$ref_array[0], $$ref_array[1];
   printf "C & L => %s\n", $$ref_hash{"C & L"};

Sie bedeuten nicht, daß es sich bei dem Bezeichner um eine Referenz auf einen Skalar handelt, auf den versucht wird, mit einem Index zuzugreifen. Perl wertet die Präfixe zuerst aus, die am nächsten zur Variablen stehen. Der Index (z.B. »[1]«) jedoch wird zuletzt ausgewertet. Im ersten Fall wird daher zuerst »$ref_array« ausgewertet, was eine Referenz auf ein Array ist. Anschließend wird auf das so erhaltene Array per Index zugegriffen. Alternativ und weniger verwirrend könnte man auch »${$ref_array}[0]« schreiben. Wichtig ist die Art der Bestimmung des einzelnen Elements des Arrays oder Hashes.

Ein Beispielprogramm, das verdeutlicht, wie Referenzen von Variablen erzeugt werden und wie diese anschließend wieder dereferenziert werden, damit sie ausgegeben werden können, sieht wie folgt aus.


   $element = "freeX";
   @array   = ("freeX", "Toolbox");
   %hash    = ("C & L" => "freeX");

   printf "Element: %s\n", $element;
   printf "Array: %s\n", join (", ", @array);
   printf "Hash: C & L => %s\n", $hash{"C & L"};

   $ref_skalar = \$element;
   $ref_array  = \@array;
   $ref_hash   = \%hash;

   $var_skalar = $$ref_skalar;
   @var_array  = @$ref_array;
   %var_hash   = %$ref_hash;

   print "\n";

   printf "<> Element: %s\n", $var_skalar;
   printf "<> Array: %s\n", join (", ", @var_array);
   printf "<> Hash: C & L => %s\n", $var_hash{"C & L"};

   print "\n";

   printf "[] Element: %s\n", $$ref_skalar;
   printf "[] Array: %s, %s\n", $$ref_array[0], $$ref_array[1];
   printf "[] Hash: C & L => %s\n", $$ref_hash{"C & L"};

Perl ist kein Interpreter im klassischen Sinne. Der Quellcode wird nicht erst zur Laufzeit interpretiert, sondern stattdessen zu Beginn in Maschinencode übersetzt und anschließend ausgeführt. Anders als bei Shell-Skripten ist es daher auch möglich, Perl-Skripte während der Laufzeit zu verändern oder gar zu löschen ohne daß es Auswirkungen auf das laufende Programm hat. Ein Shell-Skript würde wahrscheinlich mit einer Fehlermeldung abbrechen, da die Zeilen- und Bytenummern nicht mehr übereinstimmen und daher Sprungadressen ungültig geworden sind.

Wenn jedoch Referenzen verwendet werden, dann kann Perl auch zur Laufzeit einen Fehler melden und die Ausführung des laufenden Programms abbrechen. Wenn im Programm eine Referenz auf eine Zeichenkette angenommen wird, bei der es sich jedoch um die Referenz auf ein Array handelt oder umgekehrt, wird Perl die Ausführung zur Laufzeit mit einem Fehler abbrechen. An diesen Stellen kann nicht entsprechend zwischen den Datentypen konvertiert werden. Daher ist bei der Verwendung von Referenzen sehr genau darauf zu achten, daß der dereferenzierte Typ (Skalar, Array oder Hash) übereinstimmt.

Parameterübergabe

Wie die Parameterübergabe an eine Subroutine mit Hilfe von Referenzen erfolgt, ist jetzt nur noch eine Anwendung der oben beschriebenen Verwendung von Referenzen in Perl. Die erste Änderung betrifft den Aufruf der Routine. Hier wird fortan kein Array mehr direkt übergeben, sondern eine Referenz auf ein Array. Der Aufruf sieht also wie folgt aus:

   print_args (\@array);

Die Routine »print_args« muß ebenfalls geändert werden, so daß sie die ihr übergebene Referenz auflöst und auf das referenzierte Array zugreift. Dazu wird die Referenz zuerst einer lokalen Variablen zugewiesen, die anschließend für die Dereferenzierung verwendet wird. Dieses geschieht durch folgende Zeilen.

   my $arg = shift;
   my @arg = @$arg;

Die Funktion »shift« arbeitet ohne Argument auf der impliziten Variable »@_«, so wie »print«, »grep« etc. ohne Argument auf die implizite Variablen »$_« angewendet werden, die den Inhalt der aktuellen Eingabezeile enthält, falls vorhanden.

Das Beispielprogramm umgeschrieben für die Verwendung einer Referenz auf ein Array sieht dann komplett wie folgt aus:

   sub print_args
   {
       my $arg = shift;
       my @arg = @$arg;
       my $elem;
       my $i = 0;

       foreach $elem (@arg) {
	   $i++;
	   print "Argument $i: $elem\n";
       }
   }

   @array   = ("freeX", "Toolbox");

   print_args (\@array);

Kurzschreibweise

Perl unterstützt für Referenzen auf Arrays und Hashes eine weitere Kurzschreibeweise mit einer weniger verwirrenden Syntax. Dabei wird der Pfeil-Operator verwendet. Er kommt einer Dereferenzierung gleich. Es muß jedoch während der Dereferenzierung ebenfalls genau darauf geachtet werden, daß die korrekte Schreibweise der De-Indizierung verwendet wird: für normale Arrays eckige Klammern und für Hashes geschweifte Klammern. Die Syntax lautet dann »$reference->[index]« für normale Arrays und »$reference->{index}« für assoziative Arrays. Das folgende Beispiel verdeutlicht dieses:

   @array   = ("freeX", "Toolbox");
   $ref_array = \@array;

   %hash    = ("C & L" => "freeX");
   $ref_hash = \%hash;

   printf "%s\n", $ref_array->[1];

   printf "%s\n", $ref_hash->{"C & L"};

Arrays von Hashes

Mit dieser Voraussetzung können komplexe Datenstrukturen gebildet werden. Es soll ein Array bestehend aus Hashes gebildet werden. Dazu werden zuerst mehrere assoziative Arrays gebildet, die anschließend in ein Array übertragen werden.

   %hash1 = (4 => "Inhaltsverzeichnis",
	     7 => "SMS mit Linux");
   %hash2 = (4 => "Inhaltsverzeichnis",
	     7 => "Fotodruck mit Linux");

   @freeX = (0, \%hash1, \%hash2);

   $ausgabe = 1;
   $seite = 7;
   printf "freeX %d, Seite %d: %s\n",
       $ausgabe, $seite, $freeX[$ausgabe]->{$seite};

   $ausgabe = 2;
   printf "freeX %d, Seite %d: %s\n",
       $ausgabe, $seite, $freeX[$ausgabe]->{$seite};

Dieser Code ist sieht recht statisch aus. Im folgenden wird deutlich, daß er es auch ist, was zu interessanten Problemen führen kann. Damit der Code sinnvoll ist, müßte er flexibler gehalten werden. Wenn für jede neue Ausgabe der freeX ein neuer Hash statisch bezeichnet wird, ist es recht unflexibel, außerdem muß beim Kopieren eins Blocks genau darauf geachtet werden, daß der manuelle Index immer geändert wird. Daher wird folgende Änderung versucht:

   push (@freeX, 0);

   %hash = (4 => "Inhaltsverzeichnis",
	    7 => "SMS mit Linux");

   push (@freeX, \%hash);

   %hash = ();
   %hash = (4 => "Inhaltsverzeichnis",
	    7 => "Fotodruck mit Linux");

   push (@freeX, \%hash);

Wenn dieser Code korrekt wäre, müßten nur weitere assoziative Arrays zum normalen Array hinzugefügt werden, ohne daß man sich Gedanken über die Position im Array machen muß. Werden die Befehle für die Ausgabe zu diesem Beispiel hinzugefügt, so erhält man beim Aufruf folgende Ausgabe:

  freeX 1, Seite 7: Fotodruck mit Linux
  freeX 2, Seite 7: Fotodruck mit Linux

Das ist jedoch nicht das gewünschte Resultat. Doch läßt es sich leicht erklären. Offenbar wird für beide Elemente im Array das gleiche assoziative Array verwendet. Es wird zwar mit der Anweisung »%hash = ();« zurückgesetzt, doch scheint es immer noch das gleiche zu sein. Das ist auch logisch, denn übernommen wurde ins normale Array die Referenz auf dieses assoziative Array. Das assoziative Array ist jedoch noch immer das selbe, es wurde kein neues erzeugt. Daher wurden die alten Werte einfach nur mit neuen überschrieben.

Anonyme Datenstrukturen

Gesucht wird daher eine Möglichkeit, ein Hash (oder auch Array, da dort das gleiche Problem auftritt) nicht nur zurückzusetzen, sondern komplett neu zu erzeugen, sodaß auch neuer Speicherplatz belegt wird. Das ist mit Perl leicht möglich. Mit »[]« wird ein anonymes Array erzeugt und mit »{}« ein anonymes Hash. Anonym soll in diesem Zusammenhang bedeuten, daß die Arrays keiner Variablen zugewiesen wurden.

Wenn gleich bei der Erzeugung der anonymen Arrays Konstanten eingetragen werden sollen, so werden sie einfach in der passenden Syntax in die jeweiligen Klammern geschrieben. Das Beispiel von oben wird daher wie folgt abgeändert:

   $hash = {4 => "Inhaltsverzeichnis",
	    7 => "SMS mit Linux"};

   push (@freeX, $hash);

   $hash = {4 => "Inhaltsverzeichnis",
	    7 => "Fotodruck mit Linux"};

   push (@freeX, $hash);

Bei gleicher Ausgabe-Routine werden nun die korrekten Artikelthemen für die Seiten 7 der beiden Ausgaben ausgegeben.

   freeX 1, Seite 7: SMS mit Linux
   freeX 2, Seite 7: Fotodruck mit Linux

[Datenstruktur]

Abbildung 2: Datenstruktur grafisch dargestellt

Data-Dumper

Perl bietet eine sehr einfache Möglichkeit an, beliebig große und beliebig komplexe Datenstrukturen auszugeben. Sie werden dabei als Perl-Code ausgegeben. Wenn diese Ausgabe in einer Datei gespeichert wird, kann sie später von Perl extrem einfach wieder eingelesen werden, der Code muß lediglich gelesen und interpretiert werden.

Diese Funktionalität wird vom Modul »Data::Dumper« und dessen Routinen »Dumper« und »Data::Dumper->Dump« zur Verfügung gestelt. Die erste Routine wandelt die als Argumente übergebenen Datenstrukturen in Perl-Code um. Die zweite Routine bietet darüberhinaus die Möglichkeit, den Datenstrukturen andere Namen zuzuweisen.

Wurde die Datenstruktur wie oben aufgebaut, dann geben folgende Zeilen die gesamte Struktur wieder aus:

   use Data::Dumper;

   print Dumper (\@freeX);

Würde an dieser Stelle keine Referenz übergeben, sondern das Array selbst, dann würde das gleiche wie eingangs beschrieben passieren: für »Dumper« sähe es so aus, als wären mehrere Argumente übergeben worden. Die Routine wird alle Argumente einer Variablen »$VAR<n>« zuweisen, wobei die Zahl »n« bei eins beginnend hochgezählt wird.

Wenn die Datenstruktur später wieder eingelesen und weiterverwendet werden soll, kann es sinnvoll sein, ihr gleich den richtigen Namen zuzuweisen. Dieses ist mit der zweiten Funktion möglich. Dazu werden ihr zwei Parameter übergeben. Der erste Parameter ist eine Referenz auf ein Array, das die Datenstrukturen enthält, also das, was an die Funktion »Dumper« übergeben wurde. Der zweite Parameter enthält ein Array von Namen, die für die Ausgabe verwendet werden sollen. Damit sieht die Ausgabe-Routine wie folgt aus:

   print Data::Dumper->Dump ([\@freeX], ["freeX"]);

Derartige Datenstrukturen können beliebig komplex werden. In den Beispielen aus diesem Artikel sind die Strukturen relativ einfach geblieben. Im Alltag können sie jedoch leicht komplex und schwer zu überblicken werden. XML-Dateien sind im Prinzip Baumstrukturen mit vielen Ästen und Blättern. Wenn XML-Dateien geparst und verarbeitet werden sollen, dann fallen solche baumartigen Strukturen an, bestehend aus normalen und assoziativen Arrays. XML-Parser für Perl bieten diese Ausgabe an.

Ressourcen

Martin Schulze
Quelle: freeX 3/01