zurück rauf weiter Englisch Index

Kapitel 14

Rohübersetzung -- bitte um Rückmeldungen über Fehler und Unklarheiten an Gregor Lingl

Klassen und Methoden

14.1 Objektorientierte Elemente

Python ist eine objektorientierte Programmiersprache. Das heißt, dass sie Sprachelemente enthält, die objektorientiertes Programmieren unterstützen.

Es ist nicht ganz leicht, objektorientiertes Programmieren zu definieren. Wir haben aber bereits einige charakteristische Eigenschaften davon gesehen.

Zum Beispiel entspricht die Time-Klasse, die wir in Kapitel 13 definiert haben, der Art und Weise, wie wir Menschen die Tageszeit festhalten. Die Funktionen, die wir dort definiert haben, entsprechen dem, was wir mit solchen Zeitangaben machen. In ähnlicher Weise entsprechen die Point und Rectangle Klassen den mathematischen Begriffen von Punkt und Rechteck.

Bis jetzt haben wir von den Eigenschaften von Python, die objektorientiertes Programmieren unterstützen, noch keinen Gebrauch gemacht. Ganz streng genommen sind diese Sprachelemente auch nicht nötig. In der Hauptsache stellen sie nur eine alternative Syntax für jene Dinge bereit, die wir bis jetzt schon gemacht haben, aber in vielen Fällen ist diese Alternative prägnanter und vermittelt eine genauere Darstellung der Programmstruktur.

Zum Beispiel gibt es in dem Time Programm keine offensichtliche Verbindung zwischen der Klassendefinition und den Funktionsdefinitionen, die darauf folgen. Schaut man aber genauer hin, so wird offenbar, dass jede Funktion mindestens ein Time Objekt als Argument übernimmt.

Diese Beobachtung dient uns als Motivation für die Einführung von Methoden. Wir haben schon früher einige Methoden gesehen, wie etwa keys und values, die für Dictionaries aufgerufen wurden. Jede Methode gehört zu einer Klasse und ist dazu gedacht, für Instanzen dieser Klasse aufgerufen zu werden.

Methoden sind etwas ganz ähnliches wie Funktionen, jedoch mit zwei Unterschieden:

In den folgenden Abschnitten werden wir uns die Funktionen von den letzten beiden Kapiteln hernehmen und sie in Methoden umwandeln. Diese Umwandlung funktioniert ganz mechanisch; man kann sie durchführen, indem man einfach eine bestimmte Folge von Schritten ausführt. Wenn man diese Umwandlung von einer Form in die andere gut beherrscht, ist man auch im Stande, die jeweils beste Form für die gerade anstehende Aufgabe zu wählen.

14.2 printTime

In Kapitel 13, haben wir eine Time genannte Klasse definiert und du hast eine Funktion namens printTime geschrieben, die etwa so ausgesehen haben wird:

class Time:
  pass

def
printTime(time):
  print str(time.hours) + ":" +
        str(time.minutes) + ":" +
        str(time.seconds)

Beim Aufruf dieser Funktion haben wir ihr ein Time Objekt als Argument übergeben

>>> currentTime = Time()
>>> currentTime.hours = 9
>>> currentTime.minutes = 14
>>> currentTime.seconds = 30
>>> printTime(currentTime)

Um aus der Funktion printTime eine Methode zu machen, müssen wir nur die Funktionsdefinition in die Klassendefinition einfügen. Beachte dabei die Änderung bei der Einrückung.

class Time:
  def printTime(time):
    print str(time.hours) + ":" +
          str(time.minutes) + ":" +
          str(time.seconds)

Nun können wir printTime mit der Punkt-Notation aufrufen.

>>> currentTime.printTime()

Das Objekt, für das die Methode aufgerufen wird, erscheint wie gewohnt vor dem Punkt und der Name der Methode steht nach dem Punkt.

Das Objekt, für das die Methode aufgerufen wird, wird dem ersten Parameter zugewiesen. In diesem Fall wird also currentTime dem Parameter time zugewiesen.

Es gibt eine Vereinbarung, den ersten Parameter einer Methode self zu nennen. Der Grund für diese Vereinbarung ist ein wenig verwickelt, aber ihre Grundlage ist eine nützliche Metapher.

Die Syntax für einen Funktionsaufruf, printTime(currentTime), legt die Idee nahe, dass die Funktion hier das aktive Element ist. Sie besagt etwa: "He, printTime! Hier ist ein Objekt, das du ausgeben sollst."

Beim objektorientierten Programmieren sind die Objekte die aktiven Elemente. Ein Aufruf wie currentTime.printTime() besagt "He, currentTime! Bitte gib dich selbst aus"

Dieser Wechsel der Perspektive ist vielleicht höflicher, aber es ist nicht offensichtlich, dass er auch nützlich ist. In den Beispielen, die wir bisher gesehen haben, ist es vielleicht nicht so. Aber manchmal erleichtert das Verschieben der Verantwortung von den Funktionen zu den Objekten das Schreiben von vielseitiger verwendbaren **versatile?** Funktionen und es erleichtert auch die Wartung und die Wiederverwendbarkeit von Code.

14.3 Noch ein Beispiel

Wir wollen jetzt increment (aus Abschnitt 13.3) in eine Methode umwandeln. Um Platz zu sparen, werden wir früher definierte Methoden hier weglassen, aber du solltest sie in deiner Version stehen haben:

class Time:
  #früher definierte Methoden hierher...

  def increment(self, secs):
    self.secs = secs + self.seconds

    while self.seconds >= 60:
      self.seconds = self.seconds - 60
      self.minutes = self.minutes + 1

    while self.minutes >= 60:
      self.minutes = self.minutes - 60
      self.hours = self.hours + 1

Die Umwandlung geschieht rein mechanisch     wir verschieben die Funktions**method**definition in die Klassendefinition, **rücken sie um eine Ebene weiter ein** und ändern den Namen des ersten Parameters.

Nun können wir increment als Methode aufrufen.

currentTime.increment(500)

Wieder wird das Objekt, für das die Methode aufgerufen wird, dem ersten Parameter, self, zugewiesen. Der zweite Parameter, secs bekommt den Wert 500.

Übung: wandle convertToSeconds (aus Abschnitt 13.5) in eine Methode der Time Klasse um.

14.4 Ein komplizierteres Beispiel

Die after Funktion ist etwas komplizierter, weil sie auf zwei Time Objekten operiert und nicht auf nur einem. Wir können nur einen der Parameter in self umwandeln; der andere bleibt gleich:

class Time:
  # früher definierte Methoden hierher...

  def after(self, time2):
    if self.hour > time2.hour:
      return 1
    if self.hour < time2.hour:
      return 0

    if self.minute > time2.minute:
      return 1
    if self.minute < time2.minute:
      return 0

    if self.second > time2.second:
      return 1
    return 0

Wir rufen die Methode für ein Objekt auf und übergeben das andere als Argument:

if doneTime.after(currentTime):
  print "The bread will be done after it starts."

Dieser Aufruf kann fast wie ein englischer Satz gelesen werden: "If the done-time is after the current-time, then..."

14.5 Optionale Argumente

Wir kennen schon eingebaute Funktionen, die eine variable Anzahl von Argumenten übernehmen. Zum Beispiel kann string.find zwei, drei oder vier Argumente übernehmen.

Es ist möglich, benutzerdefinierte Funktionen zu schreiben, die optionale Argumente haben. Zum Beispiel können wir unsere eigene Version von find in der Weise verbessern, dass sie so funktioniert wie string.find.

Das ist die originale Version aus dem Abschitt 7.7:

def find(str, ch):
  index = 0
  while index < len(str):
    if str[index] == ch:
      return index
    index = index + 1
  return -1

Das ist die neue, verbesserte Verson:

def find(str, ch, start=0):
  index = start
  while index < len(str):
    if str[index] == ch:
      return index
    index = index + 1
  return -1

Der dritte Parameter, start, ist optional, weil für ihn ein als Voreinstellung ein Standardwert *default value* angegeben wird. Wenn wir die Funktion find nur mit zwei Argumenten aufrufen, benutzt sie den Standardwert und beginnt die Suche vom Anfang der Zeichenkette an.

>>> find("apple", "p")
1

Wenn wir ein drittes Argument übergeben, überschreibt es den voreingestellten Standardwert.

>>> find("apple", "p", 2)
2
>>> find("apple", "p", 3)
-1

Übung: Füge einen vierten Parameter, end, hinzu, der angibt, wo die Suche beendet werden soll.

Warnung: Diese Übung ist ein bißchen heikel, *tricky*. Der voreingestellte Standardwert für end sollte nämlich len(str) sein, aber das funktioniert nicht. Die voreingestellten Standardwerte werden nämlich ermittelt, wenn die Funktion definiert wird und nicht, wenn sie aufgerufen wird. Wenn find definiert wird, existiert aber str noch gar nicht. Daher kannst du zu diesem Zeitpunkt seine Länge gar nicht ermitteln.

14.6 The Initialisierungsmethode

Die Initialisierungsmethode ist eine spezielle Methode, die (automatisch?) aufgerufen wird, wenn ein Objekt erzeugt wird. Der Name dieser Methode ist __init__ (zwei Unterstrich-Zeichen gefolgt von init und zwei weiteren Unterstrich-Zeichen). Eine Initialisierungsmethode für die Time Klasse sieht so aus:

class Time:
  def __init__(self, hours=0, minutes=0, seconds=0):
    self.hours = hours
    self.minutes = minutes
    self.seconds = seconds

Es besteht kein Konflikt zwischen dem Attribut self.hours und dem Parameter hours. Durch die Punkt-Notation wird festgelegt, welche Variable jeweils gemeint ist.

Wenn wir den Time Konstruktor aufrufen, werden die Argumente, die wir übergeben an init weiter gereicht:

>>> currentTime = Time(9, 14, 30)
>>> currentTime.printTime()
>>> 9:14:30

Weil die Argumente optional sind, können wir sie weglassen:

>>> currentTime = Time()
>>> currentTime.printTime()
>>> 0:0:0

Oder nur das erste Argument übergeben:

>>> currentTime = Time (9)
>>> currentTime.printTime()
>>> 9:0:0

Oder die ersten zwei Argumente:

>>> currentTime = Time (9, 14)
>>> currentTime.printTime()
>>> 9:14:0

Schließlich können wir auch eine Teilmenge der Argumente übergeben, indem wir namentlich angeben, welchen Parametern sie zugewiesen werden sollen:

>>> currentTime = Time(seconds = 30, hours = 9)
>>> currentTime.printTime()
>>> 9:0:30

14.7 Nochmals Punkte

Schreiben wir nun die Point Klasse aus Abschnitt 12.1 neu, nun in einem mehr objektorientierten Stil:

class Point:
  def __init__(self, x=0, y=0):
    self.x = x
    self.y = y

  def __str__(self):
    return '(' + str(self.x) + ', ' + str(self.y) + ')'

Die Initialisierungsmethode übernimmt die Werte x und y als optionale Argumente; die voreingestellten Standardwerte für beide Argumente sind 0.

Die nächste Methode __str__, gibt eine String-Darstellung eines Point Objekts zurück. Wenn eine Klasse eine Methode mit dem Namen __str__ enthält, dann überschreibt diese das voreingestellte Verhalten von Pythons eingebauter str Funktion.

>>> p = Point(3, 4)
>>> str(p)
'(3, 4)'

Die Ausgabe eines Point-Objektes mit der print-Anweisung ruft hinter den Kulissen die __str__-Methode für das Objekt aus; somit ändert die Definition von __str__ in einer Klasse auch das Verhalten von print:

>>> p = Point(3, 4)
>>> print p
(3, 4)

Wenn wir eine neue Klasse schreiben, beginnen wir fast immer damit __init__ zu schreiben, weil es dadurch leichter wird, Objekte zu instanziieren, und dann __str__, weil dies fast immer nützlich fürs Debuggen ist.

14.8 Überladen von Operatoren

In manchen Sprachen ist es möglich, die Definition der eingebauten Operatoren für den Fall zu erweitern, dass sie auf benutzerdefinierte Typen angewendet werden. Dies nennt man Überladen von Operatoren. Das ist insbesondere nützlich, wenn man neue mathematische Typen definiert.

Um etwa den Additionsoperator + zu überladen, definieren wir eine Methode namens __add__:

class Point:
  #früher definierte Methoden hierher...

  def __add__(self, other):
    return Point(self.x + other.x, self.y + other.y)

Wie üblich ist der erste Parameter für das Objekt reserviert, für das die Methode aufgerufen wird. Der zweite Parameter heißt sinnigerweise other um ihn von self zu unterscheiden. Um zwei Points zu addieren, erzeugen wir einen neuen Point, der die Summen der x Koordinaten bzw. der y Koordinaten enthält und geben diesen zurück.

Wenn wir nun den + Operator auf Point Objekte anwenden, dann ruft Python hinter den Kulissen __add__ auf:

>>>   p1 = Point(3, 4)
>>>   p2 = Point(5, 7)
>>>   p3 = p1 + p2
>>>   print p3
(8, 11)

Der Ausdruck p1 + p2 ist gleichwertig mit p1.__add__(p2), aber offensichtlich eleganter.

Übung: Füge eine Methode __sub__(self, other) zur Klasse Point hinzu, die den Subtraktionsoperator überlädt und teste sie anschließend.

Es gibt verschiedene Arten, das Verhalten des Multiplikationsoperators für Punkte zu erweitern: durch Definition der Methoden __mul__, oder __rmul__, oder beide.

Wenn der linke Operand von * ein Point ist, ruft Python die Methode __mul__ auf, wobei der andere Operand ebenfalls ein Point Objekt sein muss. Sie berechnet das innere Produkt der zwei Punkte entsprechend den Regeln der linearen Algebra:

def __mul__(self, other):
  return self.x * other.x + self.y * other.y

Wenn der linke Operand von * ein *primitiver* Typ und der rechte Operand ein Point ist, ruft Python __rmul__ auf, welches eine skalare Multiplikation ausführt:

def __rmul__(self, other):
  return Point(other * self.x,  other * self.y)

Das Ergebnis ist ein neues Point Objekt, dessen Koordinaten ein Vielfaches der Koordinaten des ursprünglichen Punktes sind.

*** Nachdenken:

If other is a type that cannot be multiplied by a floating-point number, then __rmul__ will yield an error.

Wenn other ein Typ ist, der nicht mit einer Gleitkommazahl multipliziert werden kann, dann wird __rmul__ einen Fehler ergeben.

Nachdenken ***

Das folgende Beispiel zeigt beide Arten der Multiplikation:

>>> p1 = Point(3, 4)
>>> p2 = Point(5, 7)
>>> print p1 * p2
43
>>> print 2 * p2
(10, 14)

Was geschieht, wenn wir versuchen p2 * 2 auszuwerten? Weil das erste Argument ein Punkt ist, ruft Python __mul__ mit 2 als zweitem Argument auf. Bei der Ausführung von __mul__ versucht das Programm auf die x Koordinate von other zuzugreifen; das schlägt fehl, weil eine Ganzzahl keine Attribute hat:

>>> print p2 * 2
AttributeError: 'int' object has no attribute 'x'

Leider ist die Fehlermeldung ein bißchen undurchsichtig. Dieses Beispiel demonstriert einige Schwierigkeiten der objektorientierten Programmierung. Manchmal ist es schon allein recht mühsam herauszufinden, welcher Code gerade ausgeführt wird.

Ein vollständiger ausgeführtes Beispiel für Überladen von Operatoren findest du in Anhang .

14.9 Polymorphismus

Die meisten Methoden, die wir geschrieben haben, funktionieren nur für einen speziellen Typ. Wenn man eine neue Klasse (**Objekt**) erzeugt, schreibt man Methoden, die für diesen Typ aufgerufen werden.

Es gibt aber gewisse Operationen, die man auf viele Typen anwenden möchte, wie zum Beispiel die arithmetischen Operationen im vorigen Abschnitt. Wenn viele Typen die selbe Menge von Operationen aufweisen, kann man Funktionen schreiben, die auf all diesen Typen arbeiten.

Nehmen wir als Beispiel folgende multadd Operation mit drei Parametern (die in der linearen Algebra gebräuchlich ist). Sie multipliziert die ersten zwei und addiert zum Produkt den dritten. Wir können das in Python so schreiben:

def multadd (x, y, z):
  return x * y + z

Diese Methode wird für alle Werte von x und y funktionieren, die multipliziert werden können und für beliebige Werte von z, die zu dem Produkt addiert werden können.

Wir können sie mit numerischen Werten aufrufen:

>>> multadd (3, 2, 1)
7

Oder mit Point-Objekten:

>>> p1 = Point(3, 4)
>>> p2 = Point(5, 7)
>>> print multadd (2, p1, p2)
(11, 15)
>>> print multadd (p1, p2, 1)
44

Im ersten Fall wird der Point mit einem Skalar multipliziert und dann zu einem anderen Point addiert. Im zweiten Fall liefert das innere Produkt der Punkte einen Zahlenwert, daher muss das dritte Argument ebenfalls ein Zahlenwert sein.

Eine Funktion wie diese, die Argumente von verschiedenem Typ übernehmen kann wird polymorph genannt.

Berachten wir als weiteres Beispiel die Methode frontAndBack, die eine Liste zwei Mal, vorwärts und rückwärts, ausgibt:

def frontAndBack(front):
  import copy
  back = copy.copy(front)
  back.reverse()
  print str(front) + str(back)

Weil die reverse Methode eine modifizierende Methode ist, machen wir zuerst eine Kopie der Liste und kehren diese um. Auf diese Weise modifiziert diese Funktion nicht die Liste, die sie als Argument erhält.

Hier ist ein Beispiel, das frontAndBack auf eine Liste anwendet:

>>>   myList = [1, 2, 3, 4]
>>>   frontAndBack(myList)
[1, 2, 3, 4][4, 3, 2, 1]

Natürlich, wir wollten diese Funktion auf Listen anwenden, daher ist es nicht überraschend, dass sie da funktioniert. Überraschend wäre es allerdings, wenn wir diese Funktion auch auf Punkte anwenden könnten.

Um heraus zu finden, ob eine Funktion auf einen neuen Typ angewendet werden kann, benutzen wir die Grundregel des Polymorphismus:

Wenn alle Operationen innerhalb einer Funktion auf einen Typ angewendet werden können, kann auch die Funktion auf diesen Typ angewendet werden.

Die Operationen in dieser Funktion umfassen copy, reverse, und print.

copy arbeitet mit jedem Objekt, und wir haben auch schon eine __str__ Methode für Point Objekte geschrieben. Daher ist alles was wir brauchen, eine reverse Methode in der Point Klasse:

def reverse(self):
  self.x , self.y = self.y, self.x

Jetzt können wir Point Objekte an frontAndBack übergeben:

>>>   p = Point(3, 4)
>>>   frontAndBack(p)
(3, 4)(4, 3)

Die schönste Art des Polymorphismus ist die unbeabsichtigte: wenn man entdeckt, dass eine Funktion, die man bereits fertig geschrieben hat, auf einen Typ angewendet werden kann, für den sie niemals geplant war.

14.10 Glossar

objectorientierte Sprache
Eine Sprache mit Sprachelementen, die objektorientierte Programmierung unterstützt, wie zum Beispiel benutzerdefinierte Klassen und Vererbung.
object-oriented language
A language that provides features, such as user-defined classes and inheritance, that facilitate object-oriented programming.
objektorientierte Programmierung
Ein Programmierstil, bei dem Daten und Operationen, die mit diesen Daten arbeiten, als Klassen und Methoden organisiert sind.
object-oriented programming
A style of programming in which data and the operations that manipulate it are organized into classes and methods.
Methode
Eine Funktion, die innerhalb einer Klassendefinition definiert wird und für Instanzen dieser Klasse aufgerufen wird.
method
A function that is defined inside a class definition and is invoked on instances of that class.
überschreiben
Einen Standardwert ersetzen. Zum Beispiel einen Standardwert für einen Parameter durch ein bestimmtes Argument ersetzen, oder eine Standardmethode durch eine neue Methode mit demselben Namen ersetzen.
override
To replace a default. Examples include replacing a default parameter with a particular argument and replacing a default method by providing a new method with the same name.
Initialisierungsmethode
Eine spezielle Methode, die automatisch aufgerufen wird, wenn ein neues Objekt erzeugt wird und die die Attribute dieses Objekts mit Anfangswerten ausstattet.
initialization method
A special method that is invoked automatically when a new object is created and that initializes the object's attributes.
Überladung von Operatoren
Erweiterung eingabauter Operatoren (+, -, *, >, <, usw.), so dass sie auch mit benutzerdefinierten Typen arbeiten.
operator overloading
Extending built-in operators (+, -, *, >, <, etc.) so that they work with user-defined types.
Inneres Produkt
Eine Rechenoperation, die in der linearen Algebra definiert wird und die zwei Punkte, vom Typ Point, multipliziert und einen Zahlenwert als Ergebnis hat.
dot product
An operation defined in linear algebra that multiplies two Points and yields a numeric value.
skalare Multiplikation
Eine Rechenoperation, die in der linearen Algebra definiert wird, die jede Koordinate eines Punktes, vom Typ Point, mit einem Zahlenwert multipliziert.
scalar multiplication
An operation defined in linear algebra that multiplies each of the coordinates of a Point by a numeric value.
polymorph
Eine Funktion, die auf mehr als einen Typ angewendet werden kann. Wenn alle Operationen in einer Funktion auf einen Typ angewendet werden können, dann kann die Funktion auf diesen Typ angewendet werden.
polymorphic
A function that can operate on more than one type. If all the operations in a function can be applied to a type, then the function can be applied to a type.


zurück rauf weiter Englisch Index