Übernahme der Eingabefelder

Fragen zu Tkinter.
Benutzeravatar
spicer
User
Beiträge: 52
Registriert: Freitag 5. März 2021, 23:40
Kontaktdaten:

Ein konkretes Bsp wäre gut.
Ich habe den Vergleich mit einem Kuchen gelesen. Gut gemeint, aber iwie ist mir das immer noch nicht klar.
Eine def kann ja auch von überall her aufgerufen werden. Eine class doch auch. Oder wird die ausgeführt, wenn das Programm da vorbei kommt (eine def muss ja iwo aufgerufen werden)?
Wenn es mehrere Möglichkeiten gibt, eine Aufgabe zu erledigen, und eine davon in einer Katastrophe endet oder sonstwie unerwünschte Konsequenzen nach sich zieht, dann wird es jemand genau so machen. Alles, was schiefgehen kann, wird auch schiefgehen.
(Murphys Gesetz)
__deets__
User
Beiträge: 14537
Registriert: Mittwoch 14. Oktober 2015, 14:29

Eine Klasse kann Zustand haben. Als Freund von Elektronik ist dir vielleicht dieses Beispiel eingängig: eine Funktion (so heißt das, def ist nur ein Schlüsselwort) zb gut geeignet, einen ohmschen Widerstand zu modellieren. Eingabe sind zb Spannung und Widerstand, und geliefert wird der fließende Strom.

Ein Kondensator hingegen hat einen Widerstand, der von der im laufe der Zeit akkumulierten Ladung abhängt. Diese Ladung kann eine Funktion nicht speichern. Eine Klasse kann sich den aber merken, und zb über die Zeit akkumulieren.
Benutzeravatar
spicer
User
Beiträge: 52
Registriert: Freitag 5. März 2021, 23:40
Kontaktdaten:

Ah, dann werden die Variablen-Werte in der Klasse gespeichert?
Die Variablen sind dann ausserhalb der Klasse nicht "belegt" (wie eine globale Variable)?
Und eine Klasse wird auch erst beim Aufruf von dieser ausgeführt. Also nicht dann, wenn der Interpreter da vorbei kommt?
Wenn es mehrere Möglichkeiten gibt, eine Aufgabe zu erledigen, und eine davon in einer Katastrophe endet oder sonstwie unerwünschte Konsequenzen nach sich zieht, dann wird es jemand genau so machen. Alles, was schiefgehen kann, wird auch schiefgehen.
(Murphys Gesetz)
__deets__
User
Beiträge: 14537
Registriert: Mittwoch 14. Oktober 2015, 14:29

Die Klasse wird schon direkt ausgeführt. Erst danach existiert sie. Eine Blaupause muss man auch erstmal zeichnen. Und das gilt auch für eine Funktion. Die muss auch erstmal definiert werden. Darum heißt das ja auch “def blabla”. Vorher gibt’s auch kein blabla.

Aber danach werden eben wahlweise Aufrufe ( bei Funktionen) oder Instanzen (bei Klassen) erstellt. Und mit letzteren arbeitet man dann eben länger.
Benutzeravatar
spicer
User
Beiträge: 52
Registriert: Freitag 5. März 2021, 23:40
Kontaktdaten:

Also wird die Klasse auch erst ausgeführt, wenn diese aufgerufen.
Also kann diese oben im Code stehen und da wird die erst definiert, aber noch nicht ausgeführt. Richtig?
Und das mit den Variablen (siehe oben)?
Wenn es mehrere Möglichkeiten gibt, eine Aufgabe zu erledigen, und eine davon in einer Katastrophe endet oder sonstwie unerwünschte Konsequenzen nach sich zieht, dann wird es jemand genau so machen. Alles, was schiefgehen kann, wird auch schiefgehen.
(Murphys Gesetz)
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

Eine Klasse ist ein Bauplan für ein neues Objekt. Man nennt so ein neues Objekt auch Instanz einer Klasse. Es kann beliebig viele Instanzen, die nach dem Bauplan einer Klasse "gebaut" wurden geben.
Jede Instanz ist zwar nach dem Bauplan gebaut, verwaltet aber ihrer zugehörigen Daten separat von allen anderen Instanzen. (Mit Ausnahmen)
Bei Objekten also Instanzen von Klassen spricht man in der Regel nicht davon, dass sie ausgeführt werden.

Beipiel Taschenrechner:

Code: Alles auswählen

class TaschenrechnerBauplan:
    def __init__(self):
        """
        Initialisierung des Objekts mit eine Instanzvariablen Register
        self ist eine Referenz auf die jeweilige Instanz
        """
        self.register = 0

    def load(self, zahl):
        """ Instanzmethode (Funktion) zum laden einer Zahl ins Register"""
        self.register = zahl

    def addition(self, zahl):
        """ Instanzmethode zum Addieren einer Zahl zum Register"""
        self.register = self.register + zahl

    def ausgabe(self):
        """ Instanzmethode zur Ausgabe des Registers"""
        print(self.register)

# Bauen der Instanz "taschenrechner_instanz":
taschenrechner_instanz = TaschenrechnerBauplan()

# Benutzen der Instanz "taschenrechner_instanz"
taschenrechner_instanz.load(1)
taschenrechner_instanz.addition(2)
taschenrechner_instanz.ausgabe()

""" 
Ausgabe:
3
"""
Benutzeravatar
spicer
User
Beiträge: 52
Registriert: Freitag 5. März 2021, 23:40
Kontaktdaten:

Gutes Bsp. Danke
Was bringt das aber nun, weil ja die load, addition und ausgabe Funktionen direkt aufgerufen werden könnten?
Ich sehe, dass einzig die Variable register gespeichert bleibt (der Wert).
Also ist das vorallem eine Möglichkeit, globale Variablen zu vermeiden?

__init__ wird nur ausgeführt, wenn kein Wert an die Klasse TaschenrechnerBauplan übergeben wird?
Wenn es mehrere Möglichkeiten gibt, eine Aufgabe zu erledigen, und eine davon in einer Katastrophe endet oder sonstwie unerwünschte Konsequenzen nach sich zieht, dann wird es jemand genau so machen. Alles, was schiefgehen kann, wird auch schiefgehen.
(Murphys Gesetz)
__deets__
User
Beiträge: 14537
Registriert: Mittwoch 14. Oktober 2015, 14:29

__inti__ wird direkt nach der Erzeugung aufgerufen. Ob da Argumente gebraucht werden oder nicht, hängt von der Klasse ab.

Und ja, es geht um Zustand. Globaler Zustand ist bestenfalls unübersichtlich. Schlimmstenfalls Quelle von Fehlern.
Benutzeravatar
spicer
User
Beiträge: 52
Registriert: Freitag 5. März 2021, 23:40
Kontaktdaten:

Vielen Dank.
Ich werde mir auf jeden Fall den Link zu deinem Bsp notieren.
Jetzt blicke ich besser dahinter.
Wenn es mehrere Möglichkeiten gibt, eine Aufgabe zu erledigen, und eine davon in einer Katastrophe endet oder sonstwie unerwünschte Konsequenzen nach sich zieht, dann wird es jemand genau so machen. Alles, was schiefgehen kann, wird auch schiefgehen.
(Murphys Gesetz)
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Also mir gefällt ja das Taschenrechnerbeispiel nicht so wirklich. Von Funktionen kommend würde das so nicht entstehen, weil diese Funktionen so niemand schreiben würde. Man muss also OOP schon drauf haben um das so zu schreiben und es gibt keinen ”Schritt” von Nicht-OOP zu OOP, den man da nachvollziehen könnte. ”Historisch” ist das ja eher so das Objekte aus einem Verbunddatentyp (``RECORD`` in Pascal, ``TYPE`` in BASIC, ``struct`` in C) entstanden sind. Der OP kommt ja aus einer Sprache mit GOTO-Anweisung.

Sinnvoller aus der Ecke kommend ist IMHO eher ein Beispiel wo Funktionen verwendet werden, da aber mittlerweile zu viele Einzelwerte herum gereicht werden müssen, so dass man die sinnvoll zu einem Verbunddatentyp zusammenfassen kann/sollte, und wo man dann einen der Vorteile zeigen könnte die dadurch entstehen, dass man die Funktionen die darauf operieren mit in den Datentyp hineinzieht. Das wäre also Polymorphie als einfachster Vorteil. Blöderweise braucht man in Python schon Klassen wenn man das gar nicht braucht, weil das der einzige Verbunddatentyp ist. Andererseits macht es natürlich auch Sinn dafür keinen eigenen Typ einzuführen, wenn Klassen ja auch diese „plain old data“ (POD)/passive Datenstrukturen abdecken können, und letztlich in Python ja *jeder* Datentyp durch eine Klasse repräsentiert wird, ja werden muss.

OOP-Beispiele haben in der Regel das Problem, dass sie entweder sehr künstlich sind, und sich der Anfänger fragt, warum macht man dass, oder sie sind schon recht komplex, und überfordern damit Anfänger. Was in der Natur der Sache liegt, weil die Aufgabe OOP ist Komplexität zu organisieren und handhabbar zu halten. Wenn in dem Beispiel keine Polymorphie oder Vererbung vorkommt, dann ist es IMHO kein OOP-Beispiel, denn das könnte man dann auch genau so gut ohne OOP lösen.

Zurück zum Taschenrechnerbeispiel, das man in QBasic beispielsweise mit Verbunddatentyp und Prozeduren so schreiben würde:

Code: Alles auswählen

TYPE TTaschenrechner
  register AS INTEGER
END TYPE

SUB TaschenrechnerInit (rechner AS TTaschenrechner)
  rechner.register = 0
END SUB

SUB TaschenrechnerLadeRegister (rechner AS TTaschenrechner, zahl AS INTEGER)
  rechner.register = zahl
END SUB

SUB TaschenrechnerAddiere (rechner AS TTaschenrechner, zahl AS INTEGER)
  rechner.register = rechner.register + zahl
END SUB

SUB TaschenrechnerAusgabe (rechner AS TTaschenrechner)
  PRINT rechner.register
END SUB

DIM rechner AS TTaschenrechner

TaschenrechnerInit rechner
TaschenrechnerLadeRegister rechner, 1
TaschenrechnerAddiere rechner, 2
TaschenrechnerAusgabe rechner
Das sieht so schon nicht sinnvoll aus, und das auf Pseudo-OOP zu erweitern taugt IMHO nicht jemandem den Sinn von OOP näher zu bringen. Das hat keinen wirklichen Vorteil gegenüber:

Code: Alles auswählen

DIM register AS INTEGER

register = 0
register = 1
register = register + 2
PRINT register
Und das könnte man noch weiter vereinfachen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
spicer
User
Beiträge: 52
Registriert: Freitag 5. März 2021, 23:40
Kontaktdaten:

Verstehe nur Bahnhof.
Und was ist OOP?
Wenn es mehrere Möglichkeiten gibt, eine Aufgabe zu erledigen, und eine davon in einer Katastrophe endet oder sonstwie unerwünschte Konsequenzen nach sich zieht, dann wird es jemand genau so machen. Alles, was schiefgehen kann, wird auch schiefgehen.
(Murphys Gesetz)
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

OOP steht für Objekt Orientierte Programmierung

Die Idee dahinter ist, dass man zum Beispiel den Taschenrechner im Programm als Objekt mit eigenen Eigenschaften (hier nur das Register) und eigenen Funktionen (Laden, Addieren Ausgeben) modelliert.
Dem gegenüber steht die Funktionale oder Prozedurale Programmierung.

Wie du ja schon gemerkt hattest, ist es nicht zwingend erforderlich den Taschenrechner als Klasse zu definieren. Die gleiche Funktionalität hätte man auch mit einfachen Funktionen darstellen können.
Manche Programmiersprachen sind so aufgebaut, dass sie ausschließlich der Objekt Orientierten Programmierung folgen. Andere sind rein Funktional. Bei Python ist jede beliebige Mischung aus beiden Paradigmen möglich.
Buchfink
User
Beiträge: 193
Registriert: Samstag 11. September 2021, 10:16

@_blackjack_
ich hoffe ich lehne mich nicht zu weit aus dem Fenster.
Der am einfachsten zu verstehende Aspekt bei Klassen - nämlich Datenkapselung bzw. der Schnittstellen-Gedanke ist im Rahmen von Python vermutlich aufgrund der Abwesenheit von private für einen Anfänger nicht gut erkennbar.

Damit bleibt noch Polymorphie und die Verbundthematik, die Du angesprochen hast.

Das Buch "Clean Code" fand ich diesbezüglich sehr hilfreich. Die Codebeispiele sind allerdings Java.

Hinsichtlich Verbundthematik gibt's dort einen Passus, in dem es z.B. heißt, dass Methoden mit mehr als zwei Übergabeparametern Refactoring-bedürftig sind:

Code: Alles auswählen

foo = bar(spam, ham, eggs)
--> hier sollte man eben aus spam, ham und eggs eine Klasse machen und dann das entsprechende Objekt übergeben
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

--> hier sollte man eben aus spam, ham und eggs eine Klasse machen und dann das entsprechende Objekt übergeben
Das wäre dann eine Datenklasse, die ja auch die Methode "bar" enthalten könnte. So etwas gibt es natürlich auch in Python und das macht sicher auch in manchen Fällen Sinn.
Aber dass man ab drei Übergabeparametern, diese schon in eine Klasse zusammenfassen soll würde ich nicht pauschal als "Clean Code" bezeichnen. Das hängt sicher immer stark von der Situation ab.
__deets__
User
Beiträge: 14537
Registriert: Mittwoch 14. Oktober 2015, 14:29

Buchfink hat geschrieben: Sonntag 10. Oktober 2021, 20:31 Hinsichtlich Verbundthematik gibt's dort einen Passus, in dem es z.B. heißt, dass Methoden mit mehr als zwei Übergabeparametern Refactoring-bedürftig sind:

Code: Alles auswählen

foo = bar(spam, ham, eggs)
--> hier sollte man eben aus spam, ham und eggs eine Klasse machen und dann das entsprechende Objekt übergeben
Das halte ich fur fast schon groben Unfug. Es ist überhaupt nichts gewonnen, 3 Argumente durch eine Klasse mit 3 Attributen zu ersetzen. Außer es für die Programmiererin komplizierter zu machen.

Genau solche formulaischen Vorgehensweisen, schlimmstenfalls noch erzwungen durch Code-Metrik-Tools, sind eine der dunklen Ecken der Softwareentwicklung. Da wird dann ohne Sinn und Verstand etwas verkompliziert oder angekreidet.
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Buchfink: Ich verstehe nicht so recht warum man dafür Zugriffsschutz brauchen soll. Zur Datenkapselung braucht man das nicht. Dazu reicht es wenn man einfach nicht auf Sachen zugreift, die zu Interna gehören. Die kann man einfach mit einem Unterstrich als solche kenntlich machen. Und in vielen einfachen Einstiegsbeispielen gibt es dann in Sprachen wie Java triviale Getter und Setter, womit die Attribute de fakto doch wieder öffentlich sind. Das ist dann keine Kapselung beziehungsweise ist das nichts anderes als in Python direkt auf Attribute zuzugreifen. Die Argumentation bei Java ist dann, dass man die Interna beim Zugriff jederzeit ändern kann, was beim direkten Zugriff nicht geht. Das Argument zieht aber bei Python nicht, weil Python berechnete Attribute kennt, man da also nicht auf ”Vorrat” triviale Getter/Setter schreiben muss, nur für den Fall, dass man da vielleicht irgendwann einmal etwas anders machen will.

Ich sehe das auch nicht als *den* Vorteil/Grund von Klassen, denn das kann man beispielsweise in C auch durch Strukturen erreichen wo man dem Benutzer der Bibliothek grundsätzlich einfach nur einen void-Zeiger auf eine opake Datenstruktur in die Hand gibt, und dadurch auch in C leicht erzwingen kann, dass die Daten nur über die dafür passenden Funktionen manipuliert werden können.

Selbst wenn man in C auch die ``struct``-Deklaration öffentlich macht, aber Funktionen zur Manipulation zur Verfügung stellt, ist das immer noch Datenkapselung wenn die Dokumentation deutlich macht, dass man nicht direkt in diesen Strukturen herumpfuschen soll.

Datenkapselung habe ich das erste mal mit Pascal beigebracht bekommen, und zwar mit klassischem Pascal mit RECORD und Units, ohne OBJECT oder CLASS,. ”OOP” habe ich auch das erste mal mit Pascal gezeigt bekommen — und nicht verstanden warum das jetzt besser sein sollte, weil mir in dem Zusammenhang nichts gezeigt wurde, was das wirklich besser gemacht hat, als die prozedurale Lösung, die wir davor für das gestellte Problem entwickelt hatten. Also keine Polymorphie oder Vererbung. Es war halt einfach nur der selbe Code mit dem kleinen syntaktischen Unterschied, dass das erste Argument aus der Parameterliste verschwunden ist, und beim Aufruf vor den Prozedur- oder Funktionsnamen gerutscht ist, und innerhalb der Prozedur/Funktion den festen Namen `Self` und quasi so etwas wie ein ``WITH Self DO`` für den Körper der Prozedur/Funktion gilt.

Wirklich einen einfachen Vorteil erhält man IMHO erst durch Polymorphie, weil dadurch Klassen/Implementierungen von Objekten austauschbar werden, solange sie die Schnittstelle erfüllen.

In Python ist da wahrscheinlich `__str__()` eines der einfachsten Beispiele, weil selbst bei POD-Klassen oft eine eigene Zeichenkettendarstellung Sinn macht, und `print()` eine sehr einfache und zu dem Zeitpunkt sicher bekannte Funktion ist, die das dann nutzen kann, dass man eine für den Benutzer sinnvolle Darstellung als Zeichenkette ausgeben kann, in dem man ganz einfach das Objekt an `print()` übergibt.

Das mit der Argumentanzahl steht so hart nicht in „Clean Code“. Da steht das einmal hinten im Zusammenfassungskapitel „Smells und Heuristiken“, was ja schon mal keine harten Regeln sind. Ob man dann aus drei Argumenten *ein* Objekt machen sollte, kommt auch *sehr* darauf an ob das überhaupt Sinn macht die zusammenzufassen. Im Kapitel über Funktionen ist da ein gutes Beispiel mit einer `makeCircle()`-Funktion die `x`, `y`, und `radius` als Argumente bekommt und wo sich `x` und `y` als `Point` zusammenfassen lassen. Es würde eher keinen Sinn machen da auch noch `radius` mit rein zu nehmen. Denn dann würde man `makeCircle()` wahrscheinlich schon genau die Datenstruktur übergeben, die man mit dieser Funktion eigentlich erstellen möchte.

Ich persönlich werde ”erst” bei mehr als 5 bis 6 Argumenten ”hellhörig”. Die `makeCircle()`-Argumente hätte ich nicht wegen der Anzahl der Argumente zusammengefasst, sondern weil in so einem Programm ziemlich sicher an vielen Stellen auf Punkten operiert wird, und es deshalb Sinn macht die Koordinaten zu einem Objekt zusammenzufassen.

Die Argumentanzahl generell so drastisch zu reduzieren wie das von Martin vorgeschlagen wird, verschiebt das Problem IMHO auch nur zu immer mehr und immer verschachtelteren Argument-Objekten.

Mit Büchern zu statisch typisierten Sprachen wäre ich bei Python (und anderen dynamischen Sprachen) vorsichtig. Und wenn einem ein Buch etwas über objektorientierte Programmierung beibringen will und dafür Java (oder C++) verwendet, wäre ich auch vorsichtig. Man muss da immer aufpassen was tatsächlich allgemein für OOP gilt, und was nur die eingeschränkte(re) Sicht der jeweiligen Sprache auf das Thema ist.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Buchfink
User
Beiträge: 193
Registriert: Samstag 11. September 2021, 10:16

@_deets_ @rogerb @_blackjack_

Hui, :) mit so vielen interessanten Reaktionen hatte ich jetzt gar nicht gerechnet. Ich wollte allerdings gar nicht den ursprünglichen Thread "sprengen".

Eigentlich habe ich nur schriftlich überlegt, wie man den Sinn und Zweck von Klassen erklärt - und zwar ohne das Thema "Software-Architektur" zu beleuchten.
Die Schnittstellen-Thematik ist imho der am einfachsten zu verstehende Aspekt von Klassen.
Das was sie architektonisch ermöglichen (z.B. Polymorphie), ist leider wie @_blackjack_ schon anmerkte
mit den meisten (Trivial-)Beispielen nicht wirklich gut vermittelbar. (Thorngates Postulat der angemessenen Komplexität)

@_deets_
Das halte ich fur fast schon groben Unfug. Es ist überhaupt nichts gewonnen, 3 Argumente durch eine Klasse mit 3 Attributen zu ersetzen. Außer es für die Programmiererin komplizierter zu machen.
Natürlich muss man das situationsbezogen anwenden. Wenn es z. B. semantisch nicht sinnvoll ist, dann muss man davon abweichen.
Im Sinne von "Shu Ha Ri" ist es aber hin und wieder hilfreich, als Anfänger Regeln/Konventionen erst mal zu verstehen, bevor man sie bricht.

@_blackjack_
Dazu reicht es wenn man einfach nicht auf Sachen zugreift, die zu Interna gehören. Die kann man einfach mit einem Unterstrich als solche kenntlich machen.
Ich sehe den Zugriffsschutz allerdings auch z.B. als Kommunikationsmöglichkeit zwischen z.B. Framework- und Produkt-Entwickler.
Offensichtlich kann man das aber auch per Konvention sicherstellen. Leider habe ich diesbezüglich noch keine persönlichen Erfahrungen machen können. Aber nun, das kann ja noch werden, denke ich.
Und in vielen einfachen Einstiegsbeispielen gibt es dann in Sprachen wie Java triviale Getter und Setter, womit die Attribute de fakto doch wieder öffentlich sind. Das ist dann keine Kapselung beziehungsweise ist das nichts anderes als in Python direkt auf Attribute zuzugreifen. Die Argumentation bei Java ist dann, dass man die Interna beim Zugriff jederzeit ändern kann, was beim direkten Zugriff nicht geht.
Da gebe ich Dir vollkommen Recht. Triviale Getter/Setter machen aus dem bereits oben genannten Grunde keine Sinn.
Was ich aber durchaus sinnvoll finde, ist z. B. nur eine lesenden Beschränkung (sprich "nur" Getter) zu ermöglichen (je nach Semantik natürlich)
Das muss man aber als bewußte Design-Entscheidung treffen. Eine "Voratshaltung" von Code ist selbstredend fast nie sinnvoll.
Datenkapselung habe ich das erste mal mit Pascal beigebracht bekommen, und zwar mit klassischem Pascal mit RECORD und Units, ohne OBJECT oder CLASS,.
”OOP” habe ich auch das erste mal mit Pascal gezeigt bekommen — und nicht verstanden warum das jetzt besser sein sollte, weil mir in dem Zusammenhang nichts gezeigt wurde, was das wirklich besser gemacht hat, als die prozedurale Lösung, die wir davor für das gestellte Problem entwickelt hatten.
Ich kenne jetzt natürlich nicht den konkreten Fall. Aber es ist natürlich durchaus vorstellbar, dass in einigen Fällen der Unterschied im Design mit RECORD oder CLASS gering ist.
RECORDS ziehe ich dann eine CLASS vor, wenn ich absehbar keine Polymorphie brauche und wenn sich dadurch z.B. Performenceverbesserungen ergeben.
RECORDS sind auch etwas einfacher in der Handhabung im Hinblick auf Speichermanagment (zumindest in Delphi)
In Python ist da wahrscheinlich `__str__()` eines der einfachsten Beispiele, weil selbst bei POD-Klassen oft eine eigene Zeichenkettendarstellung Sinn macht, und `print()` eine sehr einfache und zu dem Zeitpunkt sicher bekannte Funktion ist, die das dann nutzen kann, dass man eine für den Benutzer sinnvolle Darstellung als Zeichenkette ausgeben kann, in dem man ganz einfach das Objekt an `print()` übergibt.
Ja das gefällt mir an Python auch sehr gut. Also dass da, wo es eben Sinn macht auch der Funktionale Part zum Tragen kommt. (Oder habe ich da was missverstanden?)
Ich bin auch kein OOP-Hardliner im Sinne von "nur OOP ist das einzig wahre".
Imho konnte OOP in vielen Aspekten seine Versprechen nicht wirklich einhalten. Aber das dürfte eine andere Diskussion sein :)#
Ich weiß nicht, ob ich mich trauen sollte, die hier aufzumachen. ;-)
Die Argumentanzahl generell so drastisch zu reduzieren wie das von Martin vorgeschlagen wird, verschiebt das Problem IMHO auch nur zu immer mehr und immer verschachtelteren Argument-Objekten.
ja, das ist korrekt. Es gibt aber eben auch Fälle, wo endlos lange Parameterlisten, die vielleicht noch Gott-weiß-wohin "durchgereicht" werden, zu unwartbarem Code führen.
Da ist man dann mit einer Klasse (oder einem Record) durchaus "besser" dran.
Letzlich hängt es auch davon ab, wie stabil eine Parameterliste potentiell ist. Bei dem Beispiel mit dem Kreis kann man natürlich davon ausgehen, dass das perspektivisch stabil bleibt.

Mit Büchern zu statisch typisierten Sprachen wäre ich bei Python (und anderen dynamischen Sprachen) vorsichtig.
Hast Du eine Empfehlung, was man da so mal alternativ lesen könnte?
Buchfink
User
Beiträge: 193
Registriert: Samstag 11. September 2021, 10:16

ich hab nochmal nachgedacht.
Ggf. braucht man "private" in Python auch gar nicht (so oft), weil es andere/bessere Möglichkeiten gibt, die ich noch nicht kenne.
Wäre es aus Eurer Sicht ratsam, sich z.B. mal die Architektur einer Lib (z.B. Pandas) näher anzusehen, um da mehr zu verstehen?
__deets__
User
Beiträge: 14537
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich denke prinzipiell braucht man das so oft oder selten wie auch in anderen Sprachen, wenn eine ähnliche Aufgabe gelöst wird. Es wird schlicht nur nicht so ein Gewese drum gemacht.
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Buchfink: Ich habe darüber noch mal nachgedacht, und ein Beispiel gesucht für etwas in BASIC, weil der OP ja schon mal von BASIC und GOTO sprach. Beim Advent of Code 2015, Tag 18 war die Aufgabe eine quadratisches Gitter aus Lichtern nach bestimmten Regeln zu animieren (Conway lässt grüssen) und im zweiten Teil gibt es zwei verschiedene Arten von Lichtern — neben den normalen welche die ständig an sind, und die sich auch nicht ausschalten lassen. Die Aufgabe in Kurzform:

Teil 1:

Lichterquadrat bei dem die Anfangskonfiguration als Puzzle-Eingabe gegeben ist, das eine gegebene Anzahl von Schritten nach folgenden Regeln animiert werden soll:
  • Ein Licht das an ist, bleibt an, falls 2 oder 3 Nachbarlichter an sind, sonst geht es aus.
  • Ein Licht das aus ist, geht an, falls genau 3 Nachbarlichter an sind, sonst bleibt es aus.
Die Regeln gelten von einem Schritt zum nächsten für alle Lichter simultan, dass heisst das Schalten eines Lichts im aktuellen Schritt, hat keinen Einfluss auf die Anzahl der eingeschalteten Nachbarn für seine Nachbarlichter.

Nachbarn sind die 8 Felder direkt um ein Licht herum. Bei Lichtern am Rand zählen nicht vorhandene Nachbarn nachvollziebarerweise als ausgeschaltet.

Am Ende der vorgegebenen Anzahl von Schritten soll die Gesamthelligkeit berechnet werden, was laut Aufgabe der Anzahl der dann eingeschalteten Lichter entspricht.

Teil 2:

Das selbe wie in Teil 1, nur das jetzt die vier Lichter in den Ecken immer eingeschaltet sind, und sich nicht ausschalten lassen.

In der Aufgabe ist ein kleines 6×6 Beispiel zur Verdeutlichung und zum testen der eigenen Lösung geeignet. "#" ist ein eingeschaltetes Licht und "." ein ausgeschaltetes. Testlauf mit Debugausgaben vom Programm (der erste ”Initial state” entspricht dem Inhalt der Eingabedatei):

Code: Alles auswählen

$ ./day18_fb -h
Usage: ./day18_fb -h | [-s steps] [-d] [filename]

      -h  print help and exit
-s steps  set number of steps (default 100)
      -d  print debug output

The default filename is input.txt.
$ ./day18_fb -s 5 -d test_input.txt 

Initial state:
.#.#.#
...##.
#....#
..#...
#.#..#
####..
[Brightness: 15]

After 1 step:
..##..
..##.#
...##.
......
#.....
#.##..
[Brightness: 11]

After 2 steps:
..###.
......
..###.
......
.#....
.#....
[Brightness: 8]

After 3 steps:
...#..
......
...#..
..##..
......
......
[Brightness: 4]

After 4 steps:
......
......
..##..
..##..
......
......
[Brightness: 4]

After 5 steps:
......
......
..##..
..##..
......
......
[Brightness: 4]
Brightness after 5 steps without stuck corners: 4

Initial state:
##.#.#
...##.
#....#
..#...
#.#..#
####.#
[Brightness: 17]

After 1 step:
#.##.#
####.#
...##.
......
#...#.
#.####
[Brightness: 18]

After 2 steps:
#..#.#
#....#
.#.##.
...##.
.#..##
##.###
[Brightness: 18]

After 3 steps:
#...##
####.#
..##.#
......
##....
####.#
[Brightness: 18]

After 4 steps:
#.####
#....#
...#..
.##...
#.....
#.#..#
[Brightness: 14]

After 5 steps:
##.###
.##..#
.##...
.##...
#.#...
##...#
[Brightness: 17]
Brightness after 5 steps with stuck corners: 17
Das Programm hat einen abstrakten Typ `TBaseLight` für ein einzelnes Licht mit Methoden zum Umwandeln in eine Zeichenkette ("#" oder ".") einem abstrakten Property um den Zustand zu setzen und abzufragen, einem Property um die Helligkeit abzufragen (0 oder 1) und eine Methode um den Zustand über eine Zeichenkette ("#" oder ".") zu setzen.

Davon abgeleitet sind dann `TLight` und `TStuckLight` die jeweils das Zustandsproperty entsprechend implementieren. Also ein einfaches Beispiel für Polymorphie über Vererbung/Untertypen.

Bei der Implementierung ist mir noch ein Vorteil aufgefallen, der einfacher als Polymorphie ist: Klassen/Typen/Objekte sind Namensräume. Man muss nicht mehr so stark aufpassen eindeutige Prozedur oder Funktionsnamen zu wählen, weil `TBaseLight.brightness` eine andere Funktion als `TLightGrid.brightness` ist, man sich beim Aufruf aber auf `brightness` für beide beschränken kann, weil der Compiler aus dem Aufrufkontext weiss was gemeint ist. Wobei das natürlich streng genommen nicht auf OOP beschränkt ist, denn das könnte man auch über das Überladen von Funktionen/Prozeduren via Signatur erreichen. FreeBASIC könnte das sogar.

In FreeBASIC ist auch die folgende Lösung der Aufgabe geschrieben. Das ist als freier Compiler für QBasic-Quelltexte gestartet, aber mittlerweise um einige Features erweitert. Unter anderem Typen als ”Klassen” mit Properties und Methoden (und Zugriffsschutz).

Code: Alles auswählen

'
' Advent of Code 2015 - day 18
'

' Basic light type with boolean state and readonly brightness properties.  The
' state can be set with a string and the object can be cast to a suitable
' string.
Type TBaseLight Extends Object
  Const ON = "#"
  Const OFF = "."
  
  Declare Const Operator Cast() As String
  
  Declare Const Abstract Property state As Boolean
  Declare Abstract Property state(value As Boolean)
  Declare Const Property brightness As Integer
  
  ' Set state from given string.  Returns TRUE if the string is a valid state
  ' representation, FALSE otherwise.
  Declare Function SetFromString(value As Const String) As Boolean
End Type

Const Operator TBaseLight.Cast() As String
  Return IIf(state, ON, OFF)
End Operator

Const Property TBaseLight.brightness As Integer
  Return IIf(state, 1, 0)
End Property

Function TBaseLight.SetFromString(value As Const String) As Boolean
  Dim ok As Boolean
  
  ok = TRUE
  Select Case value
    Case ON
      state = TRUE
    Case OFF
      state = FALSE
    Case Else
      ok = FALSE
  End Select
  Return ok
End Function


' Normal light.
Type TLight Extends TBaseLight
  Declare Const Property state As Boolean Override
  Declare Property state(value As Boolean) Override

  Private:
    _state As Boolean
End Type

Const Property TLight.state As Boolean
  Return _state
End Property

Property TLight.state(value As Boolean)
  _state = value
End Property


' A stuck light.  Reading the state is always TRUE, writing it has no effect.
Type TStuckLight Extends TBaseLight
  Declare Const Property state As Boolean Override
  Declare Property state(value As Boolean) Override
End Type

Const Property TStuckLight.state As Boolean
  Return TRUE
End Property

Property TStuckLight.state(value As Boolean)
  ' Setting the state has no effect on a stuck light.
End Property


' A square grid of lights.
'
' The file format is one line per row with "." for a light in off state and "#"
' for a light in on state.  Such a file can be loaded with the `Load` method.
' 
Type TLightGrid
  Declare Destructor
  
  ' Cast the this into a string of the format that `Load` expects in a file.
  Declare Const Operator Cast() As String
  
  ' Load the state from given `filename`.  If `hasStuckCorners` is TRUE then the
  ' lights in the four corners are always on, regardless of the value in the
  ' file.
  Declare Sub Load(filename As Const String, hasStuckCorners As Boolean = FALSE)
  
  ' Advance the grid state one step.
  '
  ' The rules are:
  '
  ' * A light that is on, stays on if 2 or 3 neighbours are on,
  '   otherwise it goes off.
  ' * A light that is off, goes on if exactly 3 neighbours are on,
  '   otherwise it stays off.
  '
  ' These rules are applied simultaneously to all lights.
  Declare Sub DoStep
  
  ' The overall brightness of the light grid.
  Declare Const Property brightness As Integer
  
  Private:
    size As Integer
    lights(Any, Any) As TBaseLight Ptr
    
    ' Free the array/memory of the lights. Pulled into an own method because the
    ' destructor and `Load` need this.
    Declare Sub Reset
    
    ' Get the light state at given coordinates.  If coordinates are outside the
    ' grid, return FALSE.
    Declare Const Function GetStateAt(i As Integer, j As Integer) As Boolean
    
    ' Test wether the given coordinates are a corner of the grid.
    Declare Const Function IsCorner(i As Integer, j As Integer) As Boolean
    
    ' Count the neighbouring lights that are on.  This considers all eight
    ' neighbours around the given coordinates.  Off grid positions are
    ' considered being off.
    Declare Const Function CountLitNeightbours(i As Integer, j As Integer) As Integer
End Type

Sub TLightGrid.Reset
  Dim i As Integer, j As Integer
  
  If size > 0 Then
    For i = 1 To size
      For j = 1 To size: Delete lights(i, j): Next
    Next
  End If
  size = 0: Erase lights
End Sub

Destructor TLightGrid
  Reset
End Destructor

Const Operator TLightGrid.Cast() As String
  Dim result As String, i As Integer, j As Integer, k As Integer
  ' Reserve space for a `size` by `size` character grid with a line ending
  ' character at the end of each row.
  result = Space(size * (size + 1))
  k = 1
  For i = 1 To size
    For j = 1 To size
      Mid(result, k) = *lights(i, j)
      k = k + 1
    Next
    Mid(result, k) = Chr(10)
    k = k + 1
  Next
  Return result
End Operator

Const Function TLightGrid.GetStateAt(i As Integer, j As Integer) As Boolean
  If 1 <= i And i <= size And 1 <= j And j <= size Then
    Return lights(i, j)->state
  Else
    Return FALSE
  End If
End Function

Const Function TLightGrid.IsCorner(i As Integer, j As Integer) As Boolean
  Return i = 1    And j = 1 _
      Or i = 1    And j = size _
      Or i = size And j = 1 _
      Or i = size And j = size
End Function

Sub TLightGrid.Load(filename As Const String, hasStuckCorners As Boolean)
  Dim f As Long, row As String, i As Integer, j As Integer, c As String
  Dim light As TBaseLight Ptr
  
  If size > 0 Then Reset
  
  f = FreeFile
  Open filename For Input As #f
  Line Input #f, row
  size = Len(row)
  ReDim lights(1 To size, 1 To size)
  For i = 1 To size
    For j = 1 To size
      If hasStuckCorners And IsCorner(i, j) Then
        light = New TStuckLight
      Else
        light = New TLight
      End If
      
      c = Mid(row, j, 1)
      If Not light->SetFromString(c) Then
        Print row
        Print Spc(j - 1); "^"
        Print "Unexpected ";
        If c = "" Then
          Print "end of file";
        Else
          Print "character '"; c; "'";
        End If
        Print " at line"; i
        End 1
      End If
      lights(i, j) = light
    Next
    Line Input #f, row
  Next
  Close #f
End Sub

Const Function TLightGrid.CountLitNeightbours(i As Integer, j As Integer) As Integer
  Dim result As Integer, di As Integer, dj As Integer
  
  result = 0
  If size > 0 Then
    For di = -1 To 1
      For dj = -1 To 1
        If Not (di = 0 And dj = 0) Then
          If GetStateAt(i + di, j + dj) Then result = result + 1
        End If
      Next
    Next
  End If
  Return result
End Function

Sub TLightGrid.DoStep
  Dim newStates(1 To size, 1 To size) As Boolean
  Dim i As Integer, j As Integer, count As Integer
  
  If size > 0 Then
    ' Calculate new states.
    For i = 1 To size
      For j = 1 To size
        count = CountLitNeightbours(i, j)
        If GetStateAt(i, j) Then
          newStates(i, j) = count = 2 Or count = 3
        Else
          newStates(i, j) = count = 3
        End If
      Next
    Next
    ' Set new states.
    For i = 1 To size
      For j = 1 To size
        lights(i, j)->state = newStates(i, j)
      Next
    Next
  End If
End Sub

Const Property TLightGrid.brightness As Integer
  Dim i As Integer, j As Integer, result As Integer
  
  result = 0
  If size > 0 Then
    For i = 1 To size
      For j = 1 To size
        result = result + lights(i, j)->brightness
      Next
    Next
  End If
  Return result
End Property


' Print the step number, the grid state, and the grid's brightness for debugging
' purposes.
Sub PrintLights(lights As Const TLightGrid, stepNumber As Integer = 0)
  Print
  If stepNumber = 0 Then
    Print "Initial state:"
  Else
    Print "After"; stepNumber; " step"; IIf(stepNumber > 1, "s", ""); ":"
  End If
  Print lights;
  Print "[Brightness:"; lights.brightness; "]"
End Sub


'
' The main program.
'
Const DEFAULT_STEP_COUNT = 100, DEFAULT_FILENAME = "input.txt"

Dim filename As String, stepCount As Integer, debug As Boolean

filename = DEFAULT_FILENAME
stepCount = DEFAULT_STEP_COUNT
debug = FALSE

Scope
  Dim i As Integer
  
  i = 1
  Do While Command(i) <> ""
    Select Case Command(i)
      Case "-h"
        Print "Usage: "; Command(0); " -h | [-s steps] [-d] [filename]"
        Print
        Print "      -h  print help and exit"
        Print "-s steps  set number of steps (default"; DEFAULT_STEP_COUNT; ")"
        Print "      -d  print debug output"
        Print
        Print "The default filename is "; DEFAULT_FILENAME; "."
        End 0
      Case "-s"
        i = i + 1
        stepCount = Val(Command(i))
      Case "-d"
        debug = TRUE
      Case Else
        If Left(Command(i), 1) = "-" Then
          Print "Unknown option: "; Command(i); "."
          Print "Try -h for help."
          End 1
        End If
        filename = Command(i)
    End Select
    i = i + 1
  Loop
End Scope

Scope
  Dim lights As TLightGrid, i As Integer, hasStuckCorners As Integer
  
  For hasStuckCorners = 0 To 1
    lights.Load filename, hasStuckCorners
    If debug Then PrintLights lights
    
    If stepCount > 0 Then
      For i = 1 To stepCount
        lights.DoStep
        If debug Then PrintLights lights, i
      Next
    End If
        
    Print "Brightness after"; stepCount; " steps "; _
        IIf(hasStuckCorners, "with", "without"); " stuck corners:"; _
        lights.brightness
  Next
End Scope
Konkreter Fall beim Erstkontakt mit ”OOP” war bei mir Informatikunterricht mit Turbo Pascal. Der Lehrplan sah die Entwicklung einer Adressverwaltung mit Units und RECORDs vor. Und das haben wir auch so durchgezogen. Und ganz am Ende hat uns der Lehrer dann die tollen neuen OOP-Konstrukte in Turbo Pascal 5.x gezeigt, in dem er das Program ”objectorientiert” umgeschrieben hat. Also letztlich aus den RECORDs halt Objekte gemacht, was syntaktisch halt ein kleines bisschen anders aussah, aber ich hatte so überhaupt nicht verstanden warum der so aus dem Häusschen war, denn so hat man natürlich nicht wirklich einen Vorteil gesehen, denn letztendlich war es ja das gleiche Programm.

Was mir zu dem Zeitpunkt natürlich auch noch nicht klar war, war das OOP ein Paradigma ist und keine Spracheigenschaft. Letztlich war die prozedurale Version von dem Programm schon sehr nah an der Idee von OOP dran. Was wohl auch ein Grund war, warum ich den Quantensprung in der OOP-Variante nicht erkennen konnte, denn der war halt wirklich sehr klein.

Klick gemacht hat es dann erst bei Java. Das ist ja die Holzhammermethode weil da *alles* in Klassen gepresst werden muss.

Hm, ob bei `__str__()` und `print()` der funktionale Part zum Tragen kommt ist eine gute Frage. Letztlich ist Python “durch und durch“ objektorientiert, denn jeder Typ ist eine Klasse und jeder Wert ist ein Objekt. Das schliesst ja Funktionen mit ein. Auch das sind Objekte, die halt *eine* sehr wichtige Methode haben: `__call__()`. Denn ``something(42)`` steht ja letztlich die Kurzform für ``something.__call__(42)``. Und ob `something` nun für ein Objekt vom Typ `function` steht, oder irgendwas anderes das `__call__()` implementiert, ist ja letztlich egal. Wenn es ``def`` für Funktionen nicht gäbe, sondern nur für Methoden, würde man halt mehr Boilerplate schreiben:

Code: Alles auswählen

class Something:
    def __call__(self, argument):
        ...

something = Something()

# statt einfach nur

def something(argument):
    ...
Ist der erste Code jetzt irgendwie objektorientierter, nur weil man da den Boilerplate mit der Klasse schreiben muss? Und wird es plötzlich funktional, wenn man den Boilerplate weglassen kann?

Die Funktionen die in der Regel einer ”magischen“ Methode entsprechen wie `str()`, `repr()`, `len()`, und so weiter, kann man auch als Operatoren auffassen. Das war mal die Antwort von Guido warum `len()` eine Funktion ist. Er sah das eher als Operator, den man überladen kann, und der einen Namen hat, statt dafür irgendein kryptisches Zeichen als extra Syntax zu definieren.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten