Das Perl-Tutorial für Computerlinguisten
-
Upload
andre-hagenbruch -
Category
Documents
-
view
330 -
download
5
Transcript of Das Perl-Tutorial für Computerlinguisten
1 Perl-Installation ........................................................................................ 1
2 Installation des Emacs ............................................................................... 3
3 Benutzung des Emacs ................................................................................ 5
3.1 Struktur des Emacs ................................................................................................ 5
3.2 Grundlegende Emacs-Befehle .................................................................................. 6
3.3 Navigation im Puffer .............................................................................................. 8
3.4 Suche im Puffer ...................................................................................................... 9
3.5 Ersetzen im Puffer.................................................................................................. 9
3.6 Perl-Code im Emacs ausführen ................................................................................10
4 Grundlegende Datenstrukturen................................................................. 12
4.1 Skalare und Operatoren.........................................................................................13
4.2 Variablen .............................................................................................................16
4.3 Konstanten ..........................................................................................................19
4.4 Exkurs: Standardein- und -ausgabe .........................................................................20
4.5 Listen ..................................................................................................................20
4.6 Arrays..................................................................................................................24
4.7 Hashes .................................................................................................................29
4.8 Zusammenfassung.................................................................................................32
5 Kontrollstrukturen................................................................................... 37
5.1 Bedingungen ........................................................................................................40
5.2 Schleifen..............................................................................................................42
5.3 Zusammenfassung.................................................................................................50
5.4 Beispielanwendung...............................................................................................51
6 Operationen auf Dateien .......................................................................... 56
6.1 Dateideskriptoren ................................................................................................56
6.2 Dateizugriff..........................................................................................................56
6.3 Spezielle Dateideskriptoren...................................................................................60
6.4 Exkurs: Formatierung numerischer Zeichenketten ...................................................61
6.5 Zusammenfassung.................................................................................................62
6.6 Beispielanwendung...............................................................................................63
7 Reguläre Ausdrücke................................................................................. 74
7.1 Muster.................................................................................................................75
7.2 Platzhalter ...........................................................................................................77
7.3 Gruppierung und Speicherung.................................................................................85
7.4 Anker ...................................................................................................................87
7.5 Modifikatoren ......................................................................................................88
7.6 Wie funktionieren reguläre Ausdrücke? ...................................................................90
7.7 Gier .....................................................................................................................94
7.8 Ersetzung.............................................................................................................95
7.9 Vor- und zurückschauen..........................................................................................96
7.10 Zusammenfassung...............................................................................................97
7.11 Beispielanwendung .............................................................................................98
8 Subroutinen.......................................................................................... 103
8.1 Argumente .........................................................................................................104
8.2 Gültigkeitsbereiche ............................................................................................107
8.3 Rückgabewerte...................................................................................................109
8.4 Zusammenfassung...............................................................................................111
8.5 Beispielanwendung.............................................................................................112
9 Referenzen........................................................................................... 123
9.1 Erzeugen einer Referenz ......................................................................................123
9.2 Verwendung von Referenzen.................................................................................126
9.3 Komplexe Datenstrukturen..................................................................................131
9.4 Referenzen als Subroutinen-Argumente.................................................................137
9.5 Referenzen auf Subroutinen .................................................................................138
9.6 Referenztypen....................................................................................................141
9.7 Zusammenfassung...............................................................................................141
9.8 Beispielanwendung.............................................................................................142
10 Perl-Module ........................................................................................ 145
10.1 Perl-Module installieren ....................................................................................145
10.2 Perl-Module verwenden .....................................................................................147
10.3 Perl-Module selbst erstellen ..............................................................................152
Literatur .................................................................................................... 161
Einführend: .................................................................................................................161
Weiterführend:............................................................................................................161
Computerlinguistik:.....................................................................................................161
Index......................................................................................................... 162
Sprachwissenschaftliches Institut
1
1 Perl-Installation Um ein Perl-Skript ausführen zu können, benötigen Sie einen Perl-Interpreter. In Unix-basierten
Betriebssystemen ist dieser meist schon vorinstalliert; Windows-Nutzer müssen ihn selbst installieren.
Eine aktuelle Version des Perl-Interpreters findet sich auf der Download-Seite der Firma
ActiveState
http://www.activestate.com/Products/Download/Download.plex?id= ActivePerl
und auf dem FTP-Server des Sprachwissenschaftlichen Instituts
ftp://ftp.linguistics.rub.de/programming/languages/perl/windows
Wählen Sie bitte Version 5.8.x für Ihr Betriebssystem aus, wobei x die jeweils aktuelle
Unterversion markiert. Darüber hinaus finden sie in der Bezeichnung auch noch eine dreistellige Build-
Nummer.1
Im ersten Schritt doppelklicken Sie bitte auf die von Ihnen herunter geladene Datei, um das
Installationsprogramm zu starten. Nachdem Sie die Lizenzvereinbarung akzeptiert haben, bekommen
Sie die Gelegenheit, die Installation an Ihre Bedürfnisse anzupassen. Beachten Sie, dass die
Installationsroutine nicht automatisch das von Ihrem Betriebssystem für Anwendungen präferierte
Verzeichnis abfragt – für gewöhnlich C:\Programme –, sondern das Installationsverzeichnis direkt unter
dem Wurzelverzeichnis C:\ anlegen will:
Die Aktivierung eines Profils im nächsten Schritt der Installation ist optional. Akzeptieren Sie
zuletzt die voreingestellten Optionen und schließen die Installation damit ab.
1 Auf der ActiveState-Seite finden Sie zwei unterschiedliche Pakete für die Installation unter Windows: Sollten Sie mit
Windows 2000 oder jünger arbeiten, wählen Sie das MSI-Paket, für ältere Versionen das AS-Paket. Beachten Sie im letzten Fall
unbedingt die Hinweise im Kasten auf der rechten Seite.
2
Um zu überprüfen, ob die Installation erfolgreich war, starten Sie bitte die Eingabeaufforderung, die
Sie im Startmenü unter Alle Programme > Zubehör finden und geben dort perl –v ein. Sie sollten
nun Informationen über die von Ihnen installierte Perl-Version angezeigt bekommen.
Sprachwissenschaftliches Institut
3
2 Installation des Emacs Da der Emacs unter Unix-basierten Betriebsystemen ebenso wie Perl für gewöhnlich zur
Standardinstallation gehört, beschränken wir uns auch in diesem Abschnitt auf die Installation unter
Windows.
Eine Windows-Version des Emacs findet sich auf dem FTP-Server des Sprachwissenschaftlichen
Instituts unter
ftp://ftp.linguistics.rub.de/programming/editors/emacs/windows/
Laden Sie dort die aktuellste Version herunter. Um die komprimierte Datei entpacken zu können,
benötigen Sie WinZip, das sie unter http://www.winzip.com bekommen, oder ein anderes
Komprimierungswerkzeug, das mit tar.gz-Dateien umgehen kann.
Entpacken Sie im Folgenden die Dateien in ein Verzeichnis Ihrer Wahl. Unter dem Startverzeichnis
des Emacs finden Sie das Verzeichnis bin, in dem sich die Anwendung addpm.exe befindet.
Doppelklicken Sie auf diese, um Emacs im Startverzeichnis zu installieren.
Abschließend sollten Sie eine Konfigurationsdatei für den Emacs anlegen, die Ihnen die Arbeit mit
Perl erleichtert. Sie finden eine solche mit der Bezeichnung _emacs unter folgender Adresse:
http://www.linguistics.rub.de/~halama/lehre/clp/emacs_install/_emacs
Sie besitzt folgenden Inhalt, den Sie ggf. auch im Emacs eintippen können:
(standard-display-european 1) (require 'latin-1) (display-time) (cond ((fboundp 'global-font-lock-mode) (global-font-lock-mode t) (setq font-lock-maximum-decoration t))) (add-hook 'perl-mode-hook 'add-initial-code) (defun add-initial-code () "Use standard modules for Perl code." (interactive) (setq modules (list "strict" "diagnostics")) (if (eq (point-min) (point-max)) (while modules (setq module (car modules)) (setq modules (cdr modules)) (insert "use " module ";\n"))))
Diese Datei wird für gewöhnlich im Home-Verzeichnis des Benutzers abgespeichert; leider ist
dieses unter Windows nicht mit dem Eigene Dateien-Ordner identisch, sondern muss in der
Eingabeaufforderung vermittels set HOMEDRIVE ermittelt werden.2
2 Für gewöhnlich liefert dieser Befehl die Ausgabe C:. Sie können diese Angabe aber Ihren Bedürfnissen anpassen, indem
Sie diese Umgebungsvariable mit set HOMEDRIVE=<Laufwerksbuchstabe>:\<Verzeichnispfad> setzen.
4
Sprachwissenschaftliches Institut
5
3 Benutzung des Emacs Der Editor Emacs stammt aus der Unix-Welt und ist eine sehr mächtige Entwicklungsumgebung für
verschiedenste Aufgaben:
• Unterstützung bei der Erstellung von Quellcode in unterschiedlichsten Programmiersprachen
durch:
o Automatische Überprüfung der Klammerung,
o Möglichkeit, eine Anwendung direkt aus dem Editor auszuführen,
o Automatische Einrückung,
o Farbliche Darstellung unterschiedlicher syntaktischer Konzepte.
• Unterstützung bei der Eingabe von Textdokumenten durch:
o Automatische Erzeugung von Dokumentgerüsten (z.B. in generischen Markupsprachen,
wie HTML et al., oder in reinen Formatierungssprachen, wie LaTeX),
o Automatische Einrückung,
o Farbliche Darstellung unterschiedlicher syntaktischer Konzepte.
3.1 Struktur des Emacs Dazu bedient sich der Emacs des Konzepts des Modus. Für die verschiedenen Aufgaben gibt es
unterschiedliche Modi, die der Benutzer auswählen kann, bzw. die durch Verknüpfung mit bestimmten
Dateiendungen automatisch beim Start geladen werden. Für den jeweiligen Modus werden sogenannte
Makros aktiviert (daher der Name Emacs: Extended Macros), die die eigentliche Funktionalität
bereitstellen.
Für Perl stehen gleich zwei Modi zur Verfügung, der cperl-mode und der perl-mode, wobei
letzterer automatisch aktiviert wird, wenn man eine Perl-Datei lädt oder eine neue Datei erstellt.
Bevor wir unsere ersten Schritte mit dem Emacs tun und eine Perl-Datei erstellen, sei kurz der
Aufbau des Editors erklärt. Beim Start sehen Sie folgenden Bildschirm:
6
Zuoberst finden Sie wie gewohnt eine Menüleiste, mit der Sie viele Funktionen steuern können. Da
es aber zu den Eigenheiten des Emacs gehört, dass seine Befehle durch (kryptische) Tastenkombina-
tionen eingegeben werden, sollten Sie sich diese Arbeitsweise so schnell wie möglich zu Eigen
machen!
Grundsätzlich macht man alle Eingaben in einem Puffer; während die Inhalte im Hauptpuffer
stehen (in diesem Fall *scratch*, weil noch keine Datei geladen/angelegt wurde), finden sich Befehle
in der Kommandozeile, dem sogenannten Minibuffer.
Da die vom Emacs verwendeten Macros in einer besonderen Variante von Lisp programmiert
wurden, befindet man sich nach Start des Editors im lisp-mode.
3.2 Grundlegende Emacs-Befehle Kommandos lassen sich (abgesehen von den Menüeinträgen) auf zweierlei Arten absetzen:
• Durch Tastenkombinationen, die meist mit Strg-x oder Strg-c beginnen.
• Durch Eingabe des vollständigen Befehls in der Kommandozeile eingeleitet durch Alt-x.
Im ersten Fall gelangen Sie zur Dateiauswahl, indem Sie Strg-x Strg-f drükken.
Die Langversion dieses Befehls leiten Sie durch Alt-x ein und tippen dann im Minibuffer find-
file. Emacs kennt für solcherlei Kommandos die Abkürzung durch die Tabulator-Taste. Geben Sie
z.B. die ersten beiden Buchstaben ein, erscheint folgende Liste der möglichen Ergänzungen:
Geben Sie nun die nächsten Buchstaben ein und drücken danach jeweils die Tabulator-Taste, bis
Sprachwissenschaftliches Institut
7
Sie das vollständige Kommando im Minibuffer sehen.3
Modifizieren Sie jetzt den im Minibuffer vorgeschlagenen Pfad zu einem Verzeichnis, in dem ihre
Perl-Dateien künftig liegen sollen. Anders als unter Windows gewohnt, werden weder im Emacs noch
in Perl Backslashes für Verzeichnispfade angegeben, sondern normale Schrägstriche.
Ihrem letzten Schrägstrich folgt der Name Ihrer Perl-Datei: Standardmäßig endet dieser Dateityp
auf .pl.
Wenn Sie nun die Eingabetaste drücken (und Sie bei der Installation des Emacs alles richtig
gemacht haben), sollten Sie einen (fast) leeren Puffer sehen, dessen Name Ihr Dateiname und dessen
Modus Perl ist:
In die ersten beiden Zeilen fügt die _emacs-Datei automatisch zwei Header-Zeilen ein, die
notwendige Module laden. Am unteren linken Bildrand sehen Sie den Pufferstatus: Erscheinen dort
zwei Sternchen, ist der Puffer modifiziert worden, d.h. er stimmt nicht mehr mit der gespeicherten
Version überein. Geben Sie nun Strg-x Strg-s ein, wird die Datei gespeichert, und anstatt der Stern-
chen erscheinen Striche.4
Wenn Sie nun Perl-Programme erstellen, unterstützt Sie der Emacs bei der Eingabe durch die
3 Wie Sie gemerkt haben, mussten Sie nur find-fi eingeben. An dieser Stelle können Sie schon die Eingabetaste drücken,
ohne vorher Tab betätigt zu haben, da diese Stufe die minimale Ergänzung für den gewünschten Befehl darstellt. 4 Sehen Sie an dieser Stelle zwei Prozentzeichen, kann auf diesen Puffer (d.h. diese Datei) nur lesend zugegriffen werden!
Dieser Zustand lässt sich mit Strg-x Strg-q ändern.
8
farbliche Kennzeichnung von Konstrukten und durch die Einrückung. Letzteres geschieht für die
jeweilige Schachtelungsebene automatisch, wenn Sie das Semikolon am Zeilenende eingeben. Sind Sie
allerdings ungeduldig, können Sie Ihre Zeile jederzeit vermittels der Tabulator-Taste richtig einrücken
lassen.
Darüber hinaus informiert der Emacs Sie darüber, ob die von Ihnen gerade eingegebene schließende
Klammer mit der letzten öffnenden Klammer übereinstimmt. Sollte dies nicht der Fall sein, bekommen
Sie im Minibuffer die Fehlermeldung Mismatched parentheses.
3.3 Navigation im Puffer Sie navigieren einen Buffer für gewöhnlich mit den Cursor-Tasten. Allerdings gibt es einige
Tastenkombinationen, die Ihnen die Arbeit erleichtern: Um an den Anfang einer Zeile zu gelangen,
drücken Sie Strg-a, um an das Ende zu kommen, Strg-e. Den Anfang des Puffers erreichen Sie mit
Esc-<, das Ende mit Esc->. Dieses Ende wird durch die Position des Cursors nach dem letzten Zeichen
markiert.
Sprachwissenschaftliches Institut
9
3.4 Suche im Puffer Um einen Puffer zu durchsuchen, gibt es mehrere Möglichkeiten. Grundsätzlich unterscheidet der
Emacs zwischen einfacher Suche, wie man sie aus den meisten Textverarbeitungen kennt, Wortsuche,
bei der nur nach vollständigen Wörtern gesucht wird, und inkrementeller Suche.
In diesem Verfahren beginnt die Suche schon mit dem Tippen des ersten Zeichens: Alle Fundstellen
werden farblich unterlegt, und die Eingabe des nächsten Zeichens schränkt den Suchraum weiter ein.
Darüber hinaus können Sie den Puffer anhand regulärer Ausdrücke durchsuchen. Diese stellen weniger
konkrete Zeichenketten dar, als vielmehr Muster für Zeichenketten-Kombinationen. Allen Suchformen
gemein ist, dass sie sowohl vorwärts als auch rückwärts funktionieren.
Die Kommandos/Tastenkombinationen der verschiedenen Suchverfahren lauten:
• Einfache Suche vorwärts: search-forward Strg-s <Eingabetaste>
• Einfache Suche rückwärts: search-backward Strg-r <Eingabetaste>
• Wortsuche vorwärts: word-search-forward Strg-s <Eingabetaste> Strg-w
• Wortsuche rückwärts: word-search-backward Strg-r <Eingabetaste> Strg-w
• Inkrementelle Suche vorwärts: isearch-forward Strg-s
• Inkrementelle Suche rückwärts: isearch-backward Strg-r
• Inkrementelle Suche nach dem Wort, über dem sich der Cursor gerade befindet: Strg-s
Strg-w • Inkrementelle Suche nach der Zeichenkette von der Cursorposition bis zum Zeilenende: Strg-
s Strg-y
Im Emacs kann – ebenso wie in Perl – nicht nur nach einfachen Zeichenketten suchen, sondern
auch anhand vorgegebener Schablonen bestimmte Muster finden. Diese Schablonen nennt man
reguläre Ausdrücke. Sie spielen für computerlinguistische Analysen eine dermaßen große Rolle, dass
sie in einem eigenen Kapitel betrachtet werden sollen. Dennoch sei hier kurz skizziert, wie man sie im
Emacs verwendet.
Die Suche mit regulären Ausdrücken funktioniert so, dass Sie an die Kommandos der einfachen
Suche bzw. der inkrementellen Suche ein -regexp anhängen; bei den Tastenkombinationen drücken
Sie vor der Strg-Sequenz die Esc-Taste (z.B. isearch-forward-regexp Esc Strg-s).5
3.5 Ersetzen im Puffer Es gibt zwei Möglichkeiten, Text zu ersetzen: Einerseits können Sie ihn markieren, um ihn zu
kopieren und an anderer Stelle wieder einzusetzen oder komplett zu löschen. Andererseits können Sie
global alle Fundstellen einer Zeichenkette durch eine andere ersetzen; dieses Verfahren lässt sich durch
Bestätigung der jeweiligen Ersetzung absichern.
Im ersten Fall greift das Konzept des sogenannten kill rings: Dies ist ein interner Zwischenspeicher,
auf dem alle Zeichenketten abgelegt werden, die gelöscht oder kopiert werden sollen. Um eine
Zeichenkette in diesen Speicher zu schieben, muss zunächst mit Strg-Leertaste oder set-mark-
command eine Markierung gesetzt werden, ab der die zu bearbeitende Region anfängt. Positionieren Sie
den Cursor an das Ende dieser Region, lassen sich mit Strg-w oder kill-region die dazwischen
liegenden Zeichen löschen bzw. mit Esc-w oder kill-ring-save kopieren. Mit Strg-k lassen sich die
5 Hinweis zur inkrementellen Suche: Haben Sie alle für Ihre Suche notwendigen Zeichen eingegeben, navigieren Sie zur
nächsten Fundstelle, indem Sie die Tastenkombination erneut drücken.
10
Zeichen von der Position des Cursors bis zum Zeilenende löschen und in den kill ring übernehmen.6
Um nun das Gelöschte oder Kopierte wieder einzufügen, positionieren Sie den Cursor an der
gewünschten Stelle und drücken Strg-y bzw. geben yank in den Minibuffer ein.
Zur globalen Ersetzung von Zeichenketten geben Sie im Minibuffer replace-string oder query-
replace-string ein, wenn Sie die Ersetzungen bestätigen wollen.
3.6 Perl-Code im Emacs ausführen Damit Perl-Code im Emacs interpretiert werden kann, gibt es das Kommando compile. Daraufhin
bietet Ihnen Emacs an, den make-Befehl auf eine Datei anzuwenden, die Sie als Argument übergeben
können. Da dies für unsere Zwecke irrelevant ist, löschen wir diesen Befehl und ersetzen ihn durch
perl, einem Leerzeichen und den Namen des aktuellen Puffers (= der Datei, die wir ausführen wollen).
Wenn Sie nun die Eingabetaste drücken, teilt sich das Anwendungs-Fenster in zwei Bereiche auf:
Im oberen sehen Sie ihren Perl-Quellcode, im unteren (mit dem Namen *compilation*) sehen Sie die
Ausgabe Ihres Programms bzw. Status- und Fehlermeldungen oder Warnungen.7 Um im
*compilation*-Puffer z.B. navigieren oder suchen zu können, müssen Sie mit Strg-x b in diesen
anderen Puffer wechseln; eine Liste aller vorhandenen Puffer bekommen Sie mit Strg-x Strg-b
angezeigt. Wollen Sie allerdings nur einen der beiden Puffer sehen, machen Sie mit Strg-x 1 den
aktuellen Puffer wieder zu einem Fenster:
6 Esc-k kopiert nicht bis zum Zeilenende, sondern löscht den Absatz, in dem der Cursor steht (kill-sentence)! 7 Um einen Interpretationsvorgang abzubrechen, geben Sie im Minibuffer kill-compilation ein. Um einen Befehl zu
wiederholen (z.B. nachdem man einen Fehler in seinem Skript beseitigt hat), drückt man Strg-x Esc Esc.
Sprachwissenschaftliches Institut
11
Um einen Puffer zu beenden, drücken Sie Strg-x k oder geben im Minibuffer kill-buffer ein.
Um den Emacs zu beenden, drücken Sie Strg-x Strg-c oder geben das Kommando save-buffers-
kill-emacs ein.
Dies sind bei weitem nicht alle Kommandos, mit denen man den Emacs steuern kann, Sie stellen
nur eine subjektive Auswahl der im alltäglichen Gebrauch nützlichsten Befehle dar. Eine exzellente
Gedächtnisstütze stellt die Emacs Reference Card dar, die Sie unter dieser Adresse
http://www.linguistics.rub.de/~halama/lehre/clp/emacs_usage/refcard.pdf
im PDF-Format finden.
12
4 Grundlegende Datenstrukturen
'What brings you to Paris?' (Small talk, not bad.)
'The Eiffel Tower.'
'Do you like towers?'
'I like structures without cladding.'
'OK, it's a good motto'.
JEANETTE WINTERSON
Computer sind Systeme, die aus Hardware und Software bestehen. Diese beiden Komponenten sind
untrennbar miteinander verbunden; gemeinsam besteht ihre Aufgabe darin, eine Eingabe
entgegenzunehmen, sie zu verarbeiten und daraufhin eine Ausgabe zu erzeugen. Die auf der Ein- und
Ausgabeseite anfallenden Informationen nennt man Daten. Diese werden anhand von Algorithmen
verarbeitet.
Ein Algorithmus ist ein Verfahren zur Lösung eines Problems. Dieses muss in endlich vielen
Schritten lösbar sein, wobei der Verlauf der Problemlösung einem festen Schema folgt, das auf ver-
schiedene Eingaben ausführbar ist.
Daten sind Informationen, die für die maschinelle Verarbeitung angepasst sind. Dabei kann es sich
sowohl um Dinge handeln, die am Computer ähnlich repräsentiert werden, wie wir es aus dem
Alltagsleben gewohnt sind oder um solche, die wir am Rechner nur sehr abstrakt erfahren können. Zur
ersten Gruppe zählen Texte, Zahlen, aber auch Bilder oder Geräusche, deren Repräsentationen durch
Anwendungsprogramme ähnlich dem richtigen Leben erfahrbar sind. In die zweite Kategorie fallen
Dinge wie ein Apfel, ein Atom oder das Radfahren: In all diesen Fällen kann man die Eigenschaften
und das Verhalten dieser Dinge in einem Modell repräsentieren, das aber nicht den Grad der Er-
fahrbarkeit besitzt, wie in der ersten Kategorie. Diese Diskrepanz ergibt sich aus der Tatsache, dass
sich die Dinge der ersten Gruppe oftmals auf einfache Datentypen und Datenstrukturen abbilden
lassen, während die Beschreibung der Dinge der zweiten Gruppe eher komplexe Typen und Strukturen
erfordert.
Einfache Datentypen beschreiben grundlegende Informationen wie z.B. einzelne Buchstaben,
Zeichenketten oder Zahlen in verschiedensten Ausprägungen, z.B. als ganze Zahl, Gleitkommazahl
oder als Binär- oder Hexadezimalzahl. Da Perl eine nicht-typisierte Sprache ist, ist die Unterscheidung
zwischen diesen Informationsarten für die weitere Betrachtung gegenstandslos. Allein zwischen Zahlen
und alphabetischen Zeichenketten besteht eine solche Trennung, wie wir im Folgenden noch sehen
werden.
Wesentlich wichtiger ist der Begriff der Datenstruktur. Betrachten wir dazu zunächst das
unvermeidliche Hallo Welt-Beispiel, das die Zeichenkette Hallo Welt auf dem Bildschirm ausgibt:8
8 Damit Sie dieses Beispiel nachvollziehen können, speichern Sie diese Zeile in eine Datei mit der Endung .pl und führen
sie wie oben beschrieben im Emacs aus oder tippen in der Eingabeaufforderung perl dateiname.pl ein.
Sprachwissenschaftliches Institut
13
print "Hallo Welt"; Eine solche Zeile bezeichnet man als Anweisung. Sie besteht aus der Funktion print und deren
Argument, der Zeichenkette Hallo Welt. Jede Anweisung wird durch ein Semikolon abgeschlossen.
Die Zeichenkette Hallo Welt kann in solch einer Anweisung durch eine Ausprägung eines
beliebigen anderen Datentyps ersetzt werden. Eine solche Ausprägung eines Datentyps bezeichnet man
als Literal. Sie besitzt die Eigenschaft, konstant zu sein, d.h., man kann den Wert der Zeichenkette
Hallo Welt nicht verändern.
print "a"; print "42"; print "3.141592"; print "0b1111111"; Die hier verarbeiteten Daten besitzen einen singulären Charakter, weshalb man diese Datenstruktur
als Skalar bezeichnet. Dieser Begriff stammt aus der Mathematik, in der er einzelne Werte von einer
Reihe von Werten abgrenzt, die dort wiederum als Vektoren bezeichnet werden. Die analoge
Datenstruktur zu solch einer Liste heißt in Perl Array. Eine besondere Form solcher Listen, in denen
ein Eintrag aus einem Schlüssel und einem dazugehörigen Wert besteht, nennt man Hash.
4.1 Skalare und Operatoren In Perl bilden Skalare die kleinste Dateneinheit, aus der die größeren Datenstrukturen, Arrays und
Hashes, zusammengesetzt sind. Skalare lassen sich aber nicht nur ausgeben, sondern man kann auch
Operationen auf ihnen durchführen.
Arithmetische Operatoren
Zahlen lassen sich addieren, subtrahieren, multiplizieren, dividieren und potenzieren. Für derartige
Operationen verwendet man die arithmetischen Operatoren +, -, * und /. Das Ergebnis einer solchen
Operation bezeichnet man als Rückgabewert: 9
print 69 + 118; # Ausgabe: 187 print "25 minus 21 ist ", 25-21; # Ausgabe: 4 In diesen Beispielen wird zunächst die Berechnung durchgeführt, die der print-Funktion dann als
Argument dient.
Des weiteren kennt Perl den Modulo-Operator %, dessen Rückgabewert der ganzzahlige Rest einer
Division ist:
print 9 % 4; # Ausgabe: 1
Exponentialrechnung erfolgt anhand des **-Operators:10
print 5 ** 2; # Ausgabe: 25
9 Die Raute # ist das Kommentarzeichen in Perl; sie kommentiert jeweils eine Zeile aus. Einen besonderen Bezeichner für
mehrzeilige Kommentare gibt es in Perl nicht. 10 In einigen anderen Programmiersprachen ist das Caret ^ der Operator für die Exponentialrechnung; in Perl bewirkt der ^-
Operator eine bitweise XOR-Operation zwischen den Operanden!
14
Klammerung von Ausdrücken
Wie in der Mathematik üblich, lässt sich die Reihenfolge der Verarbeitung von Werten durch
Klammerung beeinflussen:
print 3 + 7 * 15; # Ausgabe: 108 print (3 + 7) * 15; # Ausgabe: 10 print (3 + 7) * 10; # Ausgabe: 10
Die Resultate der zweiten und dritten Zeile sind überraschend! Für gewöhnlich würde man
erwarten, dass die zweite Berechnung den Wert 150 und die dritte den Wert 100 als Ausgabe hat. Die
Sichtweise des Perl-Interpreters auf diese Anweisung weicht jedoch von der mathematischen ab: Zuerst
wird derjenige Teil ausgeführt, der die höchste Priorität besitzt. Dies ist die Addition innerhalb der
runden Klammern. Da die print-Funktion, die wie ein Operator wirkt, höhere Priorität besitzt als die
Multiplikation, wird sie als nächstes durchgeführt. Der Rückgabewert der print-Funktion ist 1; dieser
Wert wird zurückgegeben, wenn die Ausführung einer Funktion gelingt. Weil dieser Rückgabewert der
Multiplikation als Eingabe dient, wird dieser Teil der Anweisung jeweils zum Wert des anderen
Operanden ausgewertet. Da auf diesem Wert allerdings keine Ausgabe-Funktion mehr durchgeführt
werden kann, wird er verworfen.
Die richtige Klammerung muss also lauten:
print ((3 + 7) * 15); # Ausgabe: 150
Numerische Vergleichsoperatoren
Eine weitere Art von Operatoren stellen die numerischen Vergleichsoperatoren <, >, ==, <=, >= und
!= dar. Ihr Rückgabewert ist 0, wenn der Vergleich zu falsch ausgewertet wird und 1, wenn der
Vergleich zu wahr ausgewertet wird. Wahrheit definiert sich in Perl derart, dass alles wahr ist, was
nicht 0, eine leere Zeichenkette, ein undefinierter Wert oder eine leere Liste ist:
print "Ist zwei gleich vier? ", 2 == 4; # Ausgabe: print "Ist sechs gleich sechs? ", 6 == 6; # Ausgabe: 1 print "Ist zwei ungleich vier? ", 2 != 4; # Ausgabe: 1 print "Ist sieben kleiner als acht? ", 7 < 8; # Ausgabe: 1 print "Ist zwei größer oder gleich zwei? ", 2 >= 2; # Ausgabe: 1 Argumente lassen sich vermittels Bool'scher Operatoren miteinander verknüpfen. Dazu gibt es in
Perl sowohl eine Zeichenketten- als auch eine Symbol-Repräsentation: Den Zeichenketten and, or und
not entsprechen die Symbole &&, || und !. Der Unterschied zwischen diesen beiden Formen liegt in
der Verarbeitungsreihenfolge: Die Symbole besitzen Vorrang vor den Zeichenkettenrepräsentationen:
print 6 > 3 && 12 > 4; # Ausgabe: 1 print 9 > 7 || 8 < 6; # Ausgabe: 1 print !2 > 3; # Ausgabe: print !(2 > 3); # Ausgabe: 1 print 6 > 3 && 3 > 4; # Ausgabe: print 6 > 3 and 3 > 4; # Ausgabe: 1
Sprachwissenschaftliches Institut
15
In der letzten Zeile ergibt sich der Rückgabewert 1 aus der Auswertung des Ausdrucks 6 > 3
aufgrund der niedrigen Priorität von and. Als nächstes wird die print-Funktion ausgewertet, die eben-
falls 1 als Rückgabewert hat, sodass der negative Rückgabewert des Ausdrucks 3 > 4 für die Ausgabe
irrelevant wird.
Zeichenkettenoperatoren
Da Zeichenketten im Fokus der computerlinguistischen Analyse stehen, sei dieser Datentyp ein
wenig näher betrachtet. Zeichenketten bestehen sowohl aus alphanumerischen Zeichen und Inter-
punktion als auch aus Leerraumzeichen (engl. white space). Da sich einige dieser Leerraumzeichen
nicht literal darstellen lassen, müssen sie maskiert (engl. escaped) werden. So werden Zeilenumbrüche
durch die Maskierungssequenz \n dargestellt, während Tabulatoreinschübe durch \t repräsentiert
werden. Diese Ersetzung wird allerdings nur bei Zeichenketten in doppelten Anführungszeichen
wirksam; in einfachen Anführungszeichen wird die Maskierungssequenz als Literal ausgegeben:
print "\tEine einfache Zeichenkette\n"; print '\tEine einfache Zeichenkette\n'; # Ausgabe: \tEine einfache
Zeichenkette\n
Für Zeichenketten gibt es nur wenige Operatoren: Man kann Zeichenketten vermittels des Punkts .
miteinander verknüpfen (Konkatenation von lat. concatenare, "verketten") und sie anhand des
Multiplikators x n-mal wiederholen:
print "Einige " . "Zeichenketten " . "hintereinander.\n"; print "Los! " x3, "\n"; # Ausgabe: Los! Los! Los! Zeichenketten lassen sich durch die Funktionen uc() und lc() zu vollständig groß bzw. klein
geschriebenen Zeichenketten machen:11
print uc("alles groß!\n"); # Ausgabe: ALLES GROß!12 print lc("ALLES KLEIN!\n"); # Ausgabe: alles klein!
Alternativ dazu existieren auch Schreibweisen mit den Maskierungssequenzen \U bzw. \L:
print "\Ualles groß\n"; # Ausgabe: ALLES GROß! print "\LALLES KLEIN!\n"; # Ausgabe: alles klein!
Darüber hinaus gibt es die Möglichkeit, nur den ersten Buchstaben einer Zeichenkette mit
ucfirst() groß und mit lcfirst() klein schreiben zu lassen:
print ucfirst("aller")," ",ucfirst("anfang ist schwer!"); # Ausgabe: Aller Anfang ist schwer!
11 uc ist die Abkürzung für upper case (Großschreibung) und lc für lower case (Kleinschreibung). 12 Man beachte, dass die sz-Ligatur, die in der Großschreibung keine Entsprechung hat, dennoch in der Kleinschreibung
ausgegeben wird!
16
Operatoren für Zeichenkettenvergleiche
Zeichenketten lassen sich anhand der Operatoren für Zeichenkettenvergleiche eq, lt, gt, le, ge und
ne miteinander vergleichen. Zeichen werden vom jeweiligen Betriebssystem intern in sogenannten
Codetabellen verwaltet. In solch einer Tabelle ist jedem Zeichen eine Zahl in einer bestimmten Reihen-
folge zugeordnet, über die auf das Zeichen zugegriffen werden kann. Bei einem Zeichenkettenvergleich
werden diese internen Kodierungsinformationen der einzelnen Zeichen miteinander verglichen. Die
entsprechenden Positionszahlen lassen sich anhand der Funktion ord() ermitteln:
print "Ein A ist als ", ord(A), " kodiert\n"; # Ausgabe: 65 print "Ein a ist als ", ord(a), " kodiert\n"; # Ausgabe: 97 print "A" lt "a"; # Ausgabe: 1
Darüber hinaus existiert ein ternärer Vergleichsoperator mit der Bezeichnung cmp, der ebenso wie
sein numerisches Pendant <=> (auch Raumschiff-Operator genannt) zur Sortierung von Werten
eingesetzt wird. Ihre grundsätzliche Funktionsweise lässt sich so beschreiben: Ist der Operand der
linken Seite kleiner als der der rechten Seite, ist der Rückgabewert –1, ist der Operand der rechten Seite
kleiner, wird 1 zurück gegeben, sind beide Operanden gleich, ist der Rückgabewert 0:
print "A" cmp "a"; # Ausgabe: -1
4.2 Variablen Neben Literalen kennen Programmiersprachen auch Variablen. Diese stellen Container für
veränderliche Werte dar, d.h. einem Bezeichner wird ein Wert zugewiesen, der durch Operationen ge-
gen andere Werte ausgetauscht werden kann.
Skalarvariablen
In skalaren Variablen lassen sich alle einfachen Datentypen speichern, ohne dass man den
jeweiligen Typ angeben müsste. Allein die Datenstruktur einer Variablen muss in Perl explizit gemacht
werden. Dazu stellt man dem Bezeichner der Variablen ein Sonderzeichen voran; im Fall eines Skalars
ist dies ein Dollar-Zeichen $. Man weist einer Variablen vermittels des Zuweisungsoperators = einen
Wert zu. Diese Operation bezeichnet man bei der ersten Zuweisung als Initialisierung, im allgemeinen
Fall als Variablendefinition:
$name = "Groucho";
Will man nur den Namen einer Variablen für die weitere Verwendung vereinbaren, schreibt man
diesen, gefolgt von einem Semikolon, auf eine Zeile. Diese Operation heißt Variablendeklaration.
Sprachwissenschaftliches Institut
17
Variablennamen beginnen entweder mit einem Buchstaben oder einem Unterstrich. Danach dürfen
bis zu 250 Buchstaben, Ziffern oder Unterstriche stehen:
$Ich_habe_einen_langen_Variablennamen; # OK $mail-alias; # Nicht OK! $noam chomsky; # Nicht OK! $einfach; # OK $kiste56; # OK $_apFel; # OK $10cc; # Nicht OK!
Groß- und Kleinschreibung von Variablennamen ist bedeutungsunterscheidend:
$name $Name $NAME $nAMe
Auf einer skalaren Variablen lassen sich die gleichen Operationen ausführen, wie auf einem Literal,
wobei immer über den Variablennamen auf den Wert der Variablen zugegriffen wird:
$name = "Groucho\n"; print $name; # Ausgabe: Groucho $zahl1 = 15; $zahl2 = 35; print $zahl1 + $zahl2; # Ausgabe: 50 print "Ist $zahl1 kleiner als $zahl2? ", $zahl1 < $zahl2; # Ausgabe: 1
An dieser Stelle sei kurz auf den Vorteil der Verwendung von Variablen gegenüber Literalen bei
der Berechnung arithmetischer Ausdrücke eingegangen. Im Gegensatz zum bereits besprochenen
Beispiel, in dem die Präzedenz der Funktion print() bei der Klammerung für die Berechnung
berücksichtigt werden musste, funktioniert die Klammerung im Fall von Variablen wie aus der
Mathematik gewohnt:
my $ergebnis1 = (3 + 7) * 15; my $ergebnis2 = (3 + 7) * 10; print "$ergebnis1\n"; # Ausgabe: 150 print "$ergebnis2\n"; # Ausgabe: 100
Der Wert einer Variablen lässt sich ändern, indem man ihr einen neuen Wert zuweist:
$name = "Groucho Marx"; print $name; # Ausgabe: Groucho Marx
Steht ein Variablenname bei der Ausgabe in doppelten Anführungszeichen, wird der dazugehörige
Wert in der Ausgabe interpoliert; in einfachen Anführungszeichen wird wiederum die literale
Zeichenkette ausgegeben:
print "$zahl1 + $zahl2 = ", $zahl1 + $zahl2; # Ausgabe: 15 + 35 = 50 print '$zahl1 + $zahl2 = ', $zahl1 + $zahl2; # Ausgabe: $zahl1 + $zahl2 = 50
18
Werte lassen sich auch ändern, indem man numerische oder Zeichenketten-Operationen auf ihnen
ausführt:
$a = 6 * 9; print "Sechs mal neun ist $a\n"; # Ausgabe: Sechs mal neun ist 54 $b = $a + 3; print "Plus drei ist $b\n"; # Ausgabe: Plus drei ist 57 $c = $b / 3; print "Durch drei ist $c\n"; # Ausgabe: Durch drei ist 19 $d = $c +1; print "Eins dazu ist $d\n"; # Ausgabe: Eins dazu ist 20 print "Die Zwischenergebnisse waren $a, $b, $c\n";
# Ausgabe: Die Zwischenergebnisse waren 54, 57, 19, 20
Benötigt man diese Zwischenschritte nicht, verwendet man nur eine Variable:
$a = 6 * 9; print "Sechs mal neun ist $a\n"; # Ausgabe: Sechs mal neun ist 54 $a = $a + 3; print "Plus drei ist $a\n"; # Ausgabe: Plus drei ist 57 $a = $a / 3; print "Durch drei ist $a\n"; # Ausgabe: Durch drei ist 19 $a = $a +1; print "Eins dazu ist $a\n"; # Ausgabe: Eins dazu ist 20
Die Anwendung von Operatoren und die Variablenzuweisung lassen sich in einem Schritt
vollziehen, indem man dem Zuweisungsoperator den jeweiligen Operator voranstellt und die Variable
auf der rechten Seite der Zuweisung eliminiert:
$a = 6 * 9; print "Sechs mal neun ist $a\n"; # Ausgabe: Sechs mal neun ist 54 $a += 3; print "Plus drei ist $a\n"; # Ausgabe: Plus drei ist 57 $a /= 3; print "Durch drei ist $a\n"; # Ausgabe: Durch drei ist 9 $a += 1; print "Eins dazu ist $a\n"; # Ausgabe: Eins dazu ist 10
Diese Schreibweise lässt sich in Fällen, in denen ein Wert um eins erhöht oder erniedrigt werden
soll, noch kompakter gestalten. Dazu verwendet man den Autoinkrement-Operator ++ bzw. den
Autodekrement-Operator --. Diese lassen sich vor bzw. hinter die Variable schreiben. Der Unterschied
besteht darin, dass in der ersten Variante zuerst die Operation und dann die Zuweisung ausgeführt wird,
während in der Postfix-Schreibweise zuerst die Zuweisung und dann die Operation ausgeführt wird,
was u.U. zu unerwünschten Seiteneffekten führen kann:
$a=4; $b=10; print "Initial haben wir die Werte $a und $b\n"; # 4 und 10 $b = $a++;
print "Jetzt haben wir $a und $b\n"; # 5 und 4 $b = ++$a*2;
print "Nun sind es $a und $b\n"; # 6 und 12 $a = --$b+4;
print "Zum Schluss haben wir $a und $b\n"; # 15 und 11
Sprachwissenschaftliches Institut
19
In der vierten Zeile wird zuerst zugewiesen, dann hochgezählt. In Zeile 6 zunächst inkrementiert,
dann multipliziert und danach findet die Zuweisung statt. In der vorletzten Zeile wird $b erst um eins
vermindert und um 4 erhöht und wird dann $a zugewiesen.
Nützliche Pragmata
Die bis hierhin verwendete Art der Variablendeklaration bzw. –definition birgt die Gefahr, dass der
Perl-Interpreter bei der Operation auf einer vorher bereits deklarierten/definierten Variablen einen
falsch geschriebenen Bezeichner als neue Variable interpretiert und versucht, die Operation darauf
auszuführen. Dies muss nicht immer zu einer Fehlermeldung führen, die ausgegebenen Ergebnisse
können aber sehr irritierend sein. Um dies zu vermeiden, gibt es das Perl-Pragma strict, das u.a.
solche Inkonsistenzen aufzuspüren vermag. Darüber hinaus empfiehlt es sich, vom Perl-Interpreter
möglichst ausführliche Fehler- und Warnungstexte13 ausgeben zu lassen, damit die Quelle eines Pro-
blems möglichst schnell und einfach aufgespürt werden kann. Dazu bedient man sich eines weiteren
Pragmas, das den Namen diagnostics trägt. Allgemein gesagt sind Pragmata
Verarbeitungsanweisungen an den Perl-Interpreter, die ihn zu einem bestimmten Verhalten bei der
Ausführung eines Perl-Programms veranlassen. Sie werden mit der Funktion use in den Quelltext
eingebunden:
use strict; use diagnostics;
Dies hat zur Konsequenz, dass Variablen bei ihrer Deklaration oder Definition das Schlüsselwort my
vorangestellt werden muss:14
my $name = "Groucho"; print $anme; # Ausgabe: Fehlermeldung!
Ohne use strict; bekäme man an dieser Stelle keine Fehlermeldung, sondern lediglich eine leere
Ausgabe, die bei der Fehlersuche nicht sonderlich hilfreich ist, insbesondere, wenn es sich um mehr als
zwei Zeilen Code handelt...
4.3 Konstanten Perl kennt keine eigene Datenstruktur für Werte, die in einem Container stehen, aber nicht
veränderbar sein sollen. Es hat sich allerdings eingebürgert, Konstanten in Variablen abzuspeichern,
deren Bezeichner man komplett groß schreibt:
my $PI = 3.141 my $NAME = "André";
13 Anders als eine Fehlermeldung führt eine Warnung nicht zum Abbruch des Programms, sondern informiert den
Programmierer über Stellen im Quellcode, die problematisch erscheinen. 14 Die eigentliche Funktionalität von my erschließt sich erst richtig, wenn in Kapitel 8 Subroutinen eingeführt werden.
20
4.4 Exkurs: Standardein- und -ausgabe Die bis jetzt verwendeten Daten hatten wir immer in das jeweilige Programm hineingeschrieben.
Sie waren dementsprechend für jeden Programmlauf statisch im Code selbst vorhanden. Will man eine
Möglichkeit schaffen, die Dateneingabe flexibler zu gestalten, muss man die Daten vom Programm
abtrennen. Dazu müssen wir zunächst verstehen, wie man die Tastatur während der Ausführung eines
Programms verwendet.
Das Ergebnis einer print-Funktion wird für gewöhnlich auf dem Bildschirm ausgegeben. Diesen
Kanal bezeichnet man auch als Standardausgabe. Analog dazu gibt es auch eine Standardeingabe: In
der Eingabeaufforderung lassen sich über die Tastatur Daten eingeben, auf denen eine Anwendung
operieren soll. Diese Kanäle tragen in Perl die Bezeichnungen <STDOUT> bzw. <STDIN>. Da die print-
Funktion ohne weitere Angaben immer ihre Ausgabe auf <STDOUT> ausgibt, muss man diesen Kanal
nicht explizit angeben. Bei der Standardeingabe werden zunächst Daten aus <STDIN> eingelesen und
einer Variablen übergeben, auf der dann Operationen ausgeführt werden können:
print "Wie heißt Du?\n"; my $name = <STDIN>; print "Hallo, $name";
Da die Tastatureingabe mit dem Druck der Return-Taste abgeschlossen wird, findet sich dieser
Zeilenumbruch auch in der Variablen $name. Um ihn zu entfernen, bedient man sich der Funktion
chomp(), die solch ein Zeilenendezeichen aus einer Zeile beseitigt:15
print "Wie heißt Du?\n"; my $name = <STDIN>; chomp $name; print "Hallo, $name\n";
4.5 Listen Listen kennt man aus dem alltäglichen Leben, z.B. als Handlungsanweisungen in Rezepten:
Sie besitzen die Eigenschaft, aus einzelnen Listenelementen zu bestehen, die geordnet sind. Die
einfachste Form der Liste in Perl ist die leere Liste. Sie wird durch eine öffnende und eine schließende
runde Klammer () dargestellt. Zahlenwerte lassen sich ohne Modifikationen in diese Klammern
schreiben, z.B. (42), während Zeichenketten in Anführungszeichen gesetzt werden müssen: ("Käse").
Einzelne Listenelemente werden durch Kommata voneinander getrennt:
15 Parallel dazu gibt es eine Funktion mit der Bezeichnung chop(), die jedes beliebige Zeichen am Zeilenende entfernt.
Sprachwissenschaftliches Institut
21
("Käse", "Wurst", "Brot")
Eine Funktion, die auf Listen operiert, ist bspw. die print()-Funktion: Sie nimmt eine Liste von
Argumenten, die sie ausgibt:
print ("Hallo ", "Welt", "\n");
Wie bei allen eingebauten Perl-Funktionen können die Klammern um die Argumentliste wegfallen:
print "Hallo ", "Welt", "\n";
Listen dürfen sowohl numerische als auch Zeichenkettenwerte enthalten:
use strict; use diagnostics; my $zahl = 30; print "Hier haben wir eine Liste, die Zeichenketten (diese), ", "Zahlen (", 3.6, "), und Variablen: ", $zahl, " enthält.", "\n";
Da der Perl-Interpreter beliebigen (oder gar keinen) Leerraum zwischen den Listenelementen
erwartet, darf man eine solche Anweisung beliebig formatieren.
Alternativen zu Anführungszeichen
Wie schon bei Skalaren gezeigt, werden Werte in einfachen Anführungszeichen literal dargestellt,
während Variablen und Maskierungssequenzen in doppelten Anführungszeichen interpretiert werden.
Perl kennt alternativ zu den Anführungszeichen die Operatoren q// und qq//:
print q/Eine Zahl: /, q/$zahl/; # Ausgabe: $zahl print qq/Eine Zahl: /, qq/$zahl/; # Ausgabe: 30
Das paarige Trennzeichen // kann durch ein beliebiges anderes paariges Trennzeichen ersetzt
werden. Dabei können durchaus auch Zeichen eingesetzt werden, die ansonsten als Operatoren dienen:
print qq(Eine Zahl: ), qq%$zahl%; # Ausgabe: 30
In Listen gibt es darüber hinaus den qw//-Operator, mit dem die Kommata zum Abtrennen von
Listenelementen durch Leerraum ersetzt werden. Allerdings erfolgt immer eine literale Ausgabe, d.h.,
auch Anführungszeichen werden ausgegeben; Leerraumzeichen werden dabei überlesen:
print qw/"Eine Zahl: " $test/; # Ausgabe: "EineZahl"$test
22
Eindimensionalität von Listen
Listen in Perl besitzen die Eigenschaft, flach zu sein, d.h., man kann nicht ohne weiteres Listen in
eine bestehende Liste einbetten. Dies ergibt sich aus dem Umstand, dass die Elemente einer Liste
immer Skalare sein müssen!
(3, 8, 5, 15) ((3, 8), (5, 15)) (3, (8, 5), 15) ("eins", "zwei", "drei", "vier") (('eins', 'zwei', 'drei', 'vier')) (qw(eins zwei drei), 'vier') (qw(eins zwei), q(drei), 'vier') (qw(eins zwei drei vier))
Zugriff auf Listen
Geordnet werden die Listenelemente vermittels Indizes. Ein solcher Index beginnt immer mit 0,
d.h., das erste Element einer Liste besitzt den Index 0, das zweite den Index 1, etc. Um über solch einen
Index auf ein Listenelement zugreifen zu können, schreibt man den Index in eckigen Klammern [ ]
hinter die Liste:
print 'Salz', 'Essig', 'Senf', 'Pfeffer' [2]; # Ausgabe: Senf
Anstatt anhand der literalen Zahl des Index auf ein Element zuzugreifen, kann man auch eine
Variable an diese Stelle setzen:
my $monat = 4; print qw(Januar Februar März April Mai Juni Juli August September Oktober
November Dezember) [$monat]; # Ausgabe: Mai
Verwendet man statt einer ganzen Zahl eine Gleitkommazahl, wird der Nachkommawert ignoriert:
my $monat = 4.9; print qw(Januar Februar März April Mai Juni Juli August September Oktober
November Dezember) [$monat]; # Ausgabe: Mai
Gibt man eine negative Zahl als Index an, greift man vom hinteren Ende der Liste auf die Elemente
zu: [-1] bezeichnet das letzte Element einer Liste, [-2] das vorletzte, etc.:
print qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember) [-8]; # Ausgabe: Mai
Will man auf mehrere Elemente einer Liste zugreifen, schreibt man statt eines Skalars eine Liste in
eckige Klammern. Diese Operation bezeichnet man als Slice; ihr Rückgabewert ist eine Liste:
print qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember) [3, 4, 5]; # Ausgabe: AprilMaiJuni
Sprachwissenschaftliches Institut
23
Anstatt einzelne Elemente zu spezifizieren, lassen sich auch Bereiche durch zwei Punkte angeben:
print (1 .. 6); # Ausgabe: 123456
Es gilt wiederum, dass die Nachkommastellen von Gleitkommazahlen ignoriert werden:
print (1.4 .. 6.9); # Ausgabe: 123456
Bei der Verwendung negativer Werte gilt, dass der rechts stehende Wert größer sein muss als der
linke:
print (-6 .. 6); # Ausgabe: -6-5-4-3-2-10123456
Will man die umgekehrte Reihenfolge einer Liste erzeugen, verwendet man die Funktion
reverse():
print reverse(-6 .. 6) # Ausgabe: 6543210-1-2-3-4-5-6
Ebenso wie für Zahlenliterale lassen sich auch für Zeichenkettenliterale Bereiche angeben:
print ('a' .. 'k'); # Ausgabe: abcdefghijk
Zwar ist es in Perl möglich, von einem numerischen in einen Zeichenkettenbereich überzuleiten
oder umgekehrt, führt aber zu einer Warnung:16
print (3 .. 'k'); # Ausgabe: Argument "k" isn't numeric in range (or flop)
print ('k' .. 3); # Ausgabe: 0123
Bereiche lassen sich auch in Slices verwenden, um auf eine Reihe von Indizes zugreifen zu können:
print qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember) [4 .. 6]; # Ausgabe: MaiJuniJuli
Zuweisung von mehreren Werten an eine Liste
Listen lassen sich sowohl auf der rechten als auch auf der linken Seite einer Zuweisung verwenden:
my ($eins, $zwei) = (1, 2);
Weist man einer Liste eine Liste mit den gleichen Elementen in veränderter Reihenfolge zu, wird
zunächst die Liste auf der rechten Seite aufgebaut, die dann der Liste auf der linken Seite zugewiesen
wird:
my ($eins, $zwei) = ($zwei, $eins); # $eins = 2, $zwei = 1
16 Das Zeichenkettenliteral wird als 0 interpretiert, weshalb es im ersten Beispiel ausschließlich zur Ausgabe der Warnung
kommt, im zweiten aber eine Zahlenreihe ausgegeben wird!
24
4.6 Arrays Wie schon bei den Skalaren gesehen, gibt es auch für Listen eine Datenstruktur, in der sie als
Variablen gespeichert werden können. Diese nennt man in Perl Array und sie wird durch den
Klammeraffen @ als Präfixsymbol repräsentiert. Listen lassen sich Arrayvariablen genauso zuweisen
wie skalare Daten an Skalarvariablen:
my @tage = qw(Mo Di Mi Do Fr Sa So);
Bei der Ausgabe von Arrays ist zu beachten, dass sich Arrayvariablen innerhalb von doppelten
Anführungszeichen anders verhalten, als wenn man diese weglässt:
print @tage; # Ausgabe: MoDiMiDoFrSaSo print "@tage"; # Ausgabe: Mo Di Mi Do Fr Sa So
Skalare Variablen, die den gleichen Bezeichner verwenden wie Arrayvariablen, sind von diesen
verschiedenund umgekehrt:
my @tage = qw(Mo Di Mi Do Fr Sa So); my $tage = 31; print "@tage\n"; # Ausgabe: Mo Di Mi Do Fr Sa So print $tage; # Ausgabe: 31
Listenkontext vs. Skalarkontext
Weist man einem Array ein anderes Array zu, erwartet die Variable auf der linken Seite der
Zuweisung eine Liste. Diese Operation findet im sogenannten Listenkontext statt:
my @array1 = qw(Chico Harpo Groucho Gummo Zeppo); my @array2 = @array1; # @array2 enthält die Elemente
Chico, Harpo, Groucho, Gummo und Zeppo
Weist man einer Skalarvariable ein Array zu, erzwingt diese implizit einen skalaren Kontext. In ihm
wird der Rückgabewert der Zuweisung eines Arrays an einen Skalar zur Länge des Arrays:
my @array = qw(Chico Harpo Groucho Gummo Zeppo); my $skalar = @array; # $skalar besitzt den Wert 5
Man kann den Skalarkontext für ein Array auch vermittels der Funktion scalar() auch explizit
erzwingen; das Ergebnis ist zum vorher gezeigten Beispiel identisch:
print scalar(@array); # Ausgabe: 5
Sprachwissenschaftliches Institut
25
Zuweisung von Werten an ein Array
Eine weitere Art, Arrays zu erweitern nutzt die Eigenschaft von Listen aus, grundsätzlich
eindimensional zu sein:
my @array1 = (1, 2, 3); my @array2 = (@array1, 4, 5, 6); print @array2; # Ausgabe: 123456 @array2 = (3, 5, 7, 9); @array2 = (1, @array2, 11); print @array2; # Ausgabe: 1357911
Einer Liste einzelner Elemente kann man umgekehrt auch ein Array zuweisen:
my @array = (10, 20, 30); my ($element1, $element2, $element3) = @array; print "Der erste Skalar ist: $element1\n" # Ausgabe: 10 print "Der zweite Skalar ist: $element2\n" # Ausgabe: 20 print "Der dritte Skalar ist: $element3\n" # Ausgabe: 30
Ebenso lässt sich einem Skalar ein einzelnes Element zuweisen:
my $a = (10, 20, 30)[0]; # $a hat den Wert 10 $a = $array[0]; # $a hat den Wert 10
Zugriff auf Arrays
Eine beliebte Stolperfalle stellt die Vorstellung dar, man müsse mit dem Präfixsymbol @ auf das
Array zugreifen. Entscheidend ist an dieser Stelle nicht die Datenstruktur, von der ausgegangen wird,
sondern die Datenstruktur des Rückgabewerts. Da dieser im letzten Beispiel ein Skalar war, muss auch
über das Präfixsymbol dieser Datenstruktur, das Dollarzeichen $, auf das Array zugegriffen werden!
Folgende Syntax führt zu einer Warnung:
$a = @array[0];
Hier wird versucht, der Skalarvariablen $a einen einelementigen Arrayslice zuzuweisen. Man kann
eine entsprechende Meldung des Perl-Interpreters abfangen, indem man das Array durch Klammerung
explizit zu einer Liste macht:
$a = (@array)[0];
Die korrekte Verwendung eines Arrayslices als Zugriffsmöglichkeit auf mehrere Elemente eines
Arrays hat eine Liste als Rückgabewert, weshalb man an dieser Stelle richtigerweise den Klammeraffen
@ verwendet:
my @array1 = (1, 3, 5, 7, 9); my @array2 = @array1[1, 3 .. 4]; print "@array2"; # Ausgabe: 3 7 9
26
Ein Arrayslice funktioniert also wie einfacher Filter für Listen, bei dem die Position auf der Liste
das einzige Kriterium darstellt. Komplexere Filter werden im Laufe des Kurses folgen.
Auf einzelne Elemente eines Arrays lässt sich ebenso wie mit numerischen Skalarliteralen auch mit
Skalarvariablen zugreifen:
my @array1 = (1, 3, 5, 7, 9); my $index = 3; print $array[$index]; # Ausgabe: 7
Operationen auf Arrays
Da ein Array die Eigenschaft besitzt, eine geordnete Liste zu repräsentieren, kommen die Elemente
in einer bestimmten Reihenfolge auf diese Liste:
Man kann sich solch eine Liste also als Stapel vorstellen, auf den man vorne, hinten und mittendrin
Elemente platzieren kann:
Sprachwissenschaftliches Institut
27
Um Elemente sukzessive auf eine Liste zu befördern, wendet man die Funktion push() mit einem
listenwertigen Argument auf ein Array an. Meist ist dies allerdings eine einelementige Liste:
push my @array, 1; push @array, 2; print "@array"; # Ausgabe: 1 2
Um Elemente vom rechten Ende einer Liste zu entfernen, wendet man die Funktion pop() auf ein
Array an. Der Rückgabewert dieser Funktion ist derjenige Skalar, der von der Liste entfernt wurde:
pop @array; print "@array"; # Ausgabe: 1 my $element = pop @array; print "$element\n"; # Ausgabe: 1 print "@array"; # Ausgabe:
Um Elemente von links auf eine Liste zu befördern, wendet man die Funktion unshift() wiederum
mit einem listenwertigen Argument auf ein Array an, wobei auch hier die Liste meist nur aus einem
Element besteht:
unshift @array, 1; unshift @array, 2; print "@array"; # Ausgabe: 2 1
Um Elemente von der linken Seite einer Liste zu entfernen, wendet man die Funktion shift() auf
ein Array an. Der Rückgabewert dieser Funktion ist wiederum derjenige Skalar, der von der Liste
entfernt wurde:
shift @array; print "@array"; # Ausgabe: 1 my $element = shift @array; print "$element\n"; # Ausgabe: 1 print "@array"; # Ausgabe:
Löschoperationen und die Länge von Arrays
Anders als in vielen anderen Programmiersprachen muss die Länge eines Arrays nicht vor seiner
Benutzung festgelegt werden. Hat man ein Array deklariert, kann man ein Element an beliebiger Stelle
einfügen:
my @array; $array[999] = 3; print "@array"; # Ausgabe: 998 mal undef, 317
Mit der Funktion delete() lassen sich Werte anhand ihrer Indizes aus einem Array löschen, bzw.
durch undef ersetzen. Die Länge des Arrays ändert sich nur, wenn man das letzte Element löscht. Der
Rückgabewert der delete()-Funktion ist das gelöschte Element:
17 Die Verwendung undefinierter Werte führt allerdings zu einer Warnung; die Vorbelegung eines Arrays auf diese Weise
macht in Perl nicht sonderlich viel Sinn, sondern kostet nur Speicherplatz!
28
my @alphabet = ('a' .. 'z'); my $buchstabe = delete($alphabet[24]); print $buchstabe; print @alphabet ,"\n"; # Ausgabe:
abcdefghijklmnopqrstuvwxz print scalar(@alphabet); # Ausgabe: 26
Löschen und Einfügen von Bereichen
Mit der Funktion splice() lassen sich Bereiche ab einem bestimmten Index im Array löschen.
Dazu übergibt man der Funktion den Namen des Arrays, den Startwert18 des Index, ab dem die
Operation ausgeführt werden soll und die Anzahl der Elemente, die gelöscht werden sollen. Der
Rückgabewert dieser Funktion ist eine Liste der gelöschten Elemente; dieser Seiteneffekt lässt sich sehr
gut für computerlinguistische Analysen ausnutzen, wenn man bspw. sukzessive auf Bereichen eines
Arrays operieren will, wie wir später noch sehen werden. Anders als delete() verändert splice() die
Länge eines Arrays:
my @alphabet = ('a' .. 'z'); my @buchstaben = splice(@alphabet, 5, 3); print "@buchstaben\n"; # Ausgabe: f g h print @alphabet, "\n"; # Ausgabe:
abcdeijklmnopqrstuvwxyz print scalar(@alphabet); # Ausgabe: 23
Übergibt man splice() als viertes Argument eine Liste oder ein Array, wird dieses an die Stelle
der soeben gelöschten Elemente eingefügt:
my @alphabet = ('a' .. 'z'); my @ziffern = (1 .. 3); my @buchstaben = splice(@alphabet, 5, 3, @ziffern); print "@buchstaben\n"; # Ausgabe: f g h print @alphabet, "\n"; # Ausgabe:
abcde123ijklmnopqrstuvwxyz print scalar(@alphabet); # Ausgabe: 26
Sortieren
Mit der Funktion sort() lassen sich Listen und Arrays sortieren. Da diese Funktion
zeichenkettenbasiert ist,19 verwendet sie inhärent den cmp-Operator für den Vergleich:
my @buchstaben = qw(g h e d f c b a); my @zahlen = qw(56 1 234); print sort(@buchstaben), "\n"; # Ausgabe: abcdefgh my @zahlen_sortiert = sort(@zahlen); print "@zahlen_sortiert"; # Ausgabe: 1 234 56
18 Einen solchen Startwert bezeichnet man auch als Offset. 19 In der Literatur wird diese Art der Sortierung manchmal auch als ASCII-betisch bezeichnet...
Sprachwissenschaftliches Institut
29
Will man Zahlen numerisch sortieren, verwendet man den im Abschnitt über Vergleichsoperatoren
eingeführten Raumschiff-Operator <=>. Beide verwenden die speziellen Variablen $a und $b,20 die als
Platzhalter für die zu vergleichenden Werte stehen, wobei $a das erste Argument und $b das zweite re-
präsentiert. Hierbei ist zu beachten, dass die Verwendung dieser Variablen als linker bzw. rechter
Operand die Art der Sortierung beeinflussen: Wie bereits erwähnt ist der Rückgabewert –1 wenn der
Operand der linken Seite kleiner als der der rechten Seite ist, ist der Operand der rechten Seite kleiner,
wird 1 zurück gegeben, sind beide Operanden gleich, ist der Rückgabewert 0. Daher wird aufsteigend
sortiert, wenn $a auf der linken Seite des Operators steht und absteigend, wenn er auf der rechten Seite
steht. Analog gilt dies selbstverständlich für den cmp-Operator.
Darüber hinaus ist zu beachten, dass bei dieser Art der Sortierung der Operator und seine
Operanden in einem sogenannten Block stehen. Da wir das Konzept des Blocks erst richtig definieren,
wenn wir über Kontrollstrukturen sprechen, sei er an dieser Stelle vorläufig eingeführt als Menge von
Anweisungen, die zwischen geschweiften Klammern {} steht:
my @zahlen = qw(56 1 234); my @aufsteigend_sortiert = sort({$a <=> $b} @zahlen); my @absteigend_sortiert = sort({$b <=> $a} @zahlen); print "@aufsteigend_sortiert\n"; # Ausgabe: 1 56 234 print "@absteigend_sortiert\n"; # Ausgabe: 234 56 1
4.7 Hashes Ähnlich wie Arrays sind Hashes Container für Listen. Allerdings besitzen diese Listen die
Eigenschaft, aus Schlüsseln und dazugehörigen Werten zu bestehen.21 Ein solches Schlüssel-
/Wertepaar bildet ein Element im Hash. Anstatt des Dollarzeichens $ oder des Klammeraffens @
verwendet man für diese Datenstruktur das Prozentzeichen % als Präfixsymbol. Wie schon aus Listen
und Arrays bekannt, trennt man die Elemente in einem Hash durch Kommata. Zwischen dem Schlüssel
und seinem Wert steht der Operator =>, der weitestgehend synonym zum Komma ist.
Wie in Arrays üblich, müssen Zeichenketten in Anführungszeichen gesetzt werden. In Hashes
beschränkt sich dies allerdings auf Zeichenketten, die den Wert eines Schlüssels darstellen.22 Anders
als in Arrays kann man aber in Hashes die alternativen Anführungszeichen qw nicht verwenden!
Einen Hash kann man sich z.B. als Adressbuch vorstellen, in dem jeder Bekannte an einem
bestimmten Ort wohnt. Da sich (noch) keiner unserer Bekannten einen Zweitwohnsitz leisten kann,
können wir einen solchen auch (noch) nicht abbilden; dennoch dürfen mehrere Bekannte an einem Ort
wohnen:
my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt");
20 Ihr Status ist in zweierlei Hinsicht besonders: Einerseits müssen diese Variablen vor der Verwendung nicht deklariert
werden, andererseits enthalten sie automatisch die aktuellen Werte aus der zu sortierenden Struktur für den Vergleich. 21 Deshalb hat man sie früher assoziative Arrays genannt. 22 Tut man dies nicht, wirft das strict-Pragma eine Fehlermeldung auf, laut derer ein Literal an dieser Stelle nicht erlaubt
ist!
30
Überträgt man das eben gesagte auf die Situation in Perl-Hashes, ist festzuhalten, dass ein Wert
zwar in mehreren Schlüsseln vorhanden sein darf, ein Schlüssel aber nur genau einmal existieren darf:
my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt", Frank => "Darmstadt", Peter => "Potsdam"); # Falsch!
Zugriff auf Hashes
Diese Eindeutigkeit muss sichergestellt werden, da man in Hashes nicht wie in Arrays über Indizes
auf die Werte zugreift, sondern über die Schlüssel! Daraus resultiert eine weitere Eigenschaft von
Hashes, nämlich nicht sortiert zu sein. Dies bedeutet, dass man weder von der Reihenfolge, in der man
die Einträge erzeugt hat noch auf eine andere Art auf die interne Ordnung eines Hashes schließen
kann.23
Um über einen Schlüssel auf einen Wert zuzugreifen, verwendet man wie bei Arrays Klammern:
Schrieb man für den Zugriff auf ein Element dieser Datenstruktur eine Zahl in eckige Klammern [ ],
setzt man bei Hashes den Bezeichner des Schlüssels in geschweifte Klammern {}:
my $franks_wohnort = $adressbuch{Frank}; print $franks_wohnort; # Ausgabe: Darmstadt
Ähnlich wie bei Arrays kann man auch in Hashes auf die Werte mehrerer Schlüssel zugreifen.
Diese Operation bezeichnet man folgerichtig als Hashslice. Da der Rückgabewert eine Liste der Werte
ist und kein Skalar oder Hash, verwendet man den Klammeraffen @ als Präfixsymbol für den Zugriff
auf diese Datenstruktur. Allein die geschweiften Klammern {} zeigen in diesem Fall an, dass man auf
einem Hash operiert:
my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); my @wohnorte = @adressbuch{"Susanne", "Andreas"}; print "@wohnorte"; # Ausgabe: Berlin Darmstadt
Wird ein Hash einem Array zugewiesen, beinhaltet dieses danach an den geradzahligen Indizes
(angefangen bei 0) die Schlüssel und an den ungeradzahligen Indizes die Werte. Wird umgekehrt ein
Array einem Hash zugewiesen, verwendet der Hash die geradzahligen Indizes als Schlüssel und die
ungeradezahligen Indizes als Werte.
23 Eine der wenigen Pluspunkte von Java: In dieser Programmiersprache existieren geordnete Hashes, die dort TreeMap
heißen.
Sprachwissenschaftliches Institut
31
Ebenso lassen sich alle Schlüssel bzw. alle Werte an ein Array übergeben. Im ersten Fall verwendet
man dazu die Funktion keys(), für Werte die Funktion values():
my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); my @namen = keys(%adressbuch); print "@namen\n"; # Ausgabe: Susanne Peter Andreas my @wohnorte = values(%adressbuch); print "@wohnorte"; # Ausgabe: Bonn Darmstadt Berlin
Ähnlich wie bei Arrays kann man auch für diese beiden Funktionen den Skalarkontext erzwingen,
um an die Anzahl der Elemente im Hash zu gelangen. Die folgenden vier Zeilen sind synonym
zueinander:
my $elemente = scalar(keys(%adressbuch)); # oder my $elemente = scalar(values(%adressbuch)); # oder my $elemente = keys(%adressbuch); # oder my $elemente = values(%adressbuch); print $elemente; # Ausgabe: 3
Operationen auf Hashes
Wie bei Arrays kann man mit der Funktion delete() ein Schlüssel-/Wert-Paar aus dem Hash
löschen. Der Rückgabewert ist der Wert dieses Elements:
my $geloescht = delete($adressbuch{Peter}); print $geloescht; # Ausgabe: Bonn
Man fügt einem Hash ein Element hinzu, indem man einem neuen Schlüssel einen Wert zuweist.
Da diese beiden Datenstrukturen wie immer Skalare sind, verwendet man wie beim Zugriff auf ein
Element eines Hashes das Dollar-Präfix $; die Hashstruktur wird wiederum durch die geschweiften
Klammern identifiziert. Bei dieser Operation handelt es sich allerdings um eine Zuweisung, sodass man
an dieser Stelle nicht den Komma-Operator =>, sondern – wie gewohnt – den Zuweisungsoperator =
verwendet:
my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); $adressbuch{Thomas} = "Essen";
Existiert ein Schlüssel bereits, wird gemäß der Eindeutigkeitsbedingung keine neuer Schlüssel
eingefügt, sondern der Wert des vorhandenen Schlüssels modifiziert:
$adressbuch{Susanne} = "Hamburg"; print $adressbuch{Susanne}; # Ausgabe: Hamburg
Genauso wie bei Arrayslices lassen sich auch Hashslices Listen zuweisen. Da es sich bei Hashslices
wie gesagt um Listen handelt, verwendet man wiederum den Klammeraffen @ als Präfixsymbol:
@adressbuch{"Gabi", "Hans"} = ("London", "Madrid");
32
4.8 Zusammenfassung In diesem Kapitel wurde gezeigt, wie elementare Datenstrukturen in Perl aussehen:
• Skalar: Als grundlegendste Datenstruktur besteht er aus einem singulären Datum, wie z.B.
einer Zahl oder einer Zeichenkette. Diese treten sowohl als Literale als auch als Variablen auf:
Literale sind konstante Werte, die einer Variablen zugewiesen werden können und auf denen
man verschiedene Operationen durchführen kann. Variablen sind dementsprechend Container
für unterschiedlichste Werte, die durch Zuweisung an den Bezeichner der Variablen verändert
werden können. Eine Skalarvariable wird durch das Dollarzeichen $ als Präfixsymbol gekenn-
zeichnet.
Auf Zahlen lassen sich arithmetische Operationen anhand der Operatoren +, -, *, /, **
(Exponentiation) und % (Modulus) durchführen, während Zeichenketten anhand des
Konkatenationsoperators . mit einander verbunden werden können und durch den
Multiplikationsoperator x wiederholt werden. Auf beiden Datentypen lassen sich Vergleiche
durchführen, wobei die Symbole ==, <, >, <=, => und != auf Zahlen angewandt werden,
während eq, lt, gt, ge, le und ne Zeichenketten miteinander vergleichen.
• Liste: Diese Datenstruktur besteht aus einer Reihe von Skalaren. Auf ein einzelnes Element
daraus kann man vermittels Indizes, die als Zahlen in eckige Klammern [ ] geschrieben
werden, zugreifen. Zu beachten ist, dass die Indexzählung bei 0 beginnt!
• Array: Ein Array ist ein Variablencontainer für eine Liste; das entsprechende Präfixsymbol ist
der Klammeraffe @. Auf dieser Datenstruktur kann man Skalare am Anfang und am Ende der
Liste einfügen und löschen. Dazu dienen die Funktionen unshift()/shift() und
push()/pop(). Anhand eines Indexes kann man Elemente an beliebiger Stelle einfügen und
ihren Wert mit der Funktion delete() löschen. Darüber hinaus lassen sich mehrere Elemente
gleichzeitig vermittels der Funktion splice() aus dieser Datenstruktur entfernen.
• Listen und Arrays lassen sich mit sort() zeichenkettenbasiert sortieren. Dies bedeutet, dass
Zahlen nicht numerisch, sondern nach ihrer Ziffernfolge eingeordnet werden. Will man Zahlen
numerisch sortieren, verwendet man den Raumschiff-Operator <=>, der als Operanden die
Variablen $a und $b benötigt, in denen die Werte des ersten bzw. zweiten zu sortierenden
Operanden automatisch abgelegt sind.
• Hash: Ein Hash ist ein Variablencontainer für eine Liste, die aus Schlüsseln und Werten
besteht. Sein Präfixsymbol ist das Prozentzeichen %. Anders als bei Arrays greift man nicht
über Indizes auf die Elemente eines Hashes zu, sondern über den jeweiligen Schlüssel eines
Elements. Um an alle Schlüssel bzw. Werte eines Hashes zu gelangen, bedient man sich der
Funktionen keys() bzw. values(). Ebenso wie in Arrays kann man mit exists() – dazu im
nächsten Kapitel mehr – überprüfen, ob ein Element in einem Hash vorhanden ist oder nicht
und es mit delete() löschen.
Bei der Verwendung von Datenstrukturen muss unbedingt die Art des Rückgabewerts beachtet
werden. Jede Funktion hat als Ergebnis einen solchen Rückgabewert; für gewöhnlich ist dies ein
Skalar. Führt man also Operationen auf Listen, Arrays und Hashes aus, die das Ergebnis in einer
Variablen speichern, muss darauf geachtet werden, dass bereits beim Zugriff auf die Datenstruktur das
Präfixsymbol des Rückgabewerts verwendet wird. Eine besondere Art von Rückgabewert stellen dabei
Array- und Hashslices dar, deren Datenstruktur in beiden Fällen ein Array ist!
Sprachwissenschaftliches Institut
33
Beispielanwendung
Ein zentrales Konzept computerlinguistischer Analyse stellt das des Bi- oder Trigramms dar. Ein
Bigramm ist eine Menge aus zwei Wörtern, während ein Trigramm eine Menge aus drei Wörtern
darstellt.24 Um aus folgendem Satz
(1) Eine Rose ist eine Rose ist eine Rose .
Bi- bzw. Trigramme zu extrahieren, betrachtet man immer ein Zweier- bzw. Dreier-Fenster dieses
Satzes:
Bigramme Trigramme
Eine Rose Eine Rose ist
Rose ist Rose ist eine
ist eine ist eine Rose
eine Rose eine Rose ist
Rose ist Rose ist eine
ist eine ist eine Rose
eine Rose eine Rose .
Rose .
Will man dies in Perl modellieren, empfiehlt es sich zunächst, die Datenstrukturen und die
notwendigen Algorithmen natürlichsprachlich in Kommentaren zu beschreiben, denen man dann den
eigentlichen Code hinzufügt. Auf diese Weise nähert man sich nicht nur der Implementierung intuitiv,
man sorgt durch kleinschrittige Kommentierung des Quellcodes auch dafür, dass dieser lesbarer wird
und somit besser gewartet und weiter entwickelt werden kann.
Stellen wir uns zunächst den Satz als Liste vor, in dem alle Wörter und der Punkt jeweils ein
Element sind:
# Ein Satz ist eine Liste von Wörtern. my @woerter = qw (Eine Rose ist eine Roste ist eine Rose .);
Um nun alle Bigramme auszugeben, nimmt man jeweils die ersten beiden Elemente von der Liste
und gibt sie aus:
# Ein Bigramm besteht aus zwei Wörtern des Satzes. my ($erstes, $zweites); # Entferne das erste und zweite Element des Arrays und schreibe sie in die # beiden Skalarvariablen für das Bigramm. ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: Eine Rose
24 Ein einzelnes Wort nennt man deshalb auch Unigramm.
34
Da das zweite Wort des ersten Bigramms zugleich das erste Wort des zweiten Bigramms ist, muss
man es wieder auf das Array zurücklegen:
# Das zweite Wort des ersten Bigramms ist das erste Wort des zweiten # Bigramms. # Deshalb muss es zurück auf die Liste gelegt werden! unshift(@woerter, $zweites);
Bigramme an sich sind wenig interessant. Deshalb soll vom Programm einerseits festgehalten
werden, wie viele einzelne Wörter im Beispielsatz vorhanden sind, und wie viele Bigramme sich
daraus bilden lassen. Dazu halten wir zunächst die Anzahl der Wörter fest:
# Die Anzahl der einzelnen Wörter des Satzes. my $anzahl_woerter = @woerter;
Wann immer ein Bigramm extrahiert wurde, zählen wir dieses:
# Die Anzahl der aus dem Satz extrahierten Bigramme. my $anzahl_bigramme; ... ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: Eine Rose unshift(@woerter, $zweites); # Erhöhe die Zahl der Bigramme um eins. $anzahl_bigramme++;
Da wir noch nicht wissen, wann das Array keine Bigramme mehr enthält, wiederholen wir die
letzten drei Zeilen so oft, bis wir durch Experimentieren kein Ergebnis mehr zurück geliefert
bekommen. Das komplette Programm sieht dann so aus:
use strict; use diagnostics; # Initialisierung und Definition von Variablen # Ein Satz ist eine Liste von Wörtern. my @woerter = qw (Eine Rose ist eine Roste ist eine Rose .); # Ein Bigramm besteht aus zwei Wörtern des Satzes. my ($erstes, $zweites); # Die Anzahl der einzelnen Wörter des Satzes. my $anzahl_woerter = @woerter; # Die Anzahl der aus dem Satz extrahierten Bigramme. my $anzahl_bigramme; print "Der Satz besteht aus $anzahl_woerter Wörtern\n"; # Ausgabe: 9
Sprachwissenschaftliches Institut
35
# Entferne das erste und zweite Element des Arrays und schreibe sie in die # beiden Skalarvariablen für das Bigramm. ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: Eine Rose # Das zweite Wort des ersten Bigramms ist das erste Wort des zweiten # Bigramms. Deshalb muss es zurück auf die Liste gelegt werden! unshift(@woerter, $zweites); # Erhöhe die Anzahl der Bigramme um eins. $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: Rose ist unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: ist eine unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: eine Rose unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: Rose ist unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: ist eine unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: eine Rose unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: Rose . unshift(@woerter, $zweites); $anzahl_bigramme++; ($erstes, $zweites) = splice(@woerter, 0, 2); print "$erstes $zweites\n"; # Ausgabe: . unshift(@woerter, $zweites); print "$anzahl_bigramme Bigramme extrahiert!\n"; # Ausgabe: 8
Durch die Operationen der drei letzten Zeilen erhält man ein Bigramm, das aus dem Punkt und
etwas undefiniertem besteht, weshalb der Perl-Interpreter an dieser Stelle eine Warnung ausgibt.
36
Zur Extraktion von Trigrammen verfährt man analog, nur dass in diesem Fall die ersten drei
Elemente von der Liste entfernt werden und nach der Ausgabe des Trigramms zunächst das dritte, dann
das zweite Wort auf das Array zurückgelegt werden:
# Ein Trigramm besteht aus drei Wörtern eines Satzes. my ($erstes, $zweites, $drittes); # Die Anzahl der aus dem Satz extrahierten Trigramme. my $anzahl_trigramme; # Entferne die ersten drei Elemente des Arrays und schreibe sie in die # Skalarvariablen für das Trigramm. ($erstes, $zweites, $drittes) = splice(@woerter, 0, 3); print "$erstes $zweites $drittes\n"; # Ausgabe: Eine Rose ist # Das zweite und dritte Wort sind die ersten beiden Wörter des nächsten # Trigramms, weshalb zuerst das dritte, dann das zweite an den Anfang des # Arrays zurückgelegt werden muss! unshift(@woerter, $drittes); unshift(@woerter, $zweites); # Erhöhe die Anzahl der Trigramme um eins. $anzahl_trigramme++;
Festzuhalten bleibt, dass diese Art der Problemlösung wenig effizient ist, da wir für ein und
dieselbe Operation immer wieder denselben Code hinschreiben müssen. Darüber hinaus besitzen wir
noch keine Möglichkeit, um programmatisch zu ermitteln, wann ein Array keine Elemente mehr
enthält, sodass wir ein Kriterium zur Beendigung der Aufgabe besäßen. Antworten auf diese Probleme
liefert das nächste Kapitel.
Sprachwissenschaftliches Institut
37
5 Kontrollstrukturen
We cannot always control our thoughts, but we can control our words, and
repetition impresses the sub
conscious, and we are then master of the situation.
FLORENCE SCOVEL SHINN
Bisher besaßen die besprochenen Perl-Programme immer einen linearen Programmfluss. Auf
Datenstrukturen wurden sukzessive Operationen ausgeführt, deren Auswertung neue Daten erzeugte:
Anhand von Kontrollstrukturen lässt sich der Programmfluss durch Bedingungen und Schleifen
beeinflussen. Diese Bedingungen können atomar Daten auf eine Eigenschaft prüfen oder in Schleifen
dafür sorgen, dass Operationen wiederholt auf Daten ausgeführt werden. Schleifen geben uns ein
Instrumentarium an die Hand, um Operationen auf allen Elementen eines Arrays oder Hashes
auszuführen.
Anhand von Bedingungen entscheidet man, ob an einem bestimmten Punkt im Programmablauf ein
Kriterium für eine Datenstruktur erfüllt ist. Aufgrund dessen lassen sich unterschiedliche Operationen
auf dieser durchführen, die wiederum unterschiedliche Datenstrukturen zum Resultat haben:
38
Sprachwissenschaftliches Institut
39
Schleifen beinhalten ebenfalls Bedingungen; sie werden so oft durchgeführt, bis die Bedingung
nicht mehr erfüllt ist:
Die in den Flussdiagrammen den Bedingungen nachgeordneten Operationen stehen in sogenannten
Blöcken: Ein Block fasst eine oder mehrere Anweisungen durch geschweifte Klammern {} zu einer
operationalen Einheit zusammen.
40
5.1 Bedingungen Um zu entscheiden, ob ein Wert ein bestimmtes Kriterium erfüllt, verwendet man die Funktion
if() und die bereits besprochenen Vergleichsoperatoren. Dieser Funktion folgt ein Block, in dem
mindestens eine Anweisung steht:
my $zahl = 15; if($zahl > 12){ print $zahl; # Ausgabe: 15 }
Sollte eine Bedingung zu falsch ausgewertet werden, lässt sich dieser Fall anhand der else-Klausel
behandeln:
my $zahl = 15; if($zahl < 12){ print "$zahl ist kleiner als 12\n"; } else{ print "$zahl ist nicht kleiner als 12\n"; # Ausgabe! }
if()-Bedingungen lassen sich kaskadieren, indem man nach dem ersten if() anhand der elsif()-
Klausel weitere Fälle behandelt und zuletzt auf den allgemeinsten Fall mit else reagiert:25
my $kontostand = -500; if($kontostand < -10000){ print "Gehen Sie zur Schuldnerberatung!\n"; } elsif($kontostand < -2000){ print "Wollen Sie einen Sofortkredit?\n"; } elsif($kontostand < 4000){ print "Sie sind im grünen Bereich!\n"; # Ausgabe! } else{ print "Was wollen Sie mit Ihrem Geld tun?\n"; }
Verwendet man nur die if-Bedingung ohne weitere Klauseln und will auch nur eine Anweisung
ausführen, kann man diese vor die if-Bedingung schreiben und den Block weglassen:
print "Sie sind kreditwürdig!\n" if($kontostand > -2000);
25 Anders als in verschiedenen anderen Programmiersprachen kennt Perl das Konstrukt switch/case nicht, mit dem man
kaskadierte Bedingungen sehr elegant modellieren kann. Wir werden später noch zwei Möglichkeiten aufzeigen, dies dennoch zu
verwenden.
Sprachwissenschaftliches Institut
41
Anhand der Funktion defined() kann man überprüfen, ob eine Datenstruktur definiert ist; es reicht
nicht, sie zu deklarieren. Da der Test auf eine Variable keine andere Funktionalität besitzen kann, darf
man das Schlüsselwort defined weglassen:
my $zahl = 1; print $zahl if(defined($zahl)); # Ausgabe: 1 my @array; print "@array\n" if(@array); # Ausgabe:
Analog dazu lässt sich vermittels der Funktion exists() überprüfen, ob ein Element in einem
Array oder einem Hash vorhanden ist. Im ersten Fall erfolgt der Zugriff wie gewohnt über einen Index,
im zweiten natürlich über den jeweiligen Schlüssel:
my @marxes = qw(Chico Harpo Groucho Gummo Zeppo); my %adressbuch = (Peter => "Bonn", Paul => "London"); print $marxes[1] if(exists($marxes[1])); # Ausgabe: Harpo print $adressbuch{Paul} if(exists($adressbuch{Paul})); # Ausgabe:
London
Eine weitere Möglichkeit, eine Bedingung zu überprüfen, besteht darin, zu schauen, ob sie falsch
ist. Dazu verwendet man die Funktion unless(), deren Syntax analog zu derjenigen von if() ist. Der
entsprechende Block wird – wie gesagt – nur ausgeführt, wenn die Bedingung falsch ist:
my $zahl = 5; unless($zahl < 4){ print "$zahl ist größer als 4.\n"; # Ausgabe: 5 ist größer als 4. }
unless() lässt sich dementsprechend als if(not <Ausdruck>) oder if(! <Ausdruck>)
umformulieren. Auch die Regel, bei Verwendung nur einer Anweisung diese mit der Bedingung auf
eine Zeile schreiben zu dürfen, gilt. Will man allerdings eine unless()-Bedingung mit weiteren
Klauseln koppeln, muss man sich wiederum des elsif()- bzw. else-Konstrukts bedienen. Dabei ist zu
beachten, dass diese die negative Polarität wieder aufheben:
my $zahl = 5; unless($zahl < 4){ print "$zahl ist größer als 4.\n"; # Ausgabe: 5 ist größer als 4. } elsif($zahl < 4){ print "$zahl ist kleiner als 4.\n"; } else{ print "$zahl ist gleich 4."; }
42
5.2 Schleifen Perl kennt mehrere Konstrukte, um Operationen aufgrund einer Bedingung zu wiederholen. Diese
Konstrukte unterscheiden sich in mehreren Gesichtspunkten:
• Ein Block wird so oft ausgeführt, bis eine Bedingung falsch wird. Dabei wird zunächst die
Bedingung geprüft.
• Ein Block wird so oft ausgeführt, bis eine Bedingung wahr ist. Auch hierbei wird die
Bedingung zuerst geprüft, bevor der Block ausgeführt wird.
• Ein Block wird auf jeden Fall ausgeführt; erst nach dem ersten Durchlauf wird die Bedingung
geprüft.
Anweisungen wiederholen
while()-Schleifen
Die while()-Schleife entspricht dem ersten der genannten Kriterien: Zunächst wird eine Bedingung
auf Wahrheit überprüft und dann wird ein Block, den man auch als Schleifenrumpf bezeichnet, solange
ausgeführt, wie die Bedingung erfüllt ist. Die syntaktische Struktur der while()-Schleife entspricht
derjenigen der if()-Bedingung. Zusätzlich benötigt man allerdings eine Operation, die den in der
Bedingung verwendeten Wert modifiziert, sodass ein Endzustand erreicht werden kann:
my $countdown = 5; while($countdown > 0){ print "$countdown\n"; # Ausgabe: 54321 $countdown--; } print "Start!\n"; # Ausgabe: Start!
Ließe man die Zeile $countdown--; weg, wäre die Bedingung immer wahr und man befände sich in
einer Endlosschleife.26 Dieser entgeht man allerdings durch Schleifenkontrollmechanismen, wie sie
weiter unten vorgestellt werden sollen.
until()-Schleifen
Dem zweiten Kriterium entsprechen until()-Schleifen: Vor der Verarbeitung des Schleifenrumpfs
wird eine Bedingung so lange abgeprüft, bis sie wahr wird:
my $countdown = 5; until($countdown == 0){ print "$countdown\n"; # Ausgabe: 54321 $countdown--; } print "Start!\n"; # Ausgabe: Start!
In dieser Variante muss der Operand 0 für die Bedingung gewählt werden, da ja die
Abbruchbedingung vor dem Anweisungsblock überprüft wird!
26 Aus einer solchen befreit man sich im Emacs, indem man Alt-x kill-compilation eingibt; in der Eingabeaufforderung
beendet man den aktuellen Perl-Interpreterprozess mit Strg-C.
Sprachwissenschaftliches Institut
43
do{}...while()- und do{}...until()-Schleifen
Den gleichen Effekt bekommt man, wenn man Schleifen verwendet, die dem dritten Kriterium
entsprechen, nämlich zunächst einmal den Schleifenrumpf auszuführen und dann die
Abbruchbedingung zu überprüfen. Dazu bedient man sich des do{}-Konstrukts, das man sowohl mit
while() als auch mit until() verwenden darf:
my $countdown = 5; do{ print "$countdown\n"; # Ausgabe: 54321 $countdown--; } while($countdown > 0); print "Start!\n"; # Ausgabe: Start!
my $countdown = 5; do{ print "$countdown\n"; # Ausgabe: 54321 $countdown--; } until($countdown < 1); print "Start!\n"; # Ausgabe: Start!
for()-Schleifen
Demselben Kriterium wie dem der while()-Schleife entspricht die for()-Schleife: Auch hier wird
vor dem Aufruf des Schleifenrumpfs eine Bedingung abgeprüft und der Block wird so oft wiederholt,
bis die Bedingung nicht mehr wahr ist. In der Tat sind diese beiden Konstrukte austauschbar und unter-
scheiden sich nur in ihrer Syntax.27
Für gewöhnlich benötigt die for()-Schleife drei Argumente:
• die Initialisierung einer Laufvariablen,
• eine Bedingung und
• eine Operation, die den Wert der Laufvariablen ändert:
for(my $i = 5; $i > 0; $i--;){ print "$i\n"; # Ausgabe: 54321 } print "Start!\n"; # Ausgabe: Start!
Eine syntaktisch kompaktere Variante besteht darin, einen Bereich als Argumentliste anzugeben:
for(1 .. 3){ print "Wiederholung\n"; }
27 Und auch ein wenig in den Einsatzgebieten, wie wir noch sehen werden. Grundsätzlich lässt sich aber überall, wo eine
while()-Schleife steht, auch eine for()-Schleife verwenden und umgekehrt.
44
Verwendet man nur eine Anweisung, darf man diese wiederum auf dieselbe Zeile wie die for()-
Schleife schreiben:
print "Wiederholung\n" for(1 .. 3);
Iteration über Listenstrukturen
foreach()-Schleifen
Eine syntaktische Alternative zur for()-Schleife stellt die foreach()-Schleife dar, die
ausschließlich dazu verwendet wird, über Listenstrukturen zu iterieren. Ähnlich wie bei der kano-
nischen Verwendungsart von for(), benötigt man hier eine Variable, in der der aktuelle Wert des
Listenelements gespeichert wird:
my @woerter = qw(Eine Rose ist eine Rose ist eine Rose .); foreach my $wort (@woerter){ print "$wort\n"; }
foreach() stellt die kanonische Form der Iteration über einen Hash dar. Da wir – wie bereits im
vorherigen Kapitel gesehen – über die Schlüssel auf die Werte zugreifen, sollte der Variablenname, in
dem wir die Schlüssel während jedes Schleifendurchlaufs abspeichern, die Bedeutung der Schlüssel
reflektieren. Um an den Wert eines jeden Elements zu gelangen, schreibt man wie gewohnt das
Präfixsymbol für einen Skalarwert – denn um einen solchen handelt es sich ja auch bei diesem
Rückgabewert – vor den Bezeichner des Hashes. Ihm folgt der Schlüssel in geschweiften Klammern;
da sich dieser aus der jeweiligen Iteration über den Hash ergibt, setzen wir an dieser Stelle unsere
Laufvariable ein:
my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); foreach my $name (keys %adressbuch){ print "$name wohnt in $adressbuch{$name}.\n"; } # Ausgabe: # Peter wohnt in Bonn. # Andreas wohnt in Darmstadt. # Susanne wohnt in Berlin.
Sprachwissenschaftliches Institut
45
Will man einen Hash sortiert ausgeben, kann man die Sortierung entweder auf den Schlüsseln oder
auf den Werten durchführen. Betrachten wir zunächst den ersten (eher trivialen) Fall:
foreach my $name (sort keys %adressbuch){ print "$name wohnt in $adressbuch{$name}.\n"; } # Ausgabe: # Andreas wohnt in Darmstadt. # Peter wohnt in Bonn. # Susanne wohnt in Berlin.
Soll ein Hash den Werten nach sortiert ausgeben werden, kann man nicht einfach die Funktion
keys() durch values() ersetzen!28 Vielmehr benötigt man den cmp-Operator zur
zeichenkettenbasierten Sortierung oder den Raumschiff-Operator <=> zur numerischen Sortierung in
einem sort-Block. Wie bereits erläutert, verwenden diese Operatoren die speziellen Variablen $a und
$b, um zwei Werte miteinander zu vergleichen und sie dementsprechend einzuordnen. Im Fall der
Hashes sind dies die jeweiligen Werte der dazugehörigen Schlüssel pro Iteration; d.h., man verwendet
$a und $b für die Sortierung anstelle der Laufvariablen, in der normalerweise der Schlüssel steht:
foreach my $name (sort{$adressbuch{$a} cmp $adressbuch{$b}} keys %adressbuch){
print "$name wohnt in $adressbuch{$name}.\n"; } # Ausgabe: # Susanne wohnt in Berlin. # Peter wohnt in Bonn. # Andreas wohnt in Darmstadt.
Dass foreach{} und for{} bei der Iteration über Listen synonym zueinander sind, lässt sich
dadurch zeigen, dass man foreach{} ohne weiteres durch for{} ersetzen kann:
my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); for my $name (keys %adressbuch){ print "$name wohnt in $adressbuch{$name}.\n"; }
while()-Schleifen
Auch die while()-Schleife eignet sich zur Iteration und wird vorwiegend bei Arrays eingesetzt. Sie
lässt sich zur Iteration über Listenelemente verwenden, die man mit shift() oder pop() für die weitere
Verarbeitung aus der Liste löscht, z.B. um Operationen auf den einzelnen Skalaren durchzuführen oder
sie in eine andere Datenstruktur wie einen Hash zu überführen. In diesem Fall ist die Bedingung der
while()-Schleife solange wahr, wie Elemente auf der Liste vorhanden sind:
28 values() ermöglicht zwar einen Zugriff auf die Werte eines Hashes, die dann der Laufvariablen zugewiesen würden, man
gelangt auf diesem Weg allerdings nicht zurück an die Schlüssel!
46
my @marxes = qw(Chico Harpo Groucho Gummo Zeppo); while(@marxes){ my $marx_brother = shift(@marxes); print "$marx_brother ist ein Marx Brother!\n"; } # Ausgabe: # Chico ist ein Marx Brother! # Harpo ist ein Marx Brother! # Groucho ist ein Marx Brother! # Gummo ist ein Marx Brother! # Zeppo ist ein Marx Brother!
$_
Die besondere Variable $_ wird von Perl verwendet, wenn keine andere Variable angegeben ist.
Man kann sie wie das Pronomen es lesen. Sie wird typischerweise als Ersatz für die Laufvariable in
Schleifen verwendet:29
my @woerter = qw(Eine Rose ist eine Rose ist eine Rose .); foreach (@woerter){ print "$_\n"; }
Perl geht sogar soweit, dass $_ das Standardargument für print() ist, wenn kein weiteres
angegeben wird:
my @woerter = qw(Eine Rose ist eine Rose ist eine Rose .); foreach (@woerter){ print; # Ausgabe: EineRoseisteineRoseisteineRose }
Auch bei der Iteration über einen Hash lässt sich $_ einsetzen:
my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); foreach (keys %adressbuch){ print "$_ wohnt in $adressbuch{$_}.\n"; }
Bei der Verwendung von $_ sollte man grundsätzlich zwischen der Lesbarkeit/Wartbarkeit des
dadurch entstehenden Codes und den Perl-Tugenden30 abwägen. Es gibt aber auch Anwendungsfälle, in
denen der Einsatz von $_ explizit vorgesehen ist, wie der folgende Exkurs zeigt.
29 Analog gilt dies natürlich auch für for(). 30 Die drei Tugenden eines Perl-Programmierers: Faulheit, Ungeduld und Selbstüberschätzung!
Sprachwissenschaftliches Institut
47
Exkurs: map und grep
Eine weitere Möglichkeit, Listenstrukturen zu filtern bzw. zu transformieren, stellen die Befehle
grep und map dar. Zwar sind sie in ihrer Verwendung sehr effizient, aber gerade für Einsteiger in ihrer
Funktionalität wenig eingängig, weshalb an dieser Stelle nur an einigen einfachen Beispielen gezeigt
werden soll, wie sie sich verwenden lassen.
Listenstrukturen filtern mit grep
Was die Verwendung dieser beiden Befehle erschwert, ist ihr Einsatz der impliziten Variablen $_.
Sie wird im Rumpf der grep- bzw. map-Blöcke als Container für die übergebenen Argumente
gebraucht, weshalb es nicht immer transparent erscheint, was mit den Werten passiert. Betrachten wir
dazu folgendes Beispiel, in dem aus einer Reihe von Zahlen die ungeraden herausgefiltert werden
sollen:
my @zahlen = (1..10); my @ungerade = grep{$_ %2} @zahlen; # 1, 3, 5, 7, 9
Aus dem Array @zahlen wird jedes Element an die Funktion grep übergeben, wo es sich jeweils in
der Standardvariablen $_ befindet. Auf diese wird nun die Operation Modulo 2 ausgeführt, die
erfolgreich ist, wenn die Division durch zwei auf einem ganzzahligen Wert einen Rest produziert.
Dementsprechend besitzt die Funktion grep denjenigen Wert als Rückgabewert, der innerhalb ihres
Rumpfes als wahr ausgewertet wurde; da davon auszugehen ist, dass mehr als ein Wert gefunden
werden kann, verwendet man eine listenwertige Datenstruktur, um die Rückgabewerte zu speichern.31
Genauso wie aus Arrays lassen sich auch Werte aus Hashes filtern:
my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt", Frank => "Darmstadt"); my @darmstaedter = grep {$adressbuch{$_} eq "Darmstadt"} keys %adressbuch; # Andreas, Frank
Hier wird jeder Schlüssel des Hashes %adressbuch (durch keys) an die Funktion grep übergeben.
Analog zur Iteration mit einer foreach-Schleife und ohne explizite Laufvariable finden sich die Werte
hier in $adressbuch{$_}, auf der jeweils überprüft wird, ob der Wert gleich der Zeichenkette
"Darmstadt" ist. Ist dies der Fall, wird der jeweilige Wert zurückgegeben und in das Array
@darmstaedter geschrieben.
Listenstrukturen mit map transformieren
Ließen sich mit grep bestimmte Werte aus einer Listenstruktur extrahieren, kann man mit map
Operationen auf diesen ausführen:
my @quadratzahlen = map{$_ ** 2} (2..5); # 4, 9, 16, 25 my @wurzeln = map{sqrt($_)} @quadratzahlen; # 2, 3, 4, 5
31 Natürlich hätte man in diesem Fall auch auf die Arrays verzichten können: print grep {$_ %2} (1..10);
48
Hier wird wiederum jedes Element der Liste 2 bis 5 der Funktion map als Argument zugeführt,
über die Variable $_ quadriert und im Array @quadratzahlen gespeichert. Umgekehrt wird in der
zweiten Zeile aus den Elementen dieses Arrays die Quadratwurzel gezogen.
Auch diese Funktion lässt sich wieder auf Hashes anwenden:
my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt", Frank => "Darmstadt"); print map{"$_\t$adressbuch{$_}\n"} keys %adressbuch; # Ausgabe: Andreas Darmstadt Susanne Berlin Frank Darmstadt Peter Bonn
Wie bereits bei grep gesehen, wird auch hier der Hash anhand seiner Schlüssel durchlaufen; für die
Ausgabe fügen wir nun innerhalb des map-Blocks vor jedem Wert einen Tabulatoreinschub ein.
Die hier gezeigten Beispiele muten eher trivial an, wenn man sie schrittweise nachvollzieht;
kompliziert wird es meist, wenn man $_ im grep- oder map-Rumpf verändert, weshalb es sich auch hier
empfiehlt, überlegt mit $_ umzugehen. Im nächsten Kapitel werden wir ein nützliches Idiom
kennenlernen, das map verwendet.
Schleifenkontroll-Konstrukte
Wie bereits erwähnt, besteht die Möglichkeit, dass eine Schleife nicht durch ihre eigentliche
Abbruchbedingung beendet werden kann, sondern als Endlosschleife weiterläuft. Darüber hinaus kann
es nützlich sein, innerhalb des Schleifenrumpfs Bedingungen zu formulieren, die den sofortigen
Abbruch der Schleife, einen neuerlichen Durchlauf der Schleife ohne Abarbeitung der restlichen
Anweisungen oder den Sprung an eine vordefinierte Stelle im Programm bewirkt.
Abbruch mit last()
Ein sehr gutes Beispiel für die Wirkungsweise dieser Funktion stellt das Einlesen von Daten aus der
Standardeingabe dar. Wollten wir im ersten Kapitel wiederholt Werte aus der Standardeingabe
einlesen, mussten wir unser Perl-Programm mehrfach aufrufen. Mit while() und last() bekommen
wir die Möglichkeit, solange Daten einzulesen, bis keine mehr eingegeben werden; dies wird durch
eine Leerzeile, d.h. das Drücken der Return-Taste ohne vorherige Eingabe anderer Zeichen, im
Programm ausgelöst:
Sprachwissenschaftliches Institut
49
while(my $eingabe = <STDIN>){ chomp($eingabe); last unless($eingabe); my $rueckwaerts = reverse($eingabe); print "$rueckwaerts\n"; } # Eingabe: Groucho, Ausgabe: ohcuorG # Eingabe: Marx, Ausgabe: xraM # Eingabe: => Programmende
Nach dem Löschen des Zeilenendes mit chomp() wird die Schleife wiederholt, wenn Daten
eingegeben wurden; wird nur die Return-Taste gedrückt, ist $eingabe nicht definiert und das
Programm durch last() beendet.
Anweisungen überspringen mit next()
Will man den Rumpf einer Schleife verlassen, ohne sie zu beenden, verwendet man next():
my @zahlen = qw(8 3 0 2 12 0); for(@zahlen){ if ($_ == 0){ print "Lasse 0-Element aus!\n"; next; } print "48 durch $_ ist ", 48 / $_, "\n"; } # Ausgabe: # 48 durch 8 ist 6 # 48 durch 3 ist 16 # Lasse 0-Element aus! # 48 durch 2 ist 24 # 48 durch 12 ist 4 # Lasse 0-Element aus!
Benannte Sprungpunkte
Sowohl zu last() als auch zu next() lassen sich Bezeichner angeben, die einen bestimmten Punkt
im Programm markieren. Diese Bezeichner werden komplett groß geschrieben; die Markierung erhält
zusätzlich einen Doppelpunkt angehängt.
50
Auf diese Art lässt sich beispielsweise das switch-/case-Konstrukt anderer Programmiersprachen
nachbilden:
my ($nachricht, $kontostand); $kontostand = -500; SWITCH:{ if($kontostand < -10000){ $nachricht = "Gehen Sie zur Schuldnerberatung!\n"; last SWITCH; } if($kontostand < -2000){ $nachricht = "Wollen Sie einen Sofortkredit?\n"; last SWITCH; } if($kontostand < 4000){ $nachricht = "Alles im grünen Bereich!\n"; # Ausgabe! last SWITCH; } $nachricht = "Was wollen Sie mit Ihrem Geld tun?\n"; } print $nachricht;
5.3 Zusammenfassung In diesem Kapitel haben wir den Begriff des Algorithmus ein wenig mit Leben gefüllt: Während
wir im ersten Kapitel nur einzelne Anweisungen auf Datenstrukturen ausführen konnten, wurde uns mit
Bedingungen und Schleifen ein Instrumentarium an die Hand gegeben, mit dem wir in der Lage sind,
einerseits den Programmfluss durch Verzweigungen, die unter bestimmten Kriterien genommen
werden, zu beeinflussen, andererseits können wir nun Operationen auf skalaren Daten wiederholen,
bzw. über die skalaren Elemente einer listenartigen Struktur iterieren.
if()-Bedingungen ermöglichen es, einen Wert anhand eines Kriteriums auf Wahrheit zu
überprüfen. Will man weitere Kriterien hinzuziehen, verwendet man die elsif()-Bedingung. Der
allgemeinste Fall lässt sich mit else behandeln. Das Komplement zu if() bildet unless(); dabei ist zu
beachten, dass es kein Äquivalent zu elsif() gibt, das die negative Konnotation von unless() besitzt,
weshalb weitere Kriterien zu unless() als positive Bedingungen implementiert werden müssen. Das
aus anderen Programmiersprachen bekannte switch-/case-Konstrukt unterstützt Perl nicht.
Allen Bedingungen und Schleifen ist gemein, dass die durch sie ausgewählten Operationen in
einem Block stehen. Dieser wird durch geschweifte Klammern {} gekennzeichnet und dient der
Gruppierung von Anweisungen. Verwendet man nur eine Anweisung kann diese vor die Bedingung
oder die Schleife geschrieben werden und der Block entfällt.
Sprachwissenschaftliches Institut
51
Schleifen enthalten ebenfalls Bedingungen, die ihnen als Abbruchkriterien dienen. Eine
Klassifikation der unterschiedlichen Schleifen und ihrer Funktionsweisen sei hier tabellarisch
wiedergegeben:
Schleife wird ausgeführt, bis Bedin-
gung falsch wird
Schleife wird ausgeführt, bis Bedin-
gung wahr wird
Bedingung wird zuerst überprüft while(<Bedingung>){
<Schleifenrumpf>
}
until(<Bedingung>){
<Schleifenrumpf>
}
Block wird ausgeführt, bevor Bedin-
gung überprüft wird
do{
<Schleifenrumpf>
} while(<Bedingung>);
do{
<Schleifenrumpf>
} until(<Bedingung);
Analog zu den Klassifikationskriterien der while()-Schleife funktioniert die for()-Schleife, die es
in der Variante mit expliziter Laufvariablen for(my $<Laufvariable>; <Bedingung für
Laufvariable>; <Operation auf Laufvariable>){<Schleifenrumpf>}, mit impliziter Laufvariable
for(<Zahl1> .. <Zahl2>){<Schleifenrumpf>} und ohne Argumente for() {<Schleifenrumpf}
gibt. In der Form mit numerischem Bereich lässt sich vermittels der besonderen Variablen $_ auf den
aktuellen Zahlenwert zugreifen. Diese Variable lässt sich auch mit allen anderen Schleifentypen
verwenden.
Zur Iteration über eine Listenstruktur, wie ein Array oder einen Hash, verwendet man entweder eine
while()- oder eine for()-Schleife. Da man im letzten Fall angeben müsste, aus wie vielen Elementen
die Struktur besteht, wenn man eine Form von for() mit Argumenten verwendet, gibt es mit dem
semantisch analogen foreach()-Konstrukt eine syntaktische Variante zur for()-Schleife ohne
Argumente. Will man Werte aus Listenstrukturen herausfiltern oder Listenstrukturen transformieren,
lassen sich alternativ zu den genannten Schleifen auch die Funktionen grep und map gebrauchen.
Eine weitere Möglichkeit, den Programmfluss zu beeinflussen, besteht im Einsatz von
Schleifenkontroll-Konstrukten. Diese ermöglichen es, neben den eigentlichen Schleifenbedingungen
weitere Bedingungen zu formulieren, die es im Fall von last() ermöglichen, die aktuelle Schleife
abzubrechen oder mit next() einen weiteren Schleifendurchlauf ohne die Ausführung der noch aus-
stehenden Anweisungen zu erzwingen. Da sich für Schleifenkontroll-Konstrukte benannte
Sprungpunkte angeben lassen, kann man dadurch an beliebige Punkte im Programm wechseln.
5.4 Beispielanwendung Überführen wir zunächst das im letzten Kapitel entwickelte Programm zur Zählung von Bigrammen
bzw. Trigrammen am Beispiel der Trigramme in eine Form, die mit Schleifen arbeitet, bevor wir es um
zwei wichtige computerlinguistische Konzepte erweitern.
Mit den uns nun bekannten Sprachmitteln können wir eine Bedingung angeben, die es uns
programmatisch ermöglicht, zu entscheiden, ob sich noch weitere Trigramme aus dem Beispielsatz
extrahieren lassen: Solange mehr als zwei Elemente im Array aus den Wörtern des Satzes vorhanden
sind, können wir Trigramme bilden. Dadurch wird unser Programm einerseits kompakter, andererseits
stellt es keine Ad-hoc-Lösung mehr für den einen von uns gewählten Beispielsatz dar:
52
# Solange mehr als zwei Wörter im Array vorhanden sind, lassen sich # Trigramme extrahieren. while(@woerter > 2){ ... }
Den Schleifenrumpf bildet die bereits aus der letzten Version bekannte Einheit aus der Extraktion
der ersten drei Elemente des Wörter-Arrays, der Ausgabe des jeweiligen Trigramms und dem
Zurücklegen des dritten und zweiten Elements auf das Array:
# Entferne die ersten drei Elemente des Arrays und schreibe sie in die # Skalarvariablen für das Trigramm. ($erstes, $zweites, $drittes) = splice(@woerter, 0, 3); print "$erstes $zweites $drittes\n"; # Das zweite und dritte Wort sind die ersten beiden Wörter des nächsten # Trigramms, weshalb zuerst das dritte, dann das zweite an den Anfang des # Arrays zurückgelegt werden muss! unshift(@woerter, $drittes); unshift(@woerter, $zweites);
Das komplette Programm sieht dementsprechend so aus:
use strict; use diagnostics; my @woerter = qw(Eine Rose ist eine Rose ist eine Rose .); # Solange mehr als zwei Wörter im Array vorhanden sind, lassen sich # Trigramme extrahieren. while(@woerter > 2){ # Entferne die ersten drei Elemente des Arrays und schreibe sie in die # Skalarvariablen für das Trigramm. my ($erstes, $zweites, $drittes) = splice(@woerter, 0, 3); print "$erstes $zweites $drittes\n"; # Das zweite und dritte Wort sind die ersten beiden Wörter des # nächsten Trigramms, weshalb zuerst das dritte, dann das zweite an # den Anfang des Arrays zurückgelegt werden muss! unshift(@woerter, $drittes); unshift(@woerter, $zweites); }
Anders als in der Version aus dem letzten Kapitel haben wir hier nicht die Anzahl der Wörter und
der Trigramme bestimmt. Dazu erweitern wir unser Instrumentarium um das Konzept des Tokens und
des Types. Ein Token entspricht genau einem zählbaren Vorkommen eines Worts in unserem Beispiel-
satz. Ein Type hingegen ist jedes voneinander unterscheidbare Wort:
Sprachwissenschaftliches Institut
53
Token Type
Eine Eine
Rose Rose
ist ist
eine eine
Rose
ist
eine
Rose
. .
Die Types entsprechen also dem Vokabular unseres Beispielsatzes: Während es insgesamt neun
Token gibt, finden wir in ihm nur fünf Types. Eine elegante Form der Bestimmung der Anzahl an
Token und Types in Perl besteht darin, jedes Wort als Schlüssel in einen Hash zu schreiben; der
dazugehörige Wert ist die Häufigkeit, mit der dieses Token im Satz vorhanden ist. Um diese Häufigkeit
zu bestimmen, nutzen wir einerseits die Tatsache aus, dass die Schlüssel in einem Hash eindeutig sein
müssen, andererseits bedienen wir uns des Autoinkrement-Operators: Wann immer wir ein bestimmtes
Wort sehen, erhöht sich seine Häufigkeit automatisch um eins.
In unserer Perl-Implementierung iterieren wir dazu über das Wörter-Array, schreiben das jeweilige
Wort in den Hash und erhöhen seinen Wert um eins:
# In diesem Hash stehen die Unigramm-Häufigkeiten. my %unigramm_haeufigkeiten; # Iteriere über den Satz und zähle die Wort-Häufigkeiten. foreach my $wort(@woerter){ $unigramm{$wort}++; }
Die Anzahl der Types ergibt sich aus der Anzahl der Schlüssel dieses Hashes:
my $unigramm_type_haeufigkeiten = keys(%unigramm_haeufigkeiten);
Auf die gleiche Weise lassen sich nun die Bi- und Trigramm-Häufigkeiten ermitteln; dies sei
wiederum an der Ermittlung der Trigramm-Häufigkeiten demonstriert. Um ein Trigramm in einen Hash
schreiben zu können, konkatenieren wir es jeweils zu einer Zeichenkette:
# In diesem Hash stehen die Trigramm-Häufigkeiten. my %trigramm_haeufigkeiten; while(@woerter > 2){ ... # Konkateniere die ersten drei Elemente zu einem Trigramm. my $trigramm = $erstes . " " . $zweites . " " . $drittes; # Ermittle die Trigramm-Häufigkeiten. $trigramm_haeufigkeiten{$trigramm}++; ... }
54
Die Gesamtzahl der Token im Satz lässt sich wie schon im vorherigen Kapitel gesehen dadurch
ermitteln, dass man das Wörter-Array in einen Skalarkontext bringt:
# Die Anzahl aller Token im Satz. my $anzahl_token = @woerter;
Geben wir nun die gewonnenen Häufigkeiten in absteigender Sortierung aus:
# Unigramm-Häufigkeiten absteigend sortiert. foreach my $unigramm(sort{$unigramm_haeufigkeiten{$b} <=>
$unigramm_haeufigkeiten{$a}} keys %unigramm_haeufigkeiten){ print "$unigramm: $unigramm_haeufigkeiten{$unigramm}\n"; } # Trigramm-Häufigkeiten absteigend sortiert. foreach my $trigramm(sort{$trigramm_haeufigkeiten{$b} <=>
$trigramm_haeufigkeiten{$a}} keys %trigramm_haeufigkeiten){ print "$trigramm: $trigramm_haeufigkeiten{$trigramm}\n"; }
Das komplette Programm sieht dann so aus:
use strict; use diagnostics; my @woerter = qw(Eine Rose ist eine Rose ist eine Rose .); # In diesem Hash stehen die Unigramm-Häufigkeiten. my %unigramm_haeufigkeiten; # In diesem Hash stehen die Trigramm-Häufigkeiten. my %trigramm_haeufigkeiten; # Iteriere über den Satz und zähle die Wort-Häufigkeiten. foreach my $wort(@woerter){ $unigramm{$wort}++; } # Die Anzahl aller Token im Satz. my $anzahl_token = @woerter; print "Der Satz besteht aus $anzahl_token Wörtern\n"; # Solange mehr als zwei Wörter im Array vorhanden sind, lassen sich # Trigramme extrahieren. while(@woerter > 2){ # Entferne die ersten drei Elemente des Arrays und schreibe sie in die # Skalarvariablen für das Trigramm. my ($erstes, $zweites, $drittes) = splice(@woerter, 0, 3); # Konkateniere die ersten drei Elemente zu einem Trigramm. my $trigramm = $erstes . " " . $zweites . " " . $drittes;
Sprachwissenschaftliches Institut
55
# Ermittle die Trigramm-Häufigkeiten. $trigramm_haeufigkeiten{$trigramm}++; # Das zweite und dritte Wort sind die ersten beiden Wörter des # nächsten Trigramms, weshalb zuerst das dritte, dann das zweite an # den Anfang des Arrays zurückgelegt werden muss! unshift(@woerter, $drittes); unshift(@woerter, $zweites); } print "Die nach ihrer Häufgkeit sortierten Unigramme sind:\n"; # Unigramm-Häufigkeiten absteigend sortiert. foreach my $unigramm(sort{$unigramm_haeufigkeiten{$b} <=>
$unigramm_haeufigkeiten{$a}} keys %unigramm_haeufigkeiten){ print "$unigramm: $unigramm_haeufigkeiten{$unigramm}\n"; } print "Die nach ihrer Häufigkeit sortierten Trigramme sind:\n"; # Trigramm-Häufigkeiten absteigend sortiert. foreach my $trigramm(sort{$trigramm_haeufigkeiten{$b} <=>
$trigramm_haeufigkeiten{$a}} keys %trigramm_haeufigkeiten){ print "$trigramm: $trigramm_haeufigkeiten{$trigramm}\n"; } # Ausgabe: # Der Satz besteht aus 9 Wörtern. # Die nach ihrer Häufigkeit sortierten Unigramme sind: # Rose: 3 # ist: 2 # eine: 2 # Eine: 1 # .: 1 Die nach ihrer Häufigkeit sortierten Trigramme sind: # ist eine Rose: 2 # Rose ist eine: 2 # eine Rose .: 1 # eine Rose ist: 1 # Eine Rose ist: 1
Die von uns durch dieses Programm gewonnenen Häufigkeiten sind an sich noch nicht interessant,
da sie absolute Zahlen sind. Damit sie statistisch verwertbar werden, muss man sie zu den
Gesamthäufigkeiten in Verhältnis setzen. Da diese in unserem Beispiel bisher allerdings noch zu gering
sind, um linguistisch signifikante Aussagen zu treffen, werden wir im nächsten Kapitel zeigen, wie sich
wesentlich größere Datenmengen verarbeiten lassen.
56
6 Operationen auf Dateien
It is possible to store the mind with a million facts and still be entirely
uneducated.
ALEC BOURNE
Die bisher verarbeiteten Daten stammten entweder aus der Standardeingabe oder sie waren statisch
als Datenstruktur im Programm verankert. Da der Großteil computerlinguistischer Analysen auf großen
Datenmengen operiert, gilt es einige Aspekte zu bedenken, die für unser bisheriges Vorgehen noch un-
erheblich waren:
• Die Daten sollten möglichst unabhängig von einer Implementation existieren. Dadurch werden
sie sowohl für unterschiedliche Lösungsansätze eines Problems als auch für verschiedene
Fragestellungen nutzbar.
• Es ist wenig praktikabel, größere Datenmengen für jeden Programmlauf per Hand einzugeben!
Ebenso sollte es möglich sein, Ergebnisse nicht nur auf der Standardausgabe anzuzeigen, sondern
diese permanent zu sichern. Solche Operationen finden sowohl für die Eingabe- wie auch für die
Ausgabeseite in Dateien statt. Um diese nutzen zu können, müssen wir unser vorhandenes Perl-
Instrumentarium nur unwesentlich erweitern, da die grundlegenden Konzepte mit STDIN/STDOUT und
der print()-Funktion bereits bekannt sind.
6.1 Dateideskriptoren STDIN und STDOUT bezeichnet man als Dateideskriptoren (engl. file handles). Wie Variablen stellen
sie eine Abstraktion über ihre Inhalte dar. Im Fall der Variablen waren dies Werte, die in
unterschiedlichen Datenstrukturen abgelegt wurden und über den Variablennamen zugreifbar waren.
Dateideskriptoren sind analog dazu Container für die Inhalte von Dateien und machen diese über ihren
Namen zugreifbar. Dementsprechend sind die Standardein- und –ausgabe aus der Sicht der meisten
Programmiersprachen Dateien, aus denen Daten gelesen werden bzw. in die Daten geschrieben werden.
Syntaktisch bestehen Dateideskriptoren aus einem komplett groß geschriebenen Bezeichner ohne
Präfixsymbol. Dieser Bezeichner ist bis auf einige Ausnahmen frei wählbar: Wie bereits gesehen,
stehen STDIN und STDOUT für die Standardein- und –ausgabe. Darüber hinaus existiert mit STDERR ein
Kanal, über den es möglich ist, Fehler und Warnungen unabhängig von der Standardausgabe
anzuzeigen. Da aber sowohl STDOUT als auch STDERR den Bildschirm als Ausgabemedium verwenden,
ist diese Unterscheidung für unsere Zwecke meist unerheblich und soll hier nicht weiter vertieft
werden. Des Weiteren sind die Dateideskriptoren DATA und ARGV von Perl reserviert; auf ihre
Funktionalität soll weiter unten eingegangen werden.
6.2 Dateizugriff Um auf eine Datei zugreifen zu können, muss sie für die auf ihr auszuführende Operation geöffnet
werden. Dies ist für gewöhnlich das Lesen aus einer Datei bzw. das Schreiben in eine Datei. In beiden
Fällen verwendet man die Funktion open(), auf deren Argumentliste man zunächst einen Bezeichner
für den Dateideskriptor vereinbart, den man dann mit dem Namen der gewünschten Datei verbindet:
Sprachwissenschaftliches Institut
57
open(IN, "dokument.txt");
Diese Zeile öffnet die Datei dokument.txt im selben Verzeichnis, in dem sich das aktuelle Perl-
Programm befindet, für den lesenden Zugriff. Zusätzlich zum Dateinamen lässt sich an dieser Stelle der
komplette Verzeichnispfad einschließlich Laufwerksbuchstaben angeben. Dabei ist zu beachten, dass
das Trennzeichen für die einzelnen Schritte des Verzeichnispfads unabhängig vom Betriebssystem
normale Schrägstriche / sind, und nicht wie unter Windows gewohnt Backslashes, die von Perl – wie
bereits gesehen – zur Zeichenmaskierung verwendet werden:
open(EINGABE, "C:/texte/dokument.txt");
Will man eine Datei für den schreibenden Dateizugriff öffnen, lenkt man die Ausgabe in die
entsprechende Datei um. Dies geschieht analog zur Arbeitsweise der Ausgabe-Umlenkung in den
Kommandozeilen der verschiedenen Betriebssysteme: Um eine Datei neu anzulegen, verwendet man
das größer-als-Zeichen >; in Perl besitzt es darüber hinaus die Funktionalität, eine bereits vorhandene
Datei zu überschreiben.32 Schreibt man an dieser Stelle zwei größer-als-Zeichen, werden die Daten
beim Schreiben an eine Datei angehängt bzw. eine Datei neu angelegt:
open(AUSGABE, ">C:/texte/neues_dokument.txt"); open(ANHANG, ">>C:/texte/kummulatives_dokument.txt");
Hat man die Operationen auf einer Datei beendet, schließt man sie wieder. Dazu verwendet man die
Funktion close(), die den Bezeichner des Dateideskriptors als Argument nimmt:
open(EINGABE, "dokument.txt"); ... close(EINGABE);
Da der Perl-Interpreter für gewöhnlich selbst dafür sorgt, dass ein geöffneter Dateideskriptor
spätestens bei Beendigung des Programmlaufs geschlossen wird, ist die close()-Anweisung nicht
zwingend notwendig...
Fehlerbehandlung beim Dateizugriff
Bei der Arbeit mit Dateien muss man immer damit rechnen, dass man aus irgendeinem Grund nicht
aus einer Datei lesen oder in eine Datei schreiben kann. Um solche Fälle adäquat behandeln zu können,
bietet Perl die Möglichkeit, eigene Fehlermeldungen auszugeben.33
Obwohl Perl eine interpretierte Sprache ist, unterscheidet man auch hier zwischen sogenannten
Fehlern zur Kompilierzeit und Laufzeitfehlern.34 Fehler zur Kompilierzeit treten auf, wenn der Perl-
Interpreter einen Syntaxfehler wie z.B. pritn statt print findet. Laufzeitfehler werden vom Perl-
Interpreter erst aufgeworfen, nachdem er den Quellcode auf Richtigkeit überprüft hat. So liefert z.B.
die Division durch Null einen Laufzeitfehler; die Operation ist syntaktisch richtig, aber aus
semantischen Gründen kann sie nicht durchgeführt werden. Da also der Fehler, nicht aus einer Datei
32 In den meisten Betriebssystemen ist es notwendig, dem größer-als-Zeichen ein Ausrufezeichen voranzustellen, wenn man
eine Datei überschreiben will. Dies ist unter Perl nicht so und kann u.U. zu unerwünschten Komplikationen führen! 33 Weitere Methoden zur Fehlersuche und –behandlung werden in einem späteren Kapitel vorgestellt. 34 Dies ergibt sich aus der Tatsache, dass der Perl-Interpreter den Quellcode vor der Ausführung in einen internen Bytecode
übersetzt, wobei allerdings keine kompilierte Objektdatei erzeugt wird.
58
lesen oder nicht in eine Datei schreiben zu können, einen schwerwiegenden Fehler zur Laufzeit
darstellt, muss man dafür sorgen, dass das Programm an dieser Stelle abbricht.
Um solch eine Fehlermeldung mit gleichzeitiger Beendigung des Programms zu initiieren,
verwendet man die Funktion die(). Sie wird anhand des or- oder des ||-Operators mit derjenigen
Funktion verknüpft, die zur Laufzeit Probleme bereiten könnte. Dies ist in unserem Fall die open()-
Funktion, da z.B. möglicherweise die gewünschte Datei zum Lesen nicht vorhanden ist, oder weil in
eine vorhandene Datei nicht geschrieben werden kann, da sie vom Betriebssystem oder einer anderen
Anwendung z.Zt. blockiert wird. Der die()-Funktion übergibt man für gewöhnlich ein Argument,
nämlich eine eigene Fehlermeldung, die die Situation adäquat beschreibt und/oder eine
Systemmeldung. Diese steht in der besonderen Variablen $! und wird wie mit print() gewohnt ausge-
geben:
open(EINGABE, "dokument.txt") or die "Fehler beim Öffnen der Datei: $!"; # Ausgabe bei nicht vorhandener Datei: # Uncaught exception from user code: # Fehler beim Öffnen der Datei: No such file or directory at test.pl
line 1.
Verwendung des lesenden Dateizugriffs
Genauso wie wir STDIN zum Einlesen von Daten aus der Eingabeaufforderung verwendet haben,
können wir mit unseren eigenen Dateideskriptoren Zeilen aus Dateien einlesen. Dazu umschließt man
den jeweiligen Dateideskriptor wie schon gesehen mit spitzen Klammern <>, dem sogenannten
Diamant- oder auch readline-Operator. Um nun eine Datei zeilenweise einzulesen und Operationen auf
diesen Zeilen auszuführen, verwendet man eine while()-Schleife, in der die aktuelle Zeile aus dem
Dateideskriptor einer entsprechenden Skalarvariablen explizit zugewiesen wird, oder implizit in $_
steht:
open(EINGABE, "dokument.txt") or die $!; while(my $zeile = <EINGABE>){ print $zeile; }
open(EINGABE, "dokument.txt") or die $!; while(<EINGABE>){ print; }
Eine der wichtigsten Operationen, die man bei der Behandlung von Zeichenketten vornehmen
muss, besteht darin, die eingelesene Zeile in die gewünschten Komponenten zu zerlegen; für
gewöhnlich sind dies Wörter und Buchstaben.35 Die entscheidende Grenze verläuft dabei zwischen den
Sätzen und den Wörtern: Auf den ersten Blick erscheint ein – Tokenisierung genannter – Arbeitsschritt,
der zwischen diesen beiden Komponenten unterscheidet, trivial, doch werden wir im nächsten Kapitel
noch sehen, dass dem nicht so ist! Gehen wir an dieser Stelle davon aus, dass das vorliegende
Dokument bereits tokenisiert ist, können wir vermittels der Funktion split() die einzelnen
Komponenten einer Zeile in entsprechende Arrays schreiben. Dazu verwendet split() als erstes
35 Sätze sind aus statistischer Sicht wenig interessant, da es sehr unwahrscheinlich ist, einen und denselben Satz mehrmals in
einem Dokument zu finden.
Sprachwissenschaftliches Institut
59
Argument ein Trennzeichen, das ähnlich wie bei den alternativen Anführungszeichen zwischen zwei
Schrägstrichen steht, und als zweites eine Skalarvariable, in der die zu verwendende Zeile steht. Der
Rückgabewert dieser Funktion ist eine Liste. Will man die Elemente dieser Liste in einem Array spei-
chern, reicht es nicht, sie einfach diesem Array zuzuweisen, da dieses bei jeder Schleifeniteration
überschrieben würde. Vielmehr muss man dafür sorgen, dass die Wörter bzw. Buchstaben anhand der
Funktion push() hintereinander in das Array geschrieben werden. Als Trennzeichen für die Wörter
verwenden wir ein Leerzeichen, für die Buchstaben geben wir kein Trennzeichen an, da wir in diesem
Fall ja auf jedem Zeichen trennen wollen:
my (@woerter, @buchstaben); open(EINGABE, "dokument.txt") or die $!; while(my $zeile = <EINGABE>){ push(@woerter, split(/ /, $zeile)); } foreach my $wort(@woerter){ push(@buchstaben, split(//, $wort)); } print "@woerter\n"; print "@buchstaben\n"; # Gehen wir davon aus, dass in der Datei dokument.txt der Satz # Eine Rose ist eine Rose ist eine Rose . steht, erhält man folgende # Ausgabe: # Eine Rose ist eine Rose ist eine Rose . # E i n e R o s e i s t e i n e R o s e i s t e i n e R o s e .
Im umgekehrten Fall, dass man Elemente eines Arrays oder einer Liste zu einem Skalar
zusammenfassen will, verwendet man die Funktion join(), die wiederum ein Trennzeichen und ein
Array als Argumente benötigt. Anders als bei split() kommen hier allerdings keine Schrägstriche als
Begrenzungszeichen zum Einsatz, sondern doppelte Anführungszeichen:
# Ausgehend vom obigen Array @buchstaben: print join("|", @buchstaben); # Ausgabe: # E|i|n|e|R|o|s|e|i|s|t|e|i|n|e|R|o|s|e|i|s|t|e|i|n|e|R|o|s|e|.|
Schlürfmodus
Eine weitere Möglichkeit, Dateien einzulesen besteht darin, sie nicht zeilenweise, sondern komplett
auf ein Mal in einen Skalar oder ein Array zu lesen. Diese Vorgehensweise bezeichnet man auch als
Schlürfen (engl. slurping); zwar ist diese Operation im Vergleich zum zeilenweisen Einlesen schneller,
doch ist sie auch speicherintensiver und empfiehlt sich eher bei wenig umfangreichen Dateien.
60
Liest man eine Datei in ein Array ein, so bilden ihre Zeilen die Elemente des Arrays:
open(EINGABE, "dokument.txt") or die $!; my @datei = <EINGABE>; print $datei[0]; # Ausgabe: Erste Zeile des Dokuments. print $datei[1]; # Ausgabe: Zweite Zeile des Dokuments.
Will man jedoch eine Datei in einen Skalar einlesen, muss man aufgrund der singulären Natur
dieser Datenstruktur dafür sorgen, dass Zeilenendezeichen ignoriert werden. Dazu setzt man den
sogenannten input record separator $/, in dem festgelegt ist, welches Zeichen das Zeilenende abbildet
(meist \n), auf undef.36 Sind die Operationen auf der Datei ausgeführt, muss der input record
separator wieder auf seinen Ursprungswert zurückgesetzt werden, um eventuelle Komplikationen beim
Einlesen weiterer Dateien zu vermeiden:
open(EINGABE, "dokument.txt") or die $!; undef($/); my $datei = <EINGABE>; print $file; # Ausgabe: Das gesamte Dokument einschließlich
Zeilenumbrüchen. $/ = "\n";
Verwendung des schreibenden Dateizugriffs
Will man Daten in eine Datei schreiben, übergibt man der print()-Funktion als erstes Argument
den Bezeichner des Dateideskriptors und dann die Daten:
open(AUSGABE, ">neues_dokument.txt") or die $!; print AUSGABE "In dieser Datei steht nur ein Satz!\n";
Verwendet man im Programm für die Ausgabe von Daten hauptsächlich einen anderen
Dateideskriptor als STDOUT, kann man diesen vermittels der Funktion select() als Standardausgabe
festlegen:
open(AUSGABE, ">neues_dokument.txt") or die $!; select AUSGABE; print "In dieser Datei steht nur ein Satz\n";
6.3 Spezielle Dateideskriptoren Wie bereits erwähnt, sind neben den Dateideskriptoren für die Standardein- und –ausgabe in Perl
noch zwei weitere Dateideskriptoren, nämlich DATA und ARGV, als Schlüsselwörter reserviert.
DATA ist ein im Sinne des rapid prototypings sehr nützlicher Dateideskriptor, da er Daten aus einem
mit __DATA__ oder __END__ gekennzeichneten Abschnitt nach dem eigentlichen Programm einliest,
wobei er anders als bei normalen Dateideskriptoren nicht explizit geöffnet werden muss. Ähnlich wie
in den vorherigen Kapiteln werden dabei zwar die Daten an das Programm gebunden, doch geschieht
dies nicht in Form von vorbelegten Variablen, sondern in derselben Form wie beim Lesen aus einer
Datei, sodass man zwischen dem Testen und einem "echten" Programmlauf nur den Bezeichner des
Dateideskriptors austauschen muss:
36 Dies bedeutet allerdings nicht, dass die Zeilenendezeichen wie bei chomp() entfernt werden!
Sprachwissenschaftliches Institut
61
# open(EINGABE, "nzz_schweiz.txt") or die $!; # while(<EINGABE>){ while(<DATA>){ print; } ... # Weitere Anweisungen ... __END__ Der Bürger vor den neuen Selbstenwaffnungsinitiativen Sicherheit zum Nulltarif? Genau sechs Monate nach der EWR-Abstimmung, in der sich das Schweizervolk und eine Mehrzahl der Stände für Selbstbehauptung im Alleingang und gegen die Teilnahme an der ...
Der Dateideskriptor ARGV repräsentiert den sogenannten Argumentvektor, eine Liste, in der die beim
Programmaufruf übergebenen Argumente stehen. Diese stehen wiederum für Dateinamen, die bei der
Iteration über diesen Dateideskriptor automatisch geöffnet werden:37
# Programmaufruf: perl programm.pl dokument.txt # Programm: while(<ARGV>){ print; # Ausgabe: Zeilen aus dokument.txt }
Der Diamant-Operator <> stellt eine Kurzschreibweise für die Verwendung von ARGV dar:
# Programmaufruf: perl programm.pl dokument.txt # Programm: while(<>){ print; # Ausgabe: Zeilen aus dokument.txt }
Die Argumente selbst stehen im besonderen Array @ARGV, das auf diese Art auch abgefragt werden
kann. Will man z.B. sicherstellen, dass dem Programm genau ein Dateiname übergeben wurde, testet
man dies wie bei jedem anderen Array ab:
die "Sie müssen genau einen Dateinamen angeben!\n" unless(@ARGV == 1);
6.4 Exkurs: Formatierung numerischer Zeichenketten Bei der computerlinguistischen Analyse mit statistischen Methoden bekommen wir durch
verschiedene Rechenverfahren Werte, die viele Nachkommastellen besitzen. Während diese für weitere
37 In der Literatur ist im Zusammenhang mit ARGV häufig auch die Rede vom magic open.
62
Berechnungen unverzichtbar sind, werden sie in der Repräsentation von Ergebnissen sehr schnell
unübersichtlich, weshalb man bestrebt sein sollte, die Nachkommastellen für die Ausgabe auf ein
adäquates Maß zu begrenzen und das Ergebnis dementsprechend zu runden.
Für diese Aufgabe steht uns die Funktion sprintf() zur Verfügung.38 Das erste Argument dieser
Funktion besteht aus einem Formatierungsteil, der durch ein Prozentzeichen eingeleitet wird, dem die
Formatgröße des ganzzahligen Teil der Zahl und durch einen Dezimalpunkt getrennt die Formatgröße
des Nachkommateils der Zahl folgt, und der Angabe des Typs der Zeichenkette. Dieser
Formatierungsteil steht in doppelten Anführungszeichen.
Das zweite Argument für sprintf() ist die zu formatierende Zeichenkette. Will man den
ganzzahligen Anteil oder den Nachkomma-Anteil einer Zahl nicht formatieren, kann man ihn bei der
Formatierungsangabe weglassen. Die Funktion sprintf() selbst liefert entgegen ihrer Namensgebung
keine Ausgabe, sondern hat einen Skalar als Rückgabewert:
my $radius = 1000; # Liefert die ersten sechs Nachkommastellen. my $kreistreffer = 0; my $quadrattreffer = (2 * $radius) ** 2; for(my $y = -$radius; $y <= $radius; $y++){ for(my $x = -$radius; $x <= $radius; $x++){ $kreistreffer++ if(sqrt($x ** 2 + $y ** 2) <= $radius); } } print 4 * $kreistreffer / $quadrattreffer."\n"; # Ausgabe: 3.141549 print sprintf("%.2f", (4 * $kreistreffer / $quadrattreffer))."\n";
# Ausgabe: 3.14
6.5 Zusammenfassung Die Gewinnung von Daten aus Dateien und das Speichern von Ergebnisdaten in Dateien erfolgt in
Perl über Dateideskriptoren, die ähnlich einer Variablen eine Zugriffsmöglichkeit auf die physikalisch
vorhandenen Dateien herstellen. Um den Zugriff zu ermöglichen, muss eine Datei vermittels open()
für den lesenden oder schreibenden Dateizugriff geöffnet werden. Da eine solche Operation auch
fehlschlagen kann, was vom Perl-Interpreter unbeachtet bleibt, muss man diese Möglichkeit anhand der
die()-Funktion behandeln.
Ist das Öffnen geglückt, lässt sich ein Dokument durch Iteration über seine Zeilen anhand des in
spitzen Klammern geschriebenen Dateideskriptors in eine entsprechende Datenstruktur (meist ein
Array oder einen Hash) einlesen. Darüber hinaus sieht Perl die Möglichkeit vor, eine Datei komplett in
ein Array oder einen Skalar einzulesen, wozu man den Dateideskriptor direkt einer Array- oder
Skalarvariablen zuweist. Im letzten Fall muss dazu der sogenannte record input separator außer Kraft
gesetzt werden.
Will man Ergebnisdaten in eine Datei schreiben, genügt es, beim Öffnen der Datei die Ausgabe in
diese durch eine oder zwei schließende spitze Klammern vor dem Dateinamen umzulenken und
anschließend den Bezeichner des Dateideskriptors ohne spitze Klammern zwischen print und die
auszugebenden Daten zu schreiben.
38 Diese Funktion stammt aus der Programmiersprache C und wird dort für die formatierte Ausgabe von Zeichenketten
verwendet. Wir beschränken uns an dieser Stelle auf die skizzierte Aufgabenstellung, obwohl der Funktionsumfang von
sprintf() weit darüber hinaus geht.
Sprachwissenschaftliches Institut
63
Neben den bereits bekannten Dateideskriptoren für die Standardein- und –ausgabe finden sich in
Perl noch die reservierten Dateideskriptoren DATA und ARGV. Ersterer ermöglicht die Angabe von
Testdaten in einem __END__- oder __DATA__-Abschnitt nach dem eigentlichen Programm, während
ARGV oder seine Abkürzung, der Diamant-Operator <>, den Zugriff auf Argumente des Pro-
grammaufrufs erlauben.
6.6 Beispielanwendung Datengrundlage statistischer Sprachverarbeitung sind umfangreiche Texte. Hatten sich unsere
Betrachtungen bis jetzt auf einzelne Sätze oder wenige Absätze beschränkt, haben wir mit der
Möglichkeit, Operationen auf Dateien ausführen zu können, ein Werkzeug bekommen, mit dem man
eine brauchbare Datenbasis verarbeiten kann. In der Linguistik bezeichnet man eine solche Datenbasis
als Korpus39. Diese besteht – je nach Anwendungszweck – aus vielen Texten einer Textsorte oder aus
umfangreichen Zusammenstellungen unterschiedlicher Textsorten.
Das im folgenden Beispiel verwandte Korpus besteht aus den englischsprachigen Versionen von
Fjodor Dostojevskis Der Idiot und Die Brüder Karamasow. Mit knapp einer Million Wörtern ist es für
unsere Zwecke ausreichend groß proportioniert; ernsthafte Anwendungen bewegen sich allerdings eher
in Bereichen von hundert Millionen Wörtern. Darüber hinaus ist es bereits tokenisiert, d.h. es ist so
präpariert worden, dass sich Wörter auf einfache Weise daraus extrahieren lassen.
Wie schon im letzten Kapitel werden wir auch für dieses Korpus die absoluten Häufigkeiten für
Uni-, Bi- und Trigramm-Token und -Types bestimmen.40 Darüber hinaus wollen wir in diesem Kapitel
auch die relativen Häufigkeiten für diese sprachlichen Einheiten bestimmen. Einerseits lässt sich daraus
ermitteln, ob die von uns gewählten Einheiten aus Zwei- und Drei-Wort-Kombinationen statistisch
sinnvoll sind, weshalb wir in diesem Beispiel auch Tetragramme betrachten. Andererseits wollen wir
herausfinden, welche Wörter häufig in Korpora zu finden sind und wie ihr Anteil im Verhältnis zur
Gesamtmenge der Wörter ist. In diesem Zusammenhang ist es darüber hinaus interessant, die
Häufigkeiten von n-Grammen zu bestimmen, die nur sehr selten, d.h. in diesem Fall nur genau einmal,
im Korpus zu finden sind. Über ihren Anteil an der Gesamtzahl der sprachlichen Einheiten lässt sich
ein Verhältnis absehen, wie viele Wörter selten oder gar nicht in unserem Korpus zu finden sind.
Ziel solcher Verfahren ist die Ermittlung eines Modells, das Voraussagen über Regularitäten einer
Sprache ermöglicht, so z.B. die Frage, mit welcher Wahrscheinlichkeit man das Auftreten eines
bestimmten Worts erwarten kann, wenn ihm n andere bestimmte Wörter vorangingen.
Für die Implementierung benötigen wir zunächst einige Variablen, in denen wir die Gesamt-
Häufigkeiten der n-Gramm Token41 sowie die n-Gramm Types und ihre jeweiligen Häufigkeiten
speichern wollen:
# Diese Variablen speichern die Häufigkeiten der jeweiligen n-Gramm-Token. my ( $anzahl_bigramme, $anzahl_trigramme, $anzahl_tetragramme ); # Diese Variablen speichern die Häufigkeiten von n-Gramm-Token, # die nur einmal im Korpus vorkommen. my ( $unigramm_hl, $bigramm_hl, $trigramm_hl, $tetragramm_hl ); # In diesen Hashes stehen die n-Gramme und ihre jeweiligen Häufigkeiten.
39 Das Genus dieses Worts ist das Neutrum, nicht das Maskulinum! 40 An dieser Stelle sei die terminologische Abstraktion des n-Gramms über solche sprachlichen Einheiten eingeführt. 41 Eigentlich sollte die Zahl der Bi-, Tri- und Tetragramm-Token natürlich äquivalent zur Anzahl der Token sein. Dazu
müsste man allerdings jeweils n-1 Dummy-Token anfügen; da diese aber für unsere Betrachtung irrelevant würden, verzichten
wir hier darauf und machen den Unterschied durch die verschiedenen Häufigkeiten transparent.
64
my ( %unigramm_haeufigkeit, %bigramm_haeufigkeit, %trigramm_haeufigkeit, %tetragramm_haeufigkeit );
Zuerst lesen wir die Token aus unserem vortokenisierten Korpus in ein Array ein:
# In diesem Array stehen die Token des Korpus. my @token; # Lesenden Zugriff auf das Korpus herstellen. open( IN, "dostojevski.tok" ) or die $!; # Korpus einlesen und Token in einer Liste speichern. while (<IN>) { chomp; push( @token, split(/ /) ); }
Die Gesamtzahl der Token im Korpus entspricht der Anzahl der Elemente in diesem Array:
# In dieser Variablen steht die Gesamtzahl der Token. my $anzahl_token = @token;
Um die Anzahl der Types, d.h. die Größe des Vokabulars, bestimmen zu können und gleichzeitig zu
zählen, wie oft das jeweilige Token im Korpus vorhanden ist, iterieren wir über das Array der Wörter,
schreiben jedes einzelne als Schlüssel in einen Hash, wobei sich der jeweilige Wert aus der
kumulativen Häufigkeit, die sich aus der Verwendung des Autoinkrement-Operators ergibt, speist:
# Unigramm-Types und ihre Häufigkeiten ermitteln. foreach (@token) { $unigramm_haeufigkeit{$_}++; }
Da die Operationen zur Ermittlung der Bigramme und ihrer Häufigkeiten das ursprüngliche Array
zerstören werden, legen wir Kopien davon an, um daraus auch Tri- und Tetragramme und ihre
Häufigkeiten extrahieren zu können:
# Kopie für Trigramme. my @token2 = @token; # Kopie für Tetragramme. my @token3 = @token;
Die Extraktion von Bi-, Tri- und Tetragrammen und die Ermittlung ihrer jeweiligen Häufigkeiten
erfolgt wie bereits im letzten Kapitel diskutiert und sei hier nur kurz am Beispiel der Tetragramme
vorgeführt:
# Token-/Type-Häufigkeiten der Tetragramme ermitteln. while ( @token3 > 3 ) { my ( $erstes, $zweites, $drittes, $viertes ) = splice( @token3, 0, 4
);
Sprachwissenschaftliches Institut
65
my $tetragramm = $erstes . " " . $zweites . " " . $drittes . " " . $viertes;
$tetragramm_haeufigkeit{$tetragramm}++; unshift( @token3, $viertes ); unshift( @token3, $drittes ); unshift( @token3, $zweites ); $anzahl_tetragramme++; }
Die Anzahl der jeweiligen n-Gramm-Types ergibt sich aus der Anzahl der Elemente in den
jeweiligen Hashes:
# Die Anzahl der Types bildet das Vokabular des Korpus. my $vokabular = keys %unigramm_haeufigkeit; # Diese Variablen speichern die Anzahl der jeweiligen n-Gramm-Types. my $bigramm_types = keys %bigramm_haeufigkeit; my $trigramm_types = keys %trigramm_haeufigkeit; my $tetragramm_types = keys %tetragramm_haeufigkeit;
Im nächsten Schritt bestimmen wir die Gesamtzahl der n-Gramm-Types, die genau einmal im
Korpus zu finden sind. Dies sei hier nur knapp am Beispiel der Unigramme gezeigt:42
# Ermittle die Anzahl der Types mit der Häufigkeit 1. foreach ( keys %unigramm_haeufigkeit ) { $unigramm_hl++ if ( $unigramm_haeufigkeit{$_} == 1 ); }
Für die Ausgabe berechnen wir mit dem sogenannten n-Gramm-Raum die Anzahl der möglichen zu
findenden Types für das jeweilige n-Gramm. Im Falle der Bigramme berechnet sich der Bigramm-
Raum, indem man die Zahl der Unigramm-Types quadriert. Für die Berechnung des Trigramm-Raums
potenzieren wir die Unigramm-Types mit drei, bei den Tetragrammen mit vier. Zusätzlich berechnen
wir den prozentualen Anteil der Types am jeweiligen n-Gramm-Raum, hier anhand der Tetragramme
exemplifiziert:
# Ausgabe der Anzahl der Tetragramm-Token und –Types, des Tetragramm-Raums # und des Anteils der Tetragramm-Types am Tetragramm-Raum. print "\nEs gibt $anzahl_tetragramme Tetragramm-Token und $tetragramm_types
Tetragramm-Typen.\nDas ergibt " . ( $vokabular**4 ) . " mögliche Tetragramme, von denen " . ( ( $tetragramm_types / $vokabular**4 ) * 100 ) . "%\nim Dokument vorkommen.\n";
Darüber hinaus berechnen wir den prozentualen Anteil derjenigen n-Gramm-Types, die nur einmal
im Korpus vorkamen:
42 Die Bezeichnung hl an den Variablennamen ergibt sich aus dem Begriff hapax legomenon, der ein Wort meint, das nur
einmal in einem Korpus vorkommt.
66
# Ausgabe des Anteils der Tetragramm-Types mit der Häufigkeit 1. print sprintf( "%.2f", ( ( $tetragramm_hl / $tetragramm_types ) * 100 ) ) . "% aller Tetragramm-Types kommt nur einmal im Korpus vor!\n";
Zuletzt bestimmen wir die absoluten und relativen Häufigkeiten der jeweils zehn häufigsten
Vertreter eines n-Gramms. Damit wir nach zehn Einheiten aufhören können, benötigen wir eine
Zählvariable außerhalb der Schleife, die über den jeweiligen Hash iteriert; ist der Wert zehn erreicht,
verlassen wir die Schleife mit last(). Auch diese Operationen seien wiederum am Beispiel der
Tetragramme vorgeführt:
# Ermittlung der absoluten und relativen Häufigkeiten der Tetragramme # und Ausgabe der zehn häufigsten. $i = 0; foreach ( sort { $tetragramm_haeufigkeit{$b} <=>
$tetragramm_haeufigkeit{$a} } keys %tetragramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $tetragramm_haeufigkeit{$_} / $anzahl_tetragramme ) * 100 ); print "$_: $tetragramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller Tetragramm-Token.\n"; last if ( $i > 10 ); $i++; }
Hier das komplette Programm:
use strict; use diagnostics; # Diese Variablen speichern die Häufigkeiten der jeweiligen n-Gramm-Token. my ( $anzahl_bigramme, $anzahl_trigramme, $anzahl_tetragramme ); # Diese Variablen speichern die Häufigkeiten von n-Gramm-Token, # die nur einmal im Korpus vorkommen. my ( $unigramm_hl, $bigramm_hl, $trigramm_hl, $tetragramm_hl );
Sprachwissenschaftliches Institut
67
# In diesen Hashes stehen die n-Gramme und ihre jeweligen Häufigkeiten. my ( %unigramm_haeufigkeit, %bigramm_haeufigkeit, %trigramm_haeufigkeit, %tetragramm_haeufigkeit ); # In diesem Array stehen die Token des Korpus. my @token; # Lesenden Zugriff auf das Korpus herstellen. open( IN, "dostojevski.tok" ) or die $!; # Korpus einlesen und Token in einer Liste speichern. while (<IN>) { chomp; push( @token, split(/ /) ); } # In dieser Variablen steht die Gesamtzahl der Token. my $anzahl_token = @token; print "Korpus im Umfang von $anzahl_token Token eingelesen!\n\n"; # Unigramm-Types und ihre Häufigkeiten ermitteln. foreach (@token) { $unigramm_haeufigkeit{$_}++; } # Die Operationen zur Ermittlung der Häufigkeiten von Bi-, Tri- und # Tetragrammen zerstört die ursprüngliche Wortliste, weshalb wir für # diese Operationen Kopien anlegen müssen. # Kopie für Trigramme. my @token2 = @token; # Kopie für Tetragramme. my @token3 = @token; # Token-/Type-Häufigkeiten der Bigramme ermitteln. while ( @token > 1 ) { my ( $erstes, $zweites ) = splice( @token, 0, 2 ); my $bigramm = $erstes . " " . $zweites; $bigramm_haeufigkeit{$bigramm}++; unshift( @token, $zweites ); $anzahl_bigramme++; } # Token-/Type-Häufigkeiten der Trigramme ermitteln. while ( @token2 > 2 ) { my ( $erstes, $zweites, $drittes ) = splice( @token2, 0, 3 ); my $trigramm = $erstes . " " . $zweites . " " . $drittes; $trigramm_haeufigkeit{$trigramm}++; unshift( @token2, $drittes ); unshift( @token2, $zweites ); $anzahl_trigramme++; }
68
# Token-/Type-Häufigkeiten der Tetragramme ermitteln. while ( @token3 > 3 ) { my ( $erstes, $zweites, $drittes, $viertes ) = splice( @token3, 0, 4
); my $tetragramm = $erstes . " " . $zweites . " " . $drittes . " " .
$viertes; $tetragramm_haeufigkeit{$tetragramm}++; unshift( @token3, $viertes ); unshift( @token3, $drittes ); unshift( @token3, $zweites ); $anzahl_tetragramme++; } # Die Anzahl der Types bildet das Vokabular des Korpus. my $vokabular = keys %unigramm_haeufigkeit; print "Die Größe des Vokabulars beträgt $vokabular.\n"; # Diese Variablen speichern die Anzahl der jeweiligen n-Gramm-Types. my $bigramm_types = keys %bigramm_haeufigkeit; my $trigramm_types = keys %trigramm_haeufigkeit; my $tetragramm_types = keys %tetragramm_haeufigkeit; # Ermittle die Anzahl der Types mit der Häufigkeit 1. foreach ( keys %unigramm_haeufigkeit ) { $unigramm_hl++ if ( $unigramm_haeufigkeit{$_} == 1 ); } # Ermittle die Anzahl der Bigramm-Types mit der Häufigkeit 1. foreach ( keys %bigramm_haeufigkeit ) { $bigramm_hl++ if ( $bigramm_haeufigkeit{$_} == 1 ); } # Ermittle die Anzahl der Trigramm-Types mit der Häufigkeit 1. foreach ( keys %trigramm_haeufigkeit ) { $trigramm_hl++ if ( $trigramm_haeufigkeit{$_} == 1 ); } # Ermittle die Anzahl der Tetragramm-Types mit der Häufigkeit 1. foreach ( keys %tetragramm_haeufigkeit ) { $tetragramm_hl++ if ( $tetragramm_haeufigkeit{$_} == 1 ); } # Ausgabe des Anteils der Types mit der Häufigkeit 1 am Vokabular. print sprintf( "%.2f", ( ( $unigramm_hl / $vokabular ) * 100 ) ) . "% aller Types kommt nur einmal im Korpus vor!\n"; # Ausgabe der Anzahl der Bigramm-Token und –Types, des Bigramm-Raums # und des Anteils der Bigramm-Types am Bigramm-Raum. print "\nEs gibt $anzahl_bigramme Bigramm-Token und $bigramm_types Bigramm-
Types.\nDas ergibt " . ( $vokabular**2 ) . " mögliche Bigramme, von denen " . ( ( $bigramm_types / $vokabular**2 ) * 100 ) . "%\nim Dokument vorkommen.\n";
Sprachwissenschaftliches Institut
69
# Ausgabe des Anteils der Bigramm-Types mit der Häufigkeit 1. print sprintf( "%.2f", ( ( $bigramm_hl / $bigramm_types ) * 100 ) ) . "% aller Bigramm-Types kommt nur einmal im Korpus vor!\n"; # Ausgabe der Anzahl der Trigramm-Token und –Types, des Trigramm-Raums # und des Anteils der Trigramm-Types am Trigramm-Raum. print "\nEs gibt $anzahl_trigramme Trigramm-Token und $trigramm_types Trigramm-
Types.\nDas ergibt " . ( $vokabular**3 ) . " mögliche Trigramme, von denen " . ( ( $trigramm_types / $vokabular**3 ) * 100 ) . "%\nim Dokument vorkommen.\n"; # Ausgabe des Anteils der Trigramm-Types mit der Häufigkeit 1. print sprintf( "%.2f", ( ( $trigramm_hl / $trigramm_types ) * 100 ) ) . "% aller Trigramm-Types kommt nur einmal im Korpus vor!\n"; # Ausgabe der Anzahl der Tetragramm-Token und –Types, des Tetragramm-Raums # und des Anteils der Tetragramm-Types am Tetragramm-Raum. print "\nEs gibt $anzahl_tetragramme Tetragramm-Token und $tetragramm_types
Tetragramm-Types.\nDas ergibt " . ( $vokabular**4 ) . " mögliche Tetragramme, von denen " . ( ( $tetragramm_types / $vokabular**4 ) * 100 ) . "%\nim Dokument vorkommen.\n"; # Ausgabe des Anteils der Tetragramm-Types mit der Häufigkeit 1. print sprintf( "%.2f", ( ( $tetragramm_hl / $tetragramm_types ) * 100 ) ) . "% aller Tetragramm-Types kommt nur einmal im Korpus vor!\n"; # Ermittlung der absoluten und relativen Häufigkeiten der Unigramme # und Ausgabe der zehn häufigsten. my $i = 0; print "\nDie zehn häufigsten Unigramme sind:\n"; foreach ( sort { $unigramm_haeufigkeit{$b} <=> $unigramm_haeufigkeit{$a} } keys %unigramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $unigramm_haeufigkeit{$_} / $anzahl_token ) * 100
); print "$_: $unigramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller Token.\n"; last if ( $i > 10 ); $i++; }
70
# Ermittlung der absoluten und relativen Häufigkeiten der Bigramme # und Ausgabe der zehn häufigsten. $i = 0; print "\nDie zehn häufigsten Bigramme sind:\n"; foreach ( sort { $bigramm_haeufigkeit{$b} <=> $bigramm_haeufigkeit{$a} } keys %bigramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $bigramm_haeufigkeit{$_} / $anzahl_bigramme ) *
100 ); print "$_: $bigramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller Bigramm-Token.\n"; last if ( $i > 10 ); $i++; } # Ermittlung der absoluten und relativen Häufigkeiten der Trigramme # und Ausgabe der zehn häufigsten. $i = 0; print "\nDie zehn häufigsten Trigramme sind:\n"; foreach ( sort { $trigramm_haeufigkeit{$b} <=> $trigramm_haeufigkeit{$a} } keys %trigramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $trigramm_haeufigkeit{$_} / $anzahl_trigramme ) * 100 ); print "$_: $trigramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller Trigramm-Token.\n"; last if ( $i > 10 ); $i++; } # Ermittlung der absoluten und relativen Häufigkeiten der Tetragramme # und Ausgabe der zehn häufigsten. $i = 0; print "\nDie zehn häufigsten Tetragramme sind:\n"; foreach ( sort { $tetragramm_haeufigkeit{$b} <=>
$tetragramm_haeufigkeit{$a} } keys %tetragramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $tetragramm_haeufigkeit{$_} / $anzahl_tetragramme ) * 100 ); print "$_: $tetragramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller Tetragramm-Token.\n"; last if ( $i > 10 ); $i++; }
Sprachwissenschaftliches Institut
71
# Ausgabe: # Korpus im Umfang von 995095 Token eingelesen! # Die Größe des Vokabulars beträgt 21085. # 38.43% aller Types kommt nur einmal im Korpus vor! # Es gibt 995094 Bigramm-Token und 219324 Bigramm-Types. # Das ergibt 444577225 mögliche Bigramme, von denen 0.0493331614096966% # im Dokument vorkommen. # 65.58% aller Bigramm-Types kommt nur einmal im Korpus vor! # Es gibt 995093 Trigramm-Token und 572129 Trigramm-Types. # Das ergibt 9373910789125 mögliche Trigramme, von denen
6.10341844370598e-06% # im Dokument vorkommen. # 82.12% aller Trigramm-Types kommt nur einmal im Korpus vor! # Es gibt 995092 Tetragramm-Token und 826581 Tetragramm-Types. # Das ergibt 1.97648908988701e+17 mögliche Tetragramme, von denen # 4.18206710185916e-10% im Dokument vorkommen. # 92.05% aller Tetragramm-Types kommt nur einmal im Korpus vor! # Die zehn häufigsten Unigramme sind: # ,: 66476 Das sind 6.68037% aller Token. # .: 39722 Das sind 3.99178% aller Token. # the: 30937 Das sind 3.10895% aller Token. # ": 30012 Das sind 3.01599% aller Token. # and: 23277 Das sind 2.33917% aller Token. # to: 21011 Das sind 2.11146% aller Token. # I: 17985 Das sind 1.80737% aller Token. # of: 16409 Das sind 1.64899% aller Token. # : 16151 Das sind 1.62306% aller Token. # a: 15821 Das sind 1.58990% aller Token. # he: 13039 Das sind 1.31033% aller Token. # Die zehn häufigsten Bigramme sind: # . ": 11202 Das sind 1.12572% aller Bigramm-Token. # , and: 8864 Das sind 0.89077% aller Bigramm-Token. # !: 7729 Das sind 0.77671% aller Bigramm-Token. # ?: 7673 Das sind 0.77108% aller Bigramm-Token. # " ": 5222 Das sind 0.52477% aller Bigramm-Token. # , ": 4880 Das sind 0.49041% aller Bigramm-Token. # ? ": 3446 Das sind 0.34630% aller Bigramm-Token. # , but: 3125 Das sind 0.31404% aller Bigramm-Token. # . He: 3048 Das sind 0.30630% aller Bigramm-Token. # . I: 2938 Das sind 0.29525% aller Bigramm-Token. # of the: 2917 Das sind 0.29314% aller Bigramm-Token.
72
# Die zehn häufigsten Trigramme sind: # ? ": 3446 Das sind 0.34630% aller Trigramm-Token. # ! ": 2677 Das sind 0.26902% aller Trigramm-Token. # . " ": 2465 Das sind 0.24772% aller Trigramm-Token. # ? " ": 1638 Das sind 0.16461% aller Trigramm-Token. # . " I: 992 Das sind 0.09969% aller Trigramm-Token. # , " he: 828 Das sind 0.08321% aller Trigramm-Token. # , " said: 807 Das sind 0.08110% aller Trigramm-Token. # ! " ": 694 Das sind 0.06974% aller Trigramm-Token. # I don 't: 681 Das sind 0.06844% aller Trigramm-Token. # ! I: 654 Das sind 0.06572% aller Trigramm-Token. # ... .: 629 Das sind 0.06321% aller Trigramm-Token. # Die zehn häufigsten Tetragramme sind: # ? " ": 1638 Das sind 0.16461% aller Tetragramm-Token. # ! " ": 694 Das sind 0.06974% aller Tetragramm-Token. # ! " cried: 303 Das sind 0.03045% aller Tetragramm-Token. # , of course ,: 289 Das sind 0.02904% aller Tetragramm-Token. # ? " he: 269 Das sind 0.02703% aller Tetragramm-Token. # . " " I: 254 Das sind 0.02553% aller Tetragramm-Token. # ! " he: 249 Das sind 0.02502% aller Tetragramm-Token. # ? " " I: 242 Das sind 0.02432% aller Tetragramm-Token. # , " he said: 228 Das sind 0.02291% aller Tetragramm-Token. # , " said the: 224 Das sind 0.02251% aller Tetragramm-Token. # I don 't know: 222 Das sind 0.02231% aller Tetragramm-Token.
Die Beobachtungen, die man anhand dieser Ausgabe machen kann, gliedern sich in linguistische
und programmiertechnische Erkenntnisse. Als sprachwissenschaftliches Ergebnis lässt sich festhalten,
dass der Anteil der tatsächlich im Korpus vorhandenen Bi-, Tri- und Tetragramme im Vergleich zu den
jeweils möglichen n-Grammen extrem gering ist, während der Anteil der seltenen sprachlichen
Einheiten mit zunehmendem n der n-Gramme ansteigt. Wir haben also ein Problem der knappen Daten
(engl. sparse data problem). Diese Eigenschaft natürlicher Sprache ist auch als Zipf'sches Gesetz
bekannt: Es gibt sehr wenige häufig auftretende Worttypen, während es eine große Menge an seltenen
Worttypen gibt.
Sprachwissenschaftliches Institut
73
Dieser Zustand ändert sich auch nicht, wenn man die Größe des Korpus erhöht. Es muss also eine
Möglichkeit geben, seltene oder unbekannte sprachliche Einheiten trotzdem mit den Mitteln der
Statistik erfassen zu können, um Voraussagen verschiedenster Art treffen zu können. Für das
Verständnis dieser Methoden sind tiefergehende Kenntnisse der Statistik vonnöten, die den Umfang
dieser Diskussion sprengen würden.
Darüber hinaus sehen wir, dass in den Trefferlisten der n-Gramme Interpunktionszeichen sehr
prominent vertreten sind. Dies rührt daher, dass durch die Tokenisierung die Interpunktion isoliert
wurde, wenn sie als Satzzeichen identifiziert werden konnte, sie aber nicht aus dem Korpus entfernt
wurde oder durch uns beim Einlesen ignoriert wurde. Zusätzlich finden sich in den Listen der
häufigsten n-Gramme viele Wörter geschlossener Wortklassen, wie z.B. Artikel, Konjunktionen,
Präpositionen etc. Je nach Anwendung können auch diese für die computerlinguistische Analyse
interessant sein, für gewöhnlich wird man sie aber herausfiltern, um Wörter mit höherem
Informationsgehalt zu fokussieren. Diesen Problemen werden wir uns im nächsten Kapitel widmen, in
dem wir mit regulären Ausdrücken das wohl mächtigste Werkzeug, das Perl zu bieten hat, kennen
lernen.
Auf der programmiertechnischen Seite ist festzuhalten, dass wir wiederum eine Menge redundanten
Code produziert haben, um die verschiedenen n-Gramme behandeln zu können. Dies wird im
übernächsten Kapitel adressiert, wenn wir über Subroutinen reden.
74
7 Reguläre Ausdrücke
Homo sapiens are about pattern recognition [...].
Both a gift and a trap.
WILLIAM GIBSON
Bis jetzt beschränken sich die Operationen, die wir zum Vergleich von Zeichenketten oder Zahlen
kennen, auf die vollständige Äquivalenz oder auf einen größer- oder kleiner-als-Vergleich. Dazu
benötigten wir immer zwei konkrete Instanzen für den Vergleich, seien dies Literale oder Variablen.
Anhand regulärer Ausdrücke können wir nun einerseits über konkrete zu vergleichende Werte
abstrahieren, andererseits können wir Bestandteile eines Werts auf Äquivalenz untersuchen.
Reguläre Ausdrücke sind eine Weiterentwicklung der in den 1950er Jahren vom Mathematiker
Stephen Kleene erdachten Notation zur Manipulation regulärer Mengen. Als solche stellen reguläre
Ausdrücke Muster dar, die man wie eine Schablone auf einen Wert legt, um dann zu entscheiden, ob
das Muster darauf passt, weshalb man diese Operation auch als Mustervergleich (engl. pattern
matching) bezeichnet:
Die bisher besprochenen Perl-Konstrukte finden nur die drei Vorkommen von "ein":
my @woerter; my $suchwort = "ein"; while(<DATA>){ # Daten s.o.! push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Suchwort gefunden: $wort\n" if($wort eq $suchwort); } # Ausgabe: # Suchwort gefunden: ein # Suchwort gefunden: ein # Suchwort gefunden: ein
Sprachwissenschaftliches Institut
75
Reguläre Ausdrücke finden die Zeichenkette "ein" auch, wenn sie Teil eines Worts ist oder groß
geschrieben wurde:
my @woerter; my $muster = "ein"; while(<DATA>){ # Daten s.o.! push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ m/$muster/ig); } # Ausgabe: # Muster gefunden: ein # Muster gefunden: ein # Muster gefunden: Vereinigten # Muster gefunden: Eindeutig # Muster gefunden: ein # Muster gefunden: einzuschalten
Der einzige Unterschied zwischen diesen beiden Programmen besteht in der if()-Bedingung, in
der wir im zweiten Fall drei neue Sprachkonstrukte finden:
• den Musterbindungsoperator =~,
• den Vergleichsoperator m// und
• die Modifikatoren i und g.
7.1 Muster Der Musterbindungsoperator bindet einen skalaren Ausdruck an ein Muster, d.h. das linke
Argument dieses Operators ist die zu durchsuchende Zeichenkette und das rechte Argument ein Literal
oder eine Skalarvariable, nach der gesucht werden soll. Den Vergleichsoperator kennen wir bereits in
seiner Kurzschreibweise: Wenn wir im letzten Kapitel die split()-Funktion verwendet haben, war ihr
erstes Argument ein regulärer Ausdruck, mit dem wir die eingelesene Zeile auf Leerzeichen durchsucht
haben, an denen wir die Zeichenkette nach Wörtern aufgetrennt haben. Man kann also den
Vergleichsoperator m// auch einfach als // schreiben; im Laufe dieses Kapitels werden wir mit s///
und tr/// zwei weitere Operatoren kennen lernen, die ebenfalls mit regulären Ausdrücken operieren,
die allerdings keine Kurzschreibweisen zulassen. Wie bereits bei den alternativen Anführungszeichen
gesehen, dürfen die Begrenzungszeichen // auch hier durch andere paarige Begrenzungszeichen ersetzt
werden.43 Dies ist insbesondere dann sinnvoll, wenn das Muster selbst Schrägstriche, wie z.B. in
Verzeichnispfaden enthält, da man diese dann – wie gewohnt – durch Backslashes maskieren müsste,
was sehr unübersichtlich werden kann – man spricht deshalb auch vom leaning toothpick syndrome.
Will man also eine komplette Zeichenkette oder einen Teil einer Zeichenkette finden, schreibt man
als erstes Argument einer if()-Bedingung eine gegebene Zeichenkette, d.h. die Quelle, den
Musterbindungsoperator und dann das Muster als Literal oder Skalar zwischen den Vergleichsoperator:
43 Dies bedeutet allerdings im Umkehrschluss nicht, dass die Trennzeichen bei den alternativen Anführungszeichen q//, qq//
und qw// ebenfalls Begrenzungszeichen für reguläre Ausdrücke sind!
76
my $zeichenkette = "Ein einfacher Satz zur Illustration ."; print "Ist enthalten!\n" if($zeichenkette =~ /einfacher Satz/); my $muster = "einfacher Satz"; print "Ist enthalten!\n" if($zeichenkette =~ /$muster/); # Ausgabe: # Ist enthalten! # Ist enthalten!
Eine Ausnahme bildet die Verwendung von $_ als Quelle: In diesem Fall kann sowohl auf die
Angabe des Bezeichners der Quelle, also $_, wie auf den Musterbindungsoperator verzichtet werden:
my $muster = "einfacher Satz"; while(<DATA>){ print "Ist enthalten!\n" if(/$muster/); # Ausgabe: Ist
enthalten! } __END__ Ein einfacher Satz zur Illustration .
In der symbolischen Computerlinguistik bearbeitet man allerdings häufig Aufgabenstellungen, in
denen man davon ausgehen muss, nur Teile des Musters angeben zu können, während andere Teile
veränderlich sind. Ein Beispiel hierfür wäre z.B. die Erfassung von Flexionsendungen deutscher
Verben. Gegeben eine Liste von Wortstämmen, ließe sich zwar entscheiden, ob eine gegebene
Zeichenkette das jeweilige Verb enthält, wir hätten allerdings noch keine Möglichkeit, möglichst
abstrakte Kriterien anzugeben, die es uns erlauben, die Endungen aus der Zeichenkette zu isolieren:
my @woerter; my $muster = "spiel"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . # Ausgabe: # Muster gefunden: spielen # Muster gefunden: spielt
Zwar werden die einschlägigen Wörter richtig herausgefiltert, doch können wir programmatisch
noch nicht festhalten, dass "en" und "t" Verbendungen sind. Um sie erfassen zu können, benötigt man
Platzhalter, welche die Merkmale dieser Zeichenketten auf einer abstrakten Ebene repräsentieren.
Sprachwissenschaftliches Institut
77
7.2 Platzhalter Platzhalter (engl. wildcards) repräsentieren Zeichenketten, die man nicht explizit angeben will oder
kann. Um z.B. die gefundenen Verbendungen für den Stamm "spielen" zu klassifizieren, könnte man
einen Platzhalter dergestalt formulieren, dass dem Verbstamm mindestens ein Buchstabe folgen muss,
aber auch mehrere Buchstaben folgen können. Wir machen also sowohl Angaben über das Paradigma
möglicher Zeichen als auch über das Syntagma, nämlich dass die Endung dem Stamm folgt, und dass
die zu verwendenden Zeichen eine bestimmte Quantität besitzen. In Bezug auf das Paradigma spricht
man von Zeichenklassen, die eine Abstraktion über Zeichen darstellen, während das einschlägige
Sprachmittel für das Syntagma Quantoren sind.
Zeichenklassen
Wie bereits gesehen, lassen sich in regulären Ausdrücken Zeichenketten angeben, die als
Suchmuster innerhalb gegebener Zeichenketten dienen. Will man über die Suchmuster abstrahieren,
verwendet man Zeichenklassen. Von diesen gibt es in Perl einige vordefinierte, man kann aber auch
eigene Klassen angeben.
Eine Zeichenklasse besteht aus einer Menge von Zeichen, die immer nur disjunktiv voneinander
ausgewählt werden können, d.h. aus der Menge {a, b, c, d} kann nur a oder b oder c oder d ausgewählt
werden. Im Umkehrschluss bedeutet dies aber auch, dass eine komplette Zeichenklasse genau ein
Zeichen repräsentiert!
In Perl wird eine solche Menge nicht wie aus der Mathematik gewohnt durch geschweifte
Klammern, sondern durch eckige Klammern [ ] angezeigt. Will man also beispielsweise aus
folgenden Sätzen nur die Instanzen der zweiten und dritten Person Singular des Verbs "spielen", nicht
aber die der ersten Person Singular oder Plural ermitteln, könnte man das Literal spiel durch die Zei-
chenklasse [st] erweitern:
my @woerter; my $muster="spiel[st]"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Muster gefunden: spielt # Muster gefunden: spielst
78
Beim Durchlaufen des Wörter-Arrays findet der Perl-Interpreter zunächst "spielt", da das
angegebene Muster "spiel+t" enthält, während "spielst" gefunden wird, weil das Muster "spiel+s"
enthält. Das "t" in der Zeichenklasse ist für diesen letzten Mustervergleich irrelevant, da nur eine der in
der Zeichenklasse enthaltenen Zeichen auch in der gegebenen Zeichenkette vorhanden sein muss. Die
genaue Arbeitsweise der in Perl für reguläre Ausdrücke zuständigen Instanz soll im Laufe des Kapitels
noch besprochen werden.
Alternativen in regulären Ausdrücken lassen sich nicht nur durch Zeichenklassen repräsentieren.
Das Pipe-Symbol | trennt ebenfalls Zeichen disjunktiv voneinander:
my @woerter; my $muster="spiels|t"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Muster gefunden: spielt # Muster gefunden: spielst
Neben einzelnen Zeichen lassen sich in Zeichenklassen auch Bereiche repräsentieren. So werden
z.B. alle Kleinbuchstaben mit Ausnahme der Umlaute und der sz-Ligatur als [a-z] darstellen,
Großbuchstaben durch [A-Z]. Analog dazu werden Ziffern durch die Zeichenklasse [0-9] abgebildet.
Um beispielsweise einen (naiven) Parser für l33t sp34k – einer bei jungen Hackern beliebten
Schreibvariante des Englischen, in der einige alphabetische Zeichen durch Ziffern ersetzt werden, die
ähnlich aussehen – zu bauen, könnten wir die Zeichenklasse [a-z][0-9][0-9][a-z] angeben:
my @woerter; my $muster="[a-z][0-9][0-9][a-z]"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ C4n u r34d my l33t sp34k , d00d ?
Sprachwissenschaftliches Institut
79
# Ausgabe: # Muster gefunden: r34d # Muster gefunden: l33t # Muster gefunden: sp34k # Muster gefunden: d00d
Zeichenklassen lassen sich auch negieren, doch im Gegensatz zu anderen Ausdrücken verwendet
man nicht das Ausrufezeichen !, sondern das Caret ^, das am Anfang der Zeichenklasse stehen muss.
Suchen wir also beispielsweise alle Instanzen von "spielen", die nicht die Morphologie der zweiten
oder dritten Person Singular beinhalten, können wir /spiel[^st]/ als regulären Ausdruck verwenden:
my @woerter; my $muster = "spiel[^st]"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Muster gefunden: spielen # Muster gefunden: mitspiele
Vordefinierte Zeichenklassen
Da Buchstaben und Ziffern einen Großteil der von uns verwendeten Zeichen ausmachen, gibt es in
Perl dafür vordefinierte Zeichenklassen. So entspricht die Zeichenklasse [A-Za-z0-9_] den
sogenannten Wortzeichen, weshalb sie mit \w abgekürzt wird. Ziffern sind in der abgekürzten
Zeichenklasse \d und Leerraumzeichen, also das Leerzeichen, der Tabulatorvorschub und der
Zeilenumbruch, werden durch \s abgekürzt. Der reguläre Ausdruck für l33t sp34k lässt sich
dementsprechend zu \w\d\d\w verkürzen:44
my @woerter; my $muster = "\\w\\d\\d\\w"; while(<DATA>){ push(@woerter, split(/ /)); }
44 Aufgrund der Interpolation der vordefinierten Zeichenklassen aus einer Skalarvariablen muss der Backslash aus den
abgekürzten Zeichenklassen durch einen weiteren Backslash maskiert werden!
80
while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ C4n u r34d my l33t sp34k , d00d ? # Ausgabe: # Muster gefunden: r34d # Muster gefunden: l33t # Muster gefunden: sp34k # Muster gefunden: d00d
Allerdings ist zu beachten, dass dieser reguläre Ausdruck übergeneriert, d.h. er würde auch auf
andere Zeichenketten passen, die nicht l33t sp34k sind, wie z.B. Jahreszahlen, denn durch die
Inklusion der Ziffern in der Klasse der Wortzeichen passt \w\d\d\w auch auf vierstellige Zahlen!
Zu allen genannten vordefinierten Zeichenklassen existieren analog zur Negation in eigenen
Zeichenklassen auch Komplemente. Diese werden durch einen Großbuchstaben angezeigt, sodass \W
alle Zeichen außer Wortzeichen erfasst, \D für alle Zeichen außer Ziffern und \S für alle Zeichen außer
Leerraumzeichen steht.
Darüber hinaus darf man nicht vergessen, dass \w die deutschen Umlaute und die sz-Ligatur nicht
erfasst! Will man einen deutschsprachigen Text bearbeiten, muss man diese Zeichen entweder durch
eine Erweiterung der Zeichenklasse explizit mit angeben [\wÄÖÜäöüß] oder man verwendet das Pragma
locale. Dieses liest vom Betriebssystem vorgehaltene Informationen über die zu verwendenden
Ländereinstellungen aus und erweitert \w automatisch um die oben genannten Zeichen. Man muss sich
allerdings auch darüber im Klaren sein, dass die Portierbarkeit bzw. Wiederverwendbarkeit des
jeweiligen Perl-Skripts dadurch eingeschränkt wird, da man nicht notwendigerweise davon ausgehen
kann, dass jeder Rechner, auf dem das Skript laufen soll, diese Voreinstellungen besitzt!
Universaler Mustervergleich
Einen Sonderfall im Zeichenparadigma stellt der Punkt . dar. Er steht in einem regulären Ausdruck
für jedes beliebige Zeichen inklusive Leerraumzeichen außer einem Zeilenendezeichen. Setzt man ihn
im Beispiel des Verbs "spielen" für ein beliebiges Zeichen nach dem Wortstamm ein, sorgt er dafür,
dass nur Instanzen mit einer echten Verbendung erfasst werden, der Imperativ aber nicht:
my @woerter; my $muster = "spiel."; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); }
Sprachwissenschaftliches Institut
81
__END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Muster gefunden: spielen # Muster gefunden: spielt # Muster gefunden: mitspiele # Muster gefunden: spielst
Dementsprechend muss ein literaler Punkt in einem regulären Ausdruck durch einen Backslash
maskiert werden:
my @woerter; my $muster = "\\."; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Muster gefunden: . # Muster gefunden: . # Muster gefunden: . # Muster gefunden: . # Muster gefunden: .
Quantoren
Im letzten l33t sp34k-Beispiel ist die doppelte Angabe der Zeichenklasse für Ziffern eher
suboptimal. Perl erlaubt es an dieser Stelle, vermittels Quantoren anzugeben, wie oft ein Zeichen oder
eine Zeichenklasse in einer gegebenen Zeichenkette enthalten sein muss, damit das Muster darauf
passt.
Anhand von Quantoren lassen sich sowohl konkrete Zahlen für die Häufigkeit des Vorkommens
angeben als auch über Häufigkeiten abstrahieren. Zu letzterer Kategorie von Quantoren zählen die nach
Kleene benannten Stern * und Plus + sowie das Fragezeichen ?.
Der Stern * besagt, dass von dem zu quantifizierenden Ausdruck null oder mehrere Instanzen in
einer gegebenen Zeichenkette vorhanden sein müssen. Das Plus-Zeichen + steht für mindestens eine
82
oder mehrere Instanzen des Ausdrucks, während das Fragezeichen ? Optionalität anzeigt, also keine
oder genau eine Instanz zu finden sein muss. Wenden wir dies auf unser Problem der Verbmorphologie
an, können wir die Flexionsendung als \w+ zu mindestens einem oder mehreren Wortzeichen
abstrahieren. Zwar übergeneriert auch dieser Ausdruck – aber nur, wenn man wie bei l33t sp34ak
damit rechnen müsste, Ziffern oder Unterstriche in einem Wort vorzufinden:
my @woerter; my $muster = "spiel\\w+"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Muster gefunden: spielen # Muster gefunden: spielt # Muster gefunden: mitspiele # Muster gefunden: spielst
Um auch den Imperativ erfassen zu können, muss man die Verbendung durch einen Stern oder das
Fragezeichen optional machen:
my @woerter; my $muster = "spiel\\w*"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst .
Sprachwissenschaftliches Institut
83
# Ausgabe: # Muster gefunden: spielen # Muster gefunden: spielt # Muster gefunden: spiel # Muster gefunden: mitspiele # Muster gefunden: spielst
Die Häufigkeit, eines Zeichens in einer gegebenen Zeichenkette, die durch das Muster beschrieben
werden soll, lässt sich auch konkret angeben. Dazu verwendet man die geschweiften Klammern {}
nach dem entsprechenden Zeichen im Muster. In die geschweiften Klammern kann man nun sowohl
eine Zahl schreiben, die die Häufigkeit repräsentiert, als auch einen Bereich, mit dem sich Minimal-
und/oder Maximal-Häufigkeiten abbilden lassen. Anders als bei Listen werden Bereiche hier allerdings
nicht durch zwei Punkte angezeigt, sondern durch ein Komma, das den Minimalwert vom
Maximalwert trennt. Will man also alle l33t sp34k-Wörter erfassen, die aus einem Wortzeichen, zwei
Ziffern und wiederum einem Wortzeichen bestehen, kann man beispielsweise den regulären Ausdruck
/\w\d{2}\w/ verwenden:
my @woerter; my $muster = "\\w\\d{2}\\w"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ C4n u r34d my l33t sp34k , d00d ? # Ausgabe: # Muster gefunden: r34d # Muster gefunden: l33t # Muster gefunden: sp34k # Muster gefunden: d00d
Alle Wörter, die mindestens aus drei Zeichen bestehen, bildet das Muster /\w{3,}/ ab:
my @woerter; my $muster = "\\w{3,}"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ C4n u r34d my l33t sp34k , d00d ?
84
# Ausgabe: # Muster gefunden: C4n # Muster gefunden: r34d # Muster gefunden: l33t # Muster gefunden: sp34k # Muster gefunden: d00d
Alle Wörter, die maximal aus zwei Zeichen bestehen, werden durch das Muster /\w{0,2}/ erfasst.
Man beachte, dass anders als bei der Angabe des Minimums beide Werte explizit angegeben werden
müssen, auch wenn man ein Minimum von 0 ansetzt; es reicht nicht, /\w{,2}/ zu notieren, auch wenn
dies mehr nach Perl aussieht:
my $muster = "\\w{0,2}"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Muster gefunden: $wort\n" if($wort =~ /$muster/); } __END__ C4n u r34d my l33t sp34k , d00d ? # Ausgabe: # Muster gefunden: C4n # Muster gefunden: u # Muster gefunden: r34d # Muster gefunden: my # Muster gefunden: l33t # Muster gefunden: sp34k # Muster gefunden: , # Muster gefunden: d00d # Muster gefunden: ?
Das Komma und das Fragezeichen werden gefunden, weil wir die Klasse der Wortzeichen durch
die 0 optional gemacht haben. Ansonsten entsprechen die Ergebnisse unseren Erwartungen.
Variableninterpolation
Bis jetzt sind wir relativ sorglos mit der Frage umgegangen, wie man skalare Variablen in regulären
Ausdrücken verwendet: Wie in doppelten Anführungszeichen werden sie interpoliert und ihre Werte
als Muster in den jeweiligen regulären Ausdruck übernommen. Was geschieht aber, wenn die soeben
diskutierten Quantoren oder der Punkt durch Interpolation Bestandteil des Musters werden? Betrachten
wir dazu folgendes Beispiel, in dem wir als gegebene Zeichenkette etwas wie einen regulären
Ausdruck "(.+)" (z.B. aus einem Einführungsbuch zu Perl) und als Muster den regulären Ausdruck
/.+)/ haben:
Sprachwissenschaftliches Institut
85
my $muster = ".+)"; my $zeichenkette = "(.+)"; print "$zeichenkette" if($zeichenkette =~ /$muster/); # Ausgabe: # Uncaught exception from user code: Unmatched ) in regex; marked by <-- HERE in m/.+) <-- HERE / at
programm.pl line 4.
Durch die Variableninterpolation interpretiert Perl den Punkt als Abstraktion über alle Zeichen und
das Plus als Quantor. Dann allerdings stößt es auf eine schließende runde Klammer: Aus der
Fehlermeldung lässt sich schließen, dass der Interpreter ein öffnendes Pendant dazu erwartet. Runde
Klammern besitzen also eine eigene Funktionalität innerhalb regulärer Ausdrücke, wie wir gleich sehen
werden. Dennoch sollte es doch möglich sein, die literalen Zeichen zu finden! Dazu gibt es in Perl den
sogenannten Quotemeta-Operator \Q, der innerhalb regulärer Ausdrücke die Zeichen . * ? + [ ] ( ) { }
^ $ | und \ maskiert, bis er auf ein \E oder das Ende des regulären Ausdrucks trifft:
my $muster = ".+)"; my $zeichenkette = "(.+)"; print "$zeichenkette" if($zeichenkette =~ /\Q$muster\E/); # Ausgabe:
(.+)
7.3 Gruppierung und Speicherung Bis jetzt konnten wir innerhalb der Suchmuster mit einem Quantor immer nur ein Zeichen erfassen.
Analog zur Gruppierung von Termen bei arithmetischen Operationen lassen sich auch in regulären
Ausdrücken Zeichen durch runde Klammern zu einer Einheit zusammenfassen. So passt z.B. der
reguläre Ausdruck /(tam){2}/ auf die Zeichenkette "tamtam":
my $zeichenkette = "tamtam"; my $muster = "(tam){2}"; print $zeichenkette if($zeichenkette =~/$muster/); # Ausgabe: tamtam
Doch die runden Klammern besitzen nicht nur die Funktionalität der Gruppierung von Zeichen,
sondern speichern auch die Fundstelle ab, sodass diese durch Indizes sowohl von außerhalb des
regulären Ausdrucks als auch innerhalb zugreifbar werden. Die Nummer des Index entspricht dabei der
n-ten runden Klammer im regulären Ausdruck. Dazu verwendet Perl eine Indexzählung, die anders als
bei Arrayindizes nicht bei 0 anfängt, sondern mit eins. Um auf eine Fundstelle außerhalb des regulären
Ausdrucks zuzugreifen, verwendet man den Index wie eine Skalarvariable, d.h. man schreibt ein
Dollarzeichen und die Nummer des Index:
my $zeichenkette = "tamtam"; my $muster = "(tam){2}"; print $1 if($zeichenkette =~ /$muster/); # Ausgabe: tam
86
Auf diese Weise lassen sich jetzt beispielsweise auch die Verbendungen aus den Beispielsätzen
auflisten:45
my (@woerter); my $muster = "spiel(\\w*)"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Endung gefunden: $1\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Endung gefunden: en # Endung gefunden: t # Endung gefunden: # Endung gefunden: e # Endung gefunden: st
Ebenso kann man bereits im Muster gespeicherte Zeichen innerhalb des regulären Ausdrucks
wieder aufnehmen. Dazu schreibt man einen Backslash vor den jeweiligen Index:
my $zeichenkette = "tamtam"; my $muster = "(tam)\\1"; print $1 if($zeichenkette =~ /$muster/); # Ausgabe: tam
Benötigt man nur die Funktionalität der Gruppierung, schreibt man ?: an den Anfang der
Klammern; die Verwendung einer Indexvariablen führt dementsprechend zu einer Fehlermeldung:
my $zeichenkette = "tamtam"; my $muster = "(?:tam){2}"; print $1 if($zeichenkette =~ /$muster/); # Ausgabe: Uncaught exception from user code: Reference to nonexistent group in regex; marked by <-- HERE in
m/(?:tam){2} <-- HERE / at programm.pl line 4.
Weitere durch das Fragezeichen eingeleitete Konstrukte werden uns weiter unten noch begegnen.
45 Man beachte, dass der Imperativ richtigerweise als mit dem Wortstamm identische Form ausgegeben wird.
Sprachwissenschaftliches Institut
87
7.4 Anker Eine weitere Möglichkeit, die syntagmatischen Verhältnisse innerhalb eines Musters zu
beeinflussen, liegt in der Verwendung sogenannter Anker. Diese legen bestimmte Positionen fest, ab
denen oder bis zu denen ein Muster auf die gegebene Zeichenkette, d.h. die Zeile, passen soll. Will man
sicherstellen, dass das Muster am Anfang einer Zeile gefunden wird, schreibt man ein Caret ^ nach das
erste Trennzeichen des regulären Ausdrucks. Soll das Muster umgekehrt auf das Ende einer Zeile
passen, verwendet man das Dollarzeichen $ vor dem zweiten Trennzeichen des regulären Ausdrucks.
Will man beispielsweise alle Zeilen finden, die mit einem "P" anfangen, verwendet man den regulären
Ausdruck /^P/:
while(<DATA>){ print if(/^P/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " " Schade , dass Paul keine Zeit hat ! " # Ausgabe: # Paul und Maria spielen Schach . # Peter spielt Ball . # Peter sagt , " Dann spiel halt mit . "
Umgekehrt passt /"$/ auf alle Zeilen, die mit einem doppelten Anführungszeichen enden:
while(<DATA>){ print if(/"$/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " " Schade , dass Paul keine Zeit hat ! " # Ausgabe: # Peter sagt , " Dann spiel halt mit . " # " Schade , dass Paul keine Zeit hat ! "
Einen besonderen Anker stellt \b (für eng. boundary) dar: Er markiert die Position zwischen einem
Wortzeichen \w und einem Nicht-Wortzeichen \W. Durch seine Verwendung lässt sich sicherstellen,
dass die durch das Muster gesuchte Zeichenkette nicht in einer anderen Zeichenkette enthalten ist:
88
So können wir beispielsweise durch den regulären Ausdruck /\bspiel(\w*)/ sicherstellen, dass
Endungen nur zusammen mit dem Wortstamm "spiel" gefunden werden und nicht solche, die in
Komposita, wie "mitspielen" enthalten sind:
my (@woerter); my $muster = "\\bspiel(\\w*)"; while(<DATA>){ push(@woerter, split(/ /)); } while(@woerter){ my $wort = shift(@woerter); print "Endung gefunden: $1 in $wort\n" if($wort =~ /$muster/); } __END__ Paul und Maria spielen Schach . Peter spielt Ball . Peter sagt , " Dann spiel halt mit . " Ihn freut , dass ich mitspiele . Mutter freut , dass du auch spielst . # Ausgabe: # Endung gefunden: en in spielen # Endung gefunden: t in spielt # Endung gefunden: in spiel # Endung gefunden: st in spielst
7.5 Modifikatoren Um das Verhalten regulärer Ausdrücke an unterschiedliche Gegebenheiten anzupassen, verwendet
man sogenannte Modifikatoren. Diese schreibt man hinter das zweite Trennzeichen des regulären
Ausdrucks:
• Der Modifikator i ermöglicht es, die Unterscheidung zwischen Groß- und Kleinschreibung
beim Mustervergleich auszuschalten.
• Mithilfe des Modifikators g kann man global innerhalb einer Zeichenkette nach mehreren
Instanzen eines Musters zu suchen. Sollen alle Fundstellen verarbeitet werden, verwendet man
statt der if()-Bedingung eine while()-Schleife, hier am Ausgangsbeispiel dieses Kapitels
illustriert:
my $muster = "\\b(ein)\\b"; while(<DATA>){ print "$1\n" while(/$muster/g); } __END__ Ob Frau oder Mann - Parlamentarier wie Öffentlichkeit sollten sich vor der Wahl ein
Sprachwissenschaftliches Institut
89
verlässliches Bild, insbesondere ein klares Bild von Verlässlichkeit, Denkweise und Handlungsabsichten der neuen Bundesrätin oder des neuen Bundesrats machen können. Zuverlässige und präzise Recherchen der Medien oder gegebenenfalls auch Anhörung der Kandidaten durch die Fraktionen anderer Parteien dienen solcher Klärung vor der Wahlausmarchung in der Vereinigten Bundesversammlung. Auch heikle Fragen sind dabei nicht auszusparen. Eindeutig unlauter und in jeder Hinsicht fragwürdig sind die Mittel und Methoden, mit denen sich dieser Tage ein anonymes Komitee in den Prozess der Kandidatenauslese einzuschalten versucht hat. # Ausgabe: # ein # ein # ein
• Anders als die Bezeichnungen für die Modifikatoren s (single-line) und m (multi-line) vermuten
lassen, dass man Zeichenketten als eine bzw. mehrere Zeilen verarbeiten kann, beeinflussen sie
lediglich das Verhalten der Anker ^ und $ sowie des Punkts .. Zeilen, die beispielsweise durch
das Einlesen einer Datei in einen Skalar auch Zeilenendezeichen enthalten, können immer,
unabhängig von der Verwendung dieser beiden Modifikatoren, verarbeitet werden. Verwendet
man das s, passt der Punkt auch auf ein Zeilenendezeichen. Im multi-line Modus passt das
Caret sowohl auf den Anfang der Zeichenkette als auch nach jedem Zeilenendezeichen,
während das Dollar vor jedem Zeilenendezeichen und am Ende der Zeichenkette passt; deshalb
spricht man in diesem Zusammenhang auch von logischen Zeilen. Das Verhalten des Punkts
wird durch den Modifikator m nicht beeinflusst. Kombiniert man allerdings die Funktionalität
von s und m, erhält man einen regulären Ausdruck, dessen Anker mehrere durch Zeilenende
getrennte logische Zeilen verarbeiten können, wobei der Punkt auch auf ein solches Zeilenende
passt. Da diese Funktionalität eher selten gebraucht wird, verzichten wir an dieser Stelle auf ein
Beispiel und belassen es bei der Klarstellung der Terminologie.
• Der Modifikator o macht die Verarbeitung regulärer Ausrücke effizienter, ist aber mit Bedacht
einzusetzen: Sind im Muster keine Variablen zu interpolieren, speichert Perl standardmäßig bei
der ersten Verwendung des regulären Ausdrucks eine interne Repräsentation davon ab, da sich
diese nicht verändern kann, wenn dieser Ausdruck erneut im Programm verwendet wird. Gibt
es nun andererseits eine zu interpolierende Variable, wird der reguläre Ausdruck jeweils neu in
eine interne Repräsentation übersetzt. Dies ist aber nicht immer notwendig, z.B. dann, wenn
der Wert der Variablen während des Mustervergleichs konstant bleibt und sich lediglich die zu
vergleichenden Zeichenketten ändern. In diesem Fall sorgt o dafür, dass der reguläre Ausdruck
nur einmal übersetzt wird. Dies kann aber problematisch sein, wenn man dies auch bei
regulären Ausdrücken anwendet, deren Werte sich ändern: Trotzdem der Wert in der zu
interpolierenden Variablen ein neuer ist, wird Perl den einmal übersetzten Wert für alle
weiteren Mustervergleiche verwenden und den neuen Wert komplett ignorieren!
• Durch den Modifikator x lassen sich Kommentare und Zeilenumbrüche in das Muster des
regulären Ausdrucks einfügen, um komplexe Ausdrücke besser strukturieren und
zusammengehörige Komponenten als solche kennzeichnen zu können.
90
7.6 Wie funktionieren reguläre Ausdrücke? Im Gegensatz zu den Wörtern und Sätzen der natürlichen Sprache gehören reguläre Ausdrücke zu
den formalen Sprachen. Der grundlegende Unterschied zwischen natürlichen und formalen Sprachen
liegt darin, dass man anhand eines Algorithmus nicht entscheiden kann, ob ein gegebener Satz einer
natürlichen Sprache grammatisch ist oder nicht, da eine Grammatik, die versuchen würde, alle
möglichen Sätze einer natürlichen Sprache zu erfassen, zu komplex wäre. Umgekehrt ist es – wie wir
gesehen haben – sehr wohl möglich, zu entscheiden, ob eine gegebene Zeichenkette der durch ein
Muster vorgegebenen "Grammatik" entspricht.
Damit Perl entscheiden kann, ob ein Muster auf eine Zeichenkette passt, benötigt es einen
mathematischen Formalismus, der entscheidet, ob ein Zeichen aus dem Muster mit einem Zeichen der
gegebenen Zeichenkette übereinstimmt, und ob darüber hinaus die gegebene Sequenz durch das Muster
abgedeckt wird. Dazu bedient sich Perl eines sogenannten endlichen Automaten.
Ein endlicher Automat ist ein Algorithmus, der in endlichen vielen Schritten (Zustände genannt)
entscheidet, ob eine gegebene Zeichenkette durch das jeweilige Muster verarbeitet werden kann. Dazu
befindet sich der Automat zunächst in einem Startzustand. Aus diesem liest er zeichenweise die
gegebene Zeichenkette: Jeder Zustand akzeptiert entweder das gelesene Zeichen oder verwirft es; eine
Übergangsfunktion zwischen den einzelnen Zuständen sorgt dafür, dass bei der Konsumption der
gegebenen Symbole ein Folgezustand erreicht wird. Letztendlich erreicht der Automat einen
Endzustand, an dem entschieden ist, ob die gegebene Zeichenkette auf das Muster passt oder nicht.
Betrachten wir zur Illustration einen endlichen Automaten, der die Symbole {h,a,!} kennt und
dessen Grammatik mindestens eine, aber beliebig viele Abfolgen der Zeichenkette "ha" erkennt, die
von einem Ausrufezeichen abgeschlossen wird; der reguläre Ausdruck dazu lautet /(ha)+!/:
Aus dem Startzustand gelangt der Automat durch Konsumption des Symbols "h" in den
Folgezustand , von dem er in den Zustand gelangt, indem er ein "a" konsumiert. An dieser Stelle
wäre also die Minimalbedingung unseres regulären Ausdrucks, mindestens einmal die Zeichenkette
"ha" zu lesen, erfüllt. Danach kann der Automat in den Zustand zurückkehren, wenn er erneut auf
eine Zeichenkette trifft, die mit einem "h" anfängt. Ansonsten geht er in den Zustand über, wenn er
dabei ein Ausrufezeichen lesen kann.
Dies ist aber nicht die einzige Möglichkeit, einen endlichen Automaten zu entwerfen, der derartige
Zeichenketten verarbeitet:
In diesem Automat kommt man vom Startzustand in den Folgezustand , indem das Symbol "h"
gelesen wird. Von dort gelangt man aber durch Konsumption des Zeichens "a" entweder in den
Sprachwissenschaftliches Institut
91
Zustand , sodass genau einmal die Zeichenkette "ha" gelesen wurde, oder zurück in den Startzustand
, aus dem weitere Abfolgen von "ha" konsumiert werden können. Dennoch muss die Abfolge
einmal durchlaufen werden, um nach Konsumption des Symbols "!" in den Endzustand zu gelangen.
Der Unterschied zwischen diesen beiden Automaten besteht darin, dass man im ersten Fall immer
entscheiden kann, in welchem Zustand sich der Automat nach Konsumption eines Zeichens befindet,
während dies im zweiten Automaten nicht möglich ist. Man bezeichnet daher den ersten Automaten
auch als deterministischen endlichen Automaten, während der zweite nichtdeterministischer endlicher
Automat genannt wird.
Die in den verschiedenen Programmiersprachen implementierten Maschinen zur Verarbeitung
regulärer Ausdrücke gehören meist zu den nichtdeterministischen endlichen Automaten. Betrachten wir
zunächst am einfachen Beispiel der gegebenen Zeichenkette "Ananas" und dem simplen Muster
/anas/i, wie reguläre Ausdrücke generell abgearbeitet werden:
Um vom Startzustand in den ersten Folgezustand zu gelangen, muss ein "a" konsumiert werden; der
Modifikator i sorgt an dieser Stelle dafür, dass der Großbuchstabe als ein solches erkannt wird. In den
nächsten beiden Schritten passen die in der Zeichenkette vorhandenen Symbole "n" und "a" auf das
Muster, doch müsste darauf ein "s" folgen, um dem Muster zu entsprechen. Also wird das Muster in
Schritt 5 um ein Symbol in der gegebenen Zeichenkette weiter nach rechts bewegt. Wiederum wird der
Anfang des Musters mit dem aktuellen Zeichen verglichen; da das "n" des Musters nicht auf das "a" der
Zeichenkette passt, wird das Muster erneut um ein Zeichen weiter bewegt. In Schritt 7 kann nun das
Symbol "a" konsumiert werden, ebenso wie die nachfolgenden Zeichen in den Schritten 8-10, sodass
der endliche Automat entscheiden kann, dass das Muster /anas/i auf das Ende der Zeichenkette
"Ananas" passt.
92
Wie wir oben gesehen haben, stellen Zeichenklassen, bzw. Alternation ein wichtiges Mittel der
Abstraktion über Zeichenketten dar. Betrachten wir nun exemplarisch, wie Perl die Zeichenkette
"Delphin" abarbeitet, wenn das Muster /Del(?:f|ph)in/ gegeben ist:
Aus dem Startzustand heraus werden die ersten drei Zustände durch die Identität der Symbole "D",
"e" und "l" im Muster und der Zeichenkette durchlaufen. Das nächste Zeichen, "p", passt allerdings
nicht auf das im Muster angegebene "f". Anstatt nun das Muster um ein Symbol weiter zu bewegen,
wird das "f" durch die alternativen Zeichen "p" und "h" ersetzt, wie in Schritt 5 zu sehen ist. Daraufhin
passt das "p" in der Zeichenkette auf das im Muster angegebene Symbol, und auch die weiteren
Vergleiche glücken, sodass entschieden werden kann, dass das Muster /Del(?:f|ph)in/ auf die
Zeichenkette passt.
Wieso bezeichnet man die Verarbeitung regulärer Ausdrücke in Perl als nichtdeterministischen
endlichen Automaten? Um diese Frage beantworten zu können, betrachten wir die Funktionalität der
Quantoren am Beispiel der Zeichenkette "Bootsmann" und dem Muster /B.*mann/:
Nachdem aus dem Startzustand heraus das Symbol "B" konsumiert wurde, passt der Punkt auf das
nächste Zeichen, das "o". Aufgrund des Sterns erweitert sich der Gültigkeitsbereich des Punkts auf den
Rest der gegebenen Zeichenkette (Schritte 4-10). Doch obschon durch diese Symbole des Musters die
gesamte Zeichenkette konsumiert werden könnte, müssen auch noch die übrig gebliebenen Zeichen
"m", "a", "n" und "n" überprüft werden. Um dies zu bewerkstelligen, wird das restliche Muster solange
nach links zurück geschoben, bis ihr erstes Symbol wieder auf ein Zeichen der Zeichenkette passt
(Schritte 11-14). Diesen Vorgang bezeichnet man als Backtracking: Der endliche Automat kann so
viele Zeichen zurücknehmen, wie nötig, um sich eine Option auf einen erfolgreichen Mustervergleich
offen zu halten. Dies ist bei einem deterministischen endlichen Automaten nicht nötig, da er ja zu jeder
Zeit weiß, in welchem Zustand er sich gerade befindet.
Sprachwissenschaftliches Institut
93
Zuletzt sei noch gezeigt, wie Perl Anker verarbeitet. Dazu schauen wir uns die unterschiedliche
Verarbeitung der Zeichenketten "da mal" und "damals" durch den regulären Ausdruck /\bda\b/ an:
94
In beiden Fällen passt der Anker \b auf die Anfänge der Zeichenketten. Danach werden die
Symbole "d" und "a" konsumiert. Während aber in der Zeichenkette "da mal" nun ein Leerzeichen
folgt, das durch die Zeichenklasse \W abgedeckt wird und somit eine Wortgrenze im Sinne von \b
darstellt, trifft das Muster in der Zeichenkette "damals" auf ein weiteres Wortzeichen und somit keine
Wortgrenze. Deshalb passt der reguläre Ausdruck /\bda\b/ nur auf "da mal", nicht aber auf "damals".
7.7 Gier Wie im Beispiel der Zeichenkette "Bootsmann" und des Musters /B.*mann/ gesehen, besteht eine
der Eigenschaften der multiplikativen Quantoren * und + darin, so viele Symbole wie möglich zu
konsumieren. Dies bezeichnet man auch als Gier. Dieses Verhalten ist aber oftmals gar nicht
gewünscht, wenn man z.B. sicherstellen möchte, dass man die erste Instanz einer Zeichenkette findet:
my $muster = "(.*a4)"; my $zeichenkette = "eins zwei drei a4 vier fuenf sechs a4 sieben"; print $1 if($zeichenkette =~ /$muster/); # Ausgabe: eins zwei drei a4 vier fuenf sechs a4
Will man einen multiplikativen Quantor minimieren, sodass die Verarbeitung der gegebenen
Zeichenkette durch den regulären Ausdruck nach der ersten Fundstelle erfolgreich beendet wird,
schreibt man ein Fragezeichen hinter den Quantor:
my $muster = "(.*?a4)"; my $zeichenkette = "eins zwei drei a4 vier fuenf sechs a4 sieben"; print $1 if($zeichenkette =~ /$muster/); # Ausgabe: eins zwei drei a4
Sprachwissenschaftliches Institut
95
7.8 Ersetzung Neben der Suche kann man Zeichenketten anhand regulärer Ausdrücke auch modifizieren. Dazu
verwendet man die in der Einleitung bereits erwähnten Operatoren s/// und tr///, die beide jeweils
ein Suchmuster und ein Ersetzungsmuster als Argumente benötigen; allerdings ist die Funktionalität
der beiden Operatoren unterschiedlich, sodass wir zunächst s/// diskutieren:
my $suchmuster = "toben"; my $ersetzungsmuster = "spazieren"; my $zeichenkette = "Paul und Maria toben duch den Wald ."; print "$zeichenkette\n"; # Ausgabe: Paul und Maria toben durch den Wald . $zeichenkette =~ s/$suchmuster/$ersetzungsmuster/; print "$zeichenkette\n"; # Ausgabe: Paul und Maria spazieren durch den Wald
.
Wie wir sehen, steht auch hier die gegebene Zeichenkette auf der linken Seite des
Musterbindungsoperators: Kann das Suchmuster in der Zeichenkette gefunden werden, wird es durch
das Ersetzungsmuster ausgetauscht und der Variablen zugewiesen.
Genauso wie beim Vergleichsoperator kann man auch mit dem Ersetzungsoperator Fundstellen im
regulären Ausdruck referenzieren. Allerdings kommt hier nicht der Backslash und ein numerischer
Index zum Einsatz, sondern das Dollarzeichen. So lässt sich auf einfache Weise aus einem deutschen
Aussagesatz einen Fragesatz machen:
my $zeichenkette = "du liest gerade ein buch"; $zeichenkette =~ s/(\w+)\s(\w+)/$2 $1/; print $zeichenkette."?"; # Ausgabe: liest du gerade ein buch?
Die Operation bei einem zusammengesetzten Subjekt sieht dann so aus:
my $zeichenkette = "peter und paul spielen ball"; $zeichenkette =~ s/((?:\w+\s){2}\w+)\s(\w+)/$2 $1/; print $zeichenkette."?"; # Ausgabe: spielen peter und paul
ball?
Der andere Ersetzungsoperator, tr///, operiert zwar genauso wie s/// auf einem Such- und einem
Ersetzungsmuster, korreliert er anders als s/// jedes Zeichen des Suchmusters mit jedem Zeichen des
Ersetzungsmusters. Seine Funktionsweise kann man sich also so vorstellen, dass sowohl im Such- als
auch im Ersetzungsmuster eine Zeichenklasse steht. Dementsprechend kann man in ihm weder
Zeichenklassen noch deren Abkürzungen verwenden und auch keine Zeichen gruppieren:
my $zeichenkette = "Peter und Paul spielen Ball ."; $zeichenkette =~ tr/ /_/; print $zeichenkette; # Ausgabe:
Peter_und_Paul_spielen_Ball_.
96
tr/// funktioniert also analog zu s///g. Darüber hinaus lassen sich Bereiche angeben:
my $zeichenkette = "2011064"; $zeichenkette =~ tr/0-9/a-j/; print $zeichenkette; # Ausgabe: cabbage
Der Rückgabewert von tr/// ist die Häufigkeit der vorgenommenen Ersetzungen. Da man aber
nicht notwendigerweise etwas ersetzen muss, d.h. tr/// auch mit einem leeren Ersetzungsoperator
verwendet werden kann, stellt sich der günstige Seiteneffekt ein, dass man tr/// zum Zählen einzelner
Zeichen einsetzen kann:
my $zeichenkette = "Dies ist ein schöner Satz , ein sehr schöner ."; my $haeufigkeit = $zeichenkette =~ tr/aeiou//; print $haeufigkeit; # Ausgabe: 11
In diesem Fall werden keine Zeichen ersetzt, man kann aber die gefundenen Zeichen löschen,
indem man den Modifikator d angibt:
my $zeichenkette = "Dies ist ein schöner Satz , ein sehr schöner ."; my $haeufigkeit = $zeichenkette =~ tr/aeiou//d; print "$haeufigkeit\n$zeichenkette\n"; # Ausgabe: # 11 # ds st n schönr stz n shr schönr
Darüber hinaus kann man mit tr/// die Modifikatoren c, der das Komplement eines Suchmusters
bildet, und s, der mehrere mögliche Ersetzungen auf eine reduziert, verwenden.
7.9 Vor- und zurückschauen Manchmal möchte man eine Fundstelle davon abhängig machen, ob ihr eine bestimmte
Zeichenkette folgt oder vorangeht. Dazu gibt es in Perl den sogenannten lookahead-Operator ?= und
den lookbehind-Operator ?<=. Wichtigstes Merkmal dieser Operatoren ist, dass sie selbst keine
Symbole konsumieren. Sie schauen ab der Stelle, in der sie im Muster verwendet werden, zurück oder
voraus, sodass die Entscheidung über das Akzeptieren oder das Verwerfen des Mustervergleichs von
ihnen abhängig gemacht wird. Man kann sie sich also wie eine erweiterte Variante von Bedingungen
innerhalb regulärer Ausdrücke vorstellen. Man benutzt diese beiden Operatoren, indem man sie mit
dem zu überprüfenden Teilmuster in runde Klammern schreibt. Diese besitzen allerdings keine
speichernde Funktion, da die Zeichen, wie gesagt, nicht konsumiert werden:
my $zeichenkette = "Schwarzwälder Uhren und Schwarzwälder Schinken"; $zeichenkette =~ s/Schwarzwälder(?= Uhren)/Schweizer/; print $zeichenkette; # Ausgabe: Schweizer Uhren und Schwarzwälder
Schinken
Sprachwissenschaftliches Institut
97
my $zeichenkette = "Deutsche Wertarbeit und Schweizer Wertarbeit"; $zeichenkette =~ s/(?<=Schweizer )Wertarbeit/Kreditinstitute/; print $zeichenkette; # Ausgabe: Deutsche Wertarbeit und Schweizer Kreditinstitute
Ersetzt man die Gleichheitszeichen in den Operatoren durch Ausrufezeichen, bekommt man die
jeweilige Negation der Bedingung. Daher nennt man die erste Variante auch positive
lookahead/lookbehind und die der Negation negative lookahead/lookbehind.
7.10 Zusammenfassung Die Möglichkeit, in Perl reguläre Ausdrücke zum Suchen und Ersetzen in Texten zu verwenden,
erweitert unseren bisher vorhandenen Werkzeugkasten erheblich. Wir haben gesehen, dass wir
Zeichenkettenliterale und -variablen nicht nur auf Identität untersuchen können, sondern dass wir auch
Teilstrukturen erfassen können. Dazu können wir vermittels Zeichenklassen und deren
Kurzschreibweisen über Zeichen abstrahieren. Die in Zeichenklassen stehenden Symbole werden
immer disjunktiv verarbeitet; will man mehrere Zeichen alternativ verwenden, benutzt man die
Alternation.
Innerhalb regulärer Ausdrücke besitzen wir aber nicht nur Kontrolle über die auszuwählenden
Zeichen, also das Paradigma der für das jeweilige Muster gültigen Symbole, sondern auch über ihre
sequentielle Anordnung, das Syntagma. In ihm sind es vor allem die Quantoren, die uns Aussagen über
die von uns erwarteten Häufigkeiten der Symbole zu treffen. Runde Klammern ermöglichen uns die
Gruppierung und Speicherung von Fundstellen, sodass wir sie innerhalb und außerhalb des regulären
Ausdrucks verwenden können. Anhand von Ankern können wir die Position spezifizieren, an der wir
eine Fundstelle erwarten. Darüber hinaus besitzen wir mit den lookahead- und lookbehind-Operatoren,
innerhalb des regulären Ausdrucks Bedingungen darüber zu formulieren, ob eine Zeichenkette auf das
formulierte Muster passt, abhängig davon, ob ein anderes Muster vor oder hinter der entsprechenden
Zeichenkette zu finden ist.
Modifikatoren ermöglichen die Einflussnahme auf die Verarbeitung regulärer Ausdrücke sowohl
bezüglich des Paradigmas als auch des Syntagmas. So verändert der Modifikator i beispielsweise die
Behandlung von Groß- und Kleinschreibung in regulären Ausdrücken, während g den
Gültigkeitsbereich des Musters auf die gesamte Zeichenkette ausweitet.
Fassen wir diese Sprachmittel in einem Diagramm zusammen:
98
7.11 Beispielanwendung Als wir im letzten Kapitel n-Gramme gezählt haben, liefen wir in das Problem, dass
Interpunktionszeichen durch die Vortokenisierung des Korpus ebenso durch Leerzeichen isoliert
worden sind, wie Wörter, sodass diese Interpunktionszeichen in den Listen der n-Gramm-Häufigkeiten
derart prominent vertreten waren, dass sich aus diesen Frequenzen keine linguistisch verwertbaren
Ergebnisse ableiten ließen. Mit den regulären Ausdrücken haben wir nun ein Werkzeug bei der Hand,
mit dem sich diese Zeichen schon beim Einlesen des Korpus eliminieren lassen.
Zunächst wollen wir alle Zeilen ignorieren, die ausschließlich aus Leerraumzeichen bestehen. Da
wir dies beim Einlesen der Daten tun, verlassen wir die while()-Schleife mit next, wenn wir auf eine
solche Zeile treffen:
# Zeilen ignorieren, die ausschließlich aus Leerraumzeichen bestehen. next if(/^\s+$/); Außerdem wollen wir sicherstellen, dass sich im Text keine überflüssigen Leerzeichen befinden,
die möglicherweise Eingang in die Zählung finden könnten. Dazu ersetzen wir zwei oder mehr
nacheinander vorkommende Leerzeichen durch ein einzelnes:
# Überflüssige Leerzeichen durch ein einzelnes ersetzen. s/\s{2,}/ /g;
Des Weiteren muss eine Entscheidung darüber getroffen werden, ob Klitika wie "I'll", "don't" oder
"he's" als ein oder zwei Token betrachtet werden sollen. Durch den Tokenizer wurden sie getrennt, in
unserer Analyse möchten wir sie jedoch als ein Wort zählen, weshalb wir sie jetzt wieder
zusammenfassen:
# Klitika sollen als ein Token gezählt werden! s/\s'([lst])/'$1/g;
Sprachwissenschaftliches Institut
99
Im nächsten Schritt entfernen wir die eigentlichen Interpunktionszeichen. Durch die Tokenisierung
ist einem jeden solchen Zeichen ein Leerzeichen vorangestellt worden, sodass wir auch dieses
eliminieren müssen. Dies gilt allerdings nicht pauschal für Anführungszeichen: Da diese auch am
Anfang einer Zeile vorkommen, müssen wir in diesem Fall die Anführungszeichen und das folgende
Leerzeichen entfernen:
# Interpunktion nach Tokenisierung entfernen. s/^"\s//; s/\s[.?!":;,$%&-\/()\[\]]+//g;
Der veränderte Code sieht an dieser Stelle also so aus:
# Korpus einlesen und Token in einer Liste speichern. while (<IN>) { chomp; # Zeilen ignorieren, die ausschließlich aus Leerraumzeichen bestehen. next if(/^\s+$/); # Überflüssige Leerzeichen durch ein einzelnes ersetzen. s/\s{2,}/ /g; # Klitika sollen als ein Token gezählt werden! s/\s'([lst])/'$1/g; # Interpunktion nach Tokenisierung entfernen. s/^"\s//; s/\s[.?!":;,$%&-\/()\[\]]+//g; push( @token, split(/ /) ); }
Dadurch ergeben sich folgende neue Resultate:
# Korpus im Umfang von 795881 Token eingelesen! # Die Größe des Vokabulars beträgt 22456. # 41.59% aller Types kommt nur einmal im Korpus vor! # Es gibt 795880 Bigramm-Token und 263269 Bigramm-Types. # Das ergibt 504271936 mögliche Bigramme, von denen 0.0522077437202454% # im Dokument vorkommen. # 71.47% aller Bigramm-Types kommt nur einmal im Korpus vor! # Es gibt 795879 Trigramm-Token und 592210 Trigramm-Types. # Das ergibt 11323930594816 mögliche Trigramme, von denen
5.22972120891582e-06% # im Dokument vorkommen. # 87.87% aller Trigramm-Types kommt nur einmal im Korpus vor! # Es gibt 795878 Tetragramm-Token und 746411 Tetragramm-Types. # Das ergibt 2.54290185437188e+17 mögliche Tetragramme, von denen # 2.93527254587799e-10% im Dokument vorkommen. # 96.24% aller Tetragramm-Types kommt nur einmal im Korpus vor!
100
# Die zehn häufigsten Unigramme sind: # the: 30892 Das sind 3.88148% aller Token. # and: 23258 Das sind 2.92230% aller Token. # to: 20993 Das sind 2.63771% aller Token. # of: 16387 Das sind 2.05898% aller Token. # I: 16176 Das sind 2.03246% aller Token. # a: 15792 Das sind 1.98422% aller Token. # he: 12691 Das sind 1.59459% aller Token. # you: 12522 Das sind 1.57335% aller Token. # that: 11847 Das sind 1.48854% aller Token. # in: 11707 Das sind 1.47095% aller Token. # was: 10648 Das sind 1.33789% aller Token. # Die zehn häufigsten Bigramme sind: # of the: 2918 Das sind 0.36664% aller Bigramm-Token. # in the: 2835 Das sind 0.35621% aller Bigramm-Token. # to the: 1897 Das sind 0.23835% aller Bigramm-Token. # he had: 1576 Das sind 0.19802% aller Bigramm-Token. # I am: 1563 Das sind 0.19639% aller Bigramm-Token. # on the: 1471 Das sind 0.18483% aller Bigramm-Token. # that he: 1442 Das sind 0.18118% aller Bigramm-Token. # at the: 1440 Das sind 0.18093% aller Bigramm-Token. # he was: 1349 Das sind 0.16950% aller Bigramm-Token. # to be: 1160 Das sind 0.14575% aller Bigramm-Token. # in a: 1076 Das sind 0.13520% aller Bigramm-Token. # Die zehn häufigsten Trigramme sind: # that he had: 292 Das sind 0.03669% aller Trigramm-Token. # that he was: 288 Das sind 0.03619% aller Trigramm-Token. # out of the: 262 Das sind 0.03292% aller Trigramm-Token. # I am not: 233 Das sind 0.02928% aller Trigramm-Token. # I don't know: 222 Das sind 0.02789% aller Trigramm-Token. # he did not: 187 Das sind 0.02350% aller Trigramm-Token. # that it was: 185 Das sind 0.02324% aller Trigramm-Token. # said the prince: 162 Das sind 0.02035% aller Trigramm-Token. # he had been: 162 Das sind 0.02035% aller Trigramm-Token. # he could not: 160 Das sind 0.02010% aller Trigramm-Token. # as though he: 159 Das sind 0.01998% aller Trigramm-Token. # Die zehn häufigsten Tetragramme sind: # for the sake of: 70 Das sind 0.00880% aller Tetragramm-Token. # out of the room: 60 Das sind 0.00754% aller Tetragramm-Token. # as though he were: 60 Das sind 0.00754% aller Tetragramm-Token. # What do you mean: 57 Das sind 0.00716% aller Tetragramm-Token. # at the same time: 55 Das sind 0.00691% aller Tetragramm-Token. # for the first time: 54 Das sind 0.00678% aller Tetragramm-Token. # as though he had: 53 Das sind 0.00666% aller Tetragramm-Token. # I don't want to: 49 Das sind 0.00616% aller Tetragramm-Token. # the middle of the: 47 Das sind 0.00591% aller Tetragramm-Token. # in spite of the: 44 Das sind 0.00553% aller Tetragramm-Token. # I should like to: 44 Das sind 0.00553% aller Tetragramm-Token.
Zunächst lässt sich festhalten, dass das Entfernen der Interpunktion den Umfang des Korpus um fast
ein Viertel des ursprünglichen Umfangs reduziert hat. Gleichzeitig hat die Behandlung der Klitika als
ein Token das Vokabular vergrößert. Wir sehen auch, dass sich unser Eindruck aus dem vorherigen Ka-
pitel verstärkt hat: Die Funktionswörter machen in allen vier Kategorien den größten Anteil am
sprachlichen Material aus. Während sich dies für die Uni- und Bigramme ausschließlich negativ
Sprachwissenschaftliches Institut
101
niederschlägt, da sich aus diesen Daten kaum relevante Analysen ableiten lassen, finden sich unter den
Tri- und Tetragrammen Kandidaten, die linguistisch interessant sind, da sie in just dieser Konfiguration
eine bestimmte Bedeutung tragen, die sich durch Austauschen des lexikalischen Materials oder durch
syntaktische Veränderungen nicht ergäbe. Solche n-Gramme nennt man Kollokationen. Beispiele aus
diesen Listen wären "in spite of", "for the sake of" oder "at the same time".
Wie ermittelt man nun relevante Uni- und Bigramme? Dazu bedient man sich so genannter
Stoppwort-Listen (engl. stop word list), in denen die häufigsten Vertreter der geschlossenen
Wortklassen vertreten sind. Eine solche Liste setzt man als Filter nach dem Aufbau der n-Gramm-
Hashes ein:
# Liste mit hochfrequenten Wörtern, die herausgefiltert werden sollen. my @stoppwort_liste = qw(a an and the this that as at with by to for from
of on in out up I he she it we you they me my his her him our your us them their be am is are was were has have had do does did can could will would all no not but so what or if yes);
Dementsprechend muss man sie auch auf jeden n-Gramm-Hash anwenden. Da das Verfahren an
sich immer analog ist, illustrieren wir es hier anhand der Bigramme:
# Stoppworte herausfiltern foreach(keys %bigramm_haeufigkeit){ foreach my $stoppwort(@stoppwort_liste){ delete($bigramm_haeufigkeit{$_}) if(/\b$stoppwort\b/i); next; } }
Zunächst iterieren wir über den Bigramm-Hash; für jeden seiner Schlüssel überprüfen wir, ob dieser
in der Stoppwort-Liste vorhanden ist. Ist dies der Fall, löschen wir ihn wieder und fahren mit dem
nächsten Bigramm fort.
Das Resultat dieser Operation sieht so aus:
# Korpus im Umfang von 795861 Token eingelesen! # Die Größe des Vokabulars beträgt 21832. # 41.36% aller Types kommt nur einmal im Korpus vor! # Die zehn häufigsten Unigramme sind: # said: 2606 Das sind 0.32744% aller Unigramm-Token. # one: 2270 Das sind 0.28523% aller Unigramm-Token. # been: 2243 Das sind 0.28183% aller Unigramm-Token. # know: 2113 Das sind 0.26550% aller Unigramm-Token. # about: 2088 Das sind 0.26236% aller Unigramm-Token. # there: 2047 Das sind 0.25721% aller Unigramm-Token. # now: 1968 Das sind 0.24728% aller Unigramm-Token. # only: 1810 Das sind 0.22743% aller Unigramm-Token. # very: 1756 Das sind 0.22064% aller Unigramm-Token. # man: 1742 Das sind 0.21888% aller Unigramm-Token. # who: 1673 Das sind 0.21021% aller Unigramm-Token.
102
# Es gibt 795860 Bigramm-Token und 115409 Bigramm-Types. # Das ergibt 476636224 mögliche Bigramme, von denen 0.0242132247170538% # im Dokument vorkommen. # 82.75% aller Bigramm-Types kommt nur einmal im Korpus vor! # Die zehn häufigsten Bigramme sind: # Katerina Ivanovna: 353 Das sind 0.04435% aller Bigramm-Token. # don't know: 304 Das sind 0.03820% aller Bigramm-Token. # Fyodor Pavlovitch: 255 Das sind 0.03204% aller Bigramm-Token. # just now: 253 Das sind 0.03179% aller Bigramm-Token. # old man: 237 Das sind 0.02978% aller Bigramm-Token. # Nastasia Philipovna: 216 Das sind 0.02714% aller Bigramm-Token. # more than: 206 Das sind 0.02588% aller Bigramm-Token. # three thousand: 180 Das sind 0.02262% aller Bigramm-Token. # Lizabetha Prokofievna: 168 Das sind 0.02111% aller Bigramm-Token. # Dmitri Fyodorovitch: 166 Das sind 0.02086% aller Bigramm-Token. # young man: 165 Das sind 0.02073% aller Bigramm-Token. # Es gibt 795859 Trigramm-Token und 70777 Trigramm-Types. # Das ergibt 10405922042368 mögliche Trigramme, von denen
6.80160774910954e-007% # im Dokument vorkommen. # 96.50% aller Trigramm-Types kommt nur einmal im Korpus vor! # Die zehn häufigsten Trigramme sind: # three thousand roubles: 50 Das sind 0.00628% aller Trigramm-Token. # don't know how: 39 Das sind 0.00490% aller Trigramm-Token. # day before yesterday: 38 Das sind 0.00477% aller Trigramm-Token. # Ha ha ha: 29 Das sind 0.00364% aller Trigramm-Token. # said just now: 24 Das sind 0.00302% aller Trigramm-Token. # sat down again: 23 Das sind 0.00289% aller Trigramm-Token. # don't know whether: 22 Das sind 0.00276% aller Trigramm-Token. # said Mrs. Epanchin: 22 Das sind 0.00276% aller Trigramm-Token. # burst into tears: 21 Das sind 0.00264% aller Trigramm-Token. # know nothing about: 21 Das sind 0.00264% aller Trigramm-Token. # more than once: 19 Das sind 0.00239% aller Trigramm-Token. # Es gibt 795858 Tetragramm-Token und 32221 Tetragramm-Types. # Das ergibt 2.27182090028978e+017 mögliche Tetragramme, # von denen 1.41828961939253e-011% im Dokument vorkommen. # 99.36% aller Tetragramm-Types kommt nur einmal im Korpus vor! # Die zehn häufigsten Tetragramme sind: # art just O Lord: 5 Das sind 0.00063% aller Tetragramm-Token. # fresh air fresh air: 5 Das sind 0.00063% aller Tetragramm-Token. # shall see each other: 4 Das sind 0.00050% aller Tetragramm-Token. # two thousand three hundred: 4 Das sind 0.00050% aller Tetragramm-Token. # always worth while speaking: 4 Das sind 0.00050% aller Tetragramm-Token. # expected something quite different: 4 Das sind 0.00050% aller
Tetragramm-Token. # within these peeling walls: 3 Das sind 0.00038% aller Tetragramm-Token. # these two hundred roubles: 3 Das sind 0.00038% aller Tetragramm-Token. # more than three thousand: 3 Das sind 0.00038% aller Tetragramm-Token. # thousand five hundred roubles: 3 Das sind 0.00038% aller Tetragramm-
Token. # these last few days: 3 Das sind 0.00038% aller Tetragramm-Token.
Sprachwissenschaftliches Institut
103
8 Subroutinen
The only routine with me is no routine at all.
JAQUELINE KENNEDY
Bis jetzt besaßen unsere Perl-Programme die Eigenschaft, aus einem monolithischen Code-Block
zu bestehen, der linear abgearbeitet wird. Daraus ergeben sich die Nachteile, dass unser Code einerseits
konzeptuell wenig strukturiert ist, andererseits müssen Aufgaben, die wiederholt bearbeitet werden,
auch mehrmals geschrieben werden. Konsequenz dessen ist, dass unser Quellcode schwer zu lesen und
zu pflegen ist, und dass er fehleranfällig ist. Subroutinen erlauben es uns nun, den Code sinnvoll zu
organisieren und Bestandteile wiederzuverwenden.
Subroutinen kann man sich konzeptuell als kleine Programme mit begrenzter Funktionalität
vorstellen, die aus einem großen "Hauptprogramm", aus anderen Subroutinen oder aus sich selbst
rekursiv aufgerufen werden. Wir werden noch sehen, dass sie den in Perl eingebauten Funktionen sehr
ähnlich sind. Die Definition einer Subroutine besteht aus dem Schlüsselwort sub, einem Bezeichner
und einem Codeblock:
sub marine{ ... }
Für die Benennung einer Subroutine sollte man sich an folgende Regeln halten:
• Wird durch die Subroutine eine Aktion ausgeführt, verwendet man ein Verb als
Subroutinennamen.
• Steht Information als Ergebnis der Subroutine im Vordergrund, benennt man sie danach.
• Prüft man eine Aussage auf Gültigkeit, leitet man den Namen mit "ist" oder "kann" ein.
• Überführt man Daten in ein anderes Format, nennt man beide Strukturen und verbindet sie mit
"zu", "nach" oder dem englischen "2".
In einer Subroutine dürfen alle Konstrukte verwendet werden, die auch in einem normalen
Programm vorkommen:
sub beatles_singen{ my $text = "yellow submarine"; print "We all live in a "; for (1 .. 3){ print $text; } }
Für gewöhnlich schreibt man Subroutinen – dem Top-down-Ansatz folgend – an das Ende des
Quellcodes. Davor formuliert man abstrakt die Funktionalität eines Programms in seine
Einzelprobleme zerlegt. Dies sind die Aufrufe der einzelnen Subroutinen und bilden das
Hauptprogramm. Will man eine Subroutine aufrufen, gibt es zwei Notationsmöglichkeiten:
Standardmäßig schreibt man ein "kaufmännisches und" (engl. ampersand) & vor den Bezeichner. Alter-
nativ kann man dieses weglassen, wenn man dem Aufruf eine (leere) Argumentliste übergibt:
104
&beatles_singen; # Ausgabe: We all live in a yellow submarine yellow submarine yellow submarine
beatles_singen(); # Ausgabe: We all live in a yellow submarine yellow submarine yellow submarine
Diese letzte Notation zeigt die Nähe zu den eingebauten Funktionen; wie wir gesehen haben,
können wir diese auch ohne die runden Klammern verwenden. Dies funktioniert im Fall der
Subroutinen allerdings nur, wenn man sie vor dem Aufruf definiert hat, ansonsten ist Perl dieser
Bezeichner nicht bekannt und der Interpreter gibt eine Fehlermeldung aus:
beatles_singen; # Ausgabe: Bareword "beatles_singen" not allowed...
8.1 Argumente Die meisten eingebauten Funktionen haben wir allerdings mit einer Argumentliste aufgerufen. Dies
können wir auch mit Subroutinen tun:
beatles_singen("Yesterday", "LSD", $ringo); # Ausgabe: We all live in a yellow submarine yellow submarine yellow submarine
In diesem Fall zeigt ein solcher Aufruf aber noch keine Wirkung auf die Ausführung unseres
Programms, da wir die an die Subroutine übergebenen Argumente noch nicht bearbeiten.
Im Gegensatz zu vielen anderen Programmiersprachen müssen wir bei der Definition einer
Subroutine nicht angeben, wie viele und welche Argumente sie verarbeiten soll.46 Stattdessen stehen
die Argumente im Array @_. Wie schon vorher gesagt, besitzen die verschiedenen Datenstrukturen ihre
eigenen Namensräume, sodass @_ nichts mit $_ zu tun hat und auch nicht mit diesem Skalar
verwechselt werden darf! Auf diesem Array kann man innerhalb einer Subroutine alle Operationen
ausführen, die man auch auf normalen Arrays ausführen kann. Da man der Argumentliste aber in der
Subroutine keine Elemente hinzufügen will, verwendet man nur den lesenden Zugriff über shift(),
pop() oder die einzelnen Array-Elemente $_[0], $_[1] etc.47 Es empfiehlt sich allerdings, die
Elemente von @_ nicht direkt zu manipulieren, sondern auf Kopien zu operieren, um unerwünschte
Seiteneffekte zu vermeiden:
my $zahl = 12; print erhoehe($zahl); # Ausgabe: 13 print $zahl; # Ausgabe: 13 sub erhoehe{ ++$_[0]; }
In den verschiedenen Programmiersprachen können Argumente entweder per Referenz (call by
reference) oder als Wert (call by value) an eine Subroutine übergeben werden. In Perl wird – wie wir
46 Zwar lassen sich an dieser Stelle sogenannte Prototypen angeben, mit denen sich kontrollieren lässt, welche Datenstruktur
übergeben wird. Dies ist aber aus verschiedenen Gründen nicht ganz unproblematisch, sodass hier dem in der Perl-Gemeinde
allgemein vorherrschenden Ratschlag, sie nicht zu verwenden, gefolgt wird. 47 Die wie gesagt nichts mit $_ zu tun haben!
Sprachwissenschaftliches Institut
105
sehen – nicht der Wert, sondern eine Referenz auf den Wert des Hauptprogramms an die Subroutine
übergeben. Deshalb wird der Wert der Variablen aus dem Aufruf innerhalb der Subroutine verändert.
Dies wird allgemein als schlechter Programmierstil angesehen, da wir die Funktionalität der Subroutine
nicht vom Rest des Programms trennen. Wollten wir explizit den call by value versuchen, bekommen
wir eine Fehlermeldung:
print erhoehe(12); # Ausgabe: Modification of a read-only value attempted
sub erhoehe{ ++$_[0]; }
Die richtige Verwendung eines Arguments besteht also darin, ihn aus dem Array @_48 in eine lokale
Variable zu kopieren, auf der man dann die entsprechende Operation ausführt. Zwar hat @_ nichts mit
$_ zu tun, doch besitzt es die gleiche "Pragmatik" wie $_, nämlich wie eine implizite Liste verwendet
werden zu können, d.h., man muss bei einer shift()-Operation den Bezeichner @_ nicht explizit
angeben, um an die Elemente des Arrays zu kommen:
print erhoehe($zahl); # Ausgabe: 13 print $zahl; # Ausgabe: 12 sub erhoehe{ my $zahl = shift; ++$zahl; }
Man kann an Subroutinen aber nicht nur Skalare, sondern auch Arrays und Hashes übergeben.
Hashes werden dabei zu Listen heruntergebrochen, sodass man an den geraden Indizes, angefangen bei
0, die Schlüssel und an den ungeraden die Werte findet:
my $wort = "Wort"; my @satz = qw(Hier steht ein Satz); my %haeufigkeit = (Hier => 1, steht => 1, ein => 1, Satz => 1); zeige_elemente($wort); # Ausgabe: Wort zeige_elemente(@satz); # Ausgabe: Hier steht ein Satz zeige_elemente(%haeufigkeit); # Ausgabe: Hier 1 Satz 1 ein 1 steht 1 sub zeige_elemente{ print "@_\n"; }
Im Gegensatz zu vielen anderen Programmiersprachen verhält sich Perl also nicht nur bezüglich der
Datentypen, sondern auch in Hinsicht auf Datenstrukturen agnostisch: Solange die Argumentliste ein
Element enthält, kann es von dieser Subroutine ausgegeben werden. Was passiert aber, wenn wir all
diese Datenstrukturen auf einmal übergeben wollen?
48 Hatte ich schon erwähnt, dass @_ völlig verschieden von $_ ist?!
106
my $wort = "Wort"; my @satz = qw(Hier steht ein Satz); my %haeufigkeit = (Hier => 1, steht => 1, ein => 1, Satz => 1); zeige_elemente(@satz, $wort, %haeufigkeit); sub zeige_elemente{ print "@_\n"; } # Ausgabe: Hier steht ein Satz Wort ein 1 steht 1 Satz 1 Hier 1
Die Elemente der Datenstrukturen werden wiederum zu einer Liste vereinheitlicht; wir haben also
ohne die genaue Kenntnis über den Aufbau der Datenstruktur keine Möglichkeit, zwischen den
Elementen zu unterscheiden. So können wir beispielsweise innerhalb der Subroutine nicht ohne
weiteres sagen, ob "Wort" Bestandteil des Arrays @satz ist oder nicht! Wie wir diese Funktionalität in
Perl dennoch einsetzen können, lernen wir im nächsten Kapitel.
Benannte Parameter
Übergibt man einer Subroutine eine Argumentiste, die viele Literale enthält, wird der Aufruf
schnell unübersichtlich, da man den einzelnen Argumenten meist nur mühsam eine Bedeutung
zuordnen kann, ohne in die Subroutine selbst zu schauen. Um diesem Problem zu entgehen, sollte man
die Argumente als Werte eines Hashes übergeben, deren Schlüssel das Argument näher bezeichnen.
Ein weiterer Vorteil dieser Art des Aufrufs ergibt sich aus der Tatsache, dass die Argumente nun nicht
mehr in einer bestimmten Reihenfolge angegeben und verarbeitet werden müssen, sondern dass sie
eben über Bezeichnungen innerhalb der Subroutine zugreifbar werden.
Als Beispiel schauen wir uns eine Subroutine an, die eine Verbindung zu einem anderen Rechner
aufbaut und deren Argumente aus einem Benutzernamen, dem Passwort und dem Namen des Rechners
bestehen. In manchen Konfigurationen ist es durchaus denkbar, dass Joe User für alle diese Parameter
denselben Wert vergibt, sodass sich im Programm durch Ansicht des Aufrufs kaum entscheiden lässt,
welches Argument nun welche Bedeutung trägt. Anders bei einem Aufruf mit benannten Parametern,
bei dem die Benennungen für Klarheit sorgen:
logon(Benutzer => "groucho", Passwort => "groucho", Rechner => "groucho"); sub logon{ my %args = @_; print "Benutzer $args{Benutzer} meldet sich mit dem Passwort
$args{Passwort} am Rechner $args{Rechner} an.\n"; # Weiterer Code, der die eigentliche Anmeldung implementiert... } # Ausgabe: # Benutzer groucho meldet sich mit dem Passwort groucho am Rechner groucho
an.
Sprachwissenschaftliches Institut
107
8.2 Gültigkeitsbereiche Als wir gerade über das Kopieren der Argumente in die Subroutine sprachen, hieß es, dass wir dazu
eine lokale Variable verwenden. In der Subroutine steht allerdings nur eine mit my deklarierte Variable,
die sich keineswegs von denjenigen unterscheidet, die wir bis hierhin benutzt haben. Deswegen soll an
dieser Stelle die Funktionalität von my näher beleuchtet und das Konzept des Gültigkeitsbereichs
eingeführt werden.
In der Einführung dieses Kapitels war davon die Rede, dass Subroutinen kleinen Programmen
ähneln, die u.a. aus einem "Hauptprogramm" aufgerufen werden. Auch haben wir gesehen, dass der
Wert einer mit my im Hauptprogramm deklarierten Variablen in einer Subroutine verändert werden
kann. Sie ist also auch dann zugreifbar, wenn auf ihr in einem "Unterprogramm" operiert wird.
Andererseits haben wir gesehen, dass Operationen auf einer in der Subroutine mit my deklarierten
Kopie mit dem gleichen Bezeichner wie im Hauptprogramm keinerlei Auswirkungen auf den Wert
dieser Variablen hatten. Welchen Gültigkeitsbereich besitzen also mit my deklarierte Variablen? Um
diese Frage klären zu können, müssen wir den Begriff des Programms genauer definieren. Jedes
Programm, das wir schreiben, gehört einem bestimmten Paket an. In unserem Fall ist dies immer das
Paket Main, das wir allerdings – anders als in anderen Programmiersprachen – nicht immer explizit
angeben müssen.49 Ferner ist es so, dass man sich ein Perl-Programm wie einen Block vorstellen muss,
der implizit in geschweiften Klammern steht. Daraus ergibt sich, dass eine mit my deklarierte Variable
immer lokal ist. Im ersten Fall ist sie lokal relativ zum Paket Main, im zweiten Fall besitzt sie einen
lokalen Gültigkeitsbereich innerhalb des Blocks, der die Subroutine bildet. Dies gilt aber auch für alle
anderen Blöcke, die wir bisher kennen gelernt haben: Eine beispielsweise in einem if()- oder
while()-Block mit my deklarierte Variable ist außerhalb dieses if()- oder while()-Blocks genauso
wenig zugreifbar, wie außerhalb einer Subroutine!
Wenn es einen lokalen Gültigkeitsbereich gibt, sollte Perl auch einen globalen Gültigkeitsbereich
kennen. Diesen hatten wir im Prinzip schon mit der für das Paket Main lokal deklarierten Variablen
gesehen, da sie ja von überall her verändert werden konnte. Dennoch gibt es in Perl noch das
Schlüsselwort our, das eine Variable explizit als global kennzeichnet. Dieser feine Unterschied hat
durchaus praktische Auswirkungen, wie wir gleich sehen werden!
Neben dem mit my lokal deklarierten Gültigkeitsbereich gibt es allerdings noch einen mit local
lokal deklarierten Gültigkeitsbereich. Deshalb bezeichnet man my zur Unterscheidung häufig auch als
lexikalischen Gültigkeitsbereich. Das Schlüsselwort local ermöglicht es, in einem Block auf dem Wert
einer vorher deklarierten Variablen zu operieren, ohne diesen in der ursprünglichen Variablen zu
verändern. Dies bezeichnet man auch als dynamischen Gültigkeitsbereich (engl. dynamic scoping):
our $variable = "Paket"; print "Ursprungswert: $variable\n"; local_demo1(); local_demo2(); print "Nach den Subroutinen: $variable\n"; sub local_demo1{ local $variable = "Lokal"; print "In der ersten Subroutine: $variable\n"; local_demo2(); }
49 Wenn wir in einem der nächsten Kapitel Module verwenden, bzw. selbst schreiben, erstellen wir unsere eigenen Pakete,
die wir dann sehr wohl explizit benennen müssen.
108
sub local_demo2{ print "In der zweiten Subroutine: $variable\n"; } # Ausgabe: # Ursprungswert: Paket # In der ersten Subroutine: Lokal # In der zweiten Subroutine: Lokal # In der zweiten Subroutine: Paket # Nach den Subroutinen: Paket
Hätten wir die Variable $variable ursprünglich mit my deklariert, wäre der Programmlauf mit einer
Fehlermeldung darüber, dass die Variable in der ersten Zeile der ersten Subroutine nicht in einen
lokalen Kontext gebracht werden kann, abgebrochen, da sie ja bereits lokal ist (nämlich relativ zum
Paket). Durch die Verwendung von our wird $variable zu einer echten globalen Variablen, die
dementsprechend auch in einen lokalen Kontext überführt werden darf. Ursprünglich besitzt $variable
den Wert "Paket", der in der ersten Subroutine durch die local-Deklaration mit "Lokal" überschrieben
wird. Durch den ersten Aufruf der zweiten Subroutine innerhalb der ersten wird derjenige Wert für die
weitere Verarbeitung verwendet, der in diesem Gültigkeitsbereich aktuell ist. Da dies "Lokal" ist, wird
zunächst dieser Wert ausgegeben. Danach ist die erste Subroutine abgearbeitet und die zweite wird aus
dem "Hauptprogramm" aufgerufen. Der aktuelle Wert von $variable ist immer noch "Paket", da er ja
durch die lokale Verwendung nicht überschrieben wurde, sodass dieser jetzt von der zweiten
Subroutine ausgegeben wird. Dies geschieht auch nachdem beide Subroutinen abgearbeitet sind. Hätten
wir $variable in der ersten Subroutine mit my deklariert, wäre sie nur dort lokal gewesen:
my $variable = "Paket"; print "Ursprungswert: $variable\n"; local_demo1(); local_demo2(); print "Nach den Subroutinen: $variable\n"; sub local_demo1{ my $variable = "Lokal"; print "In der ersten Subroutine: $variable\n"; local_demo2(); } sub local_demo2{ print "In der zweiten Subroutine: $variable\n"; } # Ausgabe: # Ursprungswert: Paket # In der ersten Subroutine: Lokal # In der zweiten Subroutine: Paket # In der zweiten Subroutine: Paket # Nach den Subroutinen: Paket
Hier noch einmal der Unterschied visualisiert:
Sprachwissenschaftliches Institut
109
Vorherrschende Meinung für die Verwendung von local ist aber: Immer my benutzen!
8.3 Rückgabewerte Genauso wie wir Subroutinen mit Argumenten aufrufen können, können wir auch Werte dahin
zurückgeben, wo wir die Subroutine aufgerufen haben. Dies bedeutet, dass wir in Subroutinen nicht nur
Operationen auf Datenstrukturen durchführen können, sondern analog zu den in Perl eingebauten
Funktionen eigene Rückgabewerte definieren dürfen. Ohne es bis jetzt explizit gesagt zu haben, haben
wir solche Rückgabewerte schon im ersten Beispiel verwendet:
my $zahl = 12; print erhoehe($zahl); # Ausgabe: 13 sub erhoehe{ my $zahl = shift; ++$zahl; }
In Perl ist es so, dass der Wert des zuletzt ausgewerteten Ausdrucks automatisch dorthin
zurückgegeben wird, wo die Subroutine aufgerufen wurde. Hier wurde also der aktuelle Wert von
$zahl aus der Subroutine an die Stelle des Aufrufs zurückgegeben und dient der print()-Funktion als
Argument. Für gewöhnlich geben wir aber den oder die Werte in eine Datenstruktur zurück, die mit
jener aus der Subroutine kompatibel ist. Darüber hinaus erhöht es die Lesbarkeit des Codes, wenn man
nicht diese implizite Form der Rückgabe, sondern eine durch das Schlüsselwort return() eingeleitete
Schreibweise einsetzt:
110
my $zahl = 12; $zahl = erhoehe($zahl); print $zahl; # Ausgabe: 13 sub erhoehe{ my $zahl = shift; ++$zahl; return $zahl; }
Genauso wie Skalare kann man natürlich auch Arrays und Hashes zurückgeben; zu beachten ist
allerdings wiederum, dass bei der Rückgabe mehrerer Datenstrukturen diese abermals zu einer Liste
vereinheitlicht werden. Wie wir dennoch mehrere Datenstrukturen zurückgeben können, sehen wir im
nächsten Kapitel.
Die Datenstruktur eines Rückgabewerts ist immer davon abhängig, ob die Datenstruktur im Aufruf
einen Skalar- oder einen Listenkontext erwartet:
my $anzahl_elemente = array_demo(); my @elemente = array_demo(); print "$anzahl_elemente\n"; # Ausgabe: 4 print "@elemente\n"; # Ausgabe: Dies ist ein Satz sub array_demo{ my @words = qw(Dies ist ein Satz); }
Im ersten Aufruf wird der Skalarkontext durch die Variable $anzahl_elemente erzwungen; wie
gewohnt gibt ein Array in einem Skalarkontext die Anzahl seiner Elemente zurück, d.h. in diesem Fall
die Anzahl der Elemente des Arrays in der Subroutine. Im zweiten Aufruf ist die Ziel-Datenstruktur für
den Rückgabewert ein Array, sodass hier die Elemente des Arrays in der Subroutine zurückgegeben
werden. Will man in einer Subroutine sicherstellen, dass ein Wert in einem bestimmten Kontext
zurückgegeben wird, prüft man mit der Funktion wantarray() ab, ob es sich bei der Datenstruktur im
Aufruf um eine Listenstruktur oder einen Skalar handelt – nur im ersten Fall liefert die Funktion true
zurück:
my $anzahl_elemente = array_demo(); my @elemente = array_demo(); print "$anzahl_elemente\n"; # Ausgabe: Skalar print "@elemente\n"; # Ausgabe: Liste sub array_demo{ my @words = qw(Dies ist ein Satz); unless(wantarray){ return "Skalar"; } else{ return "Liste"; } }
Sprachwissenschaftliches Institut
111
Der Funktion return() kann man auch eine leere Argumentliste übergeben. Im Skalarkontext
liefert sie dann undef, im Listenkontext eine leere Liste zurück:
my $satz1 = "Dies ist ein Satz"; my $satz2 = "Das ist ein Satz"; my @woerter1 = tokenize($satz1); my @woerter2 = tokenize($satz2); print "woerter1: @woerter1\n"; # Ausgabe: woerter1: print "woerter2: @woerter2\n"; # Ausgabe: woerter2: Das ist ein
Satz sub tokenize{ my $satz = shift; return unless($satz =~/^Das/); my @woerter = split(/ /, $satz); }
In diesem Beispiel wird eine leere Liste an @woerter1 zurückgegeben, da die in der Subroutine
formulierte Bedingung durch den ersten Satz nicht erfüllt wird. Man muss sich klar machen, dass damit
auch die weitere Verarbeitung der Anweisungen innerhalb der Subroutine abbricht, denn return()
bedeutet für den Programmfluss "nimm die Argumentliste und kehre dahin zurück, wo diese
Subroutine aufgerufen wurde":
my ($zahl1, $zahl2) = multipliziere(); print "$zahl1 $zahl2\n"; # Ausgabe: Use of uninitialized
value, 9 sub multipliziere{ my $produkt1 = 3*3; my $produkt2 = 4*4; return $produkt1; return $produkt2; }
Die zweite return()-Anweisung wird nie erreicht, weshalb $zahl2 nicht initialisiert werden kann!
8.4 Zusammenfassung Subroutinen bieten die Möglichkeit, den Quellcode besser zu strukturieren und gleiche Operationen
unabhängig von Datenstrukturen auszuführen. Einer Subroutine kann man genauso wie einer Perl-
internen Funktion Argumente übergeben und ebenso Rückgabewerte in entsprechenden
Datenstrukturen weiterverarbeiten.
Die im Aufruf einer Subroutine als Liste übergebenen Argumente sind dann innerhalb der
Subroutine aus dem Array @_ zugreifbar, das nichts mit der Skalarvariablen $_ zu tun hat. Auf @_
sollten keine anderen Operationen als das Lesen der Elemente ausgeführt werden, da die Argumentliste
per Referenz an die Subroutine übergeben wird, weshalb man auf unerwünschte Seiteneffekte stoßen
kann, wenn man die Werte aus @_ direkt manipuliert. Zwar kann man einer Subroutine mehrere, auch
112
unterschiedliche, Datenstrukturen übergeben, doch muss man sich bewusst sein, dass dabei die Werte
zu einer Liste vereinheitlicht werden.
Das gleiche passiert auch, wenn man mehrere Datenstrukturen aus einer Subroutine zurückgeben
will. Generell kann man mit der return()-Funktion Werte an den Ort des Aufrufs der Subroutine in
eine entsprechende Datenstruktur zurückgeben. Implizit geschieht dies auch ohne die Angabe von re-
turn(), indem der zuletzt ausgewertete Ausdruck automatisch zurückgegeben wird. Gibt man
return() keine Argumente mit, so liefert diese Funktion undef zurück, wenn beim Aufruf ein skalarer
Rückgabewert erwartet wurde, im Falle eines listenwertigen Rückgabewerts eine leere Liste. Innerhalb
einer Subroutine lässt sich anhand der Funktion wantarray() überprüfen, ob ein Skalar oder eine Liste
als Rückgabewert erwartet wird.
8.5 Beispielanwendung Zerlegt man die Anwendung zur Berechnung von Uni-, Bi-, Tri- und Tetragrammen in ihre
Bestandteile, können wir auf der Ebene der höchsten Abstraktion gut eine Handvoll Funktionen
ausmachen, die das Grundgerüst unserer Anwendung bilden: Zunächst lesen wir das Korpus ein und
legen es in einer von uns verarbeitbaren Datenstruktur ab; da wir diese in den weiteren Schritten
zerstören, benötigen wir noch einige Kopien davon. Dann folgt die Berechnung und Ausgabe der
Token- und Typehäufigkeiten sowie deren relative Häufigkeiten.
Das Einlesen des Korpus gliedert sich in das eigentliche Einlesen der Datei und
Vorverarbeitungsschritte. Ebenso lassen sich die n-Gramm-Berechnungen in eigene Funktionseinheiten
unterteilen: Auf die Speicherung eines n-Gramms mit seiner jeweiligen Häufigkeit in einem Hash folgt
die Eliminierung der Stoppworte und die Zählung der n-Gramm Types, die Suche nach den n-
Grammen, die jeweils nur einmal vorkommen, die Berechnungen zum Anteil aller jeweiligen n-
Gramme am Gesamtkorpus und die Berechnung der relativen Häufigkeiten der zehn prominentesten n-
Gramme. Daraus ergibt sich folgende Graphik:
Sprachwissenschaftliches Institut
113
Bevor wir uns die Struktur des Programms näher anschauen, erzeugen wir zunächst eine globale
Hash-Konstante, in der die Zahlen der n-Gramme auf griechischen Zahlen-Vorsilben abgebildet
werden:
my %GRIECHISCHE_ZAHLEN = ( 1 => "Uni", 2 => "Bi", 3 => "Tri", 4 => "Tetra", 5 => "Penta", 6 => "Hexa", 7 => "Hepta", 8 => "Okta", 9 => "Ennea", 10 => "Deka" );
Diese verwenden wir später, um die Bezeichnungen aus den als Argumenten übergebenen Zahlen
interpolieren zu können.
Das "Hauptprogramm" besteht des Weiteren aus den globalen Variablen, in denen wir die Liste der
Wörter speichern und die Größe des Vokabulars. Allerdings verwenden wir nur letztere als globale
Variable; das Array mit der Wörterliste und seine Kopien dienen jeweils den Subroutinen zur
Berechnung der n-Gramme und werden in diesen lokal verarbeitet:
my @token = korpus_einlesen("dostojevski.tok"); my @token2 = @token; my @token3 = @token; my $vokabular = unigramme_berechnen(@token); bigramme_berechnen(@token); trigramme_berechnen(@token2); tetragramme_berechnen(@token3);
Das Einlesen des Korpus ist recht einfach: Beim Aufruf übergeben wir den Dateinamen als
Argument, das in seiner lexikalischen Form wie gewohnt der open()-Funktion übergeben wird. Jede
eingelesene Zeile wird der Subroutine korpus_vorverarbeiten() übergeben, deren Rückgabewert ein
Array der Wörter ist. Diese Liste wird wiederum auf ein Array geschrieben, das schlussendlich an die
Stelle des Subroutinenaufrufs zurückgegeben wird:
sub korpus_einlesen { my @token; my $dateiname = shift; open( IN, $dateiname ) or die $!; while (<IN>) { push( @token, korpus_vorverarbeiten($_) ); } return @token; }
114
Die Subroutine korpus_vorverarbeiten() leistet die Ersetzungen von Sonderzeichen und
überflüssigem Leerraum und sorgt dafür, dass Klitika als ein Token verarbeitet werden:
sub korpus_vorverarbeiten { my $zeile = shift; chomp($zeile); next if ( $zeile =~ /^\s+$/ ); $zeile =~ s/\s{2,}/ /g; $zeile =~ s/\s'([cdlrstv])/'$1/g; $zeile =~ s/^\"\s//; $zeile =~ s/\s[.?!\":;,$%&-\/()]+//g; return @token; }
Im nächsten Schritt berechnen wir zunächst die Häufigkeiten der Token und Types. Da sich die
Unigramme bezüglich der Berechnung des n-Gramm-Raums anders verhalten als die übrigen n-
Gramme, bzw. wir eine andere Ausgabe für sie vorsehen, unterscheidet sich die Implementation dieser
Subroutine von den übrigen:
sub unigramme_berechnen { my @token = @_; my $unigramm_hl; my %unigramm_haeufigkeit; my $anzahl_token = @token; print "Korpus im Umfang von $anzahl_token Token eingelesen!\n\n"; foreach (@token) { $unigramm_haeufigkeit{$_}++; } %unigramm_haeufigkeit =
stoppworte_herausfiltern(%unigramm_haeufigkeit); my $anzahl_types = keys %unigramm_haeufigkeit; print "Die Größe des Vokabulars beträgt $anzahl_types.\n"; $unigramm_hl = hapax_legomena_berechnen(%unigramm_haeufigkeit); print sprintf( "%.2f", ( ( $unigramm_hl / $anzahl_types ) * 100 ) ), "% aller Types kommt nur einmal im Korpus vor!\n"; relative_haeufigkeit_berechnen( $anzahl_token, 1,
%unigramm_haeufigkeit ); return $anzahl_types; }
Neu sind die Filterung der Stoppwörter sowie die Berechnung der Hapax Legomena und der
relativen Häufigkeiten in eigenen Subroutinen. Da wir über diese noch sprechen werden, seien hier nur
ihre Aufrufe erklärt: Zunächst übergeben wir der Stoppwort-Filterung den Hash mit den n-Grammen
und ihren Häufigkeiten, den wir dann in verringertem Umfang aus der Subroutine zurückgegeben
bekommen. Die Hapax Legomena sind ja jeweils Bestandteile der Hashes, in denen die n-Gramm-
Häufigkeiten gespeichert sind; diese übergeben wir an die entsprechende Subroutine. Die
Sprachwissenschaftliches Institut
115
Parameterliste der Subroutine relative_haeufigkeit_berechnen() ist ein wenig komplexer, da sie
aus der jeweiligen Anzahl der Token, der Angabe der Wertigkeits des "n"s des n-Gramms und dem
oben erwähnten Hash mit Häufigkeiten besteht. Obschon wir für diesen Aufruf mehrere
Datenstrukturen verwenden, benötigen wir noch keine Referenzen, da es sich bei den Argumenten
zunächst um Skalare handelt, denen ein Hash folgt, sodass wir diese Strukturen gut auseinander halten
können.
Bevor wir uns der Funktionalität dieser Subroutinen widmen, schauen wir uns aber die Subroutinen
zur Berechnung der weiteren n-Gramme an. In ihnen gibt es zusätzlich einen Aufruf zur Berechnung
der Token-/Typehäufigkeiten. Da dieser eine Parameterliste besitzt, die ausschließlich aus Skalaren
besteht, ist er eher unproblematisch. Auch können wir die Generierung und Zählung der n-Gramme
abstrahieren, da wir dazu lediglich das "n" und das jeweilige Array mit den Token übergeben und den
Skalar mit der Anzahl der n-Gramme sowie den Hash mit den n-Grammen und ihren Häufigkeiten
zurückgeben müssen. Die Struktur dieser Subroutinen sei wiederum am Beispiel der Berechnung der
Tetragramme illustriert:
sub tetragramme_berechnen { my @token = @_; my ( $tetragramm_hl, $anzahl_tetragramme ); my %tetragramm_haeufigkeit; ( $anzahl_tetragramme, %tetragramm_haeufigkeit ) = n_gramme_zaehlen(
4, @token ); %tetragramm_haeufigkeit =
stoppworte_herausfiltern(%tetragramm_haeufigkeit); my $tetragramm_types = keys %tetragramm_haeufigkeit; $tetragramm_hl = hapax_legomena_berechnen(%tetragramm_haeufigkeit); token_type_haeufigkeiten( $anzahl_tetragramme, $tetragramm_types, $vokabular, $tetragramm_hl, 4 ); relative_haeufigkeit_berechnen( $anzahl_tetragramme, 4, %tetragramm_haeufigkeit ); }
In der Subroutine zur Filterung der Stoppwörter iterieren wir wie bereits gesehen über den als
Argument übergebenen Hash aus n-Grammen und ihren Häufigkeiten, wobei wir bei jeder Iteration
über die Stoppwort-Liste schauen, ob eines davon im jeweiligen n-Gramm enthalten ist. Der
Rückgabewert ist der gefilterte Hash:
sub stoppworte_herausfiltern { my @stoppwort_liste = qw(a an and the this that as at with by to for from of on in out
up I he she it we you they me my his her him our your us them their be am is are was were has have had do does did can could will would all no not but so what or if yes);
my %n_gramm_haeufigkeit = @_;
116
foreach(keys %n_gramm_haeufigkeit){ foreach my $stoppwort (@stoppwort_liste) { delete($n_gramm_haeufigkeit{$_}) if ( /\b$stoppwort\b/i ); next; } } return %n_gramm_haeufigkeit; }
Die Subroutine zur Berechnung der Hapax Legomena nimmt diesen Hash als Argument, berechnet
daraus die Anzahl der Elemente mit der Häufigkeit 1 und gibt diese zurück:
sub hapax_legomena_berechnen { my $n_gramm_hl; my %n_gramm_haeufigkeit = @_; foreach ( keys %n_gramm_haeufigkeit ) { $n_gramm_hl++ if ( $n_gramm_haeufigkeit{$_} == 1 ); } return $n_gramm_hl; }
In der Subroutine relative_haeufigkeit_berechnen() machen wir uns zum ersten Mal den Hash
mit den Konstanten für griechische Zahlenpräfixe zunutze: Wann immer wir eine solche Bezeichnung
ausgeben wollen, interpolieren wir sie aus dem Wert des Hashes und konkatenieren sie entsprechend:
sub relative_haeufigkeit_berechnen { my $anzahl_n_gramme = shift; my $bezeichner = shift; my %n_gramm_haeufigkeit = @_; my $i = 0; print "\nDie zehn häufigsten " . $GRIECHISCHE_ZAHLEN{$bezeichner} . "gramme sind:\n"; foreach ( sort { $n_gramm_haeufigkeit{$b} <=> $n_gramm_haeufigkeit{$a} } keys %n_gramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $n_gramm_haeufigkeit{$_} / $anzahl_n_gramme ) * 100 ); print "$_: $n_gramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller $GRIECHISCHE_ZAHLEN{$bezeichner}gramm-Token.\n"; last if ( $i > 10 ); $i++; } }
Sprachwissenschaftliches Institut
117
Diese Möglichkeit schöpfen wir in der Subroutine token_type_haeufigkeit() noch weiter aus: Da
der Exponent zur Berechnung des möglichen n-Gramm-Raums immer dem "n" entspricht, können wir
hier dieses numerische Argument sowohl zur Berechnung als auch für die Interpolation in der Ausgabe
einsetzen:
sub token_type_haeufigkeiten { my $anzahl_token = shift; my $anzahl_types = shift; my $groesse_vokabular = shift; my $n_gramm_hl = shift; my $exponent = shift; # "n" des n-Gramms zur Berechnung des n-
Gramm-Raums print "\nEs gibt $anzahl_token $GRIECHISCHE_ZAHLEN{$exponent}gramm-Token und
$anzahl_types $GRIECHISCHE_ZAHLEN{$exponent}gramm-Types.\nDas ergibt "
. ( $vokabular**$exponent ) . " mögliche $GRIECHISCHE_ZAHLEN{$exponent}gramme, von denen " . ( ( $anzahl_types / $vokabular**$exponent ) * 100 ) . "%\nim Dokument vorkommen.\n"; print sprintf( "%.2f", ( ( $n_gramm_hl / $anzahl_types ) * 100 ) ) . "% aller $GRIECHISCHE_ZAHLEN{$exponent}gramm-Types kommt nur
einmal im Korpus vor!\n"; }
Eine substantielle Verbesserung stellt die Zählung der n-Gramme dar: In dieser Subroutine
verallgemeinern wir den Prozess, der strukturell für alle n-Gramme gleich ist und einen nicht
unbeträchtlichen Raum im Programm einnahm. Algorithmisch betrachtet, verläuft die Berechnung der
n-Gramme so, dass wir solange über das Array der Token iterieren, wie n-1 Token vorhanden sind.
Innerhalb der Schleife extrahieren wir n Token in ein Array, aus dem wir wie gewohnt unser n-Gramm
durch Zusammenfügung erzeugen, das wiederum in einem Hash gezählt wird. Daraufhin entfernen wir
das erste Element dieses Arrays und fügen den Rest vorne an unser Token-Array an. Zurückgegeben
wird wie gesagt die Anzahl der n-Gramme und der Hash mit den jeweiligen Häufigkeiten:
sub n_gramme_zaehlen{ my $n = shift; my @token = @_; my %n_gramm_haeufigkeit; my $anzahl_n_gramme; while ( @token > $n-1 ) { my @n_gramm = splice( @token, 0, $n ); my $n_gramm = join(" ", @n_gramm); $n_gramm_haeufigkeit{$n_gramm}++; shift(@n_gramm); unshift( @token, @n_gramm ); $anzahl_n_gramme++; } return( $anzahl_n_gramme, %n_gramm_haeufigkeit ); }
118
Hier das gesamte Programm:50
use strict; use diagnostics; my %GRIECHISCHE_ZAHLEN = ( 1 => "Uni", 2 => "Bi", 3 => "Tri", 4 => "Tetra", 5 => "Penta", 6 => "Hexa", 7 => "Hepta", 8 => "Okta", 9 => "Ennea", 10 => "Deka" ); my @token = korpus_einlesen("dostojevski.tok"); my @token2 = @token; my @token3 = @token; my $vokabular = unigramme_berechnen(@token); bigramme_berechnen(@token); trigramme_berechnen(@token2); tetragramme_berechnen(@token3); sub korpus_einlesen { my @token; my $dateiname = shift; open( IN, $dateiname ) or die $!; while (<IN>) { push( @token, korpus_vorverarbeiten($_) ); } return @token; } sub korpus_vorverarbeiten { my $zeile = shift; chomp($zeile); next if ( $zeile =~ /^\s+$/ ); $zeile =~ s/\s{2,}/ /g; $zeile =~ s/\s'([cdlrstv])/'$1/g; $zeile =~ s/^\"\s//; $zeile =~ s/\s[.?!\":;\,$%&-\/()\[\]]+//g; my @token=split(/ /,$zeile); return @token; }
50 Da sich die Ausgabe nicht verändert hat, verzichten wir darauf, sie an dieser Stelle abermals wiederzugeben.
Sprachwissenschaftliches Institut
119
sub stoppworte_herausfiltern { my @stoppwort_liste = qw(a an and the this that as at with by to for from of on in out up I
he she it we you they me my his her him our your us them their be am is are was were has have had do does did can could will would all no not but so what or if yes);
my %n_gramm_haeufigkeit = @_; foreach(keys %n_gramm_haeufigkeit){ foreach my $stoppwort (@stoppwort_liste) { delete($n_gramm_haeufigkeit{$_}) if ( /\b$stoppwort\b/i ); next; } } return %n_gramm_haeufigkeit; } sub unigramme_berechnen { my @token = @_; my $unigramm_hl; my %unigramm_haeufigkeit; my $anzahl_token = @token; print "Korpus im Umfang von $anzahl_token Token eingelesen!\n\n"; foreach (@token) { $unigramm_haeufigkeit{$_}++; } %unigramm_haeufigkeit =
stoppworte_herausfiltern(%unigramm_haeufigkeit); my $anzahl_types = keys %unigramm_haeufigkeit; print "Die Größe des Vokabulars beträgt $anzahl_types.\n"; $unigramm_hl = hapax_legomena_berechnen(%unigramm_haeufigkeit); print sprintf( "%.2f", ( ( $unigramm_hl / $anzahl_types ) * 100 ) ), "% aller Types kommt nur einmal im Korpus vor!\n"; relative_haeufigkeit_berechnen( $anzahl_token, 1,
%unigramm_haeufigkeit ); return $anzahl_types; }
120
sub bigramme_berechnen { my @token = @_; my ( $bigramm_hl, $anzahl_bigramme ); my ( %bigramm_haeufigkeit, %anzahl_bigramme ); ( $anzahl_bigramme, %bigramm_haeufigkeit ) = n_gramme_zaehlen( 2,
@token ); %bigramm_haeufigkeit = stoppworte_herausfiltern(%bigramm_haeufigkeit); my $bigramm_types = keys %bigramm_haeufigkeit; $bigramm_hl = hapax_legomena_berechnen(%bigramm_haeufigkeit); token_type_haeufigkeiten( $anzahl_bigramme, $bigramm_types,
$vokabular, $bigramm_hl, 2 ); relative_haeufigkeit_berechnen( $anzahl_bigramme, 2,
%bigramm_haeufigkeit ); } sub trigramme_berechnen { my @token = @_; my ( $trigramm_hl, $anzahl_trigramme ); my %trigramm_haeufigkeit; ( $anzahl_trigramme, %trigramm_haeufigkeit ) = n_gramme_zaehlen( 3,
@token ); %trigramm_haeufigkeit =
stoppworte_herausfiltern(%trigramm_haeufigkeit); my $trigramm_types = keys %trigramm_haeufigkeit; $trigramm_hl = hapax_legomena_berechnen(%trigramm_haeufigkeit); token_type_haeufigkeiten( $anzahl_trigramme, $trigramm_types,
$vokabular, $trigramm_hl, 3 ); relative_haeufigkeit_berechnen( $anzahl_trigramme, 3, %trigramm_haeufigkeit ); }
Sprachwissenschaftliches Institut
121
sub tetragramme_berechnen { my @token = @_; my ( $tetragramm_hl, $anzahl_tetragramme ); my %tetragramm_haeufigkeit; ( $anzahl_tetragramme, %tetragramm_haeufigkeit ) = n_gramme_zaehlen(
4, @token ); %tetragramm_haeufigkeit =
stoppworte_herausfiltern(%tetragramm_haeufigkeit); my $tetragramm_types = keys %tetragramm_haeufigkeit; $tetragramm_hl = hapax_legomena_berechnen(%tetragramm_haeufigkeit); token_type_haeufigkeiten( $anzahl_tetragramme, $tetragramm_types, $vokabular, $tetragramm_hl, 4 ); relative_haeufigkeit_berechnen( $anzahl_tetragramme, 4, %tetragramm_haeufigkeit ); } sub hapax_legomena_berechnen { my $n_gramm_hl; my %n_gramm_haeufigkeit = @_; foreach ( keys %n_gramm_haeufigkeit ) { $n_gramm_hl++ if ( $n_gramm_haeufigkeit{$_} == 1 ); } return $n_gramm_hl; } sub relative_haeufigkeit_berechnen { my $anzahl_n_gramme = shift; my $bezeichner = shift; my %n_gramm_haeufigkeit = @_; my $i = 0; print "\nDie zehn häufigsten " . $GRIECHISCHE_ZAHLEN{$bezeichner} . "gramme sind:\n"; foreach ( sort { $n_gramm_haeufigkeit{$b} <=> $n_gramm_haeufigkeit{$a} } keys %n_gramm_haeufigkeit ) { my $relative_haeufigkeit = sprintf( "%.5f", ( $n_gramm_haeufigkeit{$_} / $anzahl_n_gramme ) * 100 ); print "$_: $n_gramm_haeufigkeit{$_} Das sind " . $relative_haeufigkeit . "% aller $GRIECHISCHE_ZAHLEN{$bezeichner}gramm-Token.\n"; last if ( $i > 10 ); $i++; }
122
} sub token_type_haeufigkeiten { my $anzahl_token = shift; my $anzahl_types = shift; my $groesse_vokabular = shift; my $n_gramm_hl = shift; my $exponent = shift; # "n" des n-Gramms zur Berechnung des n-
Gramm-Raums print "\nEs gibt $anzahl_token $GRIECHISCHE_ZAHLEN{$exponent}gramm-Token und
$anzahl_types $GRIECHISCHE_ZAHLEN{$exponent}gramm-Types.\nDas ergibt "
. ( $vokabular**$exponent ) . " mögliche $GRIECHISCHE_ZAHLEN{$exponent}gramme, von denen " . ( ( $anzahl_types / $vokabular**$exponent ) * 100 ) . "%\nim Dokument vorkommen.\n"; print sprintf( "%.2f", ( ( $n_gramm_hl / $anzahl_types ) * 100 ) ) . "% aller $GRIECHISCHE_ZAHLEN{$exponent}gramm-Types kommt nur
einmal im Korpus vor!\n"; } sub n_gramme_zaehlen{ my $n = shift; my @token = @_; my %n_gramm_haeufigkeit; my $anzahl_n_gramme; while ( @token > $n-1 ) { my @n_gramm = splice( @token, 0, $n ); my $n_gramm = join(" ", @n_gramm); $n_gramm_haeufigkeit{$n_gramm}++; shift(@n_gramm); unshift( @token, @n_gramm ); $anzahl_n_gramme++; } return( $anzahl_n_gramme, %n_gramm_haeufigkeit ); }
Sprachwissenschaftliches Institut
123
9 Referenzen
Sanity is only that which is within the frame of reference of conventional
thought.
ERICH FROMM
Aus den letzten Kapiteln ergaben sich die Desiderate, einerseits zusammengehörige komplexe
Daten in eine einzelne Datenstruktur schreiben zu können, andererseits wollen wir mehrere Variablen,
die über Skalare hinaus gehen, als Argumente an Subroutinen übergeben bzw. aus Subroutinen
zurückgeben können. In beiden Fällen verhindert die Prämisse, dass die Bestandteile von
Datenstrukturen nur Skalare sein dürfen, eine einfache Lösung: Wir können weder Arrays oder Hashes
ineinander einbetten, um eine komplexe Datenstruktur aufzubauen, noch können wir mehrere Arrays
oder Hashes an Subroutinen übergeben oder aus diesen zurückgeben, ohne dass sie zu einer einzigen
Listenstruktur vereinheitlicht würden. Wir benötigen also eine spezielle Art von Skalaren, die es uns
ermöglicht, diese Operationen durchzuführen.
Solche Skalare nennt man Referenzen. Ihre Funktionalität besteht darin, auf andere
Datenstrukturen51 zu zeigen. Diese Datenstrukturen können wiederum Skalare, Arrays oder Hashes
sein. Konzeptuell kann man sich eine Referenz wie einen Eintrag im Index eines Buches vorstellen:
Das Stichwort verweist auf eine Seite im Buch, auf der man die gewünschte Information finden kann.
Aber ebenso wenig, wie das Stichwort die Information selbst ist, stellt solch ein Skalar die referenzierte
Datenstruktur dar, sondern einen Verweis auf den Speicherort des Werts dieser Struktur:52
9.1 Erzeugen einer Referenz Die einfachste Möglichkeit, eine Referenz zu erzeugen, besteht darin, der Datenstruktur, auf die
man verweisen will, einen Backslash voranzustellen. Will man also eine Referenz auf einen Skalar
erzeugen, weist man einem Skalar eine originäre Skalarvariable zu, der man einen Backslash
voranstellt:
my $skalar = 42; my $skalar_referenz = \$skalar;
51 Wir werden im Laufe dieses Kapitels noch sehen, dass dies auch für Subroutinen gilt! 52 Anders als in anderen Programmiersprachen, sind Dinge wie Speicherarithmetik o.Ä. weder nötig noch möglich!
124
Analog dazu sieht die Erzeugung einer Arrayreferenz so aus:
my @marxes = qw(Chico Harpo Groucho Gummo Zeppo); my $marxes_referenz = \@marxes;
Und einen Hash referenziert man so:
my %woerterbuch = (apple => "pomme", pear => "poire"); my $woerterbuch_referenz = \%woerterbuch;
Da Referenzen einfache Skalare sind, können wir sie z.B. zu Listen in Arrays zusammenfassen:
my $ziffer_eins = 3; my $ziffer_zwei = 4; my $ziffer_drei = 5; my @ziffern_referenzen = (\$ziffer_eins, \$ziffer_zwei, \$ziffer_drei); #
oder... my @ziffern_referenzen = \($ziffer_eins, $ziffer_zwei, $ziffer_drei); Genauso können wir Referenzen – in diesem Fall Arrayreferenzen – in einen Hash einfügen:
my @english = qw(January February March April); my @french = qw(Janvier Fevrier Mars Avril); my %dictionary = (english => \@english, french => \@french);
Referenzen dürfen zwar rein technisch auch Schlüssel in einem Hash sein, man sollte sie allerdings
nicht so verwenden, da sie mit dem use strict-Pragma kollidieren.
Aus obigem Beispiel ergibt sich, dass wir analog zur Einbettung von Arrays in Hashes auch Arrays
in Arrays speichern können:
my @array1 = (10, 20, 30, 40); my @array2 = (1, 2, \@array1, 3, 4);
Da sich diese Operation beliebig oft wiederholen lässt, können wir auf diese Weise ineinander
geschachtelte Arrays aufbauen:
my @array3 = (2, 4, \@array2, 6, 8); my @array4 = (100, 200, \@array3, 300, 400);
Wir erzeugen also folgende komplexe Datenstruktur:
Sprachwissenschaftliches Institut
125
Erzeugen anonymer Datenstrukturen
Ein weiteres Merkmal von Referenzen besteht darin, dass sie den Aufbau anonymer
Datenstrukturen ermöglichen. Dies bedeutet, dass man mit Datenstrukturen operieren kann, denen kein
Variablenname zugeordnet ist. Da dies nur auf Arrays und Hashes anwendbar ist, betrachten wir hier
nur die Alternativen zu obigen Beispielen.
Will man statt eines benannten Arrays eine Referenz auf ein anonymes Array erzeugen, zeigt man
die Arrayreferenz durch eckige Klammern [ ] an:
my $ziffern_referenz = [1, 2, 3, 4, 5];
Analog dazu erzeugt man einen anonymen Hash durch die Verwendung geschweifter Klammern
{}:
my $woerterbuch_referenz = {apple => "pomme", pear => "poire" };
Anonyme Datenstrukturen lassen sich genauso in Arrays und Hashes einfügen, wie Referenzen, die
auf benannte Datenstrukturen verweisen:
my %dictionary = (english => [January, February, March, April], french => [Janvier, Fevrier, Mars, Avril] );
Und ebenso, wie wir die benannten Arrays ineinander geschachtelt haben, können wir dies auch mit
anonymen Arrays tun. Wollen wir die Struktur aus obigem Beispiel nachbilden, müssen wir allerdings
darauf achten, dass zunächst die Elemente aus @array4, dann an entsprechender Stelle jeweils die Ele-
mente aus @array3, @array2 und @array1 stehen:
my @array = (100, 200, [2, 4, [1, 2, [10, 20, 30, 40], 3, 4], 6, 8], 300, 400);
126
Eine solche Datenstruktur ist sehr unübersichtlich! Um dennoch überblicken zu können, wie diese
komplexe Struktur aufgebaut ist, bedient man sich des Moduls Data::Dumper, das sich mit use
Data::Dumper; genauso in Programme einbinden lässt, wie die Pragmata strict und diagnostics.
Um dieses Modul zu verwenden, schreibt man das Wort Dumper ähnlich einem Dateideskriptor
zwischen die print()-Funktion und die auszugebende Datenstruktur:
use strict; use diagnostics; use Data::Dumper; my @array = (100, 200, [2, 4, [1, 2, [10, 20, 30, 40], 3, 4], 6, 8], 300,
400); print Dumper @array; # Ausgabe: # $VAR1 = 100; # $VAR2 = 200; # $VAR3 = [ # 2, # 4, # [ # 1, # 2, # [ # 10, # 20, # 30, # 40 # ], # 3, # 4 # ], # 6, # 8 # ]; # $VAR4 = 300; # $VAR5 = 400;
Da es sich bei Data::Dumper um ein sehr nützliches Werkzeug handelt, das bei der Arbeit mit
Referenzen fast unerlässlich ist, sollte man es ab sofort immer in die Programme einbinden!53
9.2 Verwendung von Referenzen Um Datenstrukturen dereferenzieren zu können, kennt Perl drei syntaktische Varianten, die aber
alle dieselbe Funktionalität besitzen, nämlich die einzelnen Datenstrukturen zugreifbar zu machen. Wir
konzentrieren uns zunächst auf eine dieser Schreibweisen, um das Konzept der Verwendung von
Referenzen klarzumachen, und werden dann die Alternativen vorstellen.
Will man Datenstrukturen dereferenzieren, schließt man den Variablennamen der Referenz in
geschweifte Klammern {} ein und schreibt davor das Präfixsymbol der referenzierten Datenstruktur.
53 Im Emacs lässt sich dazu der letzte Block der _emacs-Datei derart modifizieren, dass diejenige Zeile, in der die
Modulnamen festgelegt werden, so aussieht: (setq modules (list "strict" "diagnostics" "Data::Dumper"))
Sprachwissenschaftliches Institut
127
Will man also einen Skalar dereferenzieren, schreibt man ein Dollar-Zeichen $ vor die Variable, bei
einem Array einen Klammeraffen @ und bei einem Hash ein Prozentzeichen %:
Dereferenzieren eines Skalars:
my $skalar = 42; my $skalar_referenz = \$skalar; my $skalar2 = ${$skalar};
Dereferenzieren eines Arrays:
my @ziffern = (1, 2, 3, 4, 5); my $ziffern_referenz = \@ziffern; my @ziffern2 = @{$ziffern_referenz};
Dereferenzieren eines Hashes:
my %woerterbuch = (apple => "pomme", pear => "poire"); my $woerterbuch_referenz = \%woerterbuch; my %woerterbuch2 = %{$woerterbuch_referenz};
Auf Referenzen lassen sich alle Operationen ausführen, die man auch auf normale Datenstrukturen
anwenden kann:
my @ziffern = (1, 2, 3, 4, 5); my $ziffern_referenz = \@ziffern; print "Das dereferenzierte Array: @{$ziffern_referenz}\n"; foreach(@{$ziffern_referenz}){ print "Element $_\n"; } # Ausgabe: # Das dereferenzierte Array: 1 2 3 4 5 # Element 1 # Element 2 # Element 3 # Element 4 # Element 5
Um auf ein bestimmtes Element dieser Arrayreferenz zugreifen zu können, gelten die gleichen
Spielregeln, wie wir sie von normalen Arrays gewohnt sind: Der Zugriff erfolgt über einen Index, der
in eckigen Klammern steht; da der Rückgabewert aber ein Skalar ist, verwenden wir für die
Dereferenzierung keinen Klammeraffen, sondern ein Dollar-Zeichen:
print "Das dritte Element ist ${$ziffern_referenz}[2]\n"; # Ausgabe: Das dritte Element ist 3
Welche Ausgabe erhalten wir, wenn wir die Referenz direkt ausgeben?
128
print "So sieht die Referenz aus: @ziffern_referenz\n"; # Ausgabe: So sieht die Referenz aus: ARRAY(0x80a2b8)
Wie bereits gesagt, verweist die Referenz auf den Speicherplatz des Werts der entsprechenden
Datenstruktur. Man muss also immer daran denken, die Datenstruktur zu dereferenzieren, bevor man
sie verwendet!
Modifiziert man den Wert einer Referenz, verändert sich auch der Wert der Datenstruktur, auf die
verwiesen wird:
my @band = qw (Gahan Gore Fletcher Wilder); my $band_referenz = \@band; print "Gruppenmitglieder vorher: @band\n"; # Ausgabe: Gahan Gore
Fletcher Wilder pop(@{$band_referenz}); print "Gruppenmitglieder nachher: @band\n"; # Ausgabe: Gahan Gore Fletcher
Das gleiche gilt im Fall mehrerer anonymer Referenzen:
my $band_referenz = [qw(Gahan Gore Fletcher Wilder)]; my $band_referenz2 = $band_referenz; print "Gruppenmitglieder vorher: @band_referenz\n"; # Ausgabe: Gahan
Gore Fletcher Wilder pop(@{$band_referenz2}); print "Gruppenmitglieder nachher: @band_referenz\n"; # Ausgabe: Gahan
Gore Fletcher
Durch die Kopie ist auch $band_referenz2 eine Referenz auf das anonyme Array, in dem die
Namen der Musiker stehen:
Sprachwissenschaftliches Institut
129
Greift man über diese Referenz auf die Datenstruktur zu, passiert das gleiche, als hätte man die
Werte über $band_referenz manipuliert! Dies lässt sich auch an einzelnen Daten eines anonymen
Arrays nachvollziehen:
my $zahlen_referenz = [68, 101, 114, 111, 117]; print "Vorher: @{$zahlen_referenz}\n"; ${$zahlen_referenz}[0] = 100; print "Nachher: @{$zahlen_referenz}\n"; # Ausgabe: # Vorher: 68 101 114 111 117 # Nachher: 100 101 114 111 117
Um die Verarbeitung ineinander geschachtelter Datenstrukturen zu demonstrieren, erzeugen wir ein
simples Konkordanzwörterbuch der Sprachen Deutsch, Englisch und Französisch, in dem die Wörter
"Haus", "Auto" und "Kind" und ihre fremdsprachigen Entsprechungen abgelegt sind:
my %dictionary; my @german = qw(Haus Auto Kind); my @english = qw(house car child); my @french = qw(maison voiture enfant); foreach(@german){ $dictionary{$_} = {english => shift(@english), french => shift(@french) }; }
130
Dazu legen wir einen Hash an, dessen Schlüssel die deutschsprachigen Wörter sind. Die Werte zu
diesen Schlüsseln sind wiederum Hashes, deren Schlüssel entweder das Wort "english" oder "french"
ist. Deren Werte sind dann die entsprechenden Übersetzungen, sodass wir folgende Datenstruktur
erhalten:
$VAR1 = 'Kind'; $VAR2 = { 'french' => 'enfant', 'english' => 'child' }; $VAR3 = 'Haus'; $VAR4 = { 'french' => 'maison', 'english' => 'house' }; $VAR5 = 'Auto'; $VAR6 = { 'french' => 'voiture', 'english' => 'car' }; Um auf diese Informationen zugreifen zu können, iterieren wir zunächst wie gewohnt über die
Schlüssel des zugrunde liegenden Hashes, d.h. der deutschen Wörter:
foreach(keys %dictionary){ print "$_ => ${$dictionary{$_}}{english}
${$dictionary{$_}}{french}\n"; }
An die englischen bzw. französischen Pendants gelangen wir, indem wir zunächst den Wert des
deutschen Schlüssels durch ${$dictionary{$_}} dereferenzieren und dann den entsprechenden
Schlüssel des eingebetteten Hashes wie gewohnt auswählen. Dementsprechend erhalten wir folgende
Ausgabe:
# Kind => child enfant # Haus => house maison # Auto => car voiture
Nicht ganz unproblematisch (aber logisch) ist die Klammerung der verschiedenen Elemente, die
schnell unübersichtlich werden kann. Als zweite syntaktische Variante der Dereferenzierung können
die geschweiften Klammern um den Bezeichner der Referenz wegfallen:
@{$array_referenz} => @$array_referenz %{$hash_referenz} => %$hash_referenz
Da auch diese Variante nur bedingt lesbarer erscheint, lassen sich Werte auch über die Pfeilnotation
-> dereferenzieren:
${$array_referenz}[0] => $array_referenz->[0] ${$hash_referenz}{$_} => $hash_referenz->{$_}
Dadurch lässt sich die Ausgabe im vorherigen Beispiel folgendermaßen entwirren:
Sprachwissenschaftliches Institut
131
foreach(keys %dictionary){ print "$_ => $dictionary{$_}->{english} $dictionary{$_}->{french}\n"; }
9.3 Komplexe Datenstrukturen Bevor wir den Einsatz von Referenzen bei Subroutinenargumenten diskutieren, betrachten wir
zunächst die Möglichkeit, Referenzen zur Erzeugung komplexer Datenstrukturen einzusetzen. Wie
bereits im letzten Abschnitt skizziert, lassen sich durch Referenzen Datenstrukturen, die über Skalare
hinausgehen, in Arrays und Hashes einbetten. Dadurch können wir folgende komplexe Datenstrukturen
erzeugen:
• Arrays of Arrays (oftmals auch fälschlicherweise als Lists of lists (LoL) genannt).
• Hashes of Hashes (HoH).
• Arrays of Hashes (AoH).
• Hashes of Arrays (HoL).
• Gemischte Datenstrukturen, die in anderen Programmiersprachen als struct oder record
bezeichnet werden.
Arrays of Arrays (LoL)
Arrays, die in Arrays eingebettet sind, kann man sich wie eine (mehrdimensionale) Tabelle
vorstellen, der die Überschriften fehlen. Stattdessen lassen sich die Daten (wie bei allen Arrays) über
ihre Indizes zugreifen. Diese lesen sich wie Koordinaten: Hat man ein zweidimensionales Array
erzeugt, greift man beispielsweise mit den Indizes ->[0]->[0] auf das erste Element des ersten Arrays
zu; bei jeder weiteren Dimension kommt ein Indexfeld hinzu. Dies bedeutet gleichzeitig, dass mit jeder
neuen Dimension auch eine weitere Ebene der Indirektion hinzukommt. Will man also über die
Elemente eines zweidimensionalen Arrays iterieren, benötigt man zwei ineinander geschachtelte
Schleifen. Betrachten wir zunächst die Möglichkeit, vermittels for()-Schleifen über die numerischen
Indizes auf die Daten zuzugreifen. Gegeben folgende Struktur
muss man sich zunächst fragen, wie sich die Größe der beiden Arrays berechnet. Auf der y-Achse
besteht das Array aus zwei Elementen, den Listen (40, 50, 60) und (10, 20, 30), die ja wiederum jeweils
auf der x-Achse drei Elemente besitzen. Es bietet sich also folgende Modellierung als anonymes Array
an:
my $lol = [[40,50,60],[10,20,30]];
132
Um nun die Anzahl der Elemente des äußeren Arrays (der y-Achse) zu ermitteln, dereferenzieren
wir $lol und bringen den Wert in einen Skalarkontext scalar(@{$lol}). Die Länge einer der beiden
Listen erhalten wir, indem wir das erste Element des anonymen Arrays dereferenzieren und den Wert
wiederum in den skalaren Kontext bringen scalar(@{$lol}->[0]):
for(my $y = 0; $y < scalar(@{$lol}); $y++){ for(my $x = 0; $x < @{scalar($lol->[0])}; $x++){ print "y: $y, x: $x, $lol->[$y]->[$x]\n"; } }
Durch die Angabe der "Koordinaten" in der Dereferenzierung erhalten wir die erwartete Ausgabe:
# Ausgabe: # y: 0, x: 0, 40 # y: 0, x: 1, 50 # y: 0, x: 2, 60 # y: 1, x: 0, 10 # y: 1, x: 1, 20 # y: 1, x: 2, 30 Die gleichen Werte bekommen wir auch, wenn wir eingebettete foreach()-Schleifen verwenden:
foreach(@{$lol}){ foreach my $data(@{$_}){ print "$data\n"; } }
In beiden Fällen ist zu beachten, dass die beiden Listen gleich groß sein müssen; ansonsten werden
entweder zu wenige Elemente oder Warnungen über nicht initialisierte Werte ausgegeben.
Hashes of Hashes (HoH)
Wie bereits gesehen, sind Hashes of Hashes Listen von Attributen, die wiederum Attribut-
/Wertepaare als Werte besitzen. Genauso wie bei einem normalen Hash gelangt man über die Schlüssel
an die Werte. Da diese allerdings wiederum Schlüssel sind, benötigt man eine entsprechende Anzahl
von Schleifen, um an den letzten (atomaren) Wert zu gelangen.
Auch die Sortierung erfolgt analog zu normalen Hashes: Mit jedem Schleifendurchlauf erhält man
die Möglichkeit, mit sort die Schlüssel und mit sort $a <=> $b die Werte zu sortieren. In beiden
Fällen ist natürlich darauf zu achten, dass die entsprechende Dereferenzierung auf der jeweiligen Ebene
eingehalten wird:
Sprachwissenschaftliches Institut
133
my %hoh = ( AB3484 => { Airline => "Air Berlin", Flugzeug => "Boeing 737-400", Schalter => "80-81", Gate => "14", Abflug => "17:40", Ziel => "Malaga" }, LH2041 => { Airline => "Lufthansa", Flugzeug => "Aerospatiale ATR 72", Schalter => "51-87", Gate => "16", Abflug => "17:20", Ziel => "München" }, BUS2 => { Fahrzeug => "Bus", Gate => "13", Abflug => "14:00", Ziel => "Flughafenführung" } ); foreach(keys %hoh){ print "Flugnummer: $_\n"; foreach my $schluessel(keys %{$hoh{$_}}){ print "$schluessel: $hoh{$_}->{$schluessel}\n"; } print "\n"; } # Ausgabe: # Flugnummer: BUS2 # Gate: 13 # Ziel: Flughafenführung # Fahrzeug: Bus # Abflug: 14:00 # # Flugnummer: AB3484 # Gate: 14 # Ziel: Malaga # Schalter: 80-81 # Abflug: 17:40 # Flugzeug: Boeing 737-400 # Airline: Air Berlin # # Flugnummer: LH2041 # Gate: 16 # Ziel: München # Schalter: 51-87 # Abflug: 17:20 # Flugzeug: Aerospatiale ATR 72 # Airline: Lufthansa
134
Sortierung über die Schlüssel, sodass die "Flüge" nach ihren Flugnummern und den Schlüsseln im
inneren Hash sortiert sind:
foreach(sort keys %hoh){ print "Flugnummer: $_\n"; foreach my $schluessel(sort keys %{$hoh{$_}}){ print "$schluessel: $hoh{$_}->{$schluessel}\n"; } print "\n"; } # Ausgabe: # Flugnummer: AB3484 # Abflug: 17:40 # Airline: Air Berlin # Flugzeug: Boeing 737-400 # Gate: 14 # Schalter: 80-81 # Ziel: Malaga # # Flugnummer: BUS2 # Abflug: 14:00 # Fahrzeug: Bus # Gate: 13 # Ziel: Flughafenführung # # Flugnummer: LH2041 # Abflug: 17:20 # Airline: Lufthansa # Flugzeug: Aerospatiale ATR 72 # Gate: 16 # Schalter: 51-87 # Ziel: München
Sortierung nach den Werten, sodass die "Flüge" nach ihren Abflugzeiten sortiert sind:
foreach(sort {$hoh{$a}{Abflug} cmp $hoh{$b}{Abflug}} keys %hoh){ print "Flugnummer: $_\n"; foreach my $schluessel(sort keys %{$hoh{$_}}){ print "$schluessel: $hoh{$_}->{$schluessel}\n"; } print "\n"; } # Ausgabe: # Flugnummer: BUS2 # Abflug: 14:00 # Fahrzeug: Bus # Gate: 13 # Ziel: Flughafenführung #
Sprachwissenschaftliches Institut
135
# Flugnummer: LH2041 # Abflug: 17:20 # Airline: Lufthansa # Flugzeug: Aerospatiale ATR 72 # Gate: 16 # Schalter: 51-87 # Ziel: München # Flugnummer: AB3484 # Abflug: 17:40 # Airline: Air Berlin # Flugzeug: Boeing 737-400 # Gate: 14 # Schalter: 80-81 # Ziel: Malaga
Eine weitere Möglichkeit, einen HoH zu erzeugen, besteht in der sogenannten Autovivikation:
Obwohl noch auf keiner Ebene unserer Datenstruktur ein Element bekannt ist, können wir es
automatisch durch Einsetzung an die gewünschte Stelle definieren. Wir hätten also auch Teile unseres
Beispiel-HoHs so aufbauen können:
my $hoh{"AB3484"}{"Gate"}=14; $hoh{"LH2041"}{"Flugzeug"}="Aerospatiale ATR 72"; $hoh{"LH2041"}{"Airline"}="Lufthansa";
Arrays of Hashes (LoH)
Wie bei einem "normalen" Array besteht ein LoH aus einer Liste, nur sind seine Werte Referenzen
auf Hashes:
my @loh = ( {"name" => "Peter", "groesse" => "1, 72", "e-mail" => "peter\@web.de"}, {"name" => "George Bush", "e-mail" => "schorsch\@whitehous.gov", "beruf" => "Präsident"}, {"name" => "Mika Häkkinen", "beruf" => "Ex-Rennfahrer", "nationalität" => "Finnisch"});
Zum Aufbau eines LoHs hätten wir ebenso gut bereits bestehende Hashes mit push(@loh,
{%hash}) in die Datenstruktur einbringen können.
Wollen wir diese Daten ausgeben, müssen wir zunächst über die Elemente des Arrays und dann
jeweils über die dereferenzierten Hashelemente iterieren. In der eigentlichen Ausgabe gelangen wir
wiederum durch Dereferenzierung des jeweiligen Hashelements via Index des Arrays und dem
entsprechenden Schlüssel an den Wert:
136
for(my $i=0;$i<@loh;$i++){ foreach(keys %{$loh[$i]}){ print "$_: $loh[$i]->{$_}\n"; } print "\n"; } # Ausgabe: # name: Peter # e-mail: [email protected] # groesse: 1, 72 # # name: George Bush # beruf: Präsident # e-mail: [email protected] # # name: Mika Häkkinen # nationalität: Finnisch # beruf: Ex-Rennfahrer
Hashes of Arrays (HoL)
HoLs sind Hashes, in denen die Schlüssel einfache Zeichenketten sind und die Werte Referenzen
auf Arrays:
my %hol = ("Paul" => ["rot", "blau"], "Rainer" => ["weiß", "grün"], "Ute" => ["orange", "blau", "schwarz"]);
Zur Ausgabe iteriert man im Gegensatz zu LoHs nun zuerst über den Hash, dann über die
jeweiligen anonymen Arrays:
foreach(sort keys %hol){ print "Lieblingsfarben von $_: "; foreach my $farbe(sort @{$hol{$_}}){ print "$farbe "; } print "\n"; } # Ausgabe: # Lieblingsfarben von Paul: blau rot # Lieblingsfarben von Rainer: grün weiß # Lieblingsfarben von Ute: blau orange schwarz
Um sukzessive Elemente auf ein anonymes Array zu packen, setzt man am Schlüssel desjenigen
Hashelements an, dessen Array man erweitern will, und wendet dann push() oder unshift() auf das
dereferenzierte Hashelement an:
foreach(keys %hol){ push(@{$hol{Paul}},"gelb"); }
Sprachwissenschaftliches Institut
137
9.4 Referenzen als Subroutinen-Argumente Um mehrere listenwertige Datenstrukturen – Arrays und/oder Hashes – an eine Subroutine
übergeben zu können oder sie aus Subroutinen zurückzugeben, macht man aus ihnen Referenzen.
Betrachten wir dazu folgendes Beispiel, in dem wir die Inhalte zweier Arrays in einer Subroutine
miteinander vergleichen:
my @a = (1, 2, 3, 4, 5); my @b = (1, 2, 4, 5, 6); my @c = (1, 2, 3, 4, 5); print "@a ist gleich @b\n" if(arrays_vergleichen(\@a, \@b)); print "@a ist gleich @c\n" if(arrays_vergleichen(\@a, \@c)); sub arrays_vergleichen{ my ($array_1, $array_2) = @_; for(my $i = 0; $i < @{$array_1}; $i++){ return unless($array_1->[$i] eq $array_2->[$i]); } return 1; } # Ausgabe: # 1 2 3 4 5 ist gleich 1 2 3 4 5
Im Aufruf der Subroutine erzeugen wir jeweils durch Voranstellen eines Backslashes wie gewohnt
Referenzen auf die Arrays. Da sie dadurch zu Skalaren geworden sind, stehen sie als einzelne Elemente
in @_. In der Folge iterieren wir über die Indizes des ersten Arrays und vergleichen an jeder Position die
dereferenzierten Elemente miteinander, wodurch wir das erwartete Ergebnis erhalten.
Umgekehrt können wir genauso mehrere listenwertige Datenstrukturen zurückgeben. Dazu setzen
wir sie einfach auf die Argumentliste der return()-Funktion. Dies sei an einer einfachen Subroutine
illustriert, die einen Hash in zwei Arrays transformiert:
my %adressbuch = (Peter => "Bonn", Susanne => "Berlin", Andreas => "Darmstadt"); my ($namen, $wohnorte) = hash2arrays(%adressbuch); print "@{$namen}\n"; print "@{$wohnorte}\n"; sub hash2arrays{ my %hash = @_; my (@array1, @array2); foreach(sort keys %hash){ push(@array1, $_); push(@array2, $hash{$_}); } return (\@array1, \@array2); }
138
# Ausgabe: # Andreas Peter Susanne # Darmstadt Bonn Berlin
Zu beachten ist, dass die Rückgabewerte Skalare sind und auch als solche auf der linken Seite des
Aufrufs vorhanden sein müssen.
9.5 Referenzen auf Subroutinen Als wir im vorigen Kapitel als Präfixsymbol ein Ampersand & vor Subroutinenaufrufe geschrieben
haben, deutete sich bereits die Nähe zu Datenstrukturen an: Denn genauso, wie wir Referenzen auf
Skalare, Arrays und Hashes erzeugen können, dürfen wir dies auch für Subroutinen tun. Dazu
schreiben wir auch hier einen Backslash vor das diesmal obligatorische Ampersand:
my $addition_referenz = \&addiere; sub addiere{ $_[0] + $_[1]; }
Ebenso lassen sich anonyme Subroutinenreferenzen durch Zuweisung eines Subroutinenblocks an
einen Skalar erzeugen. Dabei ist zu beachten, dass nach der schließenden geschweiften Klammer des
Subroutinenblocks ein Semikolon stehen muss:
my $addition_referenz = sub{$_[0] + $_[1]};
Um solch eine Subroutinenreferenz verwenden zu können, schreibt man entweder zu
Dereferenzierung ein Ampersand vor die Referenz in geschweiften Klammern oder wendet die
Pfeilnotation an. Die Argumentliste steht in beiden Fällen wie gewohnt in runden Klammern:
print &{$addition_referenz}(4, 5); # Ausgabe: 9 print $addition_referenz->(4, 5); # Ausgabe: 9
Zwar könnte man nun einwenden, dass ein solches Konstrukt nach l'art pour l'art aussieht, doch
lassen sich durchaus nützliche Dinge mit Subroutinenreferenzen realisieren. Beispielsweise können wir
nun Subroutinen abhängig von Aufrufparametern verwenden, indem wir sie zu Werten in einem Hash
machen:
my %berechne = (plus => sub{$_[0] + $_[1]}, minus => sub{$_[0] - $_[1]}, mal => sub{$_[0] * $_[1]}, durch => sub{$_[0] / $_[1]}); print $berechne{plus}->(3, 4)."\n"; # Ausgabe: 7 print $berechne{minus}->(4, 3)."\n"; # Ausgabe: 1 print $berechne{mal}->(3, 4)."\n"; # Ausgabe: 12 print $berechne{durch}->(3, 4)."\n"; # Ausgabe: 0.75
Sprachwissenschaftliches Institut
139
Callbacks
Eine wichtigere Anwendung von Subroutinenreferenzen sind allerdings sogenannte Callbacks.
Dabei dienen Subroutinenreferenzen anderen Subroutinen als Argumente, sodass unser Code unter
bestimmten Bedingungen von diesen Subroutinen wieder aufgerufen werden kann (daher der Name).
Dies erlaubt uns, eine recht allgemein gehaltene Subroutine schreiben, der wir weitere Funktionalität in
Form von Argumenten mitgeben, um unterschiedliche, detailliertere Aufgaben damit lösen zu können.
Illustrieren wir dies an einer Subroutine, die einen Filter implementiert: Unsere Subroutine soll im
allgemeinen Fall zwei Zahlen addieren; wir wollen aber auch die Möglichkeit haben, ausschließlich
Gleitkommazahlen bzw. nur ganze Zahlen zu addieren. Formulieren wir zunächst die Filter:
sub gleitkommazahlen{ return 1 if($_[0] =~ /\./); } sub ganze_zahlen{ return if($_[0] =~ /\./); return 1; } sub alle_zahlen{ return 1 if($_[0] =~ /\d+(?:\.\d+)*/); }
In den ersten beiden Fällen ist das Vorhandensein des Dezimalpunkts das Filterkriterium, die letzte
Subroutine bildet den allgemeinen Fall einer Zahl ab. In der eigentlichen Subroutine übernehmen wir
zunächst eine Referenz auf ein Array mit Zahlen und die Referenz auf einen der Filter als Argumente.
Um sicherzustellen, dass wir die Funktion mehrmals hintereinander aufrufen können, legen wir eine
lokale Kopie der Testdaten an, indem wir sie dereferenzieren. Dann iterieren wir über diese Liste der
Zahlen, aus der wir jeweils die ersten beiden für die Berechnung entfernen. Zuerst überprüfen wir, ob
überhaupt ein Filter definiert wurde; ist dies nicht der Fall, addieren wir ohne weiteres die beiden
Operanden miteinander und initiieren mit next einen neuen Schleifendurchlauf, damit der Rest des
Blocks nicht abgearbeitet wird.
Die eigentlich interessante Funktionalität findet sich in der nächsten Zeile: Hier stellen wir sicher,
dass beide Operanden dem jeweils formulierten Kriterium gehorchen, indem wir über die
Subroutinenreferenz $filter jeweils einen der Operanden übergeben und die Ergebnisse der
Überprüfung durch ein logisches "und" miteinander verknüpfen. Nur wenn beide Operanden vom glei-
chen Typ sind, gibt es ein Ergebnis:
140
sub addiere{ my ($zahlen, $filter) = @_; my @zahlen = @{zahlen}; while(@zahlen){ my($operand_1, $operand_2) = splice(@zahlen, 0, 2); unless($filter){ my $ergebnis = $operand_1 + $operand_2; print "$operand_1 + $operand_2 = $ergebnis\n" if($ergebnis); next; } my $ergebnis = $operand_1 + $operand_2 if(&{$filter}($operand_1)
&& &{$filter}($operand_2)); print "$operand_1 + $operand_2 = $ergebnis\n" if($ergebnis); } }
Rufen wir nun addiere() mit einigen Testdaten und den verschiedenen Filtern auf, bekommen wir
die zu erwartenden Ergebnisse:
my @zahlen = qw(1 14 38 57.95 62.8237 78.1245); addiere(\@zahlen, \&gleitkommazahlen); # Ausgabe: 62.8237 + 78.1245 =
140.9482 addiere(\@zahlen, \&ganze_zahlen); # Ausgabe: 1 + 14 = 15 addiere(\@zahlen, \&alle_zahlen); addiere(\@zahlen); # Ausgabe für die beiden letzten Aufrufe: # 1 + 14 = 15 # 38 + 57.95 = 95.95 # 62.8237 + 78.1245 = 140.9482
Closures
Ein weiteres sehr nützliches Werkzeug stellen die sogenannten Closures dar. Sie erhalten ihren
Namen aus der Funktionalität, eine mit my deklarierte lexikalische Variable innerhalb eines anonymen
Subroutinenblocks vor dem Zugriff von außen zu verbergen; eine solche Variable kann nur durch den
Aufruf der Subroutine manipuliert werden. Die Variable bleibt also so lange zugreifbar, wie eine
Referenz auf die Subroutine besteht.
Illustrieren wir dies am Beispiel eines einfachen Zählers. Innerhalb der Subroutine
mache_zaehler() definieren wir eine lokale Variable $wert, die in einer anonymen Subroutine
hochgezählt wird. Der Rückgabewert der anonymen Subroutine ist gleichzeitig der Rückgabewert von
mache_zaehler():
sub mache_zaehler{ my $wert = 0; return sub{return $wert++;}; }
Um von außen auf diesem Wert operieren zu können, benötigen wir zunächst eine Referenz auf
mache_zaehler():
Sprachwissenschaftliches Institut
141
my $zaehler = mache_zaehler();
Wann immer wir diese nun dereferenzieren, wird $wert um eins inkrementiert. Ansonsten gibt es
keine Möglichkeit, diese Variable zu manipulieren:
for(1 .. 5){ print &{$zaehler}."\n"; # Ausgabe: 0 1 2 3 4 }
Auch dieses Sprachkonstrukt könnte auf den ersten Blick als überflüssige Spielerei gesehen werden,
stellt aber eine wichtige Möglichkeit dar, die Integrität von Daten sicherzustellen!
9.6 Referenztypen Da Referenzen ja immer Skalare sind, sieht man nur an der Stelle, an der sie die referenzierten
Daten zugewiesen bekommen, welche Datenstruktur sie repräsentieren. Um programmatisch ermitteln
zu können, um welche Datenstruktur es sich bei einer Referenz handelt, gibt es in Perl die Funktion
ref(), deren Rückgabewert SCALAR ist, wenn es sich um einen Skalar handelt, ARRAY bei einem Array,
HASH bei einem Hash und CODE, wenn es sich um eine Subroutinenreferenz handelt:
my $skalar = \"Ich bin ein Skalar!"; my $array = [qw(Elemente eines Arrays)]; my $hash = {Schluessel => "Wert"}; my $sub = sub{return 1;}; print ref($skalar)."\n"; # Ausgabe: SCALAR print ref($array)."\n"; # Ausgabe: ARRAY print ref($hash)."\n"; # Ausgabe: HASH print ref($sub)."\n"; # Ausgabe: CODE
9.7 Zusammenfassung Anhand von Referenzen ist es uns möglich, einerseits komplexe Datenstrukturen zu erzeugen,
andererseits mehrere listenwertige Datenstrukturen an eine Subroutine zu übergeben bzw. aus dieser
zurückzugeben. Darüber hinaus können wir mit Referenzen auch auf Subroutinen verweisen, sodass
sich die Funktionalität von Subroutinen durch Callbacks erweitern lässt, während Closures die
Kapselung von Daten ermöglichen.
Da Referenzen Skalare sind, die auf den Speicherplatz der Werte einer Datenstruktur zeigen, lassen
sich mehrdimensionale Datenstrukturen erzeugen, die vorher durch die Eigenschaft der
Eindimensionalität von Listen nicht möglich gewesen wären. Solche komplexen Datenstrukturen
kommen in Perl in Form von Arrays of Arrays, Arrays of Hashes, Hashes of Arrays, Hashes of Hashes
und gemischten Strukturen vor. Grundsätzlich erzeugt man eine Referenz, indem man entweder einen
Backslash vor die zu referenzierende Datenstruktur schreibt oder indem man durch die entsprechende
Klammerung anonyme Datenstrukturen erzeugt. Wichtig ist, dass sich diese immer einem Skalar
zuweisen lassen. Um eine Referenz verwenden zu können, schreibt man das Präfixsymbol der
referenzierten Datenstruktur vor optionale geschweifte Klammern, die wiederum der Referenz
vorangehen oder man verwendet die Pfeilnotation, wenn man lediglich auf die Werte zugreift. Will
man über eine listenwertige Referenz iterieren, muss man für jede vorhandene Dimension der
142
Datenstruktur eine Schleife verwenden. Während man mit dem Modul Data::Dumper ein Werkzeug an
der Hand hat, um komplexe Datenstrukturen ohne Dereferenzierung auszugeben, kann man den Typ
einer Referenz vermittels der Funktion ref() ausgeben.
Aus der Eigenschaft, dass Referenzen Skalare sind, leitet sich auch ab, dass wir nun mehrere
listenwertige Argumente an Subroutinen übergeben können, die als einzelne Elemente in @_ stehen.
Umgekehrt lassen sich dementsprechend auch mehrere listenwertige Datenstrukturen zurückgeben; zu
beachten ist wiederum, dass es sich dabei um Skalare handelt.
Ebenso wie auf Datenstrukturen können wir auch Referenzen auf Subroutinen erzeugen, sodass sich
hinter der Verwendung einer Skalarvariablen kein Datenwert, sondern Funktionalität verbirgt. Eine
interessante Anwendung dessen ergibt sich in Callbacks, in denen Subroutinen selbst Argumente für
den Aufruf anderer Subroutinen sind. Dadurch lässt sich einer recht allgemein gehaltenen Subroutine
durch den Aufruf erweiterte Funktionalität mitgeben, sodass sich beispielsweise auf elegante Art Filter
implementieren lassen. Darüber hinaus eignen sich Referenzen auf Subroutinen dazu, als Closures in
ihnen als lexikalisch deklarierte Variablen vor Operationen zu verbergen; sie werden allein durch den
Aufruf der Referenz auf die Subroutine zugreifbar.
9.8 Beispielanwendung Eine Möglichkeit, unser Programm zur Zählung von n-Grammen zu erweitern, bestünde darin, die
n-Gramme in komplexen Datenstrukturen zu zählen. Allerdings verlören wir dadurch die Funktionalität
der Subroutine n_gramme_zaehlen(), da wir nicht in der Lage sind, dynamisch einen Hash of Hashes
zu erzeugen, dessen Dimensionalität unbekannt ist. Daher beschränken wir uns an dieser Stelle darauf,
an Bigrammen beispielhaft zu demonstrieren, wie man komplexe Datenstrukturen für eine solche
Teilaufgabe einsetzen könnte.
Wie gewohnt, bekommt eine Subroutine zur Extraktion von Bigrammen ein Array mit Token
übergeben, aus dem wiederum jeweils zwei Token entfernt werden. Anders als sonst werden diese
beiden Wörter allerdings nicht zusammengefügt und bilden somit den Schlüssel eines Hashes, sondern
das erste Wort ist Schlüssel des äußeren Hashes, während das zweite Wort einerseits der Wert zu
diesem Schlüssel ist, gleichzeitig aber der Schlüssel im eingebetteten Hash. Dessen Wert ist dann die
Häufigkeit des Bigramms:
sub bigramme_extrahieren{ my @token = @_; my %bigramme; while(@token > 2){ my ($eins, $zwei) = splice(@token, 0, 2); $bigramme{$eins}{$zwei}++; unshift(@token, $zwei); } return \%bigramme; }
Gibt man letztendlich den Hash of Hashes z.B. mit Data::Dumper aus, erhält man für das
Dostojevski-Korpus beispielsweise folgende Bigramme:
# Ausgabe: # $VAR1 = { # 'human # ' => { # 'blood' => 2,
Sprachwissenschaftliches Institut
143
# 'motives' => 1, # 'race' => 1, # 'destiny' => 1, # 'mind' => 1, # 'being' => 2, # 'contradictions' => 1, # 'endurance' => 1, # 'heart' => 3 # }, # 'prisoner # ' => { # 'chose' => 1, # 'tells' => 1, # 'himself' => 1, # 'must' => 1 # }, # ... # };
Durch Referenzen können wir nun auch für unsere Subroutinen token_type_haeufigkeiten() und
relative_haeufigkeit_berechnen() benannte Parameter angeben. Im ersten Fall wäre dies auch ohne
dieses Sprachmittel möglich gewesen, da wir ja nur Skalare übergeben. Hier zur Illustration der Aufruf
für Tetragramme und der veränderte Ausschnitt aus der Subroutine:
token_type_haeufigkeiten( anzahl => $anzahl_tetragramme, n_gramm_types => $tetragramm_types, vokabular => $vokabular, hl => $tetragramm_hl, n_gramme => 4 ); ... sub token_type_haeufigkeiten { my %args = @_; my $anzahl_token = $args{anzahl}; my $anzahl_types = $args{n_gramm_types}; my $groesse_vokabular = $args{vokabular}; my $n_gramm_hl = $args{hl}; my $exponent = $args{n_gramme}; ... }
Um bei der Berechnung der relativen Häufigkeiten die Hashes mit den n-Grammen und ihren
Häufigkeiten als Werte des Hashes der Parameterliste zu übergeben, muss man sie zu Hashreferenzen
umwandeln:
relative_haeufigkeit_berechnen( anzahl => $anzahl_tetragramme, n_gramme => 4, n_gramm_haeufigkeit => \%tetragramm_haeufigkeit ); In der Subroutine selbst muss dieser Hash dann wiederum dereferenziert werden:
144
sub relative_haeufigkeit_berechnen { my %args = @_; my $i = 0; my $anzahl_n_gramme = $args{anzahl}; my $bezeichner = $args{n_gramme}; my %n_gramm_haeufigkeit = %{ $args{n_gramm_haeufigkeit} }; ... }
Sprachwissenschaftliches Institut
145
10Perl-Module
Everything you need for better future and success has already been written.
And guess what? All you have to do is go to the library.
HENRI FREDERIC AMIEL
Module stellen eine wichtige Möglichkeit dar, sowohl quantitativ als auch qualitativ optimierten
Code zu schreiben. Oftmals sieht man sich mit (Teil-) Aufgaben konfrontiert, die sehr grundlegend sind
und von denen man annehmen kann, dass andere Leute bereits Lösungen dafür implementiert haben.
Dabei geht es weniger darum, eine komplett fertige Implementation an die Hand zu bekommen, die auf
das aktuelle Problem passt, als vielmehr Perl um Funktionen zu erweitern, die über (Teil-) Probleme
abstrahieren und deren Lösung somit vereinfachen. Mit der Verwendung von Modulen gehen also fol-
gende Vorteile einher:
• Wiederverwendbarkeit: Module lassen sich sowohl an andere Programmierer weitergeben als
auch zu einem eigenen "Werkzeugkasten" zusammenstellen.
• Zentralisierung: Die Pflege und Weiterentwicklung des Codes wird durch Abgrenzung von der
eigentlichen Verwendung wesentlich erleichtert.
• Abstraktion: Der Anwender bekommt nicht die Details der Implementierung zu sehen, sondern
kann den Code anhand einer Schnittstelle (API = Application Programming Interface)
benutzen.
Module bestehen aus (Perl-)54 Quellcode, dessen Datenstrukturen und Subroutinen für andere Perl-
Programme nutzbar sind. Wie Pragmata werden sie über die use-Funktion in ein Perl-Programm
eingebunden.55 Module unterscheiden sich von Pragmata allerdings darin, dass letztere keine
Funktionalität nutzbar machen, sondern den internen Übersetzungsprozess eines Programms
beeinflussen. Anders als "normale" Perl-Programme werden Module nicht mit der Dateiendung .pl,
sondern als .pm gespeichert.
10.1 Perl-Module installieren Module sind für die verschiedensten Einsatzgebiete in einer großen Zahl verfügbar. Neben der
Bereitstellung von Modulen auf den Homepages oder auf FTP-Servern ihrer Autoren finden sich die
meisten im Comprehensive Perl Archive Network (CPAN); größere Projekte legen ihren Code oft auch
auf http://www.sourceforge.net ab. Eine weitere Quelle – gerade für Perl unter Windows – stellen
die von ActiveState vorgehaltenen Module dar.
54 Manche Module sind in einer Sprache namens XS geschrieben, mit der man performanzkritische Funktionen in C
implementieren kann, diese gleichzeitg aber mit Perl verbinden kann. Die Konsequenzen eines solchen Vorgehens sollen weiter
unten diskutiert werden. 55 Daneben gibt es auch die Möglichkeit, Module mit require einzubinden. Der Unterschied zu use besteht darin, dass
require Module erst zur Laufzeit lädt, während use dies schon zur Übersetzungszeit tut. Da der Unterschied für unsere Zwecke
in den meisten Fällen irrelevant ist, empfiehlt sich die Verwendung von use.
146
CPAN
Das Comprehensive Perl Archive Network stellt auf einem zentralen Server, von dem es weltweit
mehrere hundert Spiegel gibt – so z.B. auf dem FTP-Server der Ruhr-Universität, die Möglichkeit
bereit, Module für den öffentlichen Gebrauch zu speichern. Dazu sind die Module nach den
Autorennamen, Modulnamen und Kategorien geordnet und als sogenannte tarballs (Archive, die mit
den Unix-Werkzeugen tar und gz komprimiert wurden, aber auch mit WinZip zu öffnen sind) zum
Download verfügbar. Allerdings müssen sie dann manuell installiert werden, was im übernächsten
Abschnitt vorgestellt werden soll.
Eine weitere Möglichkeit, auf das CPAN zuzugreifen, besteht in der Verwendung des Perl-Moduls
CPAN.pm.56 Dies funktioniert allerdings nur in Unix-Umgebungen, weshalb wir an dieser Stelle nur auf
den einschlägigen Einzeiler perl –MCPAN –e shell aufmerksam machen wollen, aus dem man dann
mit install <Modulname> das gewünschte Modul installieren kann. Dabei ist zu beachten, dass die
Modulnamen zumeist nach den Kategorien, in die sie einsortiert sind, gegliedert werden – jedes
Element eines solchen Bezeichners wird durch zwei Doppelpunkte voneinander getrennt.
PPM
Einer der großen Vorteile eines zentralisierten Archivs mit automatisierter Softwareverteilung
besteht darin, dass Abhängigkeiten zwischen Modulen automatisch aufgelöst werden können und somit
alle relevanten Module bei der Installation berücksichtigt werden. Dies ist nicht nur bei der Installation
von Perl-Modulen mit CPAN.pm der Fall, sondern auch bei der Verwendung des Programms ppm.exe,
das Bestandteil jeder ActiveState Perl-Distribution ist. Ruft man es in der Eingabeaufforderung auf,
befindet man sich in einer eigens für die Installation dieser Modul-Distribution programmierten
Kommandozeile. In ihr lassen sich Module genauso wie bei CPAN.pm mit install <Modulname>
installieren. Weitere Informationen zur Verwendung bietet die mit help aufrufbare Hilfe.
Manuelle Installation
Wenn man zum ersten Mal mit Modulen und ihren unterschiedlichen Installationsmöglichkeiten
konfrontiert wird, fragt man sich, warum es nicht eine einheitliche Lösung für diese Operation gibt. Die
Antwort liegt in der Tatsache begründet, dass nicht alle Module nur aus Perl bestehen. Oftmals werden
Funktionen in ihnen dadurch optimiert, dass sie in C geschrieben sind; eine sogenannte Glue Language
mit dem Namen XS vermittelt dann zwischen den Perl und C Funktionen. Der eigentliche Punkt ist
aber, dass solche Module durch einen C-Compiler übersetzt werden müssen, damit diese Funktionen
nutzbar werden. Unter Unix-Umgebungen ist dies meist unkritisch, da dort mit CC bzw. GCC solche
Compiler bereits vorhanden sind.
Unter Windows stellt sich die Angelegenheit allerdings ein wenig komplizierter dar: Dadurch, dass
die ActiveState-Distribution von Perl eine mit besonderen Optionen und dem Microsoft Visual Studio
6.0 bzw. Visual Studio .Net Compiler57 übersetzte Version ist, benötigt man eben diesen Compiler und
muss einiges an Vorarbeit leisten, um ein in XS vorhandenes Modul installieren zu können. Aus
diesem Grunde gibt es das bei ActiveState angesiedelte durch ppm aufrufbare Archiv von Perl-
Modulen.58
56 Bei der ersten Inbetriebnahme dieses Moduls muss es noch in einem interaktiven Prozess konfiguriert werden. 57 Die Compiler, Linker und Bibliotheken selbst sind bei Microsoft unter bestimmten Voraussetzungen kostenlos erhältlich. 58 Einige interessante Module, die aus dem Gebiet Web-Programmierung/XML-Verarbeitung stammen und zu übersetzende
Komponenten enthalten, werden nicht von ActiveState gepflegt, sondern von Randy Kobes, dessen Archiv unter
http://theoryx5.uwinnipeg.ca zugreifbar ist.
Sprachwissenschaftliches Institut
147
In ihm findet sich zwar eine große Teilmenge der auf CPAN erhältlichen Module, aber nicht alle.
Sollte es also nicht möglich sein, ein Modul mit ppm zu installieren, muss man es manuell installieren.
Zu diesem Zweck benötigt man das von Microsoft kostenlos erhältliche Werkzeug nmake.59 Nachdem
man die einschlägigen Dateien, die durch Ausführen der heruntergeladenen .exe-Datei entpackt
wurden, in ein Verzeichnis des Systempfads verschoben hat, kann man sich das gewünschte Modul von
CPAN besorgen, es entpacken und dann mit folgender Prozedur installieren:
• Im Hauptverzeichnis des entpackten Moduls befindet sich eine Datei namens Makefile.PL, aus
der man mit perl Makefile.PL ein sogenanntes Makefile generiert, in dem verschiedene
Installationsparamter für das Modul festgelegt sind. Auch überprüft diese Operation, ob ggf.
weitere Module vorher installiert werden müssen, bevor die Abhängigkeiten des aktuell
vorliegenden Moduls erfüllt sind.
• Ist ein Makefile erzeugt worden, stößt man den Installationsprozess mit dem Befehl nmake60 an.
• Im nächsten Schritt überprüft man mit nmake test, ob die Funktionsweise dieses Moduls
gewährleistet ist. Dazu hat der Autor des Moduls einige Testprogramme geschrieben, die mit
vorgegebenen Daten immer ein konsistentes Ergebnis liefern.
• Sind die Tests erfolgreich gewesen, kann man das Modul mit nmake install installieren. Dazu
wird die .pm-Datei in ein ihrer Kategorie entsprechendes Verzeichnis kopiert und die
Dokumentation des Moduls im System verfügbar gemacht.
10.2 Perl-Module verwenden Perl-Module lassen sich in zwei unterschiedlichen Programmierparadigmen implementieren: So,
wie wir bis jetzt programmiert haben, nämlich prozedural, und objektorientiert. Unter objektorientierter
Programmierung versteht man eine Herangehensweise an die Problemlösung, die Objekte (der
richtigen Welt) als komplexe Datenstrukturen begreift, die bestimmte Eigenschaften besitzen und auf
denen bestimmte Methoden ausgeführt werden können. Darüber hinaus werden Objekte als konkrete
Ausprägungen von Klassen gedacht; in ihnen werden Eigenschaften und Methoden je nach Granularität
ihrer Implementierung hierarchisch eingeordnet, wobei spezifischere Klassen Eigenschaften und
Methoden aus allgemeineren Klassen erben, diese aber auch durch eigene Implementationen
überschreiben können. Für die Benutzung eines objektorientierten Perl-Moduls reicht allerdings das
Wissen aus, dass Objekte konkrete Ausprägungen von Klassen sind, die Eigenschaften besitzen und auf
denen man Methoden ausführen kann.
Aber woher weiss man, ob es sich um ein prozedurales oder objektorientiertes Modul handelt, bzw.
welche Datenstrukturen und Subroutinen verfügbar sind? Dazu lässt man sich mit perldoc
<Modulname> die Dokumentation des Moduls ausgeben, in der die komplette Programmierschnittstelle
beschrieben ist. Oftmals lässt sich schon an der kurzen Zusammenfassung am Anfang der Doku-
mentation erkennen, um welche Art von Modul es sich handelt und wie seine typische
Verwendungsweise aussieht.
Prozedurale Module
Wie bereits gesagt, wird die durch Module bereitgestellte Funktionalität durch use im Programm
verfügbar gemacht. Dadurch importieren wir bei einem im prozeduralen Stil geschriebenen Modul
diejenigen Datenstrukturen und Subroutinen, die es exportiert. Diese sind direkt im eigenen Programm
59 ftp://ftp.microsoft.com/Softlib/MSLFILES/nmake15.exe 60 Unter Unix-Umgebungen heißt der Befehl einfach make.
148
verwendbar. Will man nicht alle, sondern nur einige bestimmte Subroutinen importieren, kann man der
use-Funktion eine Argumentliste übergeben, in der die Bezeichner dieser Subroutinen stehen.
Zur Illustration schauen wir uns an, wie man in Perl Optionen für die Verwendung des eigenen
Programms einsetzt. Dazu gibt es zwei Module: Getopt::Std erlaubt die Eingabe sogenannter
Switches, Optionen die aus einem Minuszeichen, einem Buchstaben und einem optionalen Wert, der
durch ein Leerzeichen vom Switch getrennt wird, bestehen und anzeigen, ob ein Parameter gesetzt sein
soll oder nicht. Mit Getopt::Long lassen sich darüber hinaus Optionen angeben, die aus zwei
Minuszeichen und einem ganzen Wort sowie einem optionalen Wert, der durch ein Leerzeichen oder
ein Gleichheitszeichen von der Option getrennt wird, bestehen. Da Getopt::Long das mächtigere der
beiden Module darstellt und extrem nützlich ist, soll es an dieser Stelle detaillierter betrachtet werden.
Getopt::Long stellt die Funktion GetOptions() zur Verfügung, deren Argumentliste die Optionen
beschreibt. Eine solche Beschreibung besteht aus dem eigentlichen Bezeichner der Option und einer
Referenz auf eine mit dieser Option verbundenen Variable. Modellieren wir die Funktion addiere()
aus dem letzten Kapitel mit Getopt::Long so, dass wir sowohl die Typen der Operanden als auch deren
Werte beim Aufruf des Programms angeben können. Zunächst deklarieren wir eine Reihe von
Skalarvariablen, die definiert sind, wenn sie beim Aufruf angegeben werden oder die beim Aufruf der
Option übergebenen Werte enthalten. In der Funktion GetOptions() stehen die Optionen als benannte
Argumente: Während die Typbezeichnungen wie Switches agieren, sollen die Optionen operand1 und
operand2 auch Werte entgegennehmen. Dazu erweitert man den Optionsnamen um ein
Gleichheitszeichen, wenn es sich um ein obligatorisches Argument handelt, und um einen
Doppelpunkt, wenn das Argument fakultativ sein soll. Danach gibt man den Typ des Arguments an: Da
wir ja im Programm erst entscheiden, welchen Typ unsere Zahlen besitzen, geben wir hier ein s für
String, also eine Zeichenkette an. Alternativ kann man bei einem verbindlichen ganzzahligen Argument
iac oder i bei einem optionalen ganzzahligen Argument, f-n für ein obligatorisches
Gleitkommazahlen-Argument oder f für ein fakultatives Gleitkommazahlen-Argument angeben:61
use Getopt::Long; my @zahlen; my ($gleitkommazahlen, $ganze_zahlen, $alle_zahlen, $op1, $op2); GetOptions("gleitkomma" => \$gleitkommazahlen, "ganze" => \$ganze_zahlen, "alle" => \$alle_zahlen, "operand1=s" => \$op1, "operand2=s" => \$op2, );
In der Folge überprüfen wir zunächst, ob Operanden eingegeben wurden; ist dies nicht der Fall,
verwenden wir unsere ursprüngliche Liste als Testdaten. Danach kontrollieren wir, ob einer der
Switches gesetzt wurde, ansonsten tritt der Default-Fall ein, dass wir einfach die gegebenen Operanden
miteinander addieren:
unless($op1 && $op2){ @zahlen = qw(1 14 38 57.95 62.8237 78.1245); } else{ @zahlen = ($op1, $op2); }
61 Das i steht für das englische Wort für ganze Zahlen, nämlich integer, und das f für floating point, also Gleitkommazahlen.
Sprachwissenschaftliches Institut
149
if($gleitkommazahlen){ addiere(\@zahlen, \&gleitkommazahlen); } elsif($ganze_zahlen){ addiere(\@zahlen, \&ganze_zahlen); } elsif($alle_zahlen){ addiere(\@zahlen, \&alle_zahlen); } else{ addiere(\@zahlen); }
Der Rest des Codes bleibt unverändert,
sub gleitkommazahlen{ return 1 if($_[0] =~ /\./); } sub ganze_zahlen{ return if($_[0] =~ /\./); return 1; } sub alle_zahlen{ return 1 if($_[0] =~ /\d+(?:\.\d+)*/); } sub addiere{ my ($zahlen, $filter) = @_; my @zahlen = @{zahlen}; while(@zahlen){ my($operand_1, $operand_2) = splice(@zahlen, 0, 2); unless($filter){ my $ergebnis = $operand_1 + $operand_2; print "$operand_1 + $operand_2 = $ergebnis\n" if($ergebnis); next; } my $ergebnis = $operand_1 + $operand_2 if(&{$filter}($operand_1)
&& &{$filter}($operand_2)); print "$operand_1 + $operand_2 = $ergebnis\n" if($ergebnis); } }
sodass wir beim Aufruf mit perl berechnen.pl --ganze --operand1 39 --operand2 11 die zu
erwartende Ausgabe 39 + 11 = 50 erhalten.
Objektorientierte Module
Objektorientierte Module exportieren keine Datenstrukturen oder Subroutinen, sondern machen
diese dadurch zugreifbar, dass man ein Objekt der entsprechenden Klasse erzeugt. Dazu bedient man
sich eines sogenannten Konstruktors, der – anders als in vielen anderen Programmiersprachen – keinen
festen Bezeichner hat. Da Konstruktoren in vielen Programmiersprachen allerdings new() heißen,
werden sie in Perl meist auch so benannt. Die Eigenschaften eines Objekts können in Perl auf
150
unterschiedliche Arten angegeben werden – häufig stehen sie als benannte Parameter im Aufruf des
Konstruktors. Das Verhalten eines Objekts beeinflusst man über Methoden; dies sind Referenzen des
Objekts auf Subroutinen im entsprechenden Modul.
Als Beispiel für ein einfaches objektorientiertes Modul betrachten wir Text::Ngrams, das die
Funktionalität bereitstellt, aus Texten wort- oder zeichenweise n-Gramme zu erzeugen und zu zählen.
Da dieses Modul als Voreinstellung zeichenweise n-Gramme extrahiert, muss man beim Aufruf den
type-Parameter mit dem Wert word belegen, um wortweise n-Gramme zu erhalten:
use Text::Ngrams; my $ng = Text::Ngrams->new(type => "word");
Voreingestellt ist auch die Größe der n-Gramme, nämlich als Trigramme; will man größere
Einheiten betrachten, spezifiziert man dies als Wert des Parameters windowsize im Konstruktor. Als
nächstes übergibt man der Methode process_text() eine Liste mit zu analysierenden Wörtern.
Alternativ dazu kennt das Modul auch eine Methode process_files(), die Dateinamen und
Referenzen auf Dateikennungen als Argumente akzeptiert. Darüber hinaus kann man mit
feed_tokens() auch einzelne sprachliche Einheiten analysieren lassen. Wie bereits gesagt, ist eine
Methode – hier process_text() – eine Referenz des Objekts – $ng – auf eine Subroutine –
process_text() aus dem Modul Text::Ngrams –, der man eine Argumentliste übergeben kann – in
diesem Fall eine Liste mit Wörtern:
my @woerter = <DATA>; $ng->process_text(@woerter);
Das Resultat der n-Gramm-Analyse steht nun in der komplexen Datenstruktur $ng. Um sie
ausgeben zu können, reicht es nicht, sie der print()-Funktion als Argument zu übergeben – wir
erhielten nur den Hinweis, dass es sich um einen Hash aus dem Modul Text::Ngrams an einer
bestimmten Speicheradresse handelt. Damit wir diese komplexe Datenstruktur nicht selbst analysieren
müssen, gibt es im Modul eine Methode namens to_string(), die eine spezielle Listenausgabe der
Daten erzeugt. Da wir daran interessiert sind, die Ergebnisse nach Häufigkeiten sortiert präsentiert zu
bekommen, geben wir dem Parameter orderby den Wert frequency:
print $ng->to_string(orderby => "frequency"); __END__ Eine Rose ist eine Rose ist eine Rose . # Ausgabe: # BEGIN OUTPUT BY Text::Ngrams version 1.1 # # 1-GRAMS (total count: 8) # ------------------------ # Rose 3 # eine 2 # ist 2 # Eine 1 #
Sprachwissenschaftliches Institut
151
# 2-GRAMS (total count: 7) # ------------------------ # Rose_ist 2 # eine_Rose 2 # ist_eine 2 # Eine_Rose 1 # # 3-GRAMS (total count: 6) # ------------------------ # ist_eine_Rose 2 # Rose_ist_eine 2 # Eine_Rose_ist 1 # eine_Rose_ist 1 # # END OUTPUT BY Text::Ngrams
Setzt man den Parameter normalize auf wahr,
print $ng->to_string(orderby => "frequency", normalize => 1 );
bekommt man die relativen Häufigkeiten ausgegeben:
# Ausgabe: # BEGIN OUTPUT BY Text::Ngrams version 1.1 # # 1-GRAMS (total count: 8) # ------------------------ # Rose 0.375 # ist 0.25 # eine 0.25 # Eine 0.125 # # 2-GRAMS (total count: 7) # ------------------------ # Rose_ist 0.285714285714286 # eine_Rose 0.285714285714286 # ist_eine 0.285714285714286 # Eine_Rose 0.142857142857143 # # 3-GRAMS (total count: 6) # ------------------------ # Rose_ist_eine 0.333333333333333 # ist_eine_Rose 0.333333333333333 # eine_Rose_ist 0.166666666666667 # Eine_Rose_ist 0.166666666666667 # # END OUTPUT BY Text::Ngrams
Schon dieses einfache Beispiel zeigt, wie sich die Verwendung von Modulen positiv auf den
eigenen Quellcode auswirkt: Er ist nicht nur kompakter geworden, sondern durch die Abstraktion über
die Einzelschritte konnte die Problemlösung auch wesentlich einfacher erreicht werden.
152
10.3 Perl-Module selbst erstellen
Prozedurale Module
Wie wir bereits in Kapitel 5 gesehen haben, existiert Perlcode immer in einem bestimmten Paket:
Während unsere eigenen Programme bis jetzt immer dem Paket Main angehörten, finden sich Module
in einem durch die Anweisung package <Modulname>; gekennzeichneten Namensraum. Darüber
hinaus müssen Module in bestimmten Verzeichnissen stehen, die der jeweiligen Perl-Installation
bekannt sind – man findet diese im Array @INC.
Um ein eigenes prozedurales Modul zu erstellen, schreibt man dessen Namen zunächst in eine
Zeile, die mit package beginnt. Danach bindet man das Modul Exporter zur Laufzeit vermittels
require ein und teilt dem System mit, dass man Symbole exportieren will. Diese stehen dann im Array
@EXPORT:
package berechnen; use strict; require Exporter; our @ISA = qw(Exporter); our @EXPORT = qw(plus minus mal durch hoch);
Danach folgt wie gewohnt die Definition der Subroutinen:
sub plus{ $_[0] + $_[1]; } sub minus{ $_[0] - $_[1]; } sub mal{ $_[0] * $_[1]; } sub durch{ $_[0] / $_[1]; } sub hoch{ $_[0] ** $_[1]; }
In einem eigenen Programm können wir nun diese Subroutinen durch use berechnen; verwenden:
use berechnen; print plus(3, 4); # Ausgabe: 7 print hoch(3, 2); # Ausgabe: 9
Sprachwissenschaftliches Institut
153
Objektorientierte Module
In objektorientierter Schreibung müssen wir wie gesagt nichts exportieren, sondern lediglich eine
Methode vorsehen, die unser Objekt erzeugt, den Konstruktor:
package Zahl; use strict; sub new{ my $class = shift; my $self = {@_}; bless($self, $class); return $self; }
Das erste Argument, das der Konstruktor entgegen nimmt, ist der Name des Moduls. Da dieser mit
dem Namen der Klasse identisch ist, benennt man den Bezeichner meist auch so. Die Parameter aus
dem Aufruf bilden den Rest der Argumentliste. Da wir diese gerne als benannte Parameter behandeln
wollen, machen wir daraus eine Hash-Referenz. Mit der neuen Funktion bless() binden wir das
Objekt an unsere Klasse und geben es im nächsten Schritt an den Ort des Aufrufs zurück. Dies ist die
eigentliche objektorientierte Funktionalität. Im folgenden müssen wir nur darauf achten, dass diese
Parameterliste das erste Argument einer jeden Subroutinen-Definition ist, sodass wir jetzt die Index-
zählung um eins erhöhen müssen, da wir ja in dieser Implementierung keine Parameter übergeben:
sub plus{ $_[1] + $_[2]; } sub minus{ $_[1] - $_[2]; } sub mal{ $_[1] * $_[2]; } sub durch{ $_[1] / $_[2]; } sub hoch{ $_[1] ** $_[2]; } 1;
In der letzten Zeile müssen wir einen wahren Wert zurückgeben, damit die Einbindung dieses
Codes nicht fehlschlägt. Wie bereits gesehen, verwenden wir unser Modul so, dass wir zunächst ein
Objekt erzeugen, auf dem wir dann die zur Berechnung definierten Methoden ausführen können:
154
use Zahl; my $rechner = Zahl->new(); print $rechner->mal(3, 4); # Ausgabe: 12 print $rechner->durch(3, 4); # Ausgabe: 0.75
Vererbung
Bis jetzt haben wir ein Modul, das man wohl weniger als objektorientiert, denn objektbasiert
bezeichnen würde, da wir das wohl markanteste Merkmal der Objektorientierung – nämlich Vererbung
von Eigenschaften und/oder Methoden – noch nicht implementiert haben. Betrachten wir unser Modul,
so führen wir derzeit die Grundrechenarten auf ganzen Zahlen und Dezimalzahlen aus. Eine besondere
Art solcher Zahlen stellen Brüche dar: Sie repräsentieren eine Division aus einem Zähler durch einen
Nenner, man kann auf ihnen aber auch die oben gezeigten Grundrechenarten ausführen.
Dies bedeutet für unsere Klasse entweder, dass wir die Methoden zur Berechnung auf diese
Gegebenheiten anpassen, oder dass wir diejenigen Eigenschaften und Methoden aus unserer Klasse
weiter verwenden, die auch für Brüche geeignet sind und in unserer Unterklasse die nicht geeigneten
Strukturen überschreiben.
Vergegenwärtigen wir uns zunächst noch einmal, wie man mit Brüchen rechnet. Der einfachste Fall
besteht darin, zwei Brüche miteinander zu multiplizieren, indem man die jeweiligen Werte im Zähler
und im Nenner miteinander multipliziert. Bei der Division bildet man den Kehrwert eines Bruches und
multipliziert ihn mit dem anderen. Komplizierter stellen sich die Addition und die Subtraktion dar: In
diesen beiden Fällen muss man zunächst die Nenner der beiden zu behandelnden Brüche auf einen
gemeinsamen Nenner bringen. Dazu muss aus den beiden Nennern das kleinste gemeinsame Vielfache
berechnet werden. Zwei weitere Operationen, die man auf Brüchen ausführen kann, sind das Erweitern,
wobei der Zähler und der Nenner jeweils mit demselben Faktor multipliziert werden, und das Kürzen,
bei dem Zähler und Nenner durch den eventuell vorhandenen gleichen Faktor dividiert werden. Als
kosmetische Anpassung lassen sich Brüche, deren Zähler größer als ihre Nenner sind, auf eine ganze
Zahl und einen Bruch aus Divisionsrest und Nenner normalisieren.
Um dem Perl-Interpreter mitzuteilen, dass die Klasse Bruch Eigenschaften und Methoden von der
Klasse Zahl erbt, müssen wir zuerst auf Verzeichnisebene sicherstellen, dass Bruch als Kind von Zahl
erscheint. Dazu erzeugen wir im Verzeichnis, in dem die Datei Zahl.pm abgelegt ist, ein
Unterverzeichnis namens Zahl, in das wir die Datei Bruch.pm abspeichern. Des Weiteren machen wir
in der package-Anweisung deutlich, dass Bruch von Zahl abstammt, indem wir zunächst den Namen
der Oberklasse, zwei Doppelpunkte und dann den Namen der Unterklasse schreiben:
package Zahl::Bruch;
Da Brüche wie gesagt eine Art von Zahlen sind, benötigen wir keinen eigenen Konstruktor für diese
Klasse; wir erben ihn – und alle weiteren Eigenschaften und Methoden - aus der Oberklasse, indem wir
sie einerseits mit use wie gewohnt verwenden, sie andererseits aber auch im systemeigenen Array @ISA
verfügbar machen:
use Zahl; our @ISA = qw( Zahl );
Sprachwissenschaftliches Institut
155
Diese Schreibweise schränkt die Vererbungshierarchie allerdings genau auf diese eine Klasse ein;
benötigen wir weitere Oberklassen, müssen wir sie in dieser Liste angeben oder den Klassennamen auf
das Array @ISA pushen. Ein Modul, das sowohl die Einbindung der Klasse als auch das Hinzufügen zur
Vererbungshierarchie automatisiert, ist base:
use base qw( Zahl );
Betrachten wir anhand der Multiplikation von Brüchen, wie unsere Implementierung aussehen soll.
Grundsätzlich legen wir fest, dass wir die Operanden (unsere Brüche) als Zeichenketten der Form "(-
)a/b" modellieren, sodass wir sie durch einen regulären Ausdruck in ihre Komponenten zerlegen
können:
my $obj = Zahl::Bruch->new(); print $obj->multipliziere( "3/4", "3/4" );
Beim Aufruf des Konstruktors new() sucht der Perl-Interpreter zuerst in der Klasse Zahl::Bruch
nach einer entsprechenden Klassenmethode. Da diese aber nicht dort vorhanden ist, wird das Array
@ISA in einer Tiefensuche von links nach rechts durchsucht, bis die einschlägige Methode gefunden
wurde. Dies ist in diesem Fall der Konstruktor der direkten Oberklasse Zahl.
Wie bereits an den Objektmethoden der Klasse Zahl gezeigt, ist unser erstes Argument für die
Methode multipliziere() die Referenz auf das Objekt, dem die beiden Brüche folgen:
sub multipliziere { my $self = shift; my $op1 = shift; my $op2 = shift;
Da das Ergebnis der Multiplikation wiederum ein Bruch ist, vereinbaren wir an dieser Stelle zwei
Variablen, die diesen repräsentieren:
my ( $zaehler, $nenner ) = 0;
Um nun einerseits unsere Operanden in ihre Zähler und Nenner zerlegen zu können, andererseits
sicherzustellen, dass es sich bei den eingegebenen Werten um ganze Zahlen handelt und keiner der
Nenner 0 ist, parsen wir im nächsten Schritt die Operanden und überprüfen die Nenner. Dies fassen wir
als als Initialisierung auf:
my ( $zaehler1, $nenner1, $zaehler2, $nenner2 ) = _init( $op1, $op2 );
Wie oben beschrieben, gliedern wir diesen Initialisierungsschritt in die Zerlegung der Elemente und
die Validierung der Daten:
sub _init { my $op1 = shift; my $op2 = shift; my ( $zaehler1, $nenner1 ) = _parse_ausdruck($op1); my ( $zaehler2, $nenner2 ) = _parse_ausdruck($op2); croak "Nenner darf nicht 0 sein!" if ( $nenner1 == 0 || $nenner2 == 0
); return ( $zaehler1, $nenner1, $zaehler2, $nenner2 ); }
156
Die Parsing-Routine sieht dann so aus:
sub _parse_ausdruck { return ( $1, $2 ) if ( shift() =~ /(-?\d+)\/(\d+)/ ); croak "Kann nur mit Ziffern rechnen!\n"; }
Kehren wir nun zu unserer eigentlichen Aufgabe, der Multiplikation von Brüchen, zurück. Wie
oben bereits diskutiert, besteht diese Operation aus der einfachen Multiplikation des ersten Zählers mit
dem zweiten und des ersten Nenners mit dem zweiten, also jeweils zwei ganzen Zahlen, wie wir es aus
der Multiplikation in der Basisklasse Zahl schon kennen. Da wir als Bezeichner für unsere Methode
denselben Namen wie in der Oberklasse gewählt haben, können wir nicht mehr wie im Falle des
Konstruktors einfach auf diese Methode zugreifen, sondern müssen dem Perl-Interpreter explizit sagen,
dass wir multiplizieren() aus der Oberklasse meinen. Dazu lassen sich zwei Vorgehensweisen
denken: Einerseits könnten wir über unsere Objektreferenz explizit den Namen der Klasse angeben,
deren Methode multiplizieren() wir meinen, es gibt aber in Perl die sogenannte Pseudo-Routine
SUPER, die auf die in @ISA stehende Oberklasse verweist.62 Dieses Vorgehen ist im Sinne der
Wiederverwendbarkeit wesentlich robuster als das erstgenannte, da wir nicht von einem bestimmten
Klassennamen abhängig sind:
$zaehler = $self->SUPER::multipliziere( $zaehler1, $zaehler2 ); $nenner = $self->SUPER::multipliziere( $nenner1, $nenner2 );
Im nächsten Schritt versuchen wir, den neuen Bruch zu kürzen:
( $zaehler, $nenner ) = _kuerzen( $zaehler, $nenner );
In der Klassenmethode _kuerzen() überprüfen wir zunächst, ob die Komponenten des Bruchs
gleich sind; ist dies der Fall, haben wir eine ganze Zahl, die wir in Form des Zählers zurückgeben. Sind
Zähler und Nenner unterschiedlich, müssen wir den größten gemeinsamen Teiler (ggT) dieser beiden
Zahlen ermitteln; die Rückgabewerte sind in diesem Fall die Quotienten aus dem Zähler bzw. Nenner
und dem ggT:
sub _kuerzen { my $zaehler = shift; my $nenner = shift; return $zaehler if ( $zaehler == $nenner ); my $ggt = _ggt( $zaehler, $nenner ); return ( $zaehler / $ggt, $nenner / $ggt ); }
Um den ggT zu berechnen, verwenden wir den Algorithmus von Euklid: Solange unser
ursprünglicher Nenner ungleich 0 ist, berechnen wir den Modulo-Wert aus der Division des Zählers
durch den Nenner, wobei wir den Ursprungsvariablen die Rückgabewerte jeweils über Kreuz zuweisen.
Die Klassenmethode selbst gibt dann den vorzeichenlosen größten gemeinsamen Teiler zurück:
62 Stehen dort mehrere Oberklassen, initiiert der Perl-Interpreter eine linksseitige Tiefensuche nach der angegebenen
Methode.
Sprachwissenschaftliches Institut
157
sub _ggt{ my ($x,$y)=@_; while($y!=0){ ($x,$y)=($y,$x%$y); } return abs($x); }
Im letzten Schritt überprüfen wir, ob der Zähler größer als der Nenner ist, sodass wir den Bruch zu
einer ganzen Zahl plus Bruch normalisieren können, andernfalls geben wir den neuen Bruch zurück:
if ( $zaehler > $nenner ) { my @normalisiert = _normalisiere( $zaehler, $nenner ); return "$normalisiert[0] $normalisiert[1]/$normalisiert[2]"; } else { return "$zaehler/$nenner"; } }
In der Klassenmethode _normalisiere() gewinnen wir die ganze Zahl, indem wir zunächst den
Zaehler durch den Nenner teilen und mit der Funktion int() daraus eine Zahl ohne Nachkommastellen
machen. Den Zähler unseres neuen Bruchs berechnen wir, indem wir den Modulo-Wert aus dem Zähler
und Nenner berechnen:
sub _normalisiere { my $zaehler = shift; my $nenner = shift; my $ganze = int( $zaehler / $nenner ); my $rest = $zaehler % $nenner; return ( $ganze, $rest, $nenner ); }
Die gesamte Methode multipliziere() sieht dementsprechend so aus:
sub multipliziere { my $self = shift; my $op1 = shift; my $op2 = shift; my ( $zaehler, $nenner ) = 0; my ( $zaehler1, $nenner1 ) = _parse_ausdruck($op1); my ( $zaehler2, $nenner2 ) = _parse_ausdruck($op2); croak "Nenner darf nicht 0 sein!" if ( $nenner1 == 0 || $nenner2 == 0
); $zaehler = $self->SUPER::multipliziere( $zaehler1, $zaehler2 ); $nenner = $self->SUPER::multipliziere( $nenner1, $nenner2 ); ( $zaehler, $nenner ) = _kuerzen( $zaehler, $nenner ); if ( $zaehler > $nenner ) { my @normalisiert = _normalisiere( $zaehler, $nenner ); return "$normalisiert[0] $normalisiert[1]/$normalisiert[2]"; } else { return "$zaehler/$nenner"; }
158
}
Für die Methode dividiere() nutzen wir den Umstand aus, dass die Division von Brüchen durch
die Multiplikation des ersten Bruches mit dem Kehrwert des zweiten modelliert wird.
Dementsprechend verwenden wir hier die oben beschriebene Methode multipliziere():
sub dividiere { my $self = shift; my $op1 = shift; my $op2 = shift; my ( $zaehler1, $nenner1, $zaehler2, $nenner2 ) = _init( $op1, $op2 ); $op2 = "$nenner2/$zaehler2"; # Kehrwert bilden multipliziere( $self, $op1, $op2 ); }
Die Addition und Subtraktion von Brüchen stellt sich wie gesagt ein wenig komplizierter dar, da
wir dafür sorgen müssen, dass die jeweiligen Nenner gleich sind. Zunächst überprüfen wir, ob die
Nenner ungleich sind; ist dies der Fall, berechnen wir das kleinste gemeinsame Vielfache (kgV), das
wir anschließend zu unserem neuen Nenner machen:63
if ( $nenner1 != $nenner2 ) { my $kgv = _kgv( $nenner1, $nenner2 ); $nenner = $kgv;
Zur Berechnung des kgVs nutzen wir aus, dass sich dieser Wert aus dem Produkt der beiden Nenner
durch ihren ggT berechnen lässt:
sub _kgv { my ( $x, $y ) = @_; my $kgv = ( $x * $y ) / _ggt( $x, $y ); return $kgv; }
Um nun beide Nenner auf diesen Wert erweitern zu können, berechnen wir den jeweiligen Faktor,
der sich aus der Division des kgVs durch den Nenner ergibt:
my ( $faktor1, $faktor2 ) = ( $kgv / $nenner1, $kgv / $nenner2 ); ( $zaehler1, $nenner1 ) = _erweitern( $zaehler1, $nenner1,
$faktor1 ); ( $zaehler2, $nenner2 ) = _erweitern( $zaehler2, $nenner2,
$faktor2 );
In der Klassenmethode _erweitern() multiplizieren wir lediglich den Zähler und den Nenner mit
dem vorher berechneten Faktor:
sub _erweitern { my ( $zaehler, $nenner, $faktor ) = @_; $zaehler *= $faktor; $nenner *= $faktor; return ( $zaehler, $nenner );
63 Wir verzichten an dieser Stelle der Übersichtlichkeit halber auf die Darstellung des Initialisierungsschritts, da dieser
analog zu jenen in den Methoden multipliziere() bzw. dividiere() ist.
Sprachwissenschaftliches Institut
159
}
Zuletzt rufen wir zur Addition der Zähler wiederum die Methode addiere() aus der Oberklasse mit
SUPER:: auf. Hier die vollständige Methode addiere() für Brüche:
sub addiere { my $self = shift; my $op1 = shift; my $op2 = shift; my ( $zaehler, $nenner ) = 0; my ( $zaehler1, $nenner1, $zaehler2, $nenner2 ) = _init( $op1, $op2 ); if ( $nenner1 != $nenner2 ) { my $kgv = _kgv( $nenner1, $nenner2 ); $nenner = $kgv; my ( $faktor1, $faktor2 ) = ( $kgv / $nenner1, $kgv / $nenner2 ); ( $zaehler1, $nenner1 ) = _erweitern( $zaehler1, $nenner1,
$faktor1 ); ( $zaehler2, $nenner2 ) = _erweitern( $zaehler2, $nenner2,
$faktor2 ); $zaehler = $self->SUPER::addiere( $zaehler1, $zaehler2 ); } else { $nenner = $nenner1; $zaehler = $self->SUPER::addiere( $zaehler1, $zaehler2 ); } ( $zaehler, $nenner ) = _kuerzen( $zaehler, $nenner ); if ( $zaehler > $nenner ) { my @normalisiert = _normalisiere( $zaehler, $nenner ); return "$normalisiert[0] $normalisiert[1]/$normalisiert[2]"; } else { return "$zaehler/$nenner"; } }
Völlig analog dazu verhält sich die Methode subtrahiere(), bei der die beiden Zähler vermittels
der subtrahiere()-Methode der Oberklasse voneinander abgezogen werden:
sub subtrahiere { my $self = shift; my $op1 = shift; my $op2 = shift; my ( $zaehler, $nenner ) = 0; my ( $zaehler1, $nenner1, $zaehler2, $nenner2 ) = _init( $op1, $op2 ); if ( $nenner1 != $nenner2 ) { my $kgv = _kgv( $nenner1, $nenner2 ); $nenner = $kgv; my ( $faktor1, $faktor2 ) = ( $kgv / $nenner1, $kgv / $nenner2 ); ( $zaehler1, $nenner1 ) = _erweitern( $zaehler1, $nenner1,
$faktor1 ); ( $zaehler2, $nenner2 ) = _erweitern( $zaehler2, $nenner2,
$faktor2 ); $zaehler = $self->SUPER::subtrahiere( $zaehler1, $zaehler2 ); }
160
else { $nenner = $nenner1; $zaehler = $self->SUPER::subtrahiere( $zaehler1, $zaehler2 ); } ( $zaehler, $nenner ) = _kuerzen( $zaehler, $nenner ); if ( $zaehler > $nenner ) { my @normalisiert = _normalisiere( $zaehler, $nenner ); return "$normalisiert[0] $normalisiert[1]/$normalisiert[2]"; } else { return "$zaehler/$nenner"; } }
Da diese beiden Methoden eine große Menge gleichen Codes enthalten, können wir sie zu einer
Klassenmethode _strichrechnung() zusammenfassen, der wir den Aufruf für die jeweilige Rechenart
als Zeichenkette übergeben und durch eval() auswerten lassen; dadurch werden diese beiden
Methoden auf jeweils eine Zeile reduziert:
sub addiere { _strichrechnung( @_, '$zaehler=$self->SUPER::addiere($zaehler1,$zaehler2);' ); } sub subtrahiere { _strichrechnung( @_, '$zaehler=$self->SUPER::subtrahiere($zaehler1,$zaehler2);' ); }
Hier die entsprechende Klassenmethode _strichrechnung():
sub _strichrechnung { my $self = shift; my $op1 = shift; my $op2 = shift; my $methode = shift; my ( $zaehler, $nenner ) = 0; my ( $zaehler1, $nenner1, $zaehler2, $nenner2 ) = _init( $op1, $op2 ); if ( $nenner1 != $nenner2 ) { my $kgv = _kgv( $nenner1, $nenner2 ); $nenner = $kgv; my ( $faktor1, $faktor2 ) = ( $kgv / $nenner1, $kgv / $nenner2 ); ( $zaehler1, $nenner1 ) = _erweitern( $zaehler1, $nenner1,
$faktor1 ); ( $zaehler2, $nenner2 ) = _erweitern( $zaehler2, $nenner2,
$faktor2 ); eval($methode); } else { $nenner = $nenner1; eval($methode); } ( $zaehler, $nenner ) = _kuerzen( $zaehler, $nenner );
Sprachwissenschaftliches Institut
161
if ( $zaehler > $nenner ) { my @normalisiert = _normalisiere( $zaehler, $nenner ); return "$normalisiert[0] $normalisiert[1]/$normalisiert[2]"; } else { return "$zaehler/$nenner"; } }
Literatur
Einführend: • Cozens, Simon (2000), Beginning Perl, Birmingham: Wrox (Online als PDF-Dateien unter
http://learn.perl.org/library/beginning_perl)
• Johnson, Andrew L. (2002), Einstieg in Perl, Bonn: Galileo Press
• Klier, Rainer (2002), Nitty Gritty Perl, München: Addison Wesley
• Schwartz, Randal L. und Tom Phoenix (20054), Learning Perl, Sebastopol: O'Reilly
• Ziegler, Joachim (2002), Programmieren lernen mit Perl, Berlin: Springer
Weiterführend: • Christiansen, Tom und Nathan Torkington (20032), Perl Cookbook, Sebastopol: O'Reilly
• Conway, Damian (2005), Perl Best Practices, Sebastopol: O’Reilly
• Conway, Damian (2001), Objektorientierte Programmierung mit Perl: Konzepte und
Techniken, München: Addison Wesley
• Cross, David (2001), Data Munging with Perl, Greenwich: Manning
• Friedl, Jeffrey (20032), Mastering Regular Expressions, Sebastopol: O'Reilly
• Hall, Joseph (1998), Effective Perl Programming. Writing Better Programs with Perl, Reading:
Addison Wesley
• Orwant, Jon (Hrsg.) (2002), Computer Science and Perl Programming. Best of The Perl
Journal, Sebastopol: O'Reilly
• Schwartz, Randal L. (2005), Randal Schwartz’s Perls of Wisdom, Berkeley: Apress
• Schwartz, Randal L. und Tom Phoenix (2003), Learning Perl Objects, References and
Modules. Beyond the Basics of Perl, Sebastopol: O'Reilly
• Scott, Peter (2004), Perl Medic: Transforming Legacy Code, Reading: Addison Wesley
Computerlinguistik: • Jurafsky, Daniel S. und James H. Martin (2000), Speech and Language Processing: An
Introduction to Natural Language Processing, Computational Linguistics, and Speech
Recognition. Upper Saddle River: Prentice Hall
• Klabunde, Ralf et al. (Hrsg.) (20042), Computerlinguistik und Sprachtechnologie. Eine
Einführung, München: Elsevier
• Manning, Christopher und Hinrich Schütze (2000), Foundations of Statistical Natural
Language Processing, Boston: MIT Press
Index
162
Index
$_, 46, 58, 76
@_, 104
@ARGV, 61
Algorithmus, 12
Anweisung, 13
Array
Datei einlesen in, 60
Elemente platzieren, 26
exists(), 41
Iteration mit while(), 45
pop(), 27
push(), 27
shift(), 27
sort(), 28
splice(), 28
unshift(), 27
Zugriff, 25
Arrays of Arrays (LoL), 131
Arrays of Hashes (LoH), 131, 135
Autovivikation, 135
Bedingungen, 37
else, 40
elsif(), 40
if(), 40
in Schleifen, 42, 43
unless(), 41
Begrenzungszeichen, 59, 75
leaning toothpick syndrome, 75
Bigramm, 33, 51, 63, 100, 112, 142
Block
Sortierung, 29
Callbacks, 139
Closures, 140
cmp, 45
Comprehensive Perl Archive Network
(CPAN), 145
Data::Dumper, 126
Datei
Ausgabe umlenken, 57
Lesen, 56
open(), 56
Schlürfmodus, 59
Schreiben, 56
Dateideskriptor, 56, 58, 60
ARGV, 60
DATA, 60
Daten, 12
Datenstrukturen, 12
Anonyme, 125
Array, 24
Arrays of Arrays (LoL), 131
Arrays of Hashes (LoH), 131, 135
Autovivikation, 135
Data::Dumper, 126
defined(), 41
exists(), 41
Gemischte, 131
Hash, 29
Hashes of Arrays (HoL), 131, 136
Hashes of Hashes (HoH), 131, 132
Komplexe, 131
Namensräume, 24
record, 131
Skalar, 13, 123
struct, 131
Datentypen, 12
Defaultvariable
$_, 46, 58
@_, 104
defined(), 41
delete(), 27, 31
die(), 58
do{}, 43
do{}...until(), 43
do{}...while(), 43
else, 40
elsif(), 40
Emacs
_emacs, 3
Ersetzen, 9
Installation, 3
kill ring, 9
Kommandos, 6
Konfiguration, 3
Modus, 5
Navigation, 8
Sprachwissenschaftliches Institut
163
Perl-Code ausführen, 10
Puffer, 6, 7, 10
Referenz, 11
Suche, 9
Tastenkombinationen, 6
exists(), 41
Fehler, 58
die(), 58
Variable $!, 58
for(), 43
foreach(), 44
Funktion, 13
grep, 47
Gültigkeitsbereich, 107
Dynamischer, 107
Globaler, 107
Lexikalischer, 107
Lokaler, 107
Paket, 107
Hash, 29
delete(), 31
exists(), 41
Hashslice, 30, 31
iterieren mit foreach(), 44
keys(), 31
Schlüssel, 29
Schlüssel sind Unikate, 30
Skalarkontext, 31
sortieren, 45
Tokenhäufigkeit bestimmen, 53
Typehäufigkeit bestimmen, 53
values(), 31
Wert, 29
Zugriff, 30
Hashes of Arrays (HoL), 131, 136
Hashes of Hashes (HoH), 131, 132
if(), 40
Index, 22
input record separator
Variable $/, 60
join(), 59
keys(), 31
Kleene, 74
Kollokation, 101
Konstante, 19
Kontrollstrukturen, 37
Bedingungen, 37, 40
Bedingungen in Schleifen, 42
Block, 39, 42
do{}...until(), 43
do{}...while(), 43
else, 40
elsif(), 40
for(), 43
foreach(), 44
if(), 40
last(), 48
Laufvariable, 43, 45
next(), 49
Postfix-Schreibweise ohne Block, 40
Schleifen, 37, 42
unless(), 41
until(), 42
while(), 42, 45, 58
Korpus, 63
last(), 48
Laufvariable, 43, 45
Liste, 20
Bereich, 23
Eindimensionalität, 22, 25, 106
Index, 22
Listenelement, 20
Listenkontext, 24
Slice, 22
sort(), 28
Zugriff, 22
Literal, 13
map, 47
Module, 145
Comprehensive Perl Archive Network
(CPAN), 145
Exporter, 152
Konstruktor, 149
Manuelle Installation, 146
nmake, 147
Objektorientierte, 147, 149, 153
package, 152
perldoc, 147
PPM, 146
Prozedurale, 147, 152
XS, 146
Mustervergleich, 74
Natürliche vs. formale Sprachen, 90
next(), 49
Index
164
n-Gramm, 63, 72
Bigramm, 33
extrahieren mit splice(), 33
Trigramm, 33
open(), 56
Operatoren
Anführungszeichen, 21
arithmetische Operatoren, 13
Autodekrement-Operator, 18
Autoinkrement-Operator, 18
cmp, 16, 28, 45
Diamant-Operator, 58
lookahead-Operator ?=, 96
lookbehind-Operator ?<=, 96
Musterbindungsoperator, 75
numerische Vergleichsoperatoren, 14
Operatoren für Zeichenkettenvergleiche, 16
Quotemeta-Operator, 85
Raumschiff-Operator, 16, 29, 45
readline-Operator, 58
s///, 95
ternäre Vergleichsoperatoren, 16
tr///, 95
Vergleichsoperator, 75
Zeichenkettenoperatoren, 15
Zuweisungsoperator, 16
Perl
Installation, 1
pop(), 27, 45
Pragma
diagnostics, 19
locale, 80
strict, 19
Präzedenz, 14
push(), 27, 59
Raumschiff-Operator, 45
ref(), 141
Referenzen, 123
Arrayreferenz, 124, 127
Arrays of Arrays (LoL), 131
Arrays of Hashes (LoH), 131, 135
Art der Referenz, 141
Autovivikation, 135
Callbacks, 139
Closures, 140
Data::Dumper, 126
Dereferenzierung, 126
Hash, 127
Hashes of Arrays (HoL), 131, 136
Hashes of Hashes (HoH), 131, 132
Hashreferenz, 124
Indirektion, 131
Pfeilnotation, 130
record, 131
ref(), 141
Skalarreferenz, 123, 127
struct, 131
Subroutinenargumente, 137
Subroutinenreferenz, 138
Reguläre Ausdrücke, 74
Anker, 87
Backtracking, 92
Deterministischer endlicher Automat, 91
Endlicher Automat, 90
Ersetzung, 95
Funktionsweise, 90
Gier, 94
Gruppierung, 85, 86
Indexzählung bei Speicherung, 85
Komplemente von Zeichenklassen, 80
lookahead-Operator ?=, 96
lookbehind-Operator ?<=, 96
Modifikatoren, 88
Musterbindungsoperator, 75
Nichtdeterministischer endlicher Automat,
91
Platzhalter, 77
Punkt ., 80
Quantoren, 81
Speicherung, 85
Übergenerierung, 80
Variableninterpolation, 84
Vergleichsoperator, 75
Wortgrenze, 87
Zeichenklasse, 77, 79
Zeichenklasse der Leerraumzeichen, 79
Zeichenklasse der Wortzeichen, 79
Zeichenklasse der Ziffern, 79
return(), 109
Leere Argumentliste, 111
Rückgabewert, 13, 109
Hashslice, 30
Listenkontext vs. Skalarkontext, 24, 110
Listenslice, 22
Listenwertige Datenstrukturen, 137
Sprachwissenschaftliches Institut
165
return() (leere Argumentliste), 111
split(), 59
wantarray(), 110
Schleifen, 39, 42
do{}...until(), 43
do{}...while(), 43
Endlosschleife, 42
Endlosschleifen, 48
for(), 43
foreach(), 44
last(), 48
Laufvariable, 43, 45
next(), 49
Schleifenrumpf, 42
until(), 42
while(), 42, 45, 58
select(), 60
shift(), 27, 45
Skalar, 13
Datei einlesen in, 60
Skalarkontext, 24
Slice
Arrayslice, 25
Hashslice, 30, 31
sort(), 28, 45
Sortieren, 45
sparse data problem, 72
splice(), 28
split(), 58, 75
sprintf(), 62
Standardausgabe, 20
Standardeingabe, 20
Stoppwörter, 101
Subroutinen, 103
@_, 104
Argumente, 137
Argumentliste, 104, 106
Aufruf, 103
Benannte Parameter, 106, 143
call by reference vs. call by value, 104
Callbacks, 139
Closures, 140
Listenkontext vs. Skalarkontext, 110
return(), 109
return() (leere Argumentliste), 111
Rückgabewerte, 109, 137
wantarray(), 110
Tetragramm, 63, 101, 112
Token, 52
Häufigkeit, 34
Trennzeichen, 59
Trigramm, 33, 51, 63, 101, 112
Type, 52
unless(), 41
unshift(), 27
until(), 42
values(), 31
Variable, 16
Bezeichner, 17
Initialisierung, 16
Interpolation, 17
my, 19
Skalarvariablen, 16
Variablendefinition, 16
Variablendeklaration, 16
Verzeichnispfade, 57
wantarray(), 110
while(), 42, 45, 58
Zipf'sches Gesetz, 72