Caesar

Cäsar-Verschlüsselung #

Beide hier vorgestellten Implementierungen der Cäsar-Verschlüsselung sind vom Aufbau her gleich. Die Hilfsfunktionen (im Java-Programm: statische Methoden) position_im_alphabet und buchstabe_an_position liefern Position (als Ganzzahl) bzw. Buchstaben (als Zeichenkette) zurück. Aus didaktischen Gründen beginnt die Nummerierung der 26 Buchstaben nicht bei der Ziffer 0, sondern bei der 1.

Die Funktion verschluessele zu schreiben ist Aufgabe in meinem Unterricht. In Anlehnung ans Test-driven-development werden Tests zur Verfügung gestellt.

Einige Unterschiede werfen ein interessantes Licht auf die Eignung beider Sprachen für den Unterricht (siehe unten), nämlich Iteration über Zeichenketten, damit zusammenhängend die Wahl geeigneter Datentypen, Zeichenkettenkonkatenationen und Tests.

Hier zunächst beide Implementierungen:

import doctest

alphabet = 'abcdefghijklmnopqrstuvwxyz'

def verschluessele(text: str, verschiebung: int) -> str:
    """
    Nimmt Texte (Zeichenketten) und eine Verschiebung entgegen und gibt
    entsprechend verschlüsselte Texte zurück. Die Texte werden in Kleinbuchstaben
    ausgeben, Leerzeichen und Satzzeichen bleiben erhalten.
    
    >>> verschluessele('abc', 3)
    'def'
    >>> verschluessele('Hallo', 1)
    'ibmmp'
    >>> verschluessele('xyz', 3)
    'abc'
    >>> verschluessele('Mister X, das wird nichts!', 3)
    'plvwhu a, gdv zlug qlfkwv!'
    """
    text_verschluesselt = ''
    for zeichen in text:
        vielleicht_position = position_im_alphabet(zeichen)
        if not vielleicht_position == -1:
            position_verschluesselt = ((vielleicht_position - 1 + verschiebung) % 26) + 1
            zeichen_verschluesselt = buchstabe_an_position(position_verschluesselt)
            text_verschluesselt += zeichen_verschluesselt
        else:
            text_verschluesselt += zeichen
    return text_verschluesselt

def position_im_alphabet(zeichen: str) -> int:
    """
    Die Funktion positionImAlphabet gibt für einzelne
    Buchstaben deren Position (1..26) im Alphabet zurück.
    Für Umlaute, andere Zeichen und längere Zeichenketten
    wird -1 zurückgegeben.
    
    >>> position_im_alphabet('c')
    3
    >>> position_im_alphabet('Z')
    26
    >>> position_im_alphabet(' ')
    -1
    >>> position_im_alphabet('abc')
    -1
    """
    position = -1
    if len(zeichen) == 1 and zeichen.lower() in alphabet:
        position = alphabet.index(zeichen.lower()) + 1 
    return position;

def buchstabe_an_position(position: int) -> str:
    """
    Die Funktion buchstabe_an_position gibt für Zahlen zwischen
    1 und 26 Buchstaben zurück, die an der entsprechenden
    Position im Alphabet stehen. Für alle anderen Zahlen
    wird eine leere Zeichenkette zurückgegeben.
    
    >>> buchstabe_an_position(2)
    'b'
    >>> buchstabe_an_position(25)
    'y'
    >>> buchstabe_an_position(27)
    ''
    >>> buchstabe_an_position(-1)
    ''
    """
    if 0 < position < 27:
        return alphabet[position - 1]
    return ''
        
if __name__ == '__main__':
    doctest.testmod()

Caesar.java

class Caesar {

   /**
    * Nimmt Texte (Zeichenketten) und eine Verschiebung entgegen und gibt
    * entsprechend verschlüsselte Texte zurück. Die Texte werden in Kleinbuchstaben
    * ausgeben, Leerzeichen und Satzzeichen bleiben erhalten.
    */
   static String verschluessele(String text, int verschiebung) {
      StringBuilder verschluesselterText = new StringBuilder();
      String[] textArray = text.split("");
      for (String zeichen : textArray) {
         int position = positionImAlphabet(zeichen);
         if(position != -1) {
            int positionVerschluesselt = ((position - 1 + verschiebung) % 26) + 1;
            verschluesselterText.append(buchstabeAnPosition(positionVerschluesselt));
         } else {
            verschluesselterText.append(zeichen);
         }
      }
      return verschluesselterText.toString();
   }

   /**
    * Die Methode positionImAlphabet gibt für einzelne
    * Buchstaben deren Position (1..26) im Alphabet zurück.
    * Für Umlaute, andere Zeichen und längere Zeichenketten
    * wird -1 zurückgegeben.
    */
   static int positionImAlphabet(String zeichen) {
      int position = -1;
      if(zeichen.length() == 1) {
         String alphabet = "abcdefghijklmnopqrstuvwxyz";
         position = alphabet.indexOf(zeichen.toLowerCase());
         if(position != -1) {
            position++;
         }
      }
      return position;
   }

   /**
    * Die Methode buchstabeAnPosition gibt für Zahlen zwischen
    * 1 und 26 Buchstaben zurück, die an der entsprechenden
    * Position im Alphabet stehen. Für alle anderen Zahlen
    * wird eine leere Zeichenkette zurückgegeben.
    */
   static String buchstabeAnPosition(int position) {
      String buchstabe = "";
      String alphabet = "abcdefghijklmnopqrstuvwxyz";
      if(position > 0 && position < 27) {
         buchstabe += alphabet.substring(position - 1, position);
      }
      return buchstabe;
   }

   static void println(Object o) {
       System.out.println(o.toString());
   }
}

CaesarTest.java

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class CaesarTest {

    @Test
    void testVerschluessele() {
        assertEquals("def", Caesar.verschluessele("abc", 3));
        assertEquals("ibmmp", Caesar.verschluessele("Hallo", 1));
        assertEquals("abc", Caesar.verschluessele("xyz", 3));
        assertEquals("plvwhu a, gdv zlug qlfkwv!", Caesar.verschluessele("Mister X, das wird nichts!", 3));
        assertEquals("mister x, das wird nichts!", Caesar.verschluessele("plvwhu a, gdv zlug qlfkwv!", 23));
    }

    @Test
    void testPositionImAlphabet() {
        assertEquals(3, Caesar.positionImAlphabet("c"));
        assertEquals(26, Caesar.positionImAlphabet("Z"));
        assertEquals(-1, Caesar.positionImAlphabet(" "));
        assertEquals(-1, Caesar.positionImAlphabet("abc"));
    }

    @Test
    void testBuchstabeAnPosition() {
        assertEquals("b", Caesar.buchstabeAnPosition(2));
        assertEquals("y", Caesar.buchstabeAnPosition(25));
        assertEquals("", Caesar.buchstabeAnPosition(27));
        assertEquals("", Caesar.buchstabeAnPosition(-1));
    }
}

Gedanken zu den Unterschieden #

Iteration über den Eingabestring #

Beim Iterieren über die zu verschlüsselnde Zeichenkette lässt sich in Python die von Listen gewohnte Syntax for zeichen in text: verwenden. In Java ist das nicht ganz so einfach möglich. In der vorliegenden Implementierung ist der Split in ein Array von einzelnen Zeichen (ebenfalls Strings) zu sehen, die erst dann mit der vereinfachten for (String zeichen : text) durchlaufen werden können.

Alternativ hätte im Java-Programm mit der charAt-Methode über die Länge der Zeichenkette iteriert werden können. Obwohl die Behandlung der Zeichen als char gerade für die Berechnung der Cäsar-Verschiebung einen eigenen Charme entwickelt, erscheint doch das dahinterliegende Konstrukt (ASCII-Zeichen sind gleichzeitig Ganzzahlen, mit denen einfach hin- und hergerrechnet werden kann) kontraintuitiv. In Python ließe sich eine solche Implementierung mit den Built-in-Functions chr (liefert ein Zeichen zur übergebenen Zahl) und ord (liefert eine Zahl – ggf. ASCII – zu einem gegebenen Zeichen) ebenfalls umsetzen. Ich habe mich dagegen entschieden, eine Lösung des Problems auf diesem Wege vorzuschlagen, weil die zusätzliche Ebene zu viele gedankliche Resourcen zu verschlingen scheint und die zur Verfügung gestellten Hilfsfunktionen hier eine günstigere Abstraktion zur Verfügung stellen. Ich möchte, dass die Aufmerksamkeit auf dem Verschiebe-Algorithmus liegt, aber nicht auf der ASCII-Zeichenzuordnung.

Zeichenketten-Konkatenation #

Weil Zeichenketten in Python wie in Java immutabel sind, ist Konkatenation teuer. Daher finden sich in der Python- wie in der Java-Community Diskussionen, wie sich diese Aufgabe am effizientesten erledigen lässt.

Für Java habe ich in »Java ist auch eine Insel« folgende Möglichkeiten gefunden: den +-Operator, die Methode concat, die Klassenmethode join, den Aufbau mit einem StringBuilder sowie den Aufbau per StringBuffer.

Für Python habe ich in »Python 3« folgende Möglichkeiten gefunden: den +-Operator und die Funktion join.

Vermutlich ist es gut, Beispiele im Unterricht auf nur einen Weg zu beschränken, z.B. den +-Operator – zu besichtigen in der Python-Implementierung hier. Trotzdem habe ich für das Java-Programm, weil ich das als guten Java-Stil kennengelernt habe, einen StringBuilder verwendet. Ich hoffe, dass auch an der Aufzählung oben deutlich wird, dass Java hier viele Wege anbietet, während sich Python auf wenige beschränkt. (Das Übrigens in Übereinstimmung mit dem Zen of Python: »There should be one– and preferably only one –obvious way to do it.«)

Test-Driven Development #

Es kann motivierend sein, gegen Tests zu programmieren. Neben der schnellen und hoffentlich klaren Rückmeldung, welcher Aspekt einer Implementierung noch fehlschlägt, ist auch das Ende klar markiert: wenn alle Tests grün sind, bin ich fertig.

Die Tests in den Docstring zu verfrachten, ist ein sehr eleganter Weg, der in Python üblich ist. Gleichzeitig wird mit den Testfällen das gewünschte Verhalten der Funktion dokumentiert. Einmal mehr zeigt sich, dass bei Python die batteries included sind – es müssen keine Abhängigkeiten installiert werden.

Auch in Java lässt sich gegen Tests programmieren. Weit verbreitet ist Junit, dass allerdings eigens eingerichtet werden muss, wenn die IDE das nicht übernimmt. Weniger prominent und in der Testklasse versteckt sind allerdings die konkreten Testfälle.

Fazit #

Python behandelt Strings sehr konsequent als sequenziellen Datentypen. Das erweist sich hier als Vorteil: die aussagekräftige Formulierung (for zeichen in text) iteriert über den Eingabestring, ohne dass irgendeine Vorverarbeitung nötig wäre. Java zieht hier den Kürzeren, denn entweder muss zunächst ein Split vorgenommen werden (dann kann eine ähnliche Formulierung gewählt werden), oder die kompliziertere herkömmliche for-Wiederholung und in der Folge Typumwandlungen müssen in Kauf genommen werden (for (int index=0; index < text.length(); index++) {//…}). Wenn der Informatikunterricht sich an nicht nur interessierte Schüler:innen richtet, ist es wichtig, die Einstiegshürden niedrig zu halten. Komplizierte Sprachkonstrukte sind schwerer zu verinnerlichen. Wenn sie für ein Thema (wieder) neu gelernt werden, belasten sie das Arbeitsgedächtnis. So lässt sich effektiv verhindern, dass der eigentlich neue Inhalt (der Verschiebe-Algorithmus) verarbeitet wird. Aus dieser Sicht ist der Favorit klar Python.

In Java wie in Python lassen sich Strings mit dem +-Operator konkatenieren. Doch wie ich gezeigt habe, existieren daneben einige weitere Methoden: in Java mehr als in Python. Wenn Unterrichtsbeispiele auf eine Methode beschränkt werden, ist das kein Nachteil. Allerdings wird Code häufiger gelesen als geschrieben, und in freier Wildbahn kommen alle Varianten vor. Weil Python es an dieser Stelle geschafft hat, die Anzahl der Varianten zu begrenzen, ist sie einmal mehr die zugänglichere Sprache.

Das gleiche gilt für Unittests: im Docstring fallen sie fast nicht auf und dokumentieren Funktionen mit Beispielen. Im Unterricht lässt sich dies fruchtbar nutzen. Dies ist auch in Java möglich, allerdings mit zusätzlichen Kosten: neben dem anderen Ort muss ich als Lerner:in verstehen, was mit assertEquals gemeint ist. Jedes Zu-viel-des-Neuen stellt sich dem Eigentlichen in den Weg.

Alternativen #

Für meinen Unterricht mit der Online-IDE habe ich eigene Testmethoden in Java geschrieben – dies scheint ein gangbarer Weg, wo immer Junit nicht zur Verfügung steht.