Mein Problem sitzt in Zeile 17; wenn ich als Index von klass.original
konkrete Zahlen angebe, oder auch die Variable i, die in der
for-Schleife inkrementiert wird, funktioniert alles.
Auch klass.zaehler kann ich problemlos auslesen. Wenn ich aber in der
for-Schleife die Objektvariable klass.zahler so nutze, wie hier
angegeben, ändert sich nichts an dem Bild, ich bekomme aber auch keine
Fehlermeldung. Woran kann das liegen? Ich habe schon sehr viel probiert,
aber jeder Teil des Programmes funktioniert so, wie er soll, nur an
dieser Stelle ist es, als würde ein Geist sein Unwesen treiben...
viel kann sich ja nicht ändern, Zeile 17 wird 47 mal aufgerufen, da sich
dabei in 17 nichts ändert würde wohl nur 1 Pixel gesetzt. Scheint keinen
Sinn zu ergeben.
Sascha
Hallo Sascha, die Variable klass.zaehler wird ja nach der for-Schleife
um den Wert 1 erhöht, und da 47 Dateien in dem Verzeichnis sind,
geschieht dies genau so oft. Das klappt auch, das sehe ich dann, wenn
ich mir die Variable ausgeben lasse. Eigentlich sollte also eine
Diagonale auf dem Bild erscheinen, aber das funktioniert eben nicht.
Asterix schrieb:> Eigentlich sollte also eine> Diagonale auf dem Bild erscheinen
Sascha hat Recht: Du setzt in der ersten Datei 47mal den Punkt 0/0, in
der zweiten Datei 47mal den Punkt 1/1 usw.
Jetzt kommt aber der entscheidende Fehler: Du schreibst nur das Ergebnis
des letzten Schleifendurchgangs. Zeile 21 wird nur ein einziges mal
ausgeführt.
Dieses ganze Chaos wäre viel leichter erkennbar, wenn du vernünftige
Variablenbezeichner wählen und diese Nonsense-Klasse rausschmeißen
würdest.
Für was ist dann die Schleife in 16 überhaupt da?
Wie soll da eine Diagonale entstehen wenn nach dem Laden eines Bildes
ein Pixel gesetzt wird?
Dann liest du nacheinander 47 Bilder in die selbe Variable und
speicherst am Ende nur das letzte wieder ab.
Vielleicht beschreibst du mal was das werden soll.
Sascha
Um den eigentlichen Sinn des Programmes einmal zu skizzieren:
Ich möchte aus einer Menge von 47 Bildern jeweils die x. Zeile nehmen,
die die x. Spalte des Ergebnisses ergibt.
Also Ergebnis Spalte 1 = Bild 1 Zeile 1.
Ergebnis Spalte 2 = Bild 2 Zeile 1.
Ergebnis Spalte 3 = Bild 3 Zeile 1.
Das gleiche für die weiteren Zeilen der Ausgangsbilder.
Die Funktion soll also stark vereinfacht das machen:
Asterix schrieb:> Ich möchte aus einer Menge von 47 Bildern jeweils die x. Zeile nehmen,> die die x. Spalte des Ergebnisses ergibt.
Dann ist außerdem noch Zeile 14 falsch, weil dort jeglicher
"Fortschritt" überschrieben wird.
So wie das Programm hier steht, erfüllt es natürlich nicht diesen Zweck,
aber es geht mir darum, dass in der entsprechenden Zeile irgendetwas
nicht so läuft, wie ich es erwarten würde.
Ich will zu diesem Testzweck deshalb auch vorerst gar nicht jedes Bild
bearbeiten (die Ausgangsbilder werden ja ohnehin nicht dauerhaft
bearbeitet und gespeichert), aber die Struktur des Programmes,
insbesondere das Öffnen der Dateien, habe ich drin gelassen, weil es ja
mit dem Fehler zu tun haben könnte.
Asterix schrieb:> Eigentlich sollte also eine> Diagonale auf dem Bild erscheinen, aber das funktioniert eben nicht.
Nein, du überschreibst bei jedem Schleifendurchlauf klass.original mit
Zunächst einmal Danke für eure Antworten!
Karl schrieb:> Nein, du überschreibst bei jedem Schleifendurchlauf klass.original> mitklass.original = cv2.resize(img,(47,47))
Stimmt, das ist wirklich Quatsch.
Zunächst dazu, weshalb ich die Klasse erstellt habe: ich habe Probleme,
globale Variablen aus Funktionen heraus zu verändern, irgendwie bekomme
ich das auch mit global oder nonlocal nicht hin, und habe gelesen, dass
man für solche Fälle eine Klasse erstellen soll, deren Variablen man aus
Funktionen heraus verändern kann. Wenn das doch anders geht, schmeiße
ich die Klasse sofort raus!
Ich habe jetzt das Programm noch einmal bearbeitet, sodass ich erwarten
würde, dass es den gewünschten Zweck erfüllt.
So sieht es aus (abzüglich der Definition der Klasse und der imports):
1
for p in range(47):
2
for filename in os.listdir('Fotos grau zugeschnitten\\'):
Asterix schrieb:> Zunächst einmal Danke für eure Antworten!> Karl schrieb:>> Nein, du überschreibst bei jedem Schleifendurchlauf klass.original>> mitklass.original = cv2.resize(img,(47,47))>> Stimmt, das ist wirklich Quatsch.> Zunächst dazu, weshalb ich die Klasse erstellt habe: ich habe Probleme,> globale Variablen aus Funktionen heraus zu verändern, irgendwie bekomme> ich das auch mit global oder nonlocal nicht hin, und habe gelesen, dass> man für solche Fälle eine Klasse erstellen soll, deren Variablen man aus> Funktionen heraus verändern kann. Wenn das doch anders geht, schmeiße> ich die Klasse sofort raus!
Bitte sei mir nicht böse, aber Dein ganzer Code ist... merkwürdig, um
das mal ganz vorsichtig zu sagen... und wenn Du die Schlüsselworte
"global", "nonlocal" und die Verwendung von Klassen noch nicht
verstanden hast und nur deswegen Klassen benutzt, obendrein in einer...
sehr seltsamen Art und Weise... die Du am Liebsten aber gleich wieder
"rausschmeißen" würdest... Bitte, es ist nicht böse gemeint, aber ich
habe bislang nicht das Gefühl, daß Du eine ansatzweise Ahnung hast, was
Du da tust.
Das fängt schon bei den einfachsten Einfachheiten an, zum Beispiel
os.path.join. Da benutzt Du zum Zusammenfügen von Pfadelementen aus
welchen Gründen auch immer den betriebssystemspezifischen Pfadtrenner in
Deinen Pfadelementen -- in Deinem Falle, der Du offensichtlich mit
Windows unterwegs bist, ist das der Backslash '\', den Du obendrein
maskieren mußt ("\\"), weil er das übliche Maskierungszeichen ist. Den
kannst Du einfach weglassen, denn os.path.join nimmt ein Array von str-
oder bytes- Argumenten, die dann mit os.sep konkateniert werden.
Und dann diese Klasse "Spalte0"... bitte tu' Dir selbst einen Gefallen
und schau' mal, wie Objektorientierung funktioniert. Klassen sind in
keiner Programmiersprache der Welt ein Ersatz oder irgendwas ähnliches
für globale Variablen -- und so, wie Dein Code aussieht, würdest Du Dir
auch mit globalen Variablen äußerst erfolgreich ins eigene Knie
schießen.
Laß' die Klasse doch machen, was eine Klasse tun soll: übergibt ihr den
Pfad zu der Datei, der Konstruktor kann sie dann selbständig einlesen
und eine Methode -- die natürlich auch von Konstruktor ausgeführt werden
kann -- macht dann das komische Zeugs, das Du mit Deinen Grafiken
veranstalten willst.
Wie dem auch sei: ich empfehle Dir wärmstens ein gutes Buch über Python
und eines über Objektorientierung. Viel Glück und Erfolg! ;-)
Danke für die ausführliche Antwort, Sheeva.
Du hast völlig Recht damit, dass ich Python nicht so gut kann, wie ich
es gerne würde. Vor allem die Objektorientierung finde ich schwierig,
denn bis jetzt habe ich nur 8-bit Atmel uC in C programmiert, und
Objektorientierung ist da für mich ein totaler Paradigmenwechsel.
Ich werde mir ein Buch über Python bestellen, um genau solche Sachen zu
lernen.
Der Grund, weshalb ich mir dieses Programm vorgenommen habe ist der,
dass ich ungeduldig bin, einen Simulator eines CT-Scanners zu bauen, der
rein optisch funktioniert. Die "Rohdaten" eines Prototypen dazu habe ich
bereits, und jetzt wollte ich unbedingt die Bilder zu entsprechenden
Sinogrammen zusammensetzen, sodass ich die Rückprojektion ausprobieren
kann. Aber hier hat sich eben meine Ungeduld gerächt :(
Was das Programm selber angeht: Ich dachte, ich hätte zumindest die
Syntax und Grundregeln zu Klassen verstanden, und hatte auch in anderen
Programmen, in denen ich zum Beispiel mit opencv gearbeitet habe, kein
Problem damit, Objekte zu erstellen und mit den Methoden zu bearbeiten.
Und bei dem Programm, so wie ich es hier reingestellt habe, weiß ich
natürlich, dass es keinen Sinn ergibt. Es ging mir eigentlich nur darum,
weshalb es nicht genau das macht, was ich von ihm erwarten würde.
Ich finde es grundsätzlich schwierig, autodidaktisch eine
Programmiersprache zu lernen, denn vermeintlich kleine Probleme mit
eigentlich einfacher Antwort lassen sich ohne kundigen Ansprechpartner
sehr schwer lösen.
Micha schrieb:> Ersetze mal die beiden ersten Zeilen durch for p, filename in> enumerate(os.listdir('Fotos grau zugeschnitten\\')):
Hallo Micha, das werde ich ausprobieren, sobald ich wieder zuhause bin.
Asterix schrieb:> Danke für die ausführliche Antwort, Sheeva.
Nichts zu danken -- dafür ist so ein Forum doch da, oder? ;-)
> Du hast völlig Recht damit, dass ich Python nicht so gut kann, wie ich> es gerne würde. Vor allem die Objektorientierung finde ich schwierig,> denn bis jetzt habe ich nur 8-bit Atmel uC in C programmiert, und> Objektorientierung ist da für mich ein totaler Paradigmenwechsel.
Damit hast Du doch hervorragende Voraussetzungen. Zunächst einmal ist so
eine Klasse im Prinzip so etwas Ähnliches wie ein "typedef struct" in C,
also eine Sammlung von zusammengehörigen Daten. Nehmen wir an, wir
wollen eine C-Struktur und Funktionen für einen Personennamen schreiben,
dann ist das vermutlich so etwas wie:
1
typedefstruct{
2
char*vorname;
3
char*nachname;
4
}name;
5
6
name*createName(char*vorname,char*nachname);
7
voidprintName(name*n);
8
voidfreeName(name*n);
So weit, so... mittelgut. Denn Dein Kollege hat gerade Code für
Maschinennamen entwickelt, und... seine Funktionen heißen genau wie
Deine! Deswegen benutzen viele C-Entwickler Präfixe für Funktionsnamen,
um deren Eindeutigkeit sicherzustellen.
Objektorientierte Programmierung hingegen löst dieses Grundproblem recht
einfach, indem es die Funktionen nicht im globalen Namensraum erzeugt,
sondern sie direkt an die Datenstruktur bindet, die sie bearbeiten --
und das löst gleich eine Reihe von Problemen, weit über die
Namenskollisionen hinaus.
Dann ist Dir bei der Lektüre des Code oben sicherlich aufgefallen, daß
es dort drei Funktionen gibt, von denen eine, "createName()", eine
Instanz vom Typ "name" erzeugt und allokiert und einen Zeiger ("name*")
darauf zurückgibt, während die anderen beiden jeweils einen Zeiger auf
einen "name" als ersten Parameter haben. Das macht im Prinzip jede (mit
bekannte) Objektorientierung so, mal implizit wie in C++ mit einem
eigenen Schlüsselwort ("this"), mal explizit wie in Python mit dem
Parameter "self" (der aber nicht "self heißen muß, am Rande bemerkt, das
ist nur eine sehr beliebte Konvention). Nicht zuletzt deswegen heißen
die Funktionen, die an eine Klasse gebunden sind, in der OOP nicht mehr
"Funktion", sondern "Methode".
Nun, die Funktion "createName()" ist natürlich etwas ganz Besonderes,
und genauso (wenngleich nicht ganz so offensichtlich) auch die Funktion
"freeName()". Warum ist das so? Genau: die Funktion "createName()"
erzeugt eine Struktur vom Typ "name" und gibt einen Zeiger darauf
zurück. Dafür gibt es in der Objektorientierung wieder ein nettes
Feature; eine Methode, die eine Instanz einer Klasse erzeugt, nennt sich
dort "Konstruktor", und eine Methode, die diese Instanz wieder zerstört,
ist folgerichtig natürlich ein "Destruktor".
Wie das mit dem Konstruktor geht, machen verschiedene
Programmiersprachen wieder etwas unterschiedlich. C++ zum Beispiel hat
eine (oder mehrere) Methode(n), die genauso heißen wie die Klasse, in
der sie deklariert sind, die Destruktoren heißen genauso und bekommen
das Präfix "~" vorangestellt, während Python hingegen einen(!)
reservierten Methodennamen namens "__init__()" benutzt. Achtung: eine
Klasse in C++ kann mehrere Konstruktoren haben, die sich in der Anzahl
und / oder den Datentypen ihrer Methodensignatur (Parameter)
unterscheiden, während Python es sehr leicht macht, fixe und optionale
Parameter anzugeben und daher nur einen Methodennamen (nämlich
"__init__()") braucht.
(Exkurs: im Grunde genommen ist das nicht ganz richtig, denn in Python
gibt es noch einen "versteckten" Konstruktur namens "__new__()" und
sogenannte "statische Methoden", die als "Frontend" für "__init__()" und
/ oder "__new__()" verwendet werden können. Aber für hier sollen diese
fortgeschrittenen bis esotherischen Features von Python keine Rolle
spielen.)
Nun, schauen wir uns vielleicht zunächst eine Implementierung in C++ an:
1
structName{
2
char*vorname;
3
char*nachname;
4
5
Name(char*vorname,char*nachname);
6
voidprintName(void);
7
~Name(void);
8
};
Du siehst: unsere Funktion "createName()" ist jetzt zur
Konstruktor-Methode "Name()" geworden, die Funktion "freeName()" zur
Destruktor-Methode "~Name()", und dadurch, daß die Methoden unserer
Klasse eh wissen, welche Daten sie bearbeiten sollen, kann der Name der
Funktion "printName()" zur Methode "print()" gekürzt werden.
Diese Sache mit den Konstruktoren ist ziemlich cool, denn sie sind im
Prinzip der einzige Weg, um Instanzen einer Klasse zu erzeugen. Anders
gesagt: immer, wenn eine Instanz von einer Klasse erzeugt wird, wird ein
Konstruktor aufgerufen, und wenn der Entwickler bei der Implementierung
der Konstruktoren keinen Unsinn macht, gehören uninitialisierte
Variablen damit der Vergangenheit an. ;-)
Okay, schauen wir uns nun eine Implementierung in Python3 an:
1
class Name:
2
def __init__(self, vorname, nachname):
3
self.vorname = vorname
4
self.nachname = nachname
5
6
#def __del__(self):
7
# del self.vorname
8
# del self.nachname
9
10
def print(self):
11
print("%s %s"%(self.vorname, self.nachname))
Wir sehen den Konstruktor "__init__()", den Destruktor "__del__()" und
die Methode "print()", wie zuvor. Den Destruktor "__del__()" können wir
problemlos weglassen, daher habe ich ihn auskommentiert, denn der
Python-Interpreter ist (genau wie der C++-Compiler) klug genug, um das
selbst hinzubekommen. Ach so: das Beispiel klappt natürlich nur mit
Python3, denn in Python2 war "print" noch ein Schlüsselwort und hätte
dort zu einem Syntaxfehler geführt. Aber Python2 ist ohnehin deprecated
und wird nicht mehr gepflegt, und wir wollen ja keinen alten Mist
benutzen, oder? ;-)
Nun, wie dem auch sei... es ist nicht ganz richtig, aber das hier war
und ist der wesentliche Kern der OOP: Datenstrukturen und die
Funktionen, die sie bearbeiten, in konsistenten Einheiten
zusammenzufassen. Damit lassen sich dann die Objekte unserer realen Welt
abbilden; so kann ein Flugzeug die Methode "flieg()" haben, die bei
einem Auto (bislang jedenfalls) eher sinnlos wäre, aber beide können die
Methoden "beschleunige()" und "bremse()" haben.
Und, Achtung: mit Vererbung können Flugzeug und Auto sogar dieselben
Methoden und Implementierungen benutzen -- und das Flugzeug sogar
abhängig von seinem eigenen aktuellen Zustand und womöglich beide
abhängig vom Wetter und anderen Parametern. Dieses Mal ging es mir aber
nur darum, den Kern der ganzen Sache zu erklären, und wenn meine
Ausführungen für Dich halbwegs verständlich waren, freue ich mich über
eine Rückmeldung, gerne auch mit Fragen und Anregungen. Wenn ich es
geschafft habe, Dir die grundsätzlichen Zusammenhänge ein bisschen näher
zu bringen, schreibe ich natürlich sehr gerne auch noch etwas über
Vererbung.
> Ich werde mir ein Buch über Python bestellen, um genau solche Sachen zu> lernen.
Wenn Du... wirklich verstehen willst, wie Objektorientierung
funktioniert, möchte ich Dir wärmstens das Buch "The Design and
Evolution of C++" von Bjarne Stroustrup empfehlen, dem Entwickler der
Programmiersprache C++. Ich hab' es vor über zwanzig Jahren gelesen, und
es prägt mein Verständnis und mein Entwicklerleben bis heute, was ich
dabei gelernt, erkannt und verstanden habe.
Ach, übrigens: wenn man sich ein bisschen auskennt und auf einige
"teure" Features von C++ verzichtet (zum Beispiel Exceptions und
virtuelle Methoden), dann kostet C++ auch auf winzigen AtTiny13
überhaupt gar nichts, macht Deine Programme aber deutlich lesbarer,
übersichtlicher, und Deinen Code besser wiederverwendbar, ohne daß es
nur den geringsten Einfluß auf die Programmgröße oder -Performance
hat... auch wenn die Auguren in diesem Forum Dir etwas anderes
suggerieren wollen. ;-)
> Der Grund, weshalb ich mir dieses Programm vorgenommen habe ist der,> dass ich ungeduldig bin, einen Simulator eines CT-Scanners zu bauen, der> rein optisch funktioniert. Die "Rohdaten" eines Prototypen dazu habe ich> bereits, und jetzt wollte ich unbedingt die Bilder zu entsprechenden> Sinogrammen zusammensetzen, sodass ich die Rückprojektion ausprobieren> kann. Aber hier hat sich eben meine Ungeduld gerächt :(
Naja, nur eine Anregung:
parser = ArgumentParser(description='Mach Zeug mit Bildern')
16
parser.add_argument('path', help='where my images are')
17
args = parser.parse_args()
18
19
images = list()
20
for p in os.listdir(args.path):
21
images.append(Image(p))
Am Ende hast Du eine Liste von Image-Instanzen, mit denen Du... genau.
Ach so, übrigens: ich lese meine privaten E-Mails nicht so oft, wie ich
sollte, aber wenn Du Fragen hast und ein bisschen Geduld aufbringen
kannst: you're welcome.
Abschließend wünsche ich Dir erstmal viel Spaß und Erfolg mit Python und
der OOP. Nur die Ruhe, das ist im Grunde gar nicht schwierig, erlernt
sich aber wohl auch nicht an einem Tag -- und um ein guter OO-Entwickler
zu werden, braucht es primär etwas Erfahrung und Herumspielerei. Aber
schwierig ist das nicht, Entwickler sind bekanntlich faul (das nennt
Larry Wall, der Erfinder von Perl, als eine der vier Kardinaltugenden
von Entwicklern, jedenfalls der guten), und wir neigen dazu, uns die
Arbeit so einfach wie möglich zu machen. ;-)
(edit) PS: Oh, ach so, ganz vergessen... es ist schön, mal wieder einen
TO hier zu sehen, der Interesse an der Sache hat und konstruktiv
mitarbeitet. Danke dafür! ;-)
Sheeva P. schrieb:> Nichts zu danken
-Einspruch, euer Ehren!
Ich bin gerade ziemlich weggehauen davon, wie viel Zeit du dir genommen
hast, um mir die OOP zu erklären und den Rücken zu stärken! Allein die
Mühe hat ein großes "Danke" verdient, zumal du mir tatsächlich damit
sehr weitergeholfen hast! Vor allem, dass du erst das Problem der nicht
objektorientierten Programmierung beschreibst, das dann mit der OOP
gelöst wird, macht es sooo viel einfacher, den eigentlichen Sinn
dahinter zu verstehen.
Tatsächlich glaube ich, dass ich jetzt die Verwendung von 'self' erst
richtig verstanden habe; vorher ging für mich nicht richtig zusammen,
dass der Variablenname als solcher nur Konvention ist, aber der Name des
Objekts immer als erstes Argument der Klasse eingelesen wird... wenn ich
jetzt darüber nachdenke, kommt es mir gar nicht so schwer vor, nur ist
das im Rückblick ja immer so bei solchen Sachen ;)
Überhaupt habe ich jetzt eine viel konkretere Vorstellung davon, wie ein
Objekt erstellt wird, und es scheint auf einmal viel greifbarer als
bisher.
Den Buchtipp werde ich beherzigen!
Ich muss jetzt erst einmal das neu Verstandene etwas sacken lassen,
bevor ich die ersten auftretenden Fragen in den Äther blöke; nur
folgende Frage erlaube ich mir jetzt schon: Beim Erstellen eines Objekts
einer Klasse sind ja die Argumente für alle Methoden des Objekts
verfügbar (einschließlich 'self'). Wieso aber kann ich mit einer Methode
eine Variable (in deinem Beispiel self.vorname = vorname) erstellen, und
in einer anderen Methode, die auf der selben "Ebene" sitzt, auf diese
Variable zugreifen; sollte self.vorname in der ersten Methode nicht
lediglich eine lokale Variable sein, und in der Folge nicht mehr von
anderen Methoden les-/beschreibbar?
Und wenn du noch etwas über Vererbung schreibst, freue ich mich sehr,
wenngleich ich schon jetzt genug Gedankenfutter getankt habe, um eine
Weile das Gelernte zu festigen.
Leider ist im Moment die Freizeit bei mir knapp bemessen und schon gut
gefüllt, aber ich konnte deinen Beitrag einfach nicht bis morgen
unkommentiert stehen lassen. Also noch einmal besten Dank!
Asterix schrieb:> Sheeva P. schrieb:>> Nichts zu danken> -Einspruch, euer Ehren!>> Ich bin gerade ziemlich weggehauen davon, wie viel Zeit du dir genommen> hast, um mir die OOP zu erklären und den Rücken zu stärken! Allein die> Mühe hat ein großes "Danke" verdient, zumal du mir tatsächlich damit> sehr weitergeholfen hast! Vor allem, dass du erst das Problem der nicht> objektorientierten Programmierung beschreibst, das dann mit der OOP> gelöst wird, macht es sooo viel einfacher, den eigentlichen Sinn> dahinter zu verstehen.
Pssst! ;-) Ich freue mich sehr, daß es Dir nützt, dann hat es sich
gelohnt!
> Tatsächlich glaube ich, dass ich jetzt die Verwendung von 'self' erst> richtig verstanden habe; vorher ging für mich nicht richtig zusammen,> dass der Variablenname als solcher nur Konvention ist [...]
Naja, die Konvention ist so weit verbreitet, daß mein Editor GNU Emacs
das "self" sogar wie ein Schlüsselwort darstellt. Aber das hier
1
class Dings:
2
def __init__(klaus): klaus.name = 'Sheeva'
3
def bums(self): print('in Dings.bums(): name = "{}"'.format(self.name))
4
def da(karl): print('in Dings.da(): name = "{}"'.format(karl.name))
5
6
if __name__ == '__main__':
7
dings = Dings()
8
dings.bums()
9
dings.da()
ist vollkommen legaler Python-Code und läuft problemlos sowohl mit der
veralteten Version 2 als auch mit der aktuellen Version 3. Aber wenn man
so etwas in realem Programmcode schriebe, sollte man damit rechnen, daß
ein wütender Mob mit Fackeln und Mistgabeln auftaucht und man am Ende
geteert und gefedert wird... ;-)
> Den Buchtipp werde ich beherzigen!
Bitte bedenke: das ist schon ziemlich nerdiger Kram für Menschen, die
wirklich wissen wollen, wie es funktioniert und wie es in C++
implementiert wurde...
> Ich muss jetzt erst einmal das neu Verstandene etwas sacken lassen,> bevor ich die ersten auftretenden Fragen in den Äther blöke; nur> folgende Frage erlaube ich mir jetzt schon: Beim Erstellen eines Objekts> einer Klasse sind ja die Argumente für alle Methoden des Objekts> verfügbar (einschließlich 'self'). Wieso aber kann ich mit einer Methode> eine Variable (in deinem Beispiel self.vorname = vorname) erstellen, und> in einer anderen Methode, die auf der selben "Ebene" sitzt, auf diese> Variable zugreifen; sollte self.vorname in der ersten Methode nicht> lediglich eine lokale Variable sein, und in der Folge nicht mehr von> anderen Methoden les-/beschreibbar?>> Und wenn du noch etwas über Vererbung schreibst, freue ich mich sehr,> wenngleich ich schon jetzt genug Gedankenfutter getankt habe, um eine> Weile das Gelernte zu festigen.
Alles gut, nur keinen Streß -- aus meiner eigenen Erfahrung weiß ich,
daß Neues erst einmal ein bisschen Zeit zum Sacken und womöglich zum
Rumspielen braucht. Kommen wir zunächst zu Deiner Frage...
Also, die meisten (mir bekannten) Skriptsprachen kennen ein Feature, das
nennt sich "Autovivikation". Variablen werden dabei nicht deklariert,
sondern einfach durch eine Zuweisung erzeugt. Wo Du in C also
1
inti;
2
i=0;
und damit die Variable i mit dem Datentyp Integer ("int") deklarierst
und dann mit einer Zuweisung initialisierst, ist die Deklaration der
Variablen -- also, Deinem Compiler zu sagen, daß Du Speicherplatz für
eine Variable haben möchtest und diese den Datentyp Integer haben soll)
in den meisten (mir bekannten) Skriptsprachen überflüssig, dort reicht
eine einfache Zuweisung:
1
i = 0
Dabei unterscheiden sich die Skriptsprachen allerdings: während Perl und
PHP für Variablen keinen Datentyp festlegen und also Code wie '"3" + 4;'
die Zahl 7 ergibt, sind Ruby und Python tatsächlich stark typisiert --
und '"3" + 4' ergibt dort einen Fehler. Im Unterschied zu kompilierten
Sprachen wie C, C++ oder Java ist der Typ einer Variablen in diesen
Sprachen aber nicht an den Variablennamen, sondern an ihren Wert
gebunden, so daß eine Variable, die gerade eben noch eine Ganzzahl war,
durch eine einfache Zuweisung eine Fließkommazahl werden kann:
1
i = 0
2
i = 0.01
Okay, nach dieser Vorrede kommen wir jetzt auf Deine Frage zurück. Die
Zuweisung "self.vorname = vorname" in dem Beispiel aus dem vorhierigen
Beitrag erzeugt per Autovivikation eine Variable, die an die konkrete
Instanz des Objektes gebunden ist -- also: nicht an die Klasse, sondern
an die Instanz der Klasse. Die Klasse ist dabei lediglich eine Art
Vorschrift, wie ein Etwas aussehen soll (also in C eine struct), und die
Instanz ist dann ein konkretes Etwas -- von dem es auch mehrere geben
kann:
1
class Dings:
2
def __init__(self, name):
3
self.name = name
4
5
def say(self):
6
print('Der Moderator heißt "{}".'.format(self.name))
7
8
if __name__ == '__main__':
9
10
# mit Zwischenvariable
11
mods = list()
12
for mod in ['Andreas', 'Rufus', 'Yalu']: m = Dings(mod); mods.append(m)
13
for mod in mods: mod.say()
14
15
# ohne Zwischenvariable
16
mods = list()
17
for mod in ['Andreas', 'Rufus', 'Yalu']:
18
mods.append(Dings(mod))
19
for mod in mods: mod.say()
20
21
# list comprehension
22
mods = [Dings(mod) for mod in ['Andreas', 'Rufus', 'Yalu']]
23
for mod in mods: mod.say()
(Um den Code kurz zu halten, habe ich die Oneliner hier mal in eine
Zeile gepackt, was absolut legaler Python-Code ist -- ja, auch die
Verbindung mehrerer Anweisungen mit dem ";" -- aber normalerweise sollte
man so etwas nicht machen. Man muß sich ja nicht immer an den Python
Style Guide in PEP-8 [1] halten, aber... besser ist das, weil es den
Code besser lesbar macht, und das ist in Python eines der wichtigsten
Entwicklungsziele...)
(Dieser letzte Code zeigt schon ein "fortgeschrittenes" Feature von
Python, die es meines Wissens von Lisp abgeschaut hat: die List
Comprehension. Das wollte ich nur kurz gezeigt haben, hat aber im Grunde
nichts mehr mit Objektorientierung zu tun.)
Nun, kommen wir zu Deiner Frage nach dem "self.vorname" zurück: damit
wird eine neue Variable erzeugt, die an die Instanz der Klasse gebunden
ist -- und bleibt. Wieder ein kleines Beispiel:
1
class Dings:
2
def __init__(self, name):
3
n = name
4
self.name = name
Wenn von dieser Klasse eine Instanz erzeugt wird, dann ist "name" eine
Variable, die dem Konstruktor __init__() übergeben werden muß (weswegen
ihr Wert logischerweise auch außerhalb des Konstruktors bekannt sein
muß, sonst könnte man ihm diesen Wert natürlich nicht übergeben), aber
trotzdem ist der Name lokal, also nur in dem Konstruktor bekannt. "n"
ist ebenfalls eine lokale Variable im Gültigkeitsbereich (Scope) des
Konstruktors; "name" und "n" sind dem restlichen Programm nicht mehr
bekannt, sobald die Konstruktor-Methode beendet ist. Dagegen ist
"self.name" eine Variable, die beim Aufruf des Konstruktors erzeugt
(Autovivikation) und an "self", also: an die konkrete Instanz der Klasse
gebunden. und in der Instanz dieser Klasse gespeichert wird. Sie gehört
durch das "self.<variablenname>" also zur Instanz der Klasse, und
deswegen können alle Methoden der Klasse "Dings" darauf zugreifen. (Der
Abschnitt ist vermutlich bis hierher der Wichtigste, wenn Du dazu noch
Fragen hast, bitte scheu' Dich nicht, sie zu stellen.)
Nebenbei bemerkt, funktioniert das mit der Autovivikation allerdings
nicht nur im Konstruktor, und das ist gerade bei Einsteigern und auch
beim Testen und Debuggen eine sehr beliebte Fehlerquelle:
1
class Ding:
2
def __init__(self, name):
3
self.name = name
4
5
def setAge(self, age):
6
self.age = int(age)
7
8
def say(self):
9
print('"%s" ist %d Jahre alt'%(self.name, self.age))
Schau Dir den Code bitte genau an, und wenn Du magst, spiel' ein
bisschen damit herum. Was passiert, wenn Du die Methode "say()"
aufrufst, bevor die Methode "setAge()" aufgerufen wurde? Und warum
passiert das? Bitte probier' das mal aus -- es ist beim Erlernen einer
Sprache auch immer gut, mal die Fehlermeldungen sehen und interpretieren
zu lernen, Python-Tracebacks lesen sich übrigens von unten nach oben.
Dann gibt es in Python aber auch noch sogenannte Klassenvariablen, die
dann zur Klasse gehören und nicht zur Instanz, und obendrein class- und
staticmethogs, aber da wird es dann wirklich sehr fortgeschritten und
ein bisschen... tricky, und würde deswegen hier zu weit führen... kommen
wir also zur Vererbung. ;-)
Nun, die Vererbung bietet dem Entwickler die Möglichkeit, Klassen zu
verändern, um sie beispielsweise um neue Methoden (und Variablen) zu
erweitern. Dabei geht es im Kern um die Wiederverwendbarkeit und
Erweiterung von vorhandenem Code. Weil es mir gerade als halbwegs
brauchbares Beispiel erscheint, bei dem Fortbewegungsmitteln aus meinem
vorigen Beitrag zu bleiben... machen wir das doch einfach.
Nehmen wir an, unser Chef hat ein Auto und möchte gerne ausrechnen, wie
schnell er nach n Sekunden voller Beschleunigung ist. (Ja, das ginge
auch mit einer einfachen Funktion, aber...) Weil unser Chef aber leider
nicht Jutta Kleinschmidt ist, steht das Auto zu Anfang immer... ;-)
So weit, so bekannt (mit Ausnahme der dritten Möglichkeit, Strings mit
variablen Inhalten zu erzeugen: den f-Strings). Außerdem habe ich mir
nun mal die Freiheit genommen, etwas zu tun, was eigentlich guter Stil
ist: print()-Funktionen gehören eigentlich nicht in eine Methode, sowas
kann man nur mal zum Debugging machen, im realen Leben würde man dazu
aber lieber Pythons extrem leistungsfähiges und sehr flexibles Modul
"logging" benutzen. Aber im Kern solltest Du halbwegs verstehen, was
dieser Code da macht... spätestens wenn Du damit herumgespielt hast,
wozu ich Dich an dieser Stelle allerwärmstens ermuntern möchte. ;-)
Nun... wie das nunmal so ist: wir haben unglaublich guten Python-Code
entwickelt, und unser Chef konnte damit Aufträge auf der ganzen Welt
akquirieren. Der hat nun aber ohnehin eine Pilotenlizenz und weiß daher,
daß er die Kunden mit dem eigenen Flugzeug viel schneller und billiger
abklappern kann, und legt sich deswegen eine alte Saab Jaktviggen zu --
in der er, weil er das ja so gewohnt ist, gerne seine altbekannte
Geschwindigkeitsanzeige haben will.
Hm, alles neu schreiben? I wo, nach Larry Wall (dem Erfinder von Perl)
ist Faulheit bekanntlich eine Kardinaltugend guter Entwickler. Also
erweitern wir einfach unsere Auto-Klasse zu einer Flugzeugklasse... und
nein: im realen Leben würde man das nie so machen, aber ich wollte bei
meinem Beispiel bleiben und kann daran ein paar ganz nette Features
zeigen... ;-)
1
#!/usr/bin/env python3
2
fromenumimportEnum
3
4
5
classAuto:
6
def__init__(self,marke='Porsche',accel=0):
7
self.marke=marke
8
self.accel=accel
9
self.speed=0
10
11
defbeschleunige(self,zeit):
12
self.speed+=self.speed+(zeit*self.accel)
13
14
defanzeige(self):
15
returnf'Der {self.marke} hat jetzt {self.speed} m/s'
16
17
def__str__(self):
18
returnf'Der {self.marke} hat jetzt {self.speed} m/s'
Wow, das war viel Code -- nicht zuletzt auch wegen des etwas unglücklich
gewählten Beispiels, bitte entschuldige das. Nun, unsere Auto-Klasse
kennen wir schon, nur: was macht diese neue Methode "__str()__" da? Wenn
Du genau hinschaust, siehst Du, daß sie genau dasselbe tut wie unsere
bisherige Methode "Auto.anzeige()". Warum? Weil es eine sogenannte
"Magic Method" ist, solche Methoden sind meines Wissens ein spezielles
Feature von Python -- aber dazu später.
Naja, da ist jetzt schon ein bisschen Fortgeschrittenes dabei, aber...
fangen wir einfach mal mit der Klasse "Flieger" an. Bei der Definition
der Klassen siehst Du schon, daß sie von der Klasse "Auto" erbt,
deswegen das "(Auto)" hinter dem Namen der Klasse. Was heißt das, sie
'erbt'? Genau: sie übernimmt dadurch alle Methoden und Eigenschaften
(Variablen) ihrer Elternklasse "Auto". Im Grunde ist der Flieger also
erstmal genau dasselbe wie ein Auto, und hat nur einen anderen
Klassennamen.
Jetzt hat die Klasse "Flieger" aber ein paar Methoden, die genauso
heißen wie die Methodennamen von unserer Klasse "Auto", aber etwas
anderes tun. Klar, ein Flieger ist halt kein Auto... also jedenfalls,
wenn er fliegt, was die meisten Autos ja bekanntlich (noch) nicht so gut
können ("Grizzy und die Lemminge" mal ausgenommen, natürlich). Das
heißt: die "Flieger"-Klasse überschreibt die Methoden das Klasse "Auto"
und ersetzt deren Funktionalität durch eine neue.
Schon beim Konstruktor siehst Du, daß die Fliegerklasse andere Argumente
annehmen kann als die Auto-Klasse. (Und ganz nebenbei siehst Du auch,
daß die Argumente der Konstruktoren Default-Werte haben, also: angegeben
werden können, aber nicht müssen. Das ist ähnlich wie die Varargs unter
C/C++...). Die Klasse "Flieger" kann also neben dem "accel"-Argument für
die Beschleunigung am Boden zusätzlich auch noch ein
"flugaccel"-Argument annehmen.
Nun sind wir aber -- wiedermal und immer wieder gerne -- faul. Müssen
wir den Code des Konstruktors unserer Parent-Klasse "Auto" nochmal neu
schreiben? Ach Quatsch, wir rufen den einfach auf. Das macht der Aufruf
von super().__init__(), natürlich nur mit den Argumenten, die unsere
Parent-Klasse kennt. Und danach müssen wir uns nur noch um die
Initialisierung unserer Child-Klasse "Flieger" kümmern -- den Rest hat
der Konstruktor der Parent-Klasse ja schon für uns erledigt. ;-)
Dann ist da in unserer Klasse "Flieger" auch noch die die Methode
"beschleunige()". Da habe ich die "beschleunige()"-Methode unserer
Elternklasse "Auto" einfach mal überschrieben, ohne eine Funktion der
Elternklasse "Auto" aufzurufen. Eine Instanz von "Flieger" verhält sich
also beim Aufruf der Methode "beschleunige" ganz anders als eine Instanz
der Klasse "Auto", auch wenn sie -- abhängig davon, ob der Flieger in
der Luft ist oder am Boden -- ganz verschiedene Dinge tut. Ansonsten hat
unser "Flieger" noch weitere Methoden, weil ein Flugzeug eben kein Auto
ist und deswegen starten und landen kann...
Aber jetzt kommen wir zum wesentlichen Kernpunkt der Vererbung: die
Methoden "anzeige()" und "__str__()" übernimmt unsere Klasse "Flieger"
völlig unverändert, genauso die Eigenschaften "marke" und
"beschleunigung", die nur an den Konstruktor der Elternklasse übergeben
und von den Methoden benutzt werden. Jetzt (oder morgen, möglicherweise)
verstehst Du vielleicht auch, warum ich den Teil zur Autovivikation
vorangestellt habe... ;-)
Mit diesem Wissen kannst Du jetzt auch sehen, daß ich an zwei weiteren
Stellen was vererbe, bei der Definition der Klassen "Zustand" und
"RuebennaseError". Nun, das Konzept von Enum kennst Du von C (auch wenn
Python da wesentlich mehr kann), dabei geht es um die Festlegung von
Konstanten. Beim "RuebennaseError" dagegen geht es lediglich darum,
einen neuen Namen zu erzeugen... über Exceptions kann ich gerne
weitersprechen, wenn Du Zeit und Lust hast, und das Bisherige (ich weiß,
ein sehr dicker Brocken diesmal) halbwegs verinnerlichen konntest.
Bitte versteh' mich nicht falsch: ich halte Dich keinesfalls für dumm,
dann würde ich mir die Mühe nicht machen, um ehrlich zu sein. Ich weiß
sehr gut, daß das viel Neues und zum Teil womöglich auch etwas
verstörend ist für jemanden, der sich bis dato nur mit prozeduraler
Entwicklung beschäftigt hat; Deinen Weg bin ich selbst gegangen, es hat
auch bei mir eine Weile gebraucht, und mein Vorteil ist nur, daß diese
Sachen bei mir schon drölfzig Jahre her sind.
Du sagst es absolut richtig, und das ist auch meine Erfahrung als
passionierter Segellehrer bei der Segel- und Motorbootschule Lord Nelson
[2] -- was viel mehr hands-on ist als es Beiträge in einem Forum sein
können -- man muß es ersteinmal sacken lassen. Ich möchte Dich an dieser
Stelle auch ebenso wärmstens wie herzlich dazu ermuntern, einfach mal
ein bisschen herumzuspielen. Python ist großartig und der einzige
Skriptspracheninterpreter, bei dem ich es noch nie geschafft habe, ihn
abstürzen zu lassen, obwohl schon eine Reihe C- und C++-Libraries dafür
entwickelt habe.
Wenn Du noch dabei bist, können wir nächstes Mal gerne über das
Überschreiben von Operatoren mit Pythons "Magic Methods", und über die
Übergabe und Übernahme von variablen Argumenten mit "args" und "kwargs"
reden, über Comprehensions für Dicts und Lists... oder gerne auch über
Tkinter für GUIs oder Flask für Webapplikationen, ganz wie Du magst. ;-)
Liebe Grüße, viel Erfolg und viel Spaß,
Sheeva!
[1] https://www.python.org/dev/peps/pep-0008/
[2] https://lord-nelson.com/
Sheeva P. schrieb:> Nehmen wir an, unser Chef hat ein Auto und möchte gerne ausrechnen, wie> schnell er nach n Sekunden voller Beschleunigung ist. (Ja, das ginge> auch mit einer einfachen Funktion, aber...) Weil unser Chef aber leider> nicht Jutta Kleinschmidt ist, steht das Auto zu Anfang immer... ;-)> 1class Auto:> 2 def __init__(self, marke='Porsche', beschleunigung=0):> 3 self.marke = marke> 4 self.beschleunigung = beschleunigung> 5 self.geschwindigkeit = 0> 6> 7 def beschleunige(self, zeit):> 8 self.geschwindigkeit += self.geschwindigkeit + (zeit *> self.beschleunigung)
Ist in Zeile 8 nicht der „self.geschwindigkeit“ nach der Zuweisung „+=“
überflüssig?
Uwe D. schrieb:> Sheeva P. schrieb:>> 7 def beschleunige(self, zeit):>> 8 self.geschwindigkeit += self.geschwindigkeit + (zeit *>> self.beschleunigung)>> Ist in Zeile 8 nicht der „self.geschwindigkeit“ nach der Zuweisung „+=“> überflüssig?
Oh... ja, da hast Du selbstverständlich Recht -- es muß natürlich
entweder