Seit Ver­öf­fent­li­chung von Version 3 setzt Python voll auf ob­jekt­ori­en­tier­te Pro­gram­mie­rung (OOP). Die Sprache folgt der Design-Phi­lo­so­phie „ever­y­thing is an object“, zu Deutsch „jedes Ding ist ein Objekt.“

Anders als in Java, C++ und Python 2.x findet keine Un­ter­schei­dung zwischen pri­mi­ti­ven Typen und Objekten statt. Zahlen, Strings und Listen, sogar Funk­tio­nen und Klassen sind in Python allesamt Objekte.

Im Vergleich mit anderen Sprachen zeichnet sich Pythons klas­sen­ba­sier­te OOP durch hohe Fle­xi­bi­li­tät und wenige feste Be­schrän­kun­gen aus. Damit steht die Sprache als extremes Ge­gen­bei­spiel zu Java, deren OOP-System als aus­ge­spro­chen rigide gilt. Wir erklären an­schau­lich, wie in Python ob­jekt­ori­en­tier­te Pro­gram­mie­rung funk­tio­niert.

Wozu dient in Python die ob­jekt­ori­en­tier­te Pro­gram­mie­rung?

Die ob­jekt­ori­en­tier­te Pro­gram­mie­rung ist eine Form der im­pe­ra­ti­ven Pro­gram­mie­rung. Objekte verbinden Daten und Funk­tio­na­li­tät. Ein Objekt kapselt seinen internen Zustand; Der Zugriff erfolgt über eine öf­fent­li­che Schnitt­stel­le, das so­ge­nann­te Interface des Objekts. Das Interface eines Objekts ist definiert durch seine Methoden. Objekte in­ter­agie­ren mit­ein­an­der über Nach­rich­ten, welche über Aufrufe der Methoden übergeben werden.

Tipp

Lesen Sie zum besseren Ver­ständ­nis des Hin­ter­grunds unsere Artikel „Was ist OOP“, „Pro­gram­mier­pa­ra­dig­men“ und „Python-Tutorial“.

In Python mit OOP Objekte kapseln

Be­trach­ten wie an einem Beispiel, wie sich in Python mit OOP Objekte kapseln lassen. Nehmen wir an, wir schreiben Code für eine Küche, Bar, oder ein Labor. Wir mo­del­lie­ren Behälter, wie Flaschen, Gläser, Tassen, etc. Allesamt Dinge, welche ein Volumen haben und sich befüllen lassen. Wir nennen eine Kategorie von Dingen eine „Klasse“.

Die Objekte, welche Behälter re­prä­sen­tie­ren, haben einen internen Zustand, welcher sich ändern lässt. Behälter lassen sich befüllen, ausleeren, und so weiter. Sofern mit einem Ver­schluss versehen, können wir Behälter öffnen und schließen. Jedoch ist es logisch nicht möglich, das Volumen eines Behälters nach­träg­lich zu verändern. Es ist na­he­lie­gend, ver­schie­de­ne Über­le­gun­gen in Bezug auf den Zustand eines Behälters an­zu­stel­len, z.B.:

  • „Ist das Glas voll?“
  • „Was ist das Volumen der Flasche?“
  • „Hat der Behälter einen Deckel?“

Ferner macht es Sinn, Objekte mit­ein­an­der in­ter­agie­ren zu lassen. Bei­spiels­wei­se sollte es möglich sein, den Inhalt eines Glases in eine Flasche um­zu­fül­len. Schauen wir uns zunächst an, wie die Ver­än­de­rung des internen Zustands eines Objekts in Python mit ob­jekt­ori­en­tier­ter Pro­gram­mie­rung funk­tio­niert. Die dar­ge­stell­ten Än­de­run­gen des Zustands bzw. Fragen über diesen, sind als Methoden-Aufrufe rea­li­siert:

# create an empty cup with given capacity
cup = Container(400)
assert cup.volume() == 400
assert not cup.is_full()
# add some water to the cup
cup.add('Water', 250)
assert cup.volume_filled() == 250
# add more water, filling the cup
cup.add('Water', 150)
assert cup.is_full()

In Python mit OOP Typen de­fi­nie­ren

Da­ten­ty­pen sind ein grund­le­gen­des Konzept in der Pro­gram­mie­rung. Un­ter­schied­li­che Daten lassen sich auf ver­schie­de­ne Art und Weise verwenden; Zahlen werden durch arith­me­ti­sche Ope­ra­tio­nen ver­ar­bei­tet, Zei­chen­ket­ten („Strings“) lassen sich durch­su­chen:

# addition works for two numbers
39 + 3
# we can search for a letter inside a string
'y' in 'Python'

Versuche, eine Zahl und einen String zu addieren, bzw. innerhalb einer Zahl zu suchen brechen mit einem Typfehler ab:

# addition doesn't work for a number and a string
42 + 'a'
# cannot search for a letter inside a number
'y' in 42

Pythons ein­ge­bau­te Typen sind abstrakt; eine Zahl kann alles mögliche re­prä­sen­tie­ren: Distanz, Zeit, Geld. Die Bedeutung des Werts ist nur durch den Va­ria­blen­na­men gegeben:

# are we talking about distance, time?
x = 51

Was jedoch, wenn wir spe­zia­li­sier­te Konzepte mo­del­lie­ren möchten? Auch dieses Ziel erreichen wir in Python mit ob­jekt­ori­en­tier­ter Pro­gram­mie­rung. Objekte sind Da­ten­struk­tu­ren mit iden­ti­fi­zier­ba­rem Typ, welcher sich mit der ein­ge­bau­ten type()-Funktion anzeigen lässt:

# class 'str'
type('Python')
# class 'tuple'
type(('Walter', 'White'))

In Python mit ob­jekt­ori­en­tier­ter Pro­gram­mie­rung Abs­trak­tio­nen er­schaf­fen

In der Pro­gram­mie­rung kommen Abs­trak­tio­nen zum Einsatz, um Kom­ple­xi­tät zu verbergen. Dies erlaubt Pro­gram­mie­ren­den, auf einer höheren Ebene zu operieren. Bei­spiels­hal­ber ist die Frage „ist das Glas voll?“ äqui­va­lent zur Frage „ist das Volumen des Inhaltes des Glases gleich dem Volumen des Glases?“ Die erste, abs­trak­te­re Version ist kürzer und prä­gnan­ter und damit vor­zu­zie­hen. Abs­trak­tio­nen erlauben, kom­ple­xe­re Systeme zu er­schaf­fen und zu über­bli­cken:

# instantiate an empty glass
glass = Container(250)
# add water to the glass
glass.add('Water', 250)
# is the glass full?
assert glass.is_full()
# a longer way to ask the same question
assert glass.volume_filled() == glass.volume()

In Python lassen sich mit OOP abstrakte Konzepte auf neue Ideen über­tra­gen. Ver­an­schau­li­chen wir uns das am Beispiel des Python Additions-Operators. Das Plus­zei­chen verknüpft zwei Zahlen, lässt sich jedoch auch auf Listen anwenden und fügt dann deren Inhalte zusammen:

assert 42 + 9 == 51
assert ['Jack', 'John'] + ['Jim'] == ['Jack', 'John', 'Jim']

Es ist na­he­lie­gend, das Konzept der Addition auf unser Modell zu über­tra­gen. Wir de­fi­nie­ren einen Additions-Operator für Behälter. Dies erlaubt uns, Code zu schreiben, welcher sich fast wie na­tür­li­che Sprache liest. Die Im­ple­men­ta­ti­on erklären wir weiter unten, hier zunächst ein an­schau­li­ches Beispiel der Anwendung:

# pitcher with 1000 ml capacity
pitcher = Container(1000)
# glass with 250 ml capacity
glass = Container(250)
# fill glass with water
glass.fill('Water')
# transfer the content from the glass to the pitcher
pitcher += glass
# pitcher now contains water from glass
assert pitcher.volume_filled() == 250
# glass is empty
assert glass.is_empty()

Wie funk­tio­niert in Python ob­jekt­ori­en­tier­te Pro­gram­mie­rung?

Objekte vereinen Daten und Funk­tio­na­li­tät, welche beide als Attribute be­zeich­net werden. Anders als Java, PHP und C++ bietet Pythons OOP keine Schlüs­sel­wör­ter wie private und protected, um den Zugriff auf Attribute zu be­schrän­ken. Statt­des­sen kommt eine Kon­ven­ti­on zum Einsatz: Mit einem Un­ter­strich be­gin­nen­de Attribute gelten als nicht-öf­fent­lich. Dabei kann es sich um Daten-Attribute nach dem Schema _internal_attr handeln, sowie um Methoden nach dem Schema _internal_method().

Methoden werden in Python mit der Variable self als ersten Parameter definiert. Alle Zugriffe auf Objekt-Attribute vom Innern des Objekts aus erfolgen über eine Referenz auf self. Self fungiert in Python als Platz­hal­ter für eine konkrete Instanz und spielt damit die Rolle, welche das this-Schlüs­sel­wort in Java, PHP, Ja­va­Script und C++ übernimmt.

Zu­sam­men­ge­nom­men mit der zuvor erklärten Kon­ven­ti­on ergibt sich ein einfaches Muster zur Kapselung: Der Zugriff auf ein internes Attribut als Referenz self._internal ist in Ordnung, da dieser im Inneren des Objekts statt­fin­det. Zugriffe von außen im Stil von obj._internal verstoßen gegen die Kapselung und sollten vermieden werden:

class ExampleObject:
    def public_method(self):
        self._internal = 'changed from inside method'
# instantiate object
obj = ExampleObject()
# this is fine
obj.public_method()
assert obj._internal == 'changed from inside method'
# works, but not a good idea
obj._internal = 'changed from outside'

Klassen

Eine Klasse fungiert als Vorlage für Objekte. Man sagt ein Objekt wird aus Klassen in­stan­zi­iert, also der Vorlage ent­spre­chend erzeugt. Der Kon­ven­ti­on folgend beginnen nut­zer­de­fi­nier­te Klas­sen­na­men mit einem Groß­buch­sta­ben.

Anders als in Java, C++, PHP und Ja­va­Script existiert in Python-OOP kein new-Schlüs­sel­wort. Statt­des­sen wird der Klas­sen­na­me als Funktion auf­ge­ru­fen und dient als Kon­struk­tor, welcher eine neue Instanz liefert. Implizit ruft der Kon­struk­tor die In­itia­li­sie­rungs­funk­ti­on __init__() auf, welche Objekt-Daten in­itia­li­siert.

Schauen wir uns die bisher erwähnten Muster an einem Code­bei­spiel an. Wir mo­del­lie­ren das Konzept eines Behälters als Klasse mit Namen Container und de­fi­nie­ren Methoden für wichtige In­ter­ak­tio­nen:

Methode Erklärung
__init__ In­itia­li­siert neuen Behälter mit Start­wer­ten.
__repr__ Gibt Zustand des Behälters als Text aus.
volume Gibt Fas­sungs­ver­mö­gen des Behälters aus.
volume_filled Gibt Füll­zu­stand des Behälters aus.
volume_available Gibt ver­blei­ben­den Raum des Behälters aus.
is_empty Gibt an, ob der Behälter leer ist.
is_full Gibt an, ob der Behälter voll ist.
empty Leert den Behälter und gibt Inhalt zurück.
_add Interne Methode, welche eine Substanz hinzufügt, ohne Checks vor­zu­neh­men.
add Öf­fent­li­che Methode, welche an­ge­ge­be­ne Menge Substanz hinzufügt, sofern Raum vorhanden ist.
fill Füllt den ver­blei­ben­den Raum des Behälters mit einer Substanz.
pour_into Füllt den Inhalt des Behälters komplett in einen anderen Behälter um.
__add__ Im­ple­men­tiert Additions-Operator für Behälter; greift auf pour_into-Methode zurück.

Hier der tat­säch­li­che Code der Container-Klasse. Nachdem Sie diesen in ihrem lokalen Python-REPL ausführen, können Sie die weiteren Code-Beispiel des Artikels aus­pro­bie­ren:

class Container:
    def __init__(self, volume):
        # volume in ml
        self._volume = volume
        # start out with empty container
        self._contents = {}
    
    def __repr__(self):
        """
        Textual representation of container
        """
        repr = f"{self._volume} ml Container with contents {self._contents}"
        return repr
    
    def volume(self):
        """
        Volume getter
        """
        return self._volume
    
    def is_empty(self):
        """
        Container is empty if it has no contents
        """
        return self._contents == {}
    
    def is_full(self):
        """
        Container is full if volume of contents equals capacity
        """
        return self.volume_filled() == self.volume()
    
    def volume_filled(self):
        """
        Calculate sum of volumes of contents
        """
        return sum(self._contents.values())
    
    def volume_available(self):
        """
        Calculate available volume
        """
        return self.volume() - self.volume_filled()
    
    def empty(self):
        """
        Empty the container, returning its contents
        """
        contents = self._contents.copy()
        self._contents.clear()
        return contents
    
    def _add(self, substance, volume):
        """
        Internal method to add a new substance / add more of an existing substance
        """
        # update volume of existing substance
        if substance in self._contents:
            self._contents[substance] += volume
        # or add new substance
        else:
            self._contents[substance] = volume
    
    def add(self, substance, volume):
        """
        Public method to add a substance, possibly returning left over
        """
        if self.is_full():
            raise Exception("Cannot add to full container")
        # we can fit all of the substance
        if self.volume_filled() + volume <= self.volume():
            self._add(substance, volume)
            return self
        # we can fit part of the substance, returning the left over
        else:
            leftover = volume - self.volume_available()
            self._add(substance, volume - leftover)
            return {substance: leftover}
    
    def fill(self, substance):
        """
        Fill the container with a substance
        """
        if self.is_full():
            raise Exception("Cannot fill full container")
        self._add(substance, self.volume_available())
        return self
    
    def pour_into(self, other_container):
        """
        Transfer contents of container to another container
        """
        if other_container.volume_available() < self.volume_filled():
            raise Exception("Not enough space")
        # get the contents by emptying container
        contents = self.empty()
        # add contents to other container
        for substance, volume in contents.items():
            other_container.add(substance, volume)
        return other_container
    
    def __add__(self, other_container):
        """
        Implement addition for containers:
        `container_a + container_b` <=> `container_b.pour_into(container_a)`
        """
        other_container.pour_into(self)
        return self

Spielen wir ein paar Beispiele mit unserer Behälter-Im­ple­men­ta­ti­on durch. Wir in­stan­zi­ie­ren ein Glas und füllen dieses mit Wasser. Wie zu erwarten, ist das Glas danach voll:

glass = Container(300)
glass.fill('Water')
assert glass.is_full()

Im nächsten Schritt entleeren wir das Glas und erhalten die davor ent­hal­te­ne Menge Wasser zurück. Unsere Im­ple­men­ta­ti­on scheint zu funk­tio­nie­ren, das Glas ist danach leer:

contents = glass.empty()
assert contents == {'Water': 300}
assert glass.is_empty()

Ein etwas an­spruchs­vol­le­res Beispiel. Wir mischen in einem Pitcher Wein und Oran­gen­saft zusammen. Dazu erzeugen wir zunächst die be­nö­tig­ten Behälter und füllen zwei davon mit den Zutaten:

pitcher = Container(1500)
bottle = Container(700)
carton = Container(500)
# fill ingredients
bottle.fill('Red wine')
carton.fill('Orange juice')

Im Anschluss nutzen wir den Additions-Zu­wei­sungs-Operator +=, um die Inhalte der beiden gefüllten Behälter in den Pitcher um­zu­fül­len.

# pour ingredients into pitcher
pitcher += bottle
pitcher += carton
# check that everything worked
assert pitcher.volume_filled() == 1200
assert bottle.is_empty() and carton.is_empty()

Das funk­tio­niert, weil unsere Container-Klasse die __add__()-Methode im­ple­men­tiert. Hinter den Kulissen wird die Zuweisung pitcher += bottle umgeformt zu pitcher = pitcher + bottle. Ferner wird pitcher + bottle von Python in den Methoden-Aufruf pitcher.__add__(bottle) übersetzt. Unsere __add__()-Methode liefert den Receiver zurück, in diesem Falle pitcher, so dass die Zuweisung funk­tio­niert.

Statische Attribute

Bisher haben wir gesehen, wie sich auf die Attribute von Objekten zugreifen lässt: von außen über die öf­fent­li­chen Methoden, innerhalb der Methoden über eine Referenz auf self. Der interne Zustand der Objekte wird über Daten-Attribute rea­li­siert, welche dem je­wei­li­gen Objekt gehören. Auch die Methoden eines Objekts sind an eine spe­zi­fi­sche Instanz gebunden. Es gibt jedoch auch Attribute, welche Klassen gehören. Das ergibt Sinn, denn Klassen sind in Python ebenfalls Objekte.

Klassen-Attribute werden auch als „statische“ Attribute be­zeich­net, da sie bereits vor der In­stan­zi­ie­rung eines Objekts exis­tie­ren. Dabei kann es sich sowohl um Daten-Attribute als auch Methoden handeln. Das ist nützlich für Kon­stan­ten, welche für alle Instanzen einer Klasse gleich sind, sowie Methoden, welche nicht auf self operieren. Häufig werden Um­rech­nungs­rou­ti­nen als statische Methoden im­ple­men­tiert.

Anders als Sprachen wie Java und C++, kennt Python kein static-Schlüs­sel­wort, um explizit zwischen Objekt-At­tri­bu­ten und Klassen-At­tri­bu­ten zu un­ter­schei­den. Statt­des­sen kommt ein Dekorator namens @sta­tic­me­thod zum Einsatz. Schauen wir uns bei­spiels­hal­ber an, wie eine statische Methode für unsere Container-Klasse aussehen könnte. Wir im­ple­men­tie­ren eine Um­rech­nungs­rou­ti­ne, um Mil­li­li­ter in Fluid Ounces zu kon­ver­tie­ren:

# inside of class `Container`
    ...
    @staticmethod
    def floz_from_ml(ml):
        return ml * 0.0338140227

Zugriffe auf statische Attribute erfolgen wie gewohnt über eine Attribut-Referenz mit Punkt-Notation nach dem Schema obj.attr. Der Un­ter­schied ist lediglich, dass links vom Punkt der Klas­sen­na­me steht: ClassName.static_method(). Dies ist logisch kon­sis­tent, denn in der ob­jekt­ori­en­tier­ten Pro­gram­mie­rung mit Python sind auch Klassen Objekte. Um die Um­rech­nungs­rou­ti­ne unserer Container-Klasse auf­zu­ru­fen, schreiben wir also:

floz = Container.floz_from_ml(1000)
assert floz == 33.8140227

In­ter­faces

Als „Interface“, zu Deutsch „Schnitt­stel­le“ wird die Sammlung aller öf­fent­li­cher Methoden eines Objekts be­zeich­net. Das Interface definiert und do­ku­men­tiert das Verhalten eines Objekts und dient als API. Anders als C++ kennt Python keine separaten Ebenen für Interface (Header-Dateien) und Im­ple­men­ta­ti­on. Auch gibt es, anders als in Java und PHP, kein ex­pli­zi­tes interface-Schlüs­sel­wort. In diesen Sprachen enthalten In­ter­faces Methoden-Si­gna­tu­ren und dienen als Be­schrei­bung zu­sam­men­hän­gen­der Funk­tio­na­li­tät.

Da in Python die In­for­ma­ti­on, über welche Methoden ein Objekt verfügt und aus welcher Klasse es in­stan­zi­iert wurde dynamisch zur Laufzeit bestimmt wird, benötigt die Sprache keine ex­pli­zi­ten In­ter­faces. Statt­des­sen kommt in Python-OOP das Prinzip des „Duck Typing“ zum Tragen:

Zitat

„If it walks like a duck and it quacks like a duck, then it must be a duck“ — Quelle: https://docs.python.org/3/glossary.html#term-duck-typing
Über­set­zung: „Wenn es läuft wie eine Ente und quakt wie eine Ente, dann muss es sich um eine Ente handeln.“ (übersetzt von IONOS)

Was ist nun mit Duck Typing gemeint? Kurz gesagt lässt sich ein Python-Objekt einer Klasse wie ein Objekt einer anderen Klasse benutzen, solange es die be­nö­tig­ten Methoden enthält. An­schau­lich stellt man sich eine En­ten­at­trap­pe vor: diese gibt En­ten­ge­räu­sche von sich, schwimmt wie eine Ente und wird von tat­säch­li­chen Enten als Ente wahr­ge­nom­men.

Vererbung

Wie in den meisten ob­jekt­ori­en­tier­ten Sprachen kennt auch Pythons OOP das Konzept der Vererbung: eine Klasse lässt sich als Spe­zia­li­sie­rung einer Eltern-Klasse de­fi­nie­ren. Durch Fort­set­zen des Prozesses ergibt sich eine baum­ar­ti­ge Klassen-Hier­ar­chie, mit der vor­de­fi­nier­ten Klasse Object als Wurzel. So wie C++ (aber anders als Java und PHP) erlaubt Python die mehrfache Vererbung: eine Klasse kann von mehreren Eltern-Klassen ableiten.

Die mehrfache Vererbung lässt sich flexibel einsetzen. Unter anderem kann man so die aus Ruby bekannten „Mixins“ bzw. PHP‘s „Traits“ rea­li­sie­ren. Auch die aus Java bekannte Auf­spal­tung von Funk­tio­na­li­tät in In­ter­faces und abstrakte Klassen lässt sich in Python durch mehrfache Vererbung ersetzen.

Schauen wir uns an unserem Behälter-Beispiel an, wie die mehrfache Vererbung in Python funk­tio­niert. Manche Behälter lassen sich mit einem Ver­schluss versehen, und wir möchten unsere Container-Klasse da­hin­ge­hend spe­zia­li­sie­ren. Dazu de­fi­nie­ren wir eine neue Klasse Seal­ab­le­Con­tai­ner, welche von Container erbt. Ferner de­fi­nie­ren wir eine neue Klasse Sealable, welche Methoden zum Anbringen und Entfernen eines Ver­schlus­ses enthält. Da die Sealable-Klasse lediglich dazu dient, eine andere Klasse mit weiteren Methoden-Im­ple­men­ta­tio­nen zu versorgen, handelt es sich um ein „Mixin“:

class Sealable:
    """
    Implementation needs to:
    - initialize `self._seal`
    """
    def is_sealed(self):
        return self._seal is not None
    
    def is_open(self):
        return not self.is_sealed()
    
    def is_closed(self):
        return not self.is_open()
    
    def open(self):
        """
        Opening removes and returns the seal
        """
        seal = self._seal
        self._seal = None
        return seal
    
    def seal_with(self, seal):
        """
        Closing attaches the seal and returns the Sealable
        """
        self._seal = seal
        return self

Unser Seal­ab­le­Con­tai­ner erbt von der Container-Klasse und dem Sealable-Mixin. Wir über­schrei­ben die __init__()-Methode und de­fi­nie­ren zwei neue Parameter, welche er­mög­li­chen, Inhalte und Ver­schluss des Seal­ab­le­Con­tai­ner bei der In­stan­zi­ie­rung zu setzen. Dies ist notwendig, um ver­schlos­se­ne Behälter mit Inhalten zu erzeugen. Innerhalb der __init__()-Methode rufen wir über super() die In­itia­li­sie­rung der Eltern-Klasse auf:

class SealableContainer(Container, Sealable):
    """
    Start out with empty, open container
    """
    def __init__(self, volume, contents = {}, seal = None):
        # initialize `Container`
        super().__init__(volume)
        # initialize contents
        self._contents = contents
        # initialize `self._seal`
        self._seal = seal
    
    def __repr__(self):
        """
        Append 'open' / 'closed' to textual container representation
        """
        state = "Open" if self.is_open() else "Closed"
        repr = f"{state} {super().__repr__()}"
        return repr
    
    def empty(self):
        """
        Only open container can be emptied
        """
        if self.is_open():
            return super().empty()
        else:
            raise Exception("Cannot empty sealed container")
    
    def _add(self, substance, volume):
        """
        Only open container can have its contents modified
        """
        if self.is_open():
            super()._add(substance, volume)
        else:
            raise Exception("Cannot add to sealed container")

Ähnlich wie bei der __init__()-Methode über­schrei­ben wir gezielt weitere Methoden, um unseren Seal­ab­le­Con­tai­ner vom un­ver­schlos­se­nen Behälter zu dif­fe­ren­zie­ren. Wir über­schrei­ben __repr__(), so dass der Zustand offen / ver­schlos­sen ebenfalls aus­ge­ge­ben wird. Ferner über­schrei­ben wir die empty()- und _add()-Methoden, welche nur bei ge­öff­ne­tem Behälter Sinn ergeben. So erzwingen wir, dass ein ver­schlos­se­ner Behälter vor dem Entleeren oder Befüllen geöffnet wird. Wiederum setzen wir super() ein, um auf die be­stehen­de Funk­tio­na­li­tät der El­tern­klas­se zu­zu­grei­fen.

Spielen wir ein Beispiel durch: Wir möchten einen Cuba Libre mixen. Dazu benötigen wir ein Glas, eine kleine Flasche Cola und ein Shot-Glas mit 20cl Rum:

glass = Container(330)
cola_bottle = SealableContainer(250, contents = {'Cola': 250}, seal = 'Bottlecap')
shot_glass = Container(40)
shot_glass.add('Rum', 20)

Wir geben etwas Eis in da Glas und fügen den Rum hinzu. Da die Cola-Flasche ver­schlos­sen ist, öffnen wir sie zunächst und gießen dann den Inhalt in das Glas:

glass.add('Ice', 50)
# add rum
glass += shot_glass
# open cola bottle
if cola_bottle.is_closed():
    cola_bottle.open()
# pour cola into glass
glass += cola_bottle
Zum Hauptmenü