Gutes Beispiel für Vererbung im Schulunterricht?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
EmaNymton
User
Beiträge: 174
Registriert: Sonntag 30. Mai 2010, 14:07

Hallo zusammen,
wir sind gerade dabei an unserer Schule in der Oberstufe von Java auf Python umzuschwenken und ich versuche gerade einen Anwendungsfall für Vererbung zu finden, der sinnvoll und für Schüler nachvollziehbar ist. Durch den Kurs zieht sich wie ein roter Faden ein Rechentrainings-Programm, das immer mal wieder auftaucht und im Laufe der Zeit verbessert/erweitert wird. Bei der Einführung der Klassen/Objekte wurde der Rechentrainer ebenfalls mit den Klassen Spiel, Spieler und Aufgabe modelliert.
Aus diesem Grund war meine Idee, die Vererbung nun anhand von Aufgaben und deren Spezialisierungen einzuführen, d.h. das Programm soll statt Rechenaufgaben vielleicht auch z.B.: MultipleChoice-Fragen stellen können (oder andere, das wäre nach dem ersten Beispiel den Schülern dann freigestellt, das umzusetzen).

Ein erster Implementationsversuch anhand ich dann die Vererbung erklären wollte, sieht folgendermaßen aus:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from random import randint

class Aufgabe():
    def __init__(self, frage = None, antwort = None):
        self.frage = frage
        self.antwort = antwort

    def stelle(self):
        return self.frage

    def antwort_ist_richtig(self, eingabe):
        return eingabe == self.antwort


class RechenAufgabe(Aufgabe):
    """
    maximaler Zahlenraum und Rechenart werden übergeben,
    daraus werden frage und antwort generiert
    """
    def __init__(self, zahl_max, rechenart):
        frage = self.frage_generieren(zahl_max, rechenart)
        antwort = self.antwort_generieren(frage)
        Aufgabe.__init__(self, frage, antwort)

    def frage_generieren(self, zahl_max, rechenart):
        zahl1 = randint(1, zahl_max)
        zahl2 = randint(1, zahl_max)
        return '{0} {1} {2} = '.format(zahl1, rechenart, zahl2)

    def antwort_generieren(self, frage):
        return eval(frage[:-2])


class MultipleChoiceAufgabe(Aufgabe):
    """
    bekommt eine Frage, die richtige Antwort
    und eine Liste mit möglichen Antworten übergeben,
    """
    def __init__(self, frage, richtige_antwort, moegliche_antworten):
        Aufgabe.__init__(self, frage, richtige_antwort)
        self.moegliche_antworten = moegliche_antworten

    def stelle(self):
        frage_ausgabe = Aufgabe.stelle(self)
        frage_ausgabe += '\n'
        for i, antwort in enumerate(self.moegliche_antworten):
            frage_ausgabe += "{0}: {1} \n".format(i+1, antwort)
        frage_ausgabe += "Antwort: "
        return frage_ausgabe


if __name__ == '__main__':
    print "Rechenaufgabe:"
    rechenaufgabe = RechenAufgabe(10,'+')
    antwort = input(rechenaufgabe.stelle())
    if rechenaufgabe.antwort_ist_richtig(antwort):
        print "richtig"
    else:
        print "falsch"
    print
    print "Multiple Choice Aufgabe:"
    textaufgabe = MultipleChoiceAufgabe("Wie heißt das Staatsoberhaupt in Deutschland?",2,
                                        ["Bundeskanzler", "Bundespräsident",
                                         "Bundestagspräsident","Bundesratspräsident"])
    antwort = input(textaufgabe.stelle())
    if textaufgabe.antwort_ist_richtig(antwort):
        print "richtig"
    else:
        print "falsch"
Ich hätte gerne mal eure Meinung gehört und ggf. noch Verbesserungsvorschläge auch was den Quelltext angeht.
Danke!

EmaNymton
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

Zum Quellcode:
- Klassen sollten immer von »object« erben
- um das »=« von Defaultargumenten werden keine Leerzeichen gesetzt
- Defaultargumente sollten den Standardfall abdecken, auf jeden Fall aber sinnvoll sein: ein »Aufgabe«-Exemplar ist aber mit »frage=None« oder »antwort=None« nicht sinnvoll.
- »stelle« stellt nichts, sondern gibt die Frage zurück (»liefere_frage«).
- »antwort_ist_richtig« ist eine Aussage, suggeriert also, dass damit ein Antwort-Flag auf richtig gesetzt wird: bei »ist_antwort_richtig« erwarte ich als Ergebnis dagegen »True« oder »False«.
- in »RechenAufgabe« ist weder »frage_generieren« noch »antwort_generieren« eine Methode, da »self« nicht gebraucht wird »__init__« initialisiert nicht wirklich die »RechenAufgabe« sondern generiert eine. Diese Tätigkeiten sollten klar getrennt werden. Dazu wäre eine »classmethod« »generiere_zufaellige_rechenaufgabe« passend, die die Frage und Antwort generiert, damit ein RechenAufgabe-Objekt erzeugt.
- »eval« sollte man nicht verwenden.
Um wirklich Objektorientierung zu zeigen, sollte die »RechenAufgabe« die Frage in »liefere_frage« jedesmal aus zwei Zahlen und der Operation neu erzeugen. Für das Prüfen der Antwort wird auch erst dann das Ergebnis berechnet.
- das Zusammensetzen von Strings mit »+« ist ein Anti-Pattern.
- »enumerate« nimmt als zweiten Parameter einen Startwert.
- »input« sollte wie »eval« nicht benutzt werden, da es Eingaben als Python-Code interpretiert.
Der »main«-Teil hat zu viele kopierte Teile. Es sollte zuerst eine Liste mit Aufgaben erzeugt werden, die dann in einer »for«-Schleife abgearbeitet werden.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Also als erstes finde ich es löblich, dass Du Dir viel Mühe gibst, das didaktisch wertvoll anhand eines *durchgängigen* Beispiels zu erklären :-) Ich denke jeder, der sich selber schon einmal Aufgaben ausdenken musste weiß, dass das viel aufwendiger ist, als sich für jede Themakik speziell "passende", unzusammenhängende Beispiele auszudenken.

Zum Code: Als erstes fällt auf, dass Du - obschon es sich um Python 2.x handelt - nicht von ``object`` erbst - das sollte man auf jeden Fall ;-) Ok, da es sich um ein Beispiel handelt, kann man das auch erst einmal weglassen, um die Schüler nicht zu verwirren. (Aber vielleicht kann man es später dann mal nachreichen, wenn Vererbung so weit durchgenommen worden ist!)

Mir missfällt ansonsten äußerlich der Name ``stelle``... das klingt so nach ``Keks.friss_dich()`` :mrgreen: Wieso nicht ``generiere_frage``? Oder schlicht als Property ``frage``? (Ok, mag sein, dass die Schüler mit Properties noch überfordert sind)

in der ``main`` solltest Du imho die Ausgabe der Auswertung der Aufgaben noch in eine Funktion auslagern. Zum einen vermeidet das diesen redundanten Code, zum anderen sehen die Eleven dann sofort, dass man in Python sehr wohl Funktionen und Klassen parallel benutzt ;-)

Man könnte den Aufgabentypen ein Klassenattribut mitgeben, welches ihren Screen-"Namen" beinhaltet.

Die Methode ``antwort_ist_richtig`` würde ich in ``ist_antwort_richtig`` umbennen. Das liest sich im ``if`` deutlich besser, fast schon flüssig (in Clojure z.B. ist es afaik Konvention boolschen Funktionen ein ? als Suffix mitzugeben, was sich imho noch schöner liest).

Den Entwurf von ``RechenAufgabe`` finde ich nicht so gelungen. Wo ist denn da der innere Zustand / die innere Logik? Vom ``eval`` mal ganz zu schweigen (Igitt!!!) ist es doch irgend wie sinnfrei, die ``rechenart`` zur Initialisierung zu fordern, sie dann aber nicht Klassen intern bei der Generierung zu nutzen. Selbiges gilt natürlich für ``zahl_max``! Vom API her ist es im Moment so unschön, da ich eine einmal erstelle ``RechenAufgabe`` von außen sicherlich kaum ändern kann (dazu müsste ich zu viele Details kennen), diese jedoch nach außen suggeriert, man könne Frage und Antwort auch nachträglich ändern.

In der ``init`` von ``MultipleChoiceAufgabe`` finde ich den Namen ``richtige_antwort`` unpassend. Das suggeriert, dass man etwas vom selben Typ wie die "möglichen Antworten" übergeben müsste. In Wirklichkeit handelt es sich ja aber um den Index der richtigen Antwort... wie man das wirklich elegant benennen kann, weiß ich leider grad auch nicht :-D

In der ``stelle`` der ``MultipleChoiceAufgabe`` würde ich bei enumerate den zusätzlichen Startparameter angeben, um sich die Addition in der Formatierung zu sparen. Zudem würde ich ``i`` hier wirklich in ``index`` umbenennen.

Dass man Strings in Python möglichst nicht via ``+`` zusammenbaut, weißt Du sicherlich schon. Gerade bei einer solchen Aufzählung bietet sich ``.join()`` gerade zu an. Und das sollte für die Schüler auch nicht wirklich zu schwer sein, oder? Schleifen sollten sie ja wohl schon gehabt haben.

Neben diesen vielen Kleinigkeiten bin ich ob des großen ganzen ein wenig zwiegespalten. Ok, das Beispiel ist wohl durchgängig bezüglich der bisherigen Lerninhalte und zudem relativ einfach. Aber imho kommt mir hier der Vorteil der Vererbung zu kurz! In Wirklichkeit wird ja *nur* die Auswertungsmethode von allen Typen verwendet. Werden die Schüler da wirklich den Nutzen erkennen? Auch wenn mir hier auf die schnelle kein besseres Beispiel einfällt, so würde ich aus dem Bauch heraus denken, dass eine "größere" innere Logik das Verständnis der Vorteile der Vererbung leichter greifbar macht. Ich denke einfach ein Schüler sieht den Sinn leichter, wenn wirklich viel Code "geteilt" wird und nur wenige Änderungen eine Spezialisierung auszeichnen.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@EmaNymton: Es sieht irgendwie künstlich verkompliziert aus nur damit die Vererbung ”Sinn” macht.

Zum Beispiel ist `stelle()` einfach nur eine simple Getter-Methode. Der Name ist auch irreführend, denn die Methode stellt keine Frage. Würde dort auch nicht rein gehören wenn man Programmlogik und Benutzerinteraktion sauber trennt.

Bei der `RechenAufgabe` gibt es zwei ”Methoden” die eigentlich gar keine sind, weil das ohne Probleme Funktionen hätten sein können. Die würde ich mindestens als `staticmethod()` schreiben um das deutlich zu machen, dass es Absicht ist dort Funktionen in eine Klasse gesteckt zu haben.

Das `eval()` ist „evil”. So etwas sollte man Anfängern IMHO gar nicht erst zeigen. Ich garantiere Dir einige werden dass dann im weiteren Verlauf für die hässlichsten Hacks und Abkürzungen oder Umgehungen von sauberen Code verwenden weil sie es nicht besser wissen, und durch `eval()` auch keinen Anreiz haben die richtige Lösung zu suchen, oder weil es so viel einfacher scheint als die saubere Lösung. Die Rechenart könnte man zum Beispiel als Tupel (oder `collections.namedtupel`) von Symbol (z.B. '+') und Funktion (z.B. `operator.add`) übergeben. Davon abgesehen ist das ``frage[:-2]`` auch ziemlich magisch undurchsichtig und zerbrechlich.

Bei `MultipleChoiceAufgabe` wird in `stelle()` plötzlich ein Teil der Frage generiert und bei den anderen Klassen nicht. Warum diese Asymmetrie?

Das mit der Nummer der richtigen Antwort finde ich nicht so schön. So etwas in der Richtung „Wer wird Millionär” war hier im Forum öfter schon mal Thema als Projekt für Anfänger und es lief früher oder später darauf hinaus, dass man besser die Daten so gestaltet, dass die erste Antwort immer die richtige ist, und man sie dem Benutzer in einer zufälligen Reihenfolge zeigt (`random.shuffle()`).

Wie gesagt, für mich sieht es ziemlich konstruiert aus. Das ist eines der „Probleme” mit Vererbung: Erst ab einer gewissen Komplexität macht sie Sinn, daran kranken dann viele „Spielzeugbeispiele”. Die Schüler die den Durchblick haben, fragen sich dann warum man das so kompliziert aufschreiben muss, ohne dass man wirklich etwas gewinnt. Bei dem Beispiel bleibt im Grunde nur die `ist_antwort_richtig()` als gemeinsame Methode und die ist so furchtbar trivial.

Bei Python kommt noch hinzu, dass man wegen des „duck typing” *deutlich* weniger mit Vererbung zu tun hat als in statisch typisierten Sprachen wo man oft der Form halber zumindest von Schnittstellen erben muss. In Python muss man das nur machen wenn man das tatsächlich *braucht*. Und IMHO sollte man es auch nur dann machen, weil alles andere einfach nur zusätzlicher Schreib- und Leseaufwand ist ohne tatsächlich etwas zum Programm beizutragen.

Die `Aufgabe` würde ich übrigens von `object` erben lassen, damit das eine „new style”-Klasse wird, bei der alles so wie in der Dokumentation beschrieben funktioniert. `property()` zum Beispiel.

Unterm Strich würde ich sagen man sollte nicht von einer Klasse ausgehen und fragen wie kann ich jetzt dafür irgendwie Unterklassen erstellen, sondern ein Beispiel wählen wo man beim Schreiben einer Klasse gemerkt hat, oh, das habe ich ja schon so ähnlich in dieser Klasse gelöst. Und dann kann man entscheiden ob eine Klasse eine Unterklasse der anderen sein sollte, um Code-Duplizierung zu vermeiden, oder ob man besser gemeinsamen Code in eine gemeinsame, möglicherweise abstrakte Basisklasse herausziehen kann.

Edit: Boah, ich bin heute zu langsam. :-)
EmaNymton
User
Beiträge: 174
Registriert: Sonntag 30. Mai 2010, 14:07

Danke für die vielen und ausführlichen Rückmeldungen, werde mir die Kritikpunkte annehmen und das Beispiel umschreiben.

Leider bin ich ja auch nicht wirklich zufrieden mit dem Beispiel in dieser Form, sonst hätte ich das hier ja nicht gepostet. Das Problem ist, dass ich ein relativ einfaches Beispiel haben möchte, das die Schüler verstehen und motivierend finden aber gleichzeitig nicht überfordert. Wenn man den Nutzen von Vererbung zeigen will, tritt der aber bei eher umfangreicheren Projekten auf. Hier beißt sich die Katze imho in den Schwanz. Andere Beispiele, die ich im Netz oder der Literatur gesehen habe, wirken auf mich genauso gekünzelt, so dass ich bei meinem Rechentrainer erstmal geblieben bin.

Generell merke ich, dass vieles, was in Java ein Thema war, in Python so gut wie gar nicht relevant ist bzw. man unnötigen Code produziert. Alleine die ganzen getter- und setter Methoden, die bei den UML-Diagrammen die Klassen aufblähen. Da ist man bei den zentral gestellten Aufgaben dann immer genötigt zu sagen, dass man das in anderen Programmiersprachen macht, Python hier aber dem Programmierer mehr Veranwortung zutraut.

Naja, wenn noch jemand ein Beispiel finden sollte, was meine Kriterien erfüllt oder wie man meins vielleicht noch erweitern kann, dass es ein wenig komplexer wird, aber immer noch für Schüler verständlich wird, immer her damit ;)
EmaNymton
User
Beiträge: 174
Registriert: Sonntag 30. Mai 2010, 14:07

Ich habe heute versucht mein Beispiel umzuschreiben und je mehr ich darüber nachgedacht habe, desto schlechter gefiel es mir :D
Außerdem hatte ich heute im Unterricht den Schülern die Aufgabe gestellt eigene Klassen zu entwerfen und da kamen mehrere auf die Idee, Klassen für Pen&Paper-Rollenspiele-Charaktere zu entwerfen. Mit ein wenig Phantasie und schwelgen in alten Erinnerungen :lol: kam da heute folgendes Beispiel bei mir raus:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from random import randint
from time import sleep


class Charakter(object):
    def __init__(self, name):
        self.name = name
        self.lebenspunkte = 20
        self.trefferpunkte = 2
        self.angriff = 2
        self.abwehr = 2

    def greife_an(self, charakter):
        print self.name, "greift", charakter.name, "an."
        angriffswert = randint(1,6) + self.angriff
        abwehrwert = randint(1,6) + charakter.abwehr
        if angriffswert > abwehrwert:
            print "Angriff geglückt."
            print charakter.name, "verliert", self.trefferpunkte, "Lebenspunkte."
            charakter.lebenspunkte -= self.trefferpunkte
            print charakter.name,"hat noch", charakter.lebenspunkte, "Lebenspunkte."
        else:
            print "Angriff abgewehrt."


class Krieger(Charakter):
    """
    Krieger können mehr Trefferpunkte austeilen und
    haben einen besseren Angriff.
    """
    def __init__(self, name):
        Charakter.__init__(self, name)
        self.trefferpunkte = 6
        self.angriff = 4


class Zwerg(Charakter):
    '''
    Zwerge sind robuster und können besser verteidigen.
    '''
    def __init__(self, name):
        Charakter.__init__(self, name)
        self.abwehr = 4
        self.lebenspunkte = 30


class Magier(Charakter):
    '''
    Magier können mit Magiepunkten zaubern.
    '''
    def __init__(self, name):
        Charakter.__init__(self, name)
        self.magiepunkte = 10
        self.magie_schaden = 3

    def greife_an(self, charakter):
        '''methode wird überschrieben, da der Magier
        bei einem Angriff solange zaubert,
        bis seine Magiepunkte verbraucht sind. Danach findet
        ein normaler Angriff statt
        '''
        if self.magiepunkte > 0:
            self.magiepunkte -= 2
            print self.name, "spricht einen Angriffszauber auf", charakter.name
            if randint(1,6) > 3:
                print "Zauber erfolgreich.", charakter.name, "nimmt", self.magie_schaden, "Schadenspunkte."
                charakter.lebenspunkte -= self.magie_schaden
                print charakter.name,"hat noch", charakter.lebenspunkte, "Lebenspunkte."
            else:
                print "Zauber misslungen."
        else:
            Charakter.greife_an(self, charakter)


def kampf_gegeneinander(charakter1, charakter2):
    while True:
        charakter1.greife_an(charakter2)
        sleep(2)
        if charakter2.lebenspunkte <= 0:
            print charakter1.name, "hat gewonnen."
            break
        charakter2.greife_an(charakter1)
        sleep(2)
        if charakter1.lebenspunkte <= 0:
            print charakter2.name, "hat gewonnen."
            break

if __name__ == '__main__':
    krieger = Krieger("Alrik")
    zwerg = Zwerg("Thoram")
    magier1 = Magier("Golodion")
    magier2 = Magier("Mandavar")

    kampf_gegeneinander(krieger, zwerg)
    print
    kampf_gegeneinander(magier1, magier2)
Ist zwar auch ein sehr übersichtliches und einfach gehaltenes Beispiel, eignet sich aber imho besser für meine Zwecke (Vererbung von Attributen/Methoden sowie das Überschreiben von Methoden) und motiviert die Schüler. Irgendwelche Einwände? :mrgreen:
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@EmaNymton: Dein Beispiel macht wieder aus zwei vielleicht zwei Klassen viele verschiedene. Krieger und Zwerg sind eigentlich keine eigenen Klassen, da sie nur verschiedene Werte für Attribute enthalten.
»greife_an« macht einige grundlegende Fehler bei Objektorientierung. Du weißt zuviel vom Innenleben des Charakters. Es fehlen die Methoden »bestimme_angriffswert«, »bestimme_abwehrwert« und »verliere_lebenspunkte«.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Gerade Dinge, die sich grundlegend unterscheiden sollte man eher durch Komposition lösen - der Strategie-Pattern lässt speziell bei Dingen wie "angreifen" o.ä. grüßen, eben *weil* sich Krieger, Magier usw. grundlegend darin unterscheiden... andererseits kann es auch Helden-Typen gegen, die ggf. *beides* beherrschen, also zaubern und zukloppen. Spätestens dann bist Du am Ende mit der Vererbung; ok, nur die halbe Wahrheit, da es in Python Mehrfachvererbung gibt... dennoch würde ich hier eine Komposition bevorzugen, weil man imho flexibler bleibt. Stell Dir vor, Du hast 10 Typen und "nur" drei verschiedene Angriffsmuster - da müsstest Du ja dann doppelt und dreifach denselben Code schreiben! (Last but not least kann man bei Komposition zur *Laufzeit* das Verhalten ändern)

*seufz* es ist einfach schwer, ein wirklich gutes, sinnvolles Beispiel zu finden! :-(

Du bräuchtest wirklich etwas, das *viel* innere Logik beseitzt und sich wirklich nur an einer Stelle grundlegend unterscheidet und ggf. ein weitere "optional" als überschreibbar anbietet. Ich muss da sofort an den Template-Pattern aus dem "HF-Buch Design-Patterns" denken - leider ist das Beispiel an sich auch extrem gekünstelt :-(

Gibt es da vielleicht etwas aus der Graphen-Theorie? Wobei das vermutlich für die Schüler schon wieder ein zu forderndes Domänenwissen beinhaltet...

Wie BlackJack schon feststellte ist es in Python insbesondere seltener, Vererbung zu nutzen, da man bei der meist sinnvolleren Komposition in Python nicht einmal Vererbung von Interfaces benötigt - in Java stellt alleine das Implementieren eines Interfaces ja schon eine inhärente Notwendigkeit dar, Vererbung zu nutzen ;-)

Mir fielen auf die schnelle noch GUI-Frameworks ein; bei Qt hast Du eine Menge Vererbung... leider kann man das schwer an einem simplen Beispiel demonstrieren...
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@EmaNymton: Die Vermischung zwischen Benutzerinteraktion und Programmlogik ist natürlich ein wenig unschön.

Wenn man die Standardwerte für die Standardattribute als Konstanten an die Klasse bindet und in der `Character.__init__()` von dort übernimmt, dann kann man sich die `__init__()` bei `Krieger` und `Zwerg` sparen. Eigentlich könnte man sich aber auch die Vererbung bei den beiden sparen wenn man diese Werte als Argumente an `__init__()` übergeben könnte. Dann könnte man auch wie in vielen Spielen dieser Art die Werte auswürfeln oder den Spieler eine Anzahl von Punkten selber verteilen lassen. Dann hast Du natürlich zwei Klassen weniger. :-)

Warum gibt es 10 Magiepunkte von denen immer zwei abgezogen werden statt 5 von denen immer einer abgezogen wird? Wenn die Magiepunkte durch irgendeine Programmänderung mal den Wert 1 annehmen könnten, dürfte man damit dann auch einen Zauber durchführen der zwei Punkte abzieht?

In `kampf_gegeneinander()` steht zweimal fast der gleiche Quelltext, da könnte man eine Schleife über die beiden Kämpfer schreiben. Hier auch wieder Vermischung von Benutzerinteraktion und Programmlogik. Die Funktion könnte stattdessen zum Beispiel den Gewinner zurück geben.
BlackJack

@EmaNymton: Ich habe gerade mal kurz über den selbst geschriebenen Quelltext geschaut, den ich hier gerade auf dem Laptop habe und da erbe ich von Ausnahmen, GUI-Widgets, `ctypes`-Proxies für Verbunddatentypen, SQLAlchemy-Klassen, und hier und da von Klassen von einem anderen Rahmenwerk. Ich habe kein einziges alleinstehendes Beispiel gefunden was sich für die Einführung im Unterricht eignen würde, weil da immer eine gewisse Komplexität von einem Rahmenwerk dran hängt, oder weil es zu trivial ist. Am ehesten denke ich würde sich von dem realen Code noch GUI-Kram eignen.
EmaNymton
User
Beiträge: 174
Registriert: Sonntag 30. Mai 2010, 14:07

@GUI-Kram:
Das wäre natürlich auch noch eine Option, da ich eh vorhatte Qt als Framework in den Grundzügen zu behandeln. Dann würde die Vererbung erst zu einem späteren Zeitpunkt behandelt, was aber gar nicht schlimm ist. Spontan würde mir da was mit QGraphicsItems einfallen, vielleicht bekommt man ja damit ein relativ einfaches Spiel hin.

Danke jedenfalls für den Hinweis, werde das in meiner Planung in Erwägung ziehen.
Benutzeravatar
bwbg
User
Beiträge: 407
Registriert: Mittwoch 23. Januar 2008, 13:35

Speziell für Python habe ich den Eindruck, dass man eher Beispiele für funktionale Programmierung (FP) statt OOP [1] finden kann (Lambdas, Funktionen höherer Ordnung).

Vielleicht kann/sollte man auch Beispiele bringen, wann Vererbung keinen Sinn macht um nicht das Hammer-Schraube-Syndrom herauf zu beschwören. ;)

Grüße ... bwbg

[1] Nachtrag: Sirius3 hat natürlich recht. OOP ist nicht "unnütz" per se. Ich habe hier den gedanklichen Fehler gemacht, OOP mit Vererbung gleichzusetzen.
Zuletzt geändert von bwbg am Dienstag 15. Oktober 2013, 14:25, insgesamt 1-mal geändert.
"Du bist der Messias! Und ich muss es wissen, denn ich bin schon einigen gefolgt!"
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@bwbg: OOP ist ja für Python nicht unnötig oder exotisch. Es gibt genug Konzepte von Objektorientierung, die man mit Python prototypisch umsetzen kann (Komposition, Single Level of Abstraction, Seperation of Concerns, Tell-Don't-Ask, ...) nur gibt es für Vererbung eben kaum realistische Beispiele, weil die meisten Probleme mit Komposition einfacher zu lösen sind.

Mir fällt gerade noch Schachfiguren ein. Jeder Figurtyp hat eine eigene Methode, die angibt, ob der Zug von Ax nach By erlaubt ist. In Kombination mit einer Schachbrettklasse kann die Vaterklasse Schachfigur Methoden bereit stellen, die ermitteln, welche gegnerische Figuren man Schlagen kann, ob man mit dieser Figur den Gegner Schach setzten kann, usw.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

So schlecht finde ich das Beispiel mit den Rollenspielen gar nicht. Klar, es ist nicht besonders natürlich, aber bei kurzen Beispielen wird man immer Abstriche machen müssen. Zumindest vermittelt es aber einen verständlichen Eindruck, wie Vererbung eingesetzt werden kann.

Alternativ schlage ich einfach mal TicTacToe vor. Einen allgemeinen Spieler als Basisklasse, dazu dann Mensch und Computer als Ableitungen. Neben dem perfekten Computer könnte man noch einen zufälligen einbauen oder einen vorhersagbaren. Die Züge sind komplett verschieden, nachteilig ist wohl, dass die Basisklasse nicht viel zu tun hat.
Das Leben ist wie ein Tennisball.
BlackJack

@EyDu: Wenn die Basisklasse nicht viel zu tun hat, ist es wieder kein besonders gutes Beispiel für Vererbung. Das ist doch gerade der Punkt: Ein Beispiel zu finden bei dem sowohl Basis- als auch Unterklasse nicht so aussehen als hätte man sie nur für's Beispiel auseinanderdividiert.
Benutzeravatar
diesch
User
Beiträge: 80
Registriert: Dienstag 14. April 2009, 13:36
Wohnort: Brandenburg a.d. Havel
Kontaktdaten:

Vererbung mit Qt zu erklären finde ich nicht sehr sinnvoll, da du dort nur beschränkt eigenen Klassenhierarchien aufbauen kannst, dafür aber viel ablenkenden Standard-Code hast. Außerdem ist es vermutlich nicht einfach, Qt zu erklären, ohne dabei Vererbung zu benutzen.

Das Beispiel mit den Aufgaben halte ich für ausbaufähig. Ich würde die Rechenoperation als eigene Methode implementieren und dann für Multiplikation, Division usw. jeweils eine eigene Klasse bauen. Man könnte dafür eine Basisklasse schreiben, bei deren Unterklassen man z.B. nur mit einer Klassenvariablen die Anzahl der Variablen in der Aufgabe festlegen und zwei Methoden implementieren muss, die den Text für die Fragestelleung erzeugen und die Lösung berechnen.

Wenn man die Spezialisieung auf Zahlen erst später in der Hierarchie einführt, kann man z.B. auch einen einfachen Vokabeltrainer damit bauen.

Da du auch auf GUIs eingehen willst, wäre es naheliegend, dann die Benutzer-Interaktion in einer eigene Klasse zu kapseln, so dass du einfach zwischen GUI und Text wechseln kannst.

Dabei kannst du die Klassenhierarchie nach und nach weiterentwickeln und dabei die Vor- und Nachteile einzelner Entwürfe diskutieren.
http://www.florian-diesch.de
Benutzeravatar
bwbg
User
Beiträge: 407
Registriert: Mittwoch 23. Januar 2008, 13:35

Sirius3 hat geschrieben:[...]
Mir fällt gerade noch Schachfiguren ein. Jeder Figurtyp hat eine eigene Methode, die angibt, ob der Zug von Ax nach By erlaubt ist. In Kombination mit einer Schachbrettklasse kann die Vaterklasse Schachfigur Methoden bereit stellen, die ermitteln, welche gegnerische Figuren man Schlagen kann, ob man mit dieser Figur den Gegner Schach setzten kann, usw.
Wenn es darin endet, dass man für jede Figur eine neue Klasse erstellt und eine "is_move_possible_to(board, x, y) -> bool"-Methode jedes mal überschrieben werden muss, dann böte sich hier auch wieder Komposition an und man bleibt bei einer generischen "Figur"-Klasse.

Vielleicht sollte man wirklich kurz auf sinnfreie Lehrbeispiele setzen, welche die Funktionsweise und Konsequenzen von Vererbung beschreiben und erwähnen, dass man sie (in Python) kaum benötigt ("ducktyping"). Statt dessen auf die gebräuchlicheren Vorgehensweisen ("prefer composition over inheritance") eingehen.

Grüße ... bwbg
"Du bist der Messias! Und ich muss es wissen, denn ich bin schon einigen gefolgt!"
BlackJack

Ergänzend zu bwbg: Wenn man immer alle Methoden überschreibt und die Basisklasse gar nicht braucht, kann man die Basisklasse auch ersatzlos streichen, weil es ja nicht wichtig ist von welchem Typ die konkreten Figuren sind, sondern das sie sich entsprechend verhalten.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

bwbg hat geschrieben: Wenn es darin endet, dass man für jede Figur eine neue Klasse erstellt und eine "is_move_possible_to(board, x, y) -> bool"-Methode jedes mal überschrieben werden muss, dann böte sich hier auch wieder Komposition an und man bleibt bei einer generischen "Figur"-Klasse.
Also gerade beim Schach finde ich das eigentlich nicht, denn die Anzahl der Bewegungsstrategien ist ja a priori begrenzt. Zudem benutzt jeder Typ afair *genau* eine einzigartige Bewegungsmöglichkeit, d.h. es gibt nie zwei Typen, die sich eine Strategie teilen; zur Laufzeit ändern muss sich doch eigentlich auch nie etwas, oder?

Insofern wäre Schach imho tatsächlich eine ganz gute Möglichkeit, Vererbung anschaulich zu erklären, zumal die (Zug-)Regeln an sich recht einfach und vermutlich vielen bekannt sind :-)

Und *eine* abstrakte Methode ist auch in Python sicherlich kein "Verbrechen", bei dem man schreiend von Vererbung Abstand nehmen sollte, oder ;-)

Last but not least: Man kann ja Komposition anschließend prima anhand desselben Beispiels erläutern und dann die Vor- / Nachteile gegenüber stellen :-) Ich finde diese Idee jedenfalls deutlich besser als ein komplett gekünsteltes Lehrbeispiel, welcher die Schüler dann sicherlich falsch verstehen und auf Teufel komm raus in Zukunft Vererbung einsetzen ;-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
lunar

@bwbg Was ist denn eine Schachfigur unabhängig von ihrer Bewegungsregel?! Es gibt keinen Turm, der sich bewegt wie ein Läufer…

Eigentlich bezeichnet also “Turm” keine individuelle Figur, sondern eine allgemeine Bewegungsregel, die sich in zwei anonymen, sich nur in ihren Startpositionen unterscheidenden Exemplaren manifestiert. Abstrakt gesehen ist Schach mithin nicht mehr als zwei Mengen aus Paaren "(Bewegungsregel, Startposition)". Gut, dieses Paar kann man natürlich “Figur” nennen, doch die Startposition ist halt eben nur bei Spielbeginn relevant, und muss im restlichen Spiel nicht mehr berücksichtigt werden.

“Figuren” gibt es eigentlich nur in der echten Welt. Schach ist wohl eher ein gutes Beispiel dafür, wie ein objektorientiertes Datenmodell von der Realität abstrahiert.
Antworten