Funktionen und KlassenInhaltDas vierte Kapitel geht auf die Strukturierung von Programmen durch Funktionen und Klassen ein und deckt die folgenden Konzepte ab:
FunktionenFunktionen sind ein integraler Bestandteil der prozeduralen Programmierung. Funktionen bekommen Eingabeparameter, führen Anweisungen aus und geben eventuell einen Wert zurück. Hierdurch kann man den Quelltext strukturieren und Dopplungen vermeiden. Funktionen sind daher auch eine wichtige Grundlage um Quelltext wiederverwendbar zu machen. In Python werden Funktionen mit Hilfe des Schlüsselworts def gefolgt vom Funktionsnamen und der Liste der Parameter in () definiert. Hier ist ein Beispiel für eine Funktion zur Berechnung der Quadratwurzel. Sobald eine Funktion definiert ist, kann sie jederzeit benutzt werden. Erste Funktion
>>> def my_sqrt(x): ... guess = 1 ... while(abs(guess*guess-x)>0.0001): ... guess = (1/2)*(guess+x/guess) ... return guess ... >>> my_sqrt(42) 6.480740727643494 Gibt man beim Funktionsaufruf die falsche Anzahl von Parametern an, gibt es eine Fehlermeldung. fehlerhafte Ausführung einer Funktion
>>> my_sqrt() # to few parameters --> error Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: my_sqrt() missing 1 required positional argument: 'x' >>> my_sqrt(42,42) # too many parameters --> error Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: my_sqrt() takes 1 positional argument but 2 were given Die letzte Zeile der my_sqrt Funktion beinhaltet eine Anweisung mit dem Schlüsselwort return. Hierdurch definiert man den Rückgabewert einer Funktion. Gibt es keine explizite return Anweisung, ist der Rückgabewert der Funktion None. Die return Anweisung kann prinzipiell an jeder Stelle einer Funktion stehen. Die Ausführung der Funktion wird sofort beendet, wenn return aufgerufen wird. DocstringsDokumentation von Quelltext ist wichtig für die Wiederverwendbarkeit von Quelltext. Will man eine Funktion wiederverwenden, möchte man nicht erst den kompletten Quelltext der Funktion lesen, um zu verstehen, was die Funktion macht. Stattdessen benötigt man einfach nur eine Beschreibung von der Funktionalität, den Parametern, und von den Rückgabewerten. In Python gibt es hierfür docstrings. Ein docstring ist ein mehrzeiliger String ("""…""") direkt nach der Definition einer Funktion als erste Anweisung der Funktion.
Arten von Funktionsparametern1. Default ParameterIn Python kann man Funktionsparameter auch mit vorgegeben Werten belegen, sogenannten defaults. Diese müssen dann nicht mehr zwingend angegeben werden. Hierzu weißt man den default Wert wie bei einer normalen Zuweisung direkt hinter dem Namen des Parameters zu. Anwendung Defaults
>>> def my_sqrt(x, verbose=False): ... """Returns the square root of x""" ... guess = 1 ... step = 1 ... while(abs(guess*guess-x)>0.0001): ... guess = (1/2)*(guess+x/guess) ... if verbose: ... print(f"approximation in step {step}: {guess}") ... step += 1 ... return guess ... >>> print(my_sqrt(42)) # calling my_sqrt with default value for verbose 6.480740727643494 >>> print(my_sqrt(42, True)) # calling my_sqrt with verbose=True approximation in step 1: 21.5 approximation in step 2: 11.726744186046512 approximation in step 3: 7.654150476761481 approximation in step 4: 6.570684743283659 approximation in step 5: 6.481356306333419 approximation in step 6: 6.480740727643494 6.480740727643494 Damit Funktionsaufrufe eindeutig bleiben, muss man Default Parameter von hinten nach vorne definieren. Ansonsten gibt es eine Fehlermeldung. fehlerhafte Anwendung defaults
>>> def my_sqrt(x=42, verbose):
File "<stdin>", line 1
def my_sqrt(x=42, verbose):
^^^^^^^
SyntaxError: non-default argument follows default argument
Eine Eigenheit von Python ist, dass Default Parameter nur einmal ausgewertet werden. Wenn Objekte unveränderbar sind, ist das kein Problem. Wenn man jedoch veränderbare Objekte als Default Parameter hat, kann dies zu unerwarteten Problemen führen, wenn man nicht aufpasst. erste Anwendung von Listen
>>> def create_or_append(value, list=[]): ... """Returns the value as part of a list.""" ... list.append(value) ... return list ... >>> # works as intended >>> print(create_or_append(42)) [42] >>> # same list as default parameter as before, element twice --> not as intended >>> print(create_or_append(42)) [42, 42] Man kann das Problem umgehen, in dem man als Default den Wert None angibt, und den eigentlich geplanten Default Wert erst innerhalb der Funktion zuweist. zweite Anwendung von Listen
>>> def create_or_append(value, list=[]): ... """Returns the value as part of a list.""" ... if list is None: ... list = [] ... list.append(value) ... return list ... >>> print(create_or_append(42)) # works as intended [42] >>> print(create_or_append(42)) # works as intended [42] 2. Named ParameterMan kann Parameter in Python auf zwei Arten übergeben: Über ihre Position oder über ihren Namen. Bisher haben wir die Parameter immer über die Position übergeben. Will man Parameter per Namen übergeben, weißt man Sie im Funktionsaufruf zu. Man kann auch einige Parameter durch ihren Namen übergeben, und andere durch die Position. Hier gilt eine ähnliche Regel wie bei Default Parametern: Man muss dies von Vorne nach Hinten machen, sonst gibt es Fehler. named Parameter 1
>>> my_sqrt(x=42) 6.480740727643494 >>> my_sqrt(42,verbose=True) approximation in step 1: 21.5 approximation in step 2: 11.726744186046512 approximation in step 3: 7.654150476761481 approximation in step 4: 6.570684743283659 approximation in step 5: 6.481356306333419 approximation in step 6: 6.480740727643494 6.480740727643494 >>> # error because keyword parameters must be last >>> my_sqrt(x=42,True) File "<stdin>", line 1 my_sqrt(x=42,True) ^ SyntaxError: positional argument follows keyword argument >>> # error because python cannot detect that the first parameter was given as keyword >>> my_sqrt(True,x=42) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: my_sqrt() got multiple values for argument 'x' Man kann Parameter auch über ihren Namen als Dictionary übergeben. Hierzu muss man ein Dict übergeben und mit ** definieren, dass es sich hierbei um Funktionsparameter handelt. parameter Dictionary
>>> param_dict = {"x": 42} >>> # use ** to signify that the dict is not a normal parameter, but contains named parameters >>> y_sqrt(**param_dict) 6.480740727643494 Gibt man nichts weiter an, akzeptieren Funktionen die Parameter sowohl über die Position, als auch über ihren Namen. Will man, dass Parameter nur über ihren Namen übergeben werden können, kann man dies erzwingen. Hierzu fügt man einen Parameter * hinzu. Dieser Parameter existiert nicht wirklich und gibt lediglich an, dass alle folgenden Parameter nur über ihren Namen akzeptiert werden. named Parameter 2
>>> def my_sqrt_keyword_only(*, x, verbose=False): ... """Returns the square root of x""" ... guess = 1 ... step = 1 ... while(abs(guess*guess-x)>0.0001): ... guess = (1/2)*(guess+x/guess) ... if verbose: ... print(f"approximation in step {step}: {guess}") ... step += 1 ... return guess >>> my_sqrt_keyword_only(x=42) # still works 6.480740727643494 Probiert man Parameter über ihre Position zu übergeben gibt es eine Fehlermeldung. fehlerhafte Anwendung von named Parameter
>>> my_sqrt_keyword_only(42) # error because only key word arguments are accepted
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: my_sqrt_keyword_only() takes 0 positional arguments but 1 was given
3. Beliebige ParameterEs gibt Funktionen, für die man die Anzahl der Parameter nicht genau kennt. Die print Funktion ist ein Beispiel hierfür. Bisher wurde print immer mit genau einem Parameter aufgerufen. Dies ist gar nicht nötig, da print beliebig viele Parameter auf einmal ausgeben kann. Diese Parameter müssen noch nichtmal alle den selben Typ haben. Sie können also beliebig sein. Anwendung von print Funktion
>>> print("first string", 42, "second string", 3.14)
first string 42 second string 3.14
Man definiert eine Liste von beliebigen Parametern, in dem man einen Parameternamen mit * beginnt. Man bekommt diese Parameter in der Funktion als Tupel übergeben. Anwendung von Liste von beliebigen Parametern
>>> def print_with_linesnumbers(*args): ... """Prints every argument in a new line and adds line numbers""" ... for i,arg in enumerate(args): ... print(f"{i:03d}: {arg}") ... >>> print_with_linesnumbers("first string", 42, "second string", 3.14) 000: first string 001: 42 002: second string 003: 3.14 Man kann auch feste Positionsparameter mit beliebigen Parametern kombinieren. Funktion mit beliebigen Parametern 2
>>> def print_with_linesnumbers(header, *args): ... """Prints every argument in a new line and adds line numbers""" ... print(header) ... for i,arg in enumerate(args): ... print(f"{i:03d}: {arg}") ... >>> print_with_linesnumbers("Here is the output", "first string", 42, "second string", 3.14) Here is the output 000: first string 001: 42 002: second string 003: 3.14 Parameter können auch weiterhin über ihren Namen übergeben werden, jedoch nur, wenn diese nach der Parameterliste definiert werden. Diese Parameter müssen dann auch zwingend über ihren Namen übergeben werden. Funktion mit beliebigen Parametern 3
>>> def print_with_linesnumbers(header, *args, numberwidth): ... """Prints every argument in a new line and adds line numbers""" ... print(header) ... for i,arg in enumerate(args): ... print(f"{i:0{numberwidth}d}: {arg}") ... >>> print_with_linesnumbers("Here is the output", "first string", 42, ... "second string", 3.14, numberwidth=4) Here is the output 0000: first string 0001: 42 0002: second string 0003: 3.14 Default Werte können auch nur für die Named Parameter definiert werden, also nur die Parameter nach der beliebigen Liste. Neben beliebigen Postionsparametern kann man auch beliebige Named Parameter erlauben. Hierzu muss der letzte Parameter einer Funktion mit beginnen. Dieser bekommt dann ein Dictionary mit allen Named Parametern, die nicht explizit in der Funktion definiert sind. Funktion mit beliebigen Parametern 4
>>> def print_with_linesnumbers(header, *args, numberwidth, **kwargs): ... """Prints every argument in a new line and adds line numbers""" ... print(header) ... for i,arg in enumerate(args): ... print(f"{i:0{numberwidth}d}: {arg}") ... if len(kwargs) > 0: ... print("there are additional named parameters:") ... for key,value in kwargs.items(): ... print(f"{key}: {value}") ... >>> print_with_linesnumbers("Here is the output", "first string", 42, ... "second string", 3.14, numberwidth=4, some_parameter="value") Here is the output 0000: first string 0001: 42 0002: second string 0003: 3.14 there are additional named parameters: some_parameter: value Lambda AusdrückeMit Lambda Ausdrücken kann man einfache Funktionen erstellen, die keinen Namen haben, also ohne def erzeugt werden. Lambda Ausdrücke werden mit dem Schlüsselwort lamdba erstellt und bestehen aus einer Parameterliste und einer einzigen Ausdruck. Lambda Funktion
>>> def multiplier(mult): ... """Creates functions that multiply values with a defined number""" ... # anonymous function that takes x as parameter and multiplies the value with mult ... return lambda x : x*mult ... >>> doubler = multiplier(2) # creates a function that multiplies values with 2 >>> tripler = multiplier(3) # creates a function that multiplies values with 3 >>> print(doubler(2)) 4 >>> print(tripler(2)) 6 Auch wenn Lambda Ausdrücke erstmal sehr Abstrakt erscheinen, sind Sie insbesondere für die Verarbeitung von Daten sehr hilfreich. Daher wird sich der wahre Nutzen von Lambdas erst später in der Vorlesung erschließen. KlassenDefiniert werden Klassen mit Hilfe des Schlüsselworts class.
Eine Klasse kann benutzt werden in dem sie instanziiert wird um ein Objekt zu erstellen. Das instanziieren eines Objektes ähnelt einem Funktionsaufruf. Objekte der Klasse Person wird zum Besipiel durch Person() erstellt. Die Methode print_name hat den Parameter self. Dieser Parameter refenziert das Objekt selbst, also eine Instanz der Klasse. Die Referenz zum Objekt selbst ist immer der erste Parameter von Methoden, der Name self ist lediglich eine Konvention und kein Schlüsselwort. self muss auch niemals selbst bei einem Methodenaufruf angegeben werden. Durch self ist es möglich auf Variablen und Methoden der Klasse zuzugreifen. Mit self.name wird somit auf die name Variable der Klasse zugegriffen. Man kann auch von außerhalb der Klasse auf alle Methoden und Variablen der Klasse zugreifen. Hierdurch unterscheiden sich Klassen in Python stark von Klassen in anderen objektorientierten Sprachen, wie zum Beispiel Java. Instanziieren von einer Klasse
>>> # instantiates an object of the class Person >>> my_person = Person() >>> # calls the method print_name on the object my_person; self must not be passed explicitly >>> my_person.print_name() Jane Doe >>> ''' class variables can also be access from the outside, ... there is no concept of information hiding in python ''' >>> print(Person.name) Jane Doe 1. init MethodeWenn die Personen alle den gleichen Namen haben, macht das natürlich semantisch nicht viel Sinn. Besser wäre es, wenn man den Namen der Person direkt beim instanziieren des Objektes angeben kann. Hierfür gibt es in der Objektorientierung das Konzept der Konstruktoren. Konstruktoren sind Methoden, mit denen neue Objekte erstellt werden. In Python wird dies durch die __init___ Methode umgesetzt. Es gibt zwar Streng genommen auch noch __new__, aber in der Regel reicht es wenn man davon ausgeht, dass __init__ der Konstruktor ist. Daher wird __new__ hier nicht genauer betrachtet.
In dieser neuen Personenklasse wird die Variable name nicht mehr statisch auf den Wert "Jane Doe" initialisiert. Stattdessen wird sie über die __init__ Methode gesetzt. Jetzt kann man Personen mit verschiedenen Namen erstellen. Instanziieren von einer Klasse mit __init__ Methode
>>> john_smith = Person("John Smith") >>> jane_doe = Person("Jane Doe") >>> john_smith.print_name() John Smith >>> jane_doe.print_name() Jane Doe Man kann jetzt auch keine Personen mehr erstellen ohne den Namen anzugeben, weil es keine __init___ Methode gibt, die ohne einen Parameter auskommt. fehlerhafte Instanziieren von einer Klasse
>>> Person()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Person.__init__() missing 1 required positional argument: 'name'
2. Instanzvariablen und KlassenvariablenDas Beispiel zeigt auch einen weiteren wichtigen Unterschied zwischen Python und anderen objekt-orientierten Sprachen. Variablen von Klassen müssen nicht vorher deklariert werden, sondern können jederzeit angelegt werden. Daher spricht man bei diesen Variablen in Python auch von Instanzvariablen, da sie nur von der Instanz einer Klasse zur Laufzeit abhängen. Welche Variablen im Objekt einer Klasse zur Verfügung stehen, kann sich jederzeit ändern. new Instanz von einer Klasse
>>> jane_doe.eye_color = "blue" # sets a new instance variable eye_color for jane_doe >>> print(jane_doe.eye_color) blue Ein Korrolar hiervon ist, das Objekte der gleichen Klasse nicht zwingend die gleichen Variablen haben müssen. new Instanz von einer Klasse 2
>>> john_smith.eye_color
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute 'eye_color'
Als Klassenvariablen bezeichnet man Variablen, die sich alle Instanzen einer Klasse teilen. Hierzu definiert man eine Variable direkt in der Klasse, wie wir es im ersten Beispiel mit dem Namen getan haben. >>> class Person: ... """A very simple class""" ... ... kind = "human" # the same in all instances of this class ... ... def __init__(self, name): ... self.name = name ... ... def print_name(self): ... print(self.name) ... ... def print_kind(self): ... print(self.kind) ... >>> python3 Person >>> jane_doe = Person("Jane Doe") >>> john_smith = Person("John Smith") >>> jane_doe.print_kind() human >>> john_smith.print_kind() human Klassenvariablen werden, genau wie Default Parameter, nur einmal initialisiert. Mit veränderbaren Objekten muss man also aus dem gleichen Grund aufpassen. Eine weitere Eigenheit von Klassenvariablen ist, dass man diese einfach wieder zu Instanzvariablen werden lassen kann, in dem man diese neu zuweist. Echte Klassenvariablen, bei denen garantiert ist das sie immer in allen Instanzen gleich sind, gibt es in Python nicht. >>> john_smith.kind = "neanderthal" >>> jane_doe.print_kind() human >>> john_smith.print_kind() neanderthal 3. Information HidingEin zentrales Konzept der Objektorientierung ist das information hiding. Hierdurch kann man sichergehen, dass bestimmte Informationen nur innerhalb einer Klasse sichtbar ist, in dem man diese Informationen als private deklariert. Dies kann sowohl für Methoden, als auch für Variablen sinnvoll sein. In Python gibt es kein information hiding, alle Variablen und Methoden sind immer von überall aus zugreifbar, wenn man ihren Namen kennt. In Python arbeitet man daher mit Namenskonventionen um etwas als private zu deklarieren. Dies kann dann zwar immernoch zugegriffen werden, aber durch die Namenskonvention ist zumindest bekannt, dass man dies nicht tun sollte. Für ein einfachen Hinweis auf private beginnt man einen Methoden, beziehungsweise Variablennamen, mit einem Unterstrich _.
VererbungEin weiteres wichtiges Konzept der Objektorientierung ist die Vererbung. Wenn eine Klasse von einer anderen erbt, übernimmt sie deren Verhalten. Man nennt die Klasse von der geerbt wird Elternklasse, die erbende Klasse ist das Kind. Statt dem Begriff erben wird auch häufig der Begriff ableiten verwendet. Man sagt also das eine Kindklasse von einer Elternklasse abgeleitet wird.
Im obigen Beispiel erben die Klassen Bicycle und Car von der gemeinsamen Elternklasse Vehicle. Mit anderen Worten, alle Autos und Fahrräder sind Fahrzeuge. Die Umkehrrichtung gilt nicht, es kann durchaus Fahrzeuge geben, die weder Autos, noch Fahhräder sind. Durch die Vererbung entsteht daher eine Klassenhierarchie. Die Kinder sind in dieser Hierarchie Spezialisierungen der Eltern, bzw. die Eltern Generalisierungen der Kinder. Entsprechend können die Kinder auch alles, was die Eltern können, die Eltern jedoch nicht alles, was die Kinder können. Im __init__ der beiden Kindklassen wird die Methode super() verwendet. Hiermit wird explizit die Elternklasse refenziert. super().__init__(1) ruft also die __init__() Methode der Elternklasse mit dem Parameter 1 auf. Vererbung Vehicle
>>> my_car = Car(3,5) >>> my_bicycle = Bicycle() >>> my_other_vehicle = Vehicle(5000) >>> # get_num_seats can be called on all three objects >>> print(f"my_car has {my_car.get_num_seats()} seats.") my_car has 5 seats. >>> print(f"my_bicycle has {my_bicycle.get_num_seats()} seats.") my_bicycle has 1 seats. >>> print(f"my_other_vehicle has {my_other_vehicle.get_num_seats()} seats.") my_other_vehicle has 5000 seats. >>> # get_num_doors can only be called on the car >>> print(f"my_car has {my_car.get_num_doors()} doors.") my_car has 3 doors. Magic MethodsEin weiteres wichtiges Konzept von Python sind magic methods. Im bisherigen Verlauf haben wir bereits öfter solche Methoden benutzt, ohne es zu merken, zum Beispiel str, len und init. Grundsätzlich werden die magic methods als Teil von Klassen definiert und ihre Namen haben die Struktur __name__. Die "Magie" dieser Methoden besteht darin, dass man sie nicht explizit aufruft. Stattdessen werden Sie von Python direkt aufgerufen. Oben haben wir zum Beispiel schon gelernt, das __init__ aufgerufen wird, wenn eine neue Instanz einer Klasse erstellt wird. Dies geschieht automatisch bei der Objekterstellung. Die Alternative wäre, dass man erst ein Objekt erstellen muss, und dann __init__ explizit aufrufen müsste. Wichtige magic methods sind:
Das folgende Beispiel zeigt eine Variante der Person Klasse, die zeigt wie man Magic Methods benutzt.
Personenklasse without magic methods
>>> john_smith = Person("John Smith") >>> john_smith2 = Person("John Smith") >>> print(john_smith) <__main__.Person object at 0x7f004c769f28> >>> print(repr(john_smith)) <__main__.Person object at 0x7f004c769f28> >>> print(john_smith==john_smith2) False Personenklasse with magic methods
>>> jane_doe = MagicPerson("Jane Doe") >>> jane_doe2 = MagicPerson("Jane Doe", age=42) >>> print(jane_doe) Jane Doe >>> print(jane_doe2) Jane Doe >>> print(repr(jane_doe)) Person(name=Jane Doe, age=unknown) >>> print(repr(jane_doe2)) Person(name=Jane Doe, age=42) >>> print(jane_doe==jane_doe2) True >>> del jane_doe Tell my family that I loved them Weiteres zur ObjektorientierungDiese kurze Einführung in die Welt der Klassen berührt nur die Oberfläche der objektorientierten Programmierung, ist aber für diesen Kurs ausreichend und deckt auch die meisten für Python relevanten Aspekte ab. Wenn man sich sehr für Programmierung interessiert und später auch selbst Software entwickeln möchte, ist es sinnvoll sich weiter mit Objektorientierung zu befassen. Themen wie Entwurfsmuster für objektorientierte Sprachen, Unterschiede zwischen Interfaces und Klassen, und Mehrfachvererbung füllen ganze Bücher.
|