OOP in Python#

Python ist eine objektorientierte Programmiersprache. Dennoch haben wir es bis zu diesem Punkt geschafft, uns nicht wirklich tiefergehend mit dem Thema objektorientiertes Programmieren auseinandersetzen zu müssen. Unser Programmierstil war nicht wirklich objektorientiert. Wir haben zwar mit Objekten herumhantiert (in Python ist eigentlich alles ein Objekt) und die Methoden bereits vorhandener Objekt verwendet, wir selbst haben unsere eigenen Programmteile aber nicht in Objekten organisiert, also z.B. keine eigenen Objekte bzw. Objekt-Klassen erstellt. In diesem Kapitel wenden wir uns nun der objektorientierten Programmierung und der Definition eigener Objekt-Klassen zu, weil damit agentenbasiert Modelle noch einmal deutlich natürlicher, eleganter und effizienter umgesetzt werden können. Auch viele Python-Pakete setzen Grundkenntnisse zur objektorientierten Programmierung voraus.

Übrigens

Wir haben bisher all unsere agentenbasierten Modelle mehr oder weniger from scratch programmiert d.h. wir haben ohne die Hinzunahme spezieller ABM-Pakete gearbeitet. Natürlich gibt es aber auch in Python spezielle Pakete, deren Sinn es ist, die Entwicklung und Auswertung von ABM zu vereinfachen und zu vereinheitlichen. Zu nennen sind da z.B. die Pakete Mesa, AgentPy oder defSim. Für alle Pakete gilt jedoch, dass man einerseits grob verstehen muss, wie wie ABM als Computercode umgesetzt werden können - was also hinter den Kulissen geschieht. Andererseits muss man in den Grundlagen der Programmierung mit Python und v.a. in der objektorientierten Programmierung sattelfest sein. Nachdem du diese nun kommenden, letzten Kapitel dieses Kurses durchgearbeitet hast, stehen dir die Türen offen, um solche Pakete bei Bedarf zu nutzen.

Was ist denn eigentlich ein (Software-)Objekt? Ein Objekt ist eine abgeschlossene Einheit, die sowohl Daten als auch bestimmte Funktionen (Methoden) enthält. Vielleicht erinnerst du dich noch an das erste Kapitel dieses Lehrskriptes, in dem ich erklärt habe, dass jedes Programm letztlich aus irgendwelchen Daten und irgendwelchen Funktionen, die irgendwas mit den Daten machen, besteht. Ein Objekt vereint nun diese beiden grundlegenden Elemente eines Programms innerhalb einer Einheit.

Objektorientierte Programmierung macht vor allem Sinn, weil dieser Programmierstil dem menschlichen Denken über die Wirklichkeit entspricht. Ein Mensch sieht die Welt als Ansammlung verschiedenster, voneinander abgrenzbarer Objekte: Tische, Menschen, Autos, Computer, Atome, Vögel, Universitäten und so weiter. Ob die Welt wirklich so ist, ob sie also wirklich aus diesen Objekten besteht oder ob diese Objekte allein ein Konstrukt unserer Wahrnehmung sind, ist eine andere Frage. Dennoch ist Wahrnehmung des Menschen ohne Frage durch die Unterscheidung verschiedenster Einheiten bzw. Objekte von einer Umwelt anderer Objekte strukturiert. Wir schreiben den Objekten dabei bestimmte Eigenschaften zu. Diese Eigenschaften können eher “passive” Zustände der Objekte, aber auch eher “aktive” Dynamiken, ein “Objektverhalten” beschreiben. Die Eigenschaften und Fähigkeiten eines Objektes sind dabei Folge der Beschaffenheit des Objektes. Sie sind Folge der internen Struktur, des internen Funktionierens, der internen Organisation der Bestandteile, der internen Mechanismen des Objektes. Was ein Objekt kann und was es nicht kann, wie es sich verhält, welche Zustände und welche Fähigkeiten es aufweist und aufweisen kann, wird durch die interne Struktur ermöglicht und zugelassen. Dabei besteht ein Objekt im Inneren in der Regel aus weiteren Objekten. Die Untereinheiten einer Einheit sind selbst Objekte, egal wie tief wir in die innerste Struktur der Welt hinabsteigen. Ein Objekt steht typischerweise in Interaktion mit dessen Umwelt, kann von dieser beeinflusst werden und kann selbst auf diese wirken. Wie diese Interaktion stattfindet und wie das Objekt durch die Umwelt beeinflusst werden kann, wird ebenfalls durch das Objekt bzw. die Beschaffenheit des Objektes (sowie der Umwelt, die aber ja ebenfalls aus Objekten besteht) bestimmt.

Ein konkretes Objekt gehört für uns in der Regel zu einer ganzen Klasse von Objekten, die eine allgemeine Struktur, die Organisation ihrer Bestandteile, teilen, aber dennoch alle etwas andere Attribute aufweisen. Ein Esstisch, ein Schreibtisch und ein Tischtennistisch gehören für uns alle zu der Objekt-Klasse der Tische. Ein Adler, ein Spatz und eine Meise gehören für uns zur Klasse der Vögel. Angela, Elena und Tobi gehören für uns zur Klasse der Menschen. Jedes existierende Objekt ist also eine konkrete Verkörperung, eine Instanz einer allgemeinen Klasse bestimmter Objekten. Klassen können unterschiedlich weitgefasst und ein unterschiedliches Level an Abstraktion aufweisen. Angela, Elena und Tobi gehören nicht nur zur Klasse der Menschen, sie gehören z.B. auch zur etwas allgemeineren Klasse der Menschenaffen und zur noch allgemeineren Klasse der Säugetiere. Natürlich gehören sie auch zur noch abstrakteren Klasse der lebenden Organismen und letztlich auch zur abstraktesten aller Klassen: Materie. Dabei kann man Klassen meist hierarchisch von umfassenderen, abstrakteren Klassen zu engeren, konkreteren Klassen ordnen: alle Instanzen der unteren, konkreteren Klasse weisen in der Regel alle Eigenschaften der oberen Klasse auf, aber die obere, allgemeineren Klasse weist nicht alle Eigenschaften der Instanzen der unteren Klasse auf.

Mittels objektorientierter Programmierung können wir genau ein solches Denken über Objekte in unserem Programm nachbilden. Wir definieren Klassen von Objekten, die gewisse Eigenschaften aufweisen, bestimmte Dinge tun oder nicht tun können, indem wir die interne Struktur der Objekte, die Daten und Funktionen aus denen das Objekt besteht, bestimmen. Mithilfe einer Klassen können wir dann prinzipiell unendlich viele, konkrete Objekte d.h. Instanzen dieser Klasse, erstellen. Jedes Objekt ist in sich geschlossen, hat eine durch die Klasse festgelegte, interne “Funktionsweise” und gibt vor, an welchen Stellen es wie mit der Umwelt interagieren kann. Zudem können wir allgemeine Klassen definieren und dann von dieser allgemeinen Klasse etwas spezifischere Unterklassen erstellen. Wenn wir objektortientiert programmieren, dann sollten wir uns zu Beginn immer fragen, aus welchen Bestandteilen d.h. Objekten unser Programm besteht. Wir können uns dann “bildlich” vorstellen, dass unser Programm wirklich aus “handelnden” Objekten, die irgendetwas tun, miteinander interagieren und jeweils bestimmte Aufgabenbereiche haben, bestehen. Dann müssen wir uns Fragen, wie ein Objekt aufgebaut sein muss, um diese und jene Fähigkeit und Eigenschaft aufzuweisen.

Für agentenbasierte Modellierung eignet sich ein objektorientiert Programmierstil optimal. Unsere Agenten repräsentieren in der Regel in stark vereinfachter Form handelnde Menschen mit bestimmten Eigenschaften und Fähigkeiten. Durch objektorientierte Programmierung können wir nun einen Agenten auch im Programm als eine eigene Einheit, in der die Daten und Funktionen der Agenten zusammen kommen, konzipieren. Wir können die Handlungsregeln der Agenten nun wirklich als Eigenschaften dieser Agenten im Programmcode umsetzen. Ist ein Agent konzipiert, können wir unendlich viele, von der Struktur her gleichartige Instanzen dieses Agenten-Typs erstellen und handeln lassen.

Eine allgemeine Erklärung von objektorientierter Programmierung als Programmierkonzept - unabhängig von ABM - findest du auch in diesem Video:

from IPython.display import YouTubeVideo
YouTubeVideo("2le2YYr3N7s", width = 600, height = 400)

Klassen & Instanzen#

Am Anfang ist es wichtig zu verstehen, was eine Objekt-Klasse und was eine Objekt-Instanz ist.

  • Eine Objekt-Klasse beschreibt die allgemeine Struktur, den allgemeinen Bauplan eines Objektes - und zwar nicht nur von einem ganz bestimmten Objekt, sondern prinzipiell von allen Objekten desselben Typs bzw. derselben Klasse. In der Definition der Objekt-Klasse legen wir allgemein fest, über welche Attribute (Daten, “passive” Eigenschaften) und Methoden (Funktionen, “aktive” Eigenschaften) ein Objekt dieser Art verfügt. Mit der Definition der Klasse wird selbst noch kein Objekt erstellt.

  • Eine Objekt-Instanz ist ein Objekt, das nach den Bauplänen einer Objekt-Klasse erstellt wurde. Auf Basis einer einzigen Objekt-Klasse können prinzipiell unendlich viele Objekt-Instanzen kreiert werden. Jede Objekt-Instanz hat eine eigene Identität, ist ein Objekt für sich.

Eine Objekt-Klasse definieren#

Die Definition einer Klasse wird in Python mit dem Schlüsselwort class eingeleitet. In derselben Zeile folgt ein (fast) freiwählbarer Name für diese Klasse und schließlich ein Doppelpunkt. Die allgemein Form der “Kopfzeile” einer Klassendefinition lautet also:

class CLASS_NAME:

Konkret könnte ich beispielsweise die Klasse Agent erstellen wollen:

class Agent:

Übrigens

Bei Variablen- und Funktionsnamen gilt in Python die Konvention, kleingeschriebene Buchstaben sowie Unterstriche zur Verbindung mehrerer Wörter zu nutzen. Bei der Vergabe von Klassen-Namen ist es jedoch Konvention, eine sogenannte Binnenmajuskel bzw. camel case zu verwenden. Jedes Wort im Namen einer Objekt-Klasse beginnt somit mit einem Großbuchstaben und alle Wörter werden ohne Leerzeichen direkt aneinander gereiht.*

Nach der einleitenden Kopfzeile eines Objektes folgt die Definition der Objekt-Methoden. Als Methoden bezeichnet man die Funktionen, die einer Objekt-Klasse bzw. einer existierenden Instanz inhärent sind. Eine Methode definiert man bis auf zwei kleine Ausnahmen genau wie eine normale Funktion. Der Unterschied zur normalen Funktion bei der Definition einer Methode ist:

  1. Eine Methode wird innerhalb einer Klasse definiert d.h. auf dem entsprechenden Einrücklevel.

  2. Der erste Input einer Methode ist immer ein Verweis auf das Objekt, das die Methode ausführt, - und somit ein Verweis des Objekts auf sich selbst. Dieser erste Paramater wird typischerweise mit self bezeichnet. Der Parameter self dient dazu, dass das “handelnde” Objekt innerhalb der Methode auf sich selbst zugreifen kann, also “Handlungen” auf sich selbst beziehen kann. Wir werden uns später dieses self noch genauer anschauen. Zu diesem Zeitpunkt musst du einfach als gegeben hinnehmen, dass bei der Definition von Methoden der erste Parameter IMMER den Namen self trägt. Nach dem obligatorischen Parameter self können, wie bei normalen Funktionen, weitere Parameter folgen.

Die allgemeine Syntax kann man so aufschreiben:

class CLASS_NAME:
    
    def METHOD_NAME1(self, PARAMETER2, PARAMETER3, ...):
        DO_SOMETHING
    
    def METHOD_NAME2(self):
        DO_SOMETHING
        

Tatsächlich ist das auch schon die allgemeine Form, wie man Objekte in Python definiert. Man definiert ausschließlich Objekt-Methoden. Möchte man bei der Definition einer Klasse Objet-Attribute festlegen, so macht man das ebenfalls innerhalb einer Methode, jedoch innerhalb einer ganz bestimmten Methode, die wir uns weiter unten genauer anschauen werden. Zunächst erstelle ich nun aber ein Objekt ohne Attribute.

Unten definiere ich eine Klasse namens Agent, welche über eine Methode namens say_hello_world() verfügt. Diese macht nichts anderes als den allseits beliebten String "hello world" zu printen.

# Klassendefinition einleiten
class Agent:
    
    # eine Methode für diese Klasse definieren
    def say_hello_world(self):
        print("hello world")

Instanzen erstellen#

Wir haben jetzt zwar eine Klasse definiert, aber wir haben dadurch noch kein existierendes Objekt, noch keine Instanz dieser Klasse erstellt. Um eine Instanz einer Klasse zu erstellen, muss man den Konstruktor einer Klasse ausführen. Das bedeutet nichts anderes als den Namen der Objekt-Klasse zusammen mit zwei runden Klammern - wie bei dem Ausführen einer Funktion - aufzurufen. Allgemein formuliert schreibt man also einfach CLASS_NAME(), um eine Instanz einer Klasse zu erstellen. Die so erstellte Instanz der Klasse kann man dann z.B. einer Variable zuordnen oder direkt in eine Liste einfügen. Unten erstelle ich eine Instanz der Klasse Agent und speichere diese Instanz unter der Variable agent1 ab:

agent1 = Agent()

Unter der Variable agent1 verbirgt sich nun ein Objekt des Typs bzw. eine Instanz der Klasse Agent. Das können wir überprüfen, indem wir die Funktion type() auf das Objekt agent1 anwenden:

type(agent1)
__main__.Agent

(Lass dich nicht von dem __main__ vor der Klassenbezeichnung verwirren. Das __main__ bezieht sich darauf, dass die Klasse Agent Teil des gerade ausgeführten Skripts ist. Jedes Skript, das ausgeführt wird, nennt sich selbst __main__.)

Wir können auch eine weitere Instanz der Klasse Agent erstellen und diese unter einem anderen Variablennamen abspeichern:

agent2 = Agent()

Auch das Objekt agent2 ist eine Instanz der Klasse Agent:

type(agent1)
__main__.Agent

Die beiden Objekte agent1 und agent2 sind beide Instanzen der Klasse Agent, sind aber dennoch unterschiedliche, voneinander unabhängig existierende, abgeschlossene Objekte - so wie zwei Menschen eben beide der Kategorie Mensch angehören, nach dem selben Bauplan gebaut wurden, aber eben dennoch zwei unterschiedliche Objekte sind. Für agent1 und agent2 können wir überprüfen, ob es sich um unterschiedliche Objekte handelt, indem wir Python danach fragen, ob die beiden Objekte gleich sind:

agent1 == agent2
False

Prinzipiell könnten wir nun unendlich viele Instanzen der Klasse Agent erstellen. Statt jedes Objekt in einer eigenen Variable zu speichern, können wir - und so ist es v.a. bei der Programmierung von ABM typisch - viele Instanzen einer Klasse in einer Liste - unserer typischen Populationsliste - speichern. Unten befülle ich innerhalb eines For-Loops eine Liste population mit 10 Instanzen der Klasse Agent:

population = []
for i in range(10):
    agent = Agent()
    population.append(agent)

Wenn wir uns nun die Liste anschauen, dann sehen wir, dass sich in dieser Liste 10 unterschiedliche Instanzen der Klasse Agent befinden.

population
[<__main__.Agent at 0x1c2c4e33508>,
 <__main__.Agent at 0x1c2c4e33588>,
 <__main__.Agent at 0x1c2c4e335c8>,
 <__main__.Agent at 0x1c2c4e33608>,
 <__main__.Agent at 0x1c2c4e33648>,
 <__main__.Agent at 0x1c2c4e336c8>,
 <__main__.Agent at 0x1c2c4e33708>,
 <__main__.Agent at 0x1c2c4e33748>,
 <__main__.Agent at 0x1c2c4e33788>,
 <__main__.Agent at 0x1c2c4e33688>]

Methoden ausführen#

Oben hatten wir unserer Klasse Agent bereits eine Methode namens say_hello_world() zugewiesen. Wir können diese aufrufen, indem wir den Namen der Methode mit einem Punkt an eine entsprechende Instanz der Klasse hängen. Allgemein sieht das so aus:

INSTANCE.METHOD()

Doch was ist mit dem obligatorischen ersten Parameter self, der ja in jede Methode bei der ihrer Definition eingefügt wird? Der Parameter self wird beim Aufruf der Methode nicht explizit angeben. Der Parameter self verweist ja auf das Objekt selbst, wodurch das Objekt sich selbst in die Methode einfügt. Da wir aber Methoden sowieso immer von einem bestimmten Objekt aus aufrufen, wird der Parameter self quasi automatisch im Hintergrund in die Methode eingegeben. Der Parameter self muss also immer nur bei der Definition der Methode als Parameter angegeben werden, beim Aufruf der Methode wird dieser nicht explizit angegeben, weil er automatisch der Methode übergeben wird.

Haben wir bei einer Methode neben dem standardmäßigem Parameter self noch einen weiteren Parameter, dann müssten diese jedoch beim Aufruf der Methode explizit eingegeben werden. Der “zweite” Parameter bei der Definition der Methode wird somit zum “ersten”, beim Aufruf der Methode einzugebenden Parameter.

Unten lasse ich das Objekt agent1 die Methode say_hello_world() ausführen. Da neben dem standardmäßigem Parameter self kein weiterer Parameter erwartet wird, bleiben die runden Klammern der Methode leer:

agent1.say_hello_world()
hello world

Die Instanz agent1 hat nun “gehandelt” und die Funktion say_hello_world() ausgeführt. Das gleiche kann natürlich auch agent2 sowie jede weitere Instanz der Klasse Agent:

agent2.say_hello_world()
hello world

Und natürlich können auch all unsere “namenlosen” Agenten in der Liste population diese Methode ausführen:

for agent in population:
    agent.say_hello_world()
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world
hello world

Der obige Loop ist bei der Programmierung von ABM ein ganz typischer Loop. Wir gehen die Populationsliste durch und bringen alle Agenten nacheinander zum handeln. Natürlich ist auch hier der Name der Schleifenvariable willkürlich gewählt. Man kann statt agent auch element oder sonst was schreiben - wichtig ist zu verstehen, dass sich in jeder Runde hinter dem Namen der Schleifenvariable eine (andere) Instanz der Klasse Agent befindet.

Instanz-Attribute & die Methode _init_()#

Okay, wir haben nun zwar handelnde Objekte, aber einen wirklichen Vorteil hat uns das Ganze noch nicht gebracht. Das liegt daran, dass unsere Klasse Agent zwar über Methoden, aber über gar keine Attribute d.h. Daten verfügt. Die wirkliche Stärke von Objekten ist ja aber gerade die Vereinigung von Daten und Funktionen bzw. Attributen und Methoden innerhalb einer abgeschlossenen Einheit.

Prinzipiell kann man jedem Objekt, wenn es bereits existiert, nachträglich Attribute hinzufügen und/oder bereits existierende Attribute verändern. Dies machen wir, indem wir einen Attribut-Namen mit einem Punkt an den Instanz-Namen anhängen und diesem einen Wert zuweisen:

INSTANCE.ATTRIBUTE = VALUE

Existiert das Attribut bei dieser Instanz bereits, wird der Wert des Attributs überschrieben. Existiert das Attribut noch nicht, dann wird es der Instanz mit dem entsprechenden Wert hinzugefügt.

Unten weise ich der bereits existierenden Agent-Instanz namens agent1 nachträglich das Attribut opinion mit dem Wert "weiß nicht" hinzu:

agent1.opinion = "weiß nicht"

Nun rufe ich das Attribut opinion von agent1 auf:

agent1.opinion
'weiß nicht'

Die allgemeine Syntax für den Zugriff auf das Attribut einer Instanz sieht damit so aus:

INSTANCE.ATTRIBUTE

Okay, wir haben eben einer bereits existierenden Instanz nachträglich ein Attribut hinzugefügt. Das kann man so machen, ist aber in den meisten Fällen nicht besondern sinnvoll. Denn nun verfügt die Instanz agent1 über das Attribut opinion, die Instanz agent2 verfügt aber nicht über dieses Attribut, wie wir hier feststellen können:

agent2.opinion
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-15-541b3739163f> in <module>
----> 1 agent2.opinion

AttributeError: 'Agent' object has no attribute 'opinion'

Statt bereits existierenden Instanzen nachträglich Attribute hinzuzufügen, können und sollten wir die Attribute (wenn es möglich ist) bereits bei der Definition der Klasse festlegen, wodurch alle erstellten Instanzen über dieselben Attribute verfügen.

Das Hinzufügen der Attribute passiert bei der Definition einer Klasse an einer ganz bestimmten Stelle: innerhalb der Methode __init__(). Ja du siehst richtig: Zwei Unterstriche, dann ein “init”, dann wieder zwei Unterstriche und dann die typischen runden Klammern. Und ja, der Name ist so festgelegt! Die Methode __init__() ist eine besondere Methode, denn sie hat nicht nur diesen wunderschönen Namen, sondern wird - und das ist das wichtige - bei der Erstellung einer Instanz automatisch im Hintergrund ausgeführt. Die Methode __init__() ist dadurch wesentlicher Bestandteil bei der Initialisierung eines Objektes bzw. einer Instanz. Innerhalb der Methode __init__() können wir dann direkt bei der Erstellung der Instanzen allen die gewünschten Attribute hinzufügen.

Die Methode __init__() erwartet, wie jede andere Methode auch, als ersten Parameter ein self und somit das Objekt selbst. Da sich hinter dem self das Objekt selbst verbirgt, können wir innerhalb der Methode __init__() diesem self und somit dem Objekt selbst Attribute zuweisen. Genau genommen weist das Objekt, da es ja selbst die Methode __init__() ausführt, somit sich selbst Attribute zu. Klingt alles kompliziert, aber die Beispiele unten werden es klarer machen.

Unten erstelle ich wieder die Klasse Agent. Als erste Methode definiere ich jedoch die Methode __init__(), übergebe ihr den obligatorischen Parameter self - das Objekt selbst - und weise dann innerhalb der Methode __init__() dem Objekt self das Attribut opinion zu.

class Agent:
    def __init__(self):
        self.opinion = 5

Jede Instanz, die von der Klasse Agent erstellt wird, weist sich somit selbst bei ihrer Erstellung das Attribut opinion mit dem Wert 5 zu. Wir brauchen aber eigentlich gar nicht so kompliziert denken und können uns zunächst einfach darauf einigen, dass wir eben bei der Definition einer Klasse im Bereich der Methode __init__() alle Attribute angeben, über die die Instanzen verfügen sollen. Oben ist es eben das Attribut opinion mit dem Wert 5.

Unten erstelle ich eine Instanz der Klasse Agent und speichere diese in der Variable agent1:

agent1 = Agent()

Nun lasse ich mir das Attribut opinion von agent1, eine Instanz der Klasse Agent, ausgeben:

agent1.opinion
5

Und auch alle weiteren Instanzen, die mit einer solchen Klassen-Definition erstellt werden, verfügen nun über dasselbe Attribut mit demselben Wert:

agent2 = Agent()
agent2.opinion
5

Und natürlich kann man nicht nur ein Attribut, sonder prinzipiell unendlich viele Attribute innerhalb der Methode __init__() zuweisen. Unten weise ich den Instanzen der Klasse Agent innerhalb der Methode __init__() noch zwei weitere Attribute zu:

class Agent:
    def __init__(self):
        self.opinion = 5
        self.tolerance = 2
        self.friends = []
agent1 = Agent()
agent1.opinion
5
agent1.tolerance
2
agent1.friends
[]

Übrigens

Die Attribute, die wir konkreten Instanzen zuweisen, wie wir es z.B. in der Methode __init__() tun, nennt man auch Instanz-Attribute. Instanz-Attribute sind diejenigen Attribute einer Instanz, deren Werte unabhängig von den Werten anderer Instanzen sind. Es kann sich also der Wert eines Instanz-Attributes einer bestimmten Instanz ändern, ohne dass sich der Wert bei anderen Instanzen ändert. Neben den Instanz-Attributen gibt es nämlich noch die sogenannten Klassen-Attribute. Klassen-Attribute sind über die gesamte Klasse gleich d.h. jede Instanz der Klasse hat den gleichen Wert auf diesem Attribut. Ändert man ein Klassen-Attribut, so ändert sich der Wert bei allen Instanzen dieser Klasse. Klassen-Attribute brauchen wir hier aber erstmal nicht, können aber sehr nützlich sein.

Nun, haben wir einen Bauplan für Objekte des Typs Agent und können prinzipiell unendlich viele Objekte dieser Art erstellen. Doch nach unserem bisherigen Bauplan wären dann tatsächlich alle Instanzen der Klasse Agent exakt gleich. Die Objekte verfügen nicht nur über dieselben Attribute, sondern weisen auch alle dieselben Ausprägungen bzw. Werte der Attribute auf. Cool wäre es aber, wenn wir Objekte mit derselben abstrakten “Struktur” d.h. mit denselben Attributen, aber mit unterschiedlichen Ausprägungen auf diesen Attributen erstellen könnten. So wie jeder Mensch beispielsweise das Attribut “Körpergröße” hat, aber nicht alle Menschen gleich groß sind.

Eine Möglichkeit Objekte zu individualisieren, ist bei ihrer Erstellung die Werte ihrer Attribute individuell festzulegen. Dies können wir machen, indem wir die Attribut-Werte per Parameter übergeben. Wir können der Methode __init__() neben dem standardmäßigem Parameter self noch weitere Parameter übergeben. Die so eingegebenen Werte können wir dann den Attributen zuweisen. Später, wenn wir Instanzen des Objekts erstellen, können wir die Parameter der Methode __init__() dem Objekt übergeben.

Unten erstelle ich wieder die Klasse Agent, lege aber den Wert des Attributs opinion nicht konkret fest, sondern weise diesem den Parameter initial_opinion zu.

class Agent:
    def __init__(self, initial_opinion):
        self.opinion = initial_opinion
        self.tolerance = 2
        self.friends = []

Der Wert des Attributs opinion ist nun also nicht durch die Klassendefinition direkt festgelegt, sondern kann bei der Erstellung einer Instanz individuell über den Parameter initial_opinion festgelegt werden. Beim Erstellen einer Instanz der Klasse Agent wird nun erwartet, dass in die runden Klammern der Instanz ein Wert für den Parameter initial_opinion eingegeben wird. Die runden Klammern bei der Erstellung einer Klassen-Instanz führen also genaugenommen zur Methode __init__(). Dass der Parameter initial_opinion nun erwartet wird, kann man nun u.a. an der untigen Fehlermeldung ablesen, die erscheint, wenn man nun versucht eine Instanz der Klasse Agent zu erstellen, ohne den Parameter initial_opinion zu übergeben.

agent1 = Agent()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-26-19ee5eec049e> in <module>
----> 1 agent1 = Agent()

TypeError: __init__() missing 1 required positional argument: 'initial_opinion'

Unten erstelle ich eine Instanz der Klasse Agent und füge den Wert 3 als Argument für den Parameter initial_opinion ein:

agent1 = Agent(3)

Und wir können sehen, dass das Objekt agent1 nun auf dem Attribut opinion den Wert 3 aufweist.

agent1.opinion
3

Sowohl die Namen der Attribute als auch die Namen der Parameter sind wieder komplett willkürlich. In der Praxis sieht man es übrigens oft, dass Instanz-Attribut und einzugebender Parameter den gleichen Namen aufweisen, so wie unten:

class Agent:
    def __init__(self, opinion):
        self.opinion = opinion
        self.tolerance = 2
        self.friends = []

Auch die Erstellung von Instanzen, bei denen wir bestimmte Parameter bei der Erstellung übergeben müssen, können wir natürlich auch wieder per For-Loop vollziehen. Bei agentenbasierter Modellierung erstellen wir in der Regel keine Agenten einzeln “per Hand”. Stattdessen erstellen wir meist große Populationen von Agenten in Massenproduktion.

Zu Beginn einer Simulation haben wir daher nicht selten das untige Schema: In einem For-Loop erstellen wir in jeder Runde Instanzen der entsprechenden Klasse, legen für jede Instanz einen bestimmten Wert für die zu übergebenden Paramter fest, geben die Werte ein und hängen das Objekt, den Agenten, dann wie gewohnt an eine “Populationsliste”. Wie sich die Werte für die Parameter ergeben, kommt auf die Simulation drauf an, manchmal übergeben wir auch allen Agenten dieselben Werte. Unten ergeben sich die Werte für den Parameter opinion einfach aus dem Wert der Schleifenvariable i.

# leere Populationsliste erstellen
population = []

# 10 mal
for i in range(10):

    # Agenten-Instanz erstellen und Meinung als Parameter eingeben
    agent = Agent(i)
    
    # Agenten-Instanz an Population anhängen
    population.append(agent)

Unten sehen wir nun den Inhalt der Liste population:

population
[<__main__.Agent at 0x1c2c4ed2d48>,
 <__main__.Agent at 0x1c2c4ed2dc8>,
 <__main__.Agent at 0x1c2c4ed2e08>,
 <__main__.Agent at 0x1c2c4ed2e48>,
 <__main__.Agent at 0x1c2c4ed2e88>,
 <__main__.Agent at 0x1c2c4ed2f08>,
 <__main__.Agent at 0x1c2c4ed2f48>,
 <__main__.Agent at 0x1c2c4ed2f88>,
 <__main__.Agent at 0x1c2c4ed2fc8>,
 <__main__.Agent at 0x1c2c4ed2ec8>]

Unten iteriere ich nun direkt durch die Agenten und greife für jeden Agenten auf dessen Attribut opinion zu und printe es in die Konsole:

for agent in population:
    print(agent.opinion)
0
1
2
3
4
5
6
7
8
9

Zwar ist es sinnvoll, die Attribute einer Klasse direkt bei der Definition der Klasse festzulegen. Oft verändern sich aber die Werte der Attribute im Laufe eines Lebens eines Agenten. Wir hatten weiter oben schon gesehen, wie wir auf die Attribute einer Instanz zugreifen können und die Werte ändern können.

Haben wir die Instanzen in einer Liste, dann können wie die Attribute der Instanzen auch direkt innerhalb eines For-Loops ändern, da unsere selbstdefinierten Objekte veränderbare Objekte sind. Unten iteriere ich direkt durch die Agenten und verändere unmittelbar und dauerhaft die Attribute der Agenten:

import random

for agent in population:
    agent.opinion = random.randint(0,9)

Schauen wir, ob die Veränderung der Agenten-Attribute funktioniert hat:

for agent in population:
    print(agent.opinion)
4
9
8
7
4
5
6
9
8
7

Methoden als Funktionen aus der Perspektive der Objekte#

Weiter oben hatten wir der Klasse Agent mit der Methode say_hello_world() bereits eine “Fähigkeit” gegeben. Diese Fähigkeit war jedoch nicht besonders individuell, weil die persönlichen Attribute eines Agenten nicht in die Methode einbezogen wurden. Im Folgenden zeige ich, wie Objekte innerhalb ihrer Methoden auf sich selbst und auch auf andere Instanzen zugreifen können.

Instanzen können innerhalb ihrer Methoden auf sich selbst bzw. auf ihre eigenen Attribute zugreifen, indem sie sich auf den in jeder Methode vorhandenen Parameter self beziehen. Aus der Sicht einer Instanz ist die Instanz selbst immer unter self zu finden. Hilfreich ist es, sich bei der Definition einer Klasse in die Klasse bzw. eine Instanz dieser Klasse hineinzuversetzen und diese aus der Sicht dieser Instanz zu programmieren. Alles was man selbst ist oder hat, findet man unter dem Parameter self. Das bin ich, die Instanz.

Unten definiere ich eine Klasse Agent, die über ein Attribut favourite_food, das bei der Erstellung per Parameter festgelegt wird, und die Methode order_favourite_food() verfügt. In der Methode order_favourite_food() greift der Agent auf sich selbst bzw. auf sein eigenes Attribut favourite_food zu, indem er dieses vom Objekt self, was er selbst ist, mittels des Ausdrucks self.favourite_food abruft.

class Agent():
    def __init__(self, favourite_food):
        self.favourite_food = favourite_food
    
    def order_favourite_food(self):
        print("Hallo, ich will", self.favourite_food, "!")

Unten erstelle ich eine Instanz der Klasse Agent und weise dem Attribut favourite_food den Wert "Sushi" zu.

agent1 = Agent("Sushi")

Jetzt lasse ich die Instanz die Methode order_favourite_food() ausführen, in welcher diese Instanz auf ihr eigenes Attribut, wie oben definiert, zugreift.

agent1.order_favourite_food()
Hallo, ich will Sushi !

Unten das gleiche nochmal mit einer weiteren Instanz:

agent2 = Agent("Pizza")
agent2.order_favourite_food()
Hallo, ich will Pizza !

Und hier nochmal mit “namenlosen” Agenten in einer Populationsliste:

population = []
for i in range(10):
    favourite_food = random.choice(["Pizza", "Sushi", "Döner", "Maultaschen"])
    agent = Agent(favourite_food)
    population.append(agent)
for agent in population:
    agent.order_favourite_food()
Hallo, ich will Döner !
Hallo, ich will Maultaschen !
Hallo, ich will Döner !
Hallo, ich will Pizza !
Hallo, ich will Döner !
Hallo, ich will Döner !
Hallo, ich will Döner !
Hallo, ich will Pizza !
Hallo, ich will Pizza !
Hallo, ich will Pizza !

Ich erweitere die Klasse nun um eine Methode evaluate_other_agent(), bei welcher eine Instanz der Klasse Agent als Parameter übergeben werden soll. Der Agent greift innerhalb dieser Methode auf sein eigenes Attribut favourite_food sowie auf das Attribut des eingegebenen Agenten zu und gleicht diese ab.

class Agent:
    def __init__(self, favourite_food):
        self.favourite_food = favourite_food
    
    def order_favourite_food(self):
        print("Hallo, ich will", self.favourite_food, "!")
    
    def evaluate_other_agent(self, agent):
        # evaluieren, ob das Lieblingsessen des eingegebenen Agenten gleich meinem Lieblingsessen ist
        if agent.favourite_food == self.favourite_food: 
            print("Sympathisch!!!")
        else:
            print("Unsympathisch!!!")

Ich erstelle drei Instanzen dieser Klasse:

agent1 = Agent("Döner")
agent2 = Agent("Döner")
agent3 = Agent("Pizza")

Die Instanz agent1 lasse ich nun die Methode evaluate_other_agent() ausführen, wobei ich unterschiedliche Instanzen der Klasse Agent als Argument in die Methode eingebe:

agent1.evaluate_other_agent(agent2)
Sympathisch!!!
agent1.evaluate_other_agent(agent3)
Unsympathisch!!!

Und natürlich kann dieser “andere” Agent auch wieder man selbst sein:

agent1.evaluate_other_agent(agent1)
Sympathisch!!!