Depuis la sortie de sa version 3, Python mise avant tout sur la pro­gram­ma­tion orientée objet (OOP, de l’anglais « object-oriented pro­gram­ming »). Ce langage s’appuie sur la phi­lo­so­phie de con­cep­tion « eve­ry­thing is an object », selon laquelle « tout est un objet ».

Con­trai­re­ment à Java, C++ et Python 2.x, il n’existe ici aucune dis­tinc­tion entre les valeurs pri­mi­tives et les objets. Dans le langage Python, les nombres, les chaînes de ca­rac­tères et les listes, voire les fonctions et les classes, sont tous con­si­dé­rés comme des objets.

Par rapport à d’autres langages, l’OOP basée sur les classes avec Python se distingue par sa grande flexi­bi­lité et son faible nombre de con­traintes fixes. Ce langage est donc aux antipodes de Java, qui propose un système d’OOP décrit comme étant tout par­ti­cu­liè­re­ment rigide. Découvrez notre ex­pli­ca­tion détaillée sur le fonc­tion­ne­ment de la pro­gram­ma­tion orientée objet dans Python.

Pro­gram­ma­tion orientée objet dans Python : à quoi sert-elle ?

La pro­gram­ma­tion orientée objet cor­res­pond à une forme de pro­gram­ma­tion im­pé­ra­tive. Les objets servent à associer données et fonc­tion­na­li­tés. Un objet encapsule son état interne avec un accès par l’in­ter­mé­diaire d’une interface publique, l’interface de l’objet. L’interface d’un objet est définie par les méthodes de ce dernier. Les objets in­te­ra­gis­sent entre eux par des messages, transmis grâce aux appels de ces méthodes.

Conseil

Pour mieux com­prendre le contexte auquel nous faisons référence, in­té­res­sez-vous aux articles « Qu’est-ce que l’OOP ? », « Pa­ra­digmes de pro­gram­ma­tion » et « Tutoriel Python », rédigés par nos soins.

En­cap­su­ler des objets grâce à l’OOP dans Python

Découvrez avec nous comment les objets peuvent être en­cap­su­lés avec l’OOP dans Python. Supposons que nous devons écrire du code pour une cuisine, un bar ou un la­bo­ra­toire. Pour ce faire, nous allons modéliser des ré­ci­pients tels que des bou­teilles, des verres, des tasses, etc. ; tous re­pré­sen­tent un volume et peuvent être remplis. Ces objets ap­par­tien­nent à dif­fé­rentes ca­té­go­ries, appelées « classes ».

Les objets re­pré­sen­tant ces ré­ci­pients possèdent un état interne, qu’il nous est possible de modifier. Nous sommes en effet en mesure de remplir ces ré­ci­pients, de les vider, etc. S’ils disposent d’un système de fermeture (d’un bouchon, par exemple), nous pouvons aussi les ouvrir et les fermer. En toute logique, il n’est toutefois pas possible de modifier le volume d’un récipient a pos­te­riori. À l’évidence, dif­fé­rentes ré­flexions sont possibles en ce qui concerne l’état d’un récipient, notamment :

  • « Le verre est-il plein ? »
  • « Quel est le volume de la bouteille ? »
  • « Le récipient peut-il se fermer ? »

De la même manière, il est logique de créer des in­te­rac­tions entre les objets. À titre d’exemple, il est nor­ma­le­ment possible de trans­va­ser le contenu d’un verre dans une bouteille. Com­men­çons par nous in­té­res­ser à la mo­di­fi­ca­tion de l’état interne d’un objet avec la pro­gram­ma­tion orientée objet proposée par Python. Pour modifier les dif­fé­rents états ou poser des questions sur ces derniers, nous utilisons des appels de méthodes :

# 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()
Python

Définir des types avec l’OOP dans Python

Les types de données cons­ti­tuent l’un des concepts fon­da­men­taux de la pro­gram­ma­tion. Dif­fé­rentes données se prêtent à divers usages ; les nombres sont traités à l’aide d’opé­ra­tions arith­mé­tiques, tandis que les chaînes de ca­rac­tères (ou « strings ») peuvent être explorées :

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

Si nous essayons d’ad­di­tion­ner un nombre et une chaîne de ca­rac­tères ou d’effectuer une recherche au sein d’un nombre, nous nous re­trou­vons face à une erreur de ce type :

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

Les types intégrés à Python sont abstraits, ce qui signifie qu’un nombre peut re­pré­sen­ter tout et n’importe quoi : une distance, du temps, de l’argent, etc. Seul le nom de la variable donne alors une in­di­ca­tion sur la sig­ni­fi­ca­tion d’une telle valeur :

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

Mais alors, comment procéder pour modéliser des concepts spé­cia­li­sés ? Pour atteindre cet objectif, il faut encore une fois recourir à la pro­gram­ma­tion orientée objet dans Python. Les objets sont des struc­tures de données dont le type est iden­ti­fiable. Pour l’afficher, il convient d’utiliser la fonction intégrée « type() » :

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

Créer des abs­trac­tions grâce à la pro­gram­ma­tion orientée objet dans Python

Dans le domaine de la pro­gram­ma­tion, les abs­trac­tions sont utilisées pour dis­si­mu­ler toute com­plexité. Elles per­met­tent aux pro­gram­meurs d’exercer leurs activités à un niveau supérieur. La question « Le verre est-il plein ? » est, par exemple, identique à la question « Le volume du contenu du verre est-il égal au volume du verre ? ». La première question, qui est plus abstraite, est aussi plus courte et plus concise ; c’est donc celle-ci qu’il convient de pri­vi­lé­gier. Les abs­trac­tions per­met­tent donc de créer des systèmes plus complexes, mais également d’en avoir une bonne vue d’ensemble :

# 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()
Python

L’OOP utilisée avec Python permet d’appliquer des concepts abstraits à de nouvelles idées. In­té­res­sons-nous par exemple à l’opérateur d’addition de Python. Le signe « + » peut être utilisé pour relier deux nombres, mais nous pouvons aussi l’appliquer à des listes pour combiner leurs contenus :

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

Il est ici logique d’appliquer ce concept d’addition à notre modèle. Pour ce faire, nous dé­fi­nis­sons un opérateur d’addition pour les ré­ci­pients. Celui-ci nous permet d’écrire un code se lisant presque comme un langage naturel. Nous vous ex­pli­que­rons un peu plus tard comment l’im­plé­men­ter. Pour l’heure, in­té­res­sons-nous à cet exemple d’ap­pli­ca­tion :

# 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()
Python

Comment fonc­tionne la pro­gram­ma­tion orientée objet dans Python ?

Les objets combinent des données et des fonc­tion­na­li­tés, également connues sous le nom d’« attributs ». Con­trai­re­ment à Java, PHP et C++, l’OOP dans Python ne propose pas de mots-clés sem­blables à « private » et « protected » afin de res­treindre l’accès aux attributs. Il convient plutôt d’utiliser une con­ven­tion : les attributs com­men­çant par un tiret bas sont con­si­dé­rés comme privés. Il peut s’agir d’attributs de données répondant au schéma « _internal_attr », mais aussi de méthodes répondant au schéma « _internal_method() ».

Dans le langage Python, les méthodes sont définies en tant que premier paramètre par la variable « self ». Tout accès aux attributs d’un objet depuis l’intérieur de celui-ci s’effectue à l’aide d’une référence à « self ». Avec Python, « self » fonc­tionne comme un caractère générique pour une instance concrète ; ainsi, il joue un rôle identique à celui du mot-clé « this » pour Java, PHP, Ja­vaS­cript et C++.

Grâce à une com­bi­nai­son avec la con­ven­tion men­tion­née pré­cé­dem­ment, un modèle simple d’en­cap­su­la­tion peut être créé : il est alors possible d’accéder à un attribut interne avec la référence « self._internal », car il s’effectue à l’intérieur de l’objet. Toute tentative d’accès externe, dans un style similaire à « obj._internal », est quant à elle contraire au principe d’en­cap­su­la­tion, et doit donc être évitée :

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'
Python

Classes

Une classe agit comme un modèle pour les objets. Un objet est instancié à partir de classes, ce qui veut dire qu’il est créé con­for­mé­ment au modèle en question. Par con­ven­tion, les noms de classes per­son­na­li­sés com­men­cent toujours par une majuscule.

Au contraire de Java, C++, PHP et Ja­vaS­cript, l’OOP dans Python ne fait pas appel au mot-clé « new ». C’est plutôt le nom de la classe qui est appelé en tant que fonction et qui sert alors de cons­truc­teur pour fournir une nouvelle instance. Le cons­truc­teur appelle, de façon implicite, la fonction « __init__() », qui permet d’ini­tia­li­ser les données de l’objet.

In­té­res­sons-nous à nouveau aux modèles déjà étudiés avec un exemple de code. Nous mo­dé­li­sons le concept d’un récipient sous la forme d’une classe portant le nom « Container » et dé­fi­nis­sons des méthodes pour les in­te­rac­tions im­por­tantes :

Méthode Ex­pli­ca­tion
__init__ Ini­tia­li­sa­tion d’un nouveau récipient avec des valeurs de départ.
__repr__ In­di­ca­tion de l’état du conteneur sous forme de texte.
volume In­di­ca­tion du volume du récipient.
volume_filled In­di­ca­tion de l’état de rem­plis­sage du récipient.
volume_available In­di­ca­tion du volume encore dis­po­nible dans le récipient.
is_empty In­di­ca­tion donnée si le récipient est vide.
is_full In­di­ca­tion donnée si le récipient est plein.
empty Vidange du récipient et res­ti­tu­tion du contenu.
_add Méthode interne ajoutant une substance sans procéder à des contrôles.
add Méthode publique ajoutant le volume de substance indiqué s’il reste suf­fi­sam­ment d’espace dis­po­nible.
fill Rem­plis­sage du volume dis­po­nible restant dans le récipient à l’aide d’une substance.
pour_into Transfert complet du contenu du récipient dans un autre récipient.
__add__ Im­plé­men­ta­tion de l’opérateur d’addition pour les ré­ci­pients (uti­li­sa­tion de la méthode « pour_into »).

Voici le code réel de la classe « Container ». Une fois celui-ci exécuté dans votre REPL Python local, n’hésitez pas à tester les autres exemples de code présents dans l’article :

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
Python

Amusons-nous un peu avec certains exemples dans le cadre de l’im­plé­men­ta­tion des ré­ci­pients. Nous allons ici ins­tan­cier un verre et le remplir d’eau. Ce verre est alors plein :

glass = Container(300)
glass.fill(’Water’)
assert glass.is_full()
Python

L’étape suivante consiste à vider le verre en ré­cu­pé­rant le volume d’eau qu’il contenait jusqu’ici. Si l’im­plé­men­ta­tion fonc­tionne, le verre est alors vide :

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

In­té­res­sons-nous main­te­nant à un exemple plus complexe. Pour ce faire, nous mé­lan­geons du vin et du jus d’orange dans un pichet. Nous com­men­çons donc par créer les ré­ci­pients né­ces­saires, avant de remplir deux d’entre eux avec chacun des liquides :

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

Pour finir, nous utilisons l’opérateur d’addition et d’as­sig­na­tion « += » pour trans­va­ser le contenu des deux con­te­neurs ainsi remplis dans le pichet.

# 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()
Python

Si cela fonc­tionne, c’est parce que notre classe « Container » im­plé­mente la méthode « __add__() ». En arrière-plan, l’as­sig­na­tion « pitcher += bottle » se trans­forme en « pitcher = pitcher + bottle ». De plus, le langage Python traduit « pitcher + bottle » en un appel de méthode « pitcher.__add__(bottle) ». Notre méthode « __add__() » renvoie alors le récipient des­ti­na­taire (dans ce cas, il s’agit du pichet), de sorte que l’as­sig­na­tion fonc­tionne comme il se doit.

Attributs statiques

Nous avons vu ensemble comment accéder aux attributs des objets : de manière externe avec les méthodes publiques, ou de manière interne en faisant référence à « self ». L’état interne des objets se réalise à l’aide d’attributs de données ap­par­te­nant à l’objet en question. Les méthodes d’un objet sont elles aussi reliées à une instance spé­ci­fique. Il existe néanmoins des attributs pouvant ap­par­te­nir à des classes. Cela semble logique, car les classes sont également con­si­dé­rées comme des objets dans Python.

Les attributs de classe sont également qualifiés d’attributs « statiques », car ils sont déjà présents avant l’ins­tan­cia­tion d’un objet. In­dif­fé­rem­ment, il peut s’agir d’attributs de données ou de méthodes. Cela peut s’avérer utile pour les cons­tantes, qui sont iden­tiques pour toutes les instances d’une même classe, mais aussi pour les méthodes ne fonc­tion­nant pas sur « self ». Les routines de con­ver­sion sont souvent im­plé­men­tées en tant que méthodes statiques.

Con­trai­re­ment à des langages comme Java et C++, Python ne propose pas le mot-clé « static » pour effectuer une dis­tinc­tion explicite entre les attributs d’objet et les attributs de classe. Il convient plutôt d’utiliser un dé­co­ra­teur, nommé « @staticmethod ». À titre d’exemple, imaginons l’apparence que pourrait prendre une méthode statique pour notre classe « Container ». Com­men­çons par im­plé­men­ter une routine de con­ver­sion afin de convertir des mil­li­litres en onces liquides :

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

Pour accéder aux attributs statiques, il faut comme toujours utiliser une référence d’attribut avec une notation par point, en suivant le schéma « obj.attr ». Ici, la dif­fé­rence tient au fait que le nom de la classe est placé avant le point : « ClassName.static_method() ». Dans la logique, tout ceci est cohérent, car dans le cadre de la pro­gram­ma­tion orientée objet avec Python, les classes sont elles aussi des objets. Pour appeler la routine de con­ver­sion de notre classe « Container », il convient donc d’écrire ce qui suit :

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

In­ter­faces

Une interface cor­res­pond à la col­lec­tion de toutes les méthodes publiques d’un objet. En plus de définir et de do­cu­men­ter le com­por­te­ment d’un objet, cette interface sert également d’API. Con­trai­re­ment au langage C++, Python ne propose pas de niveaux séparés pour l’interface (fichiers d’en-tête) et l’im­plé­men­ta­tion. De plus, il n’existe aucun mot-clé explicite pour l’interface ; il se distingue en cela de Java et de PHP. Dans ces langages, les in­ter­faces sont équipées de sig­na­tures de méthodes et per­met­tent de décrire une fonc­tion­na­lité cohérente.

En langage Python, les in­for­ma­tions sur les méthodes mises à la dis­po­si­tion d’un objet et sur la classe à partir de laquelle il a été instancié sont dé­ter­mi­nées de façon dynamique au moment de l’exécution, de sorte qu’aucune interface explicite n’est né­ces­saire pour ce langage. En revanche, l’OOP avec Python est basée sur le principe du « duck typing » :

Citation

« If it walks like a duck and it quacks like a duck, then it must be a duck » — Source : https://docs.python.org/3/glossary.html#term-duck-typing. Tra­duc­tion : « Si ça marche comme un canard et si ça cancane comme un canard, c’est qu’il s’agit sans doute d’un canard » (tra­duc­tion de IONOS).

Mais alors, qu’est-ce que le « duck typing » ? Pour faire simple, un objet Python d’une classe donnée peut être utilisé en tant qu’objet d’une autre classe, à condition qu’il contienne les méthodes né­ces­saires. Prenons le cas d’un faux canard : s’il émet les mêmes bruits et qu’il nage comme un canard, alors il est perçu en tant que tel par les vé­ri­tables canards.

Héritage

Comme avec la plupart des langages orientés objet, l’OOP avec Python s’appuie sur le concept d’héritage : une classe peut donc être définie comme la spé­cia­li­sa­tion d’une autre classe parente. En répétant ce processus, il est possible d’obtenir une ar­bo­res­cence hié­rar­chique des classes, avec la classe pré­dé­fi­nie « Object » en tant que racine. Comme c’est le cas avec C++ (mais pas avec les langages Java et PHP), Python prend en charge l’héritage multiple, ce qui signifie qu’une classe peut venir de plusieurs classes parentes.

L’héritage multiple s’utilise de manière flexible. Il permet entre autres d’obtenir les classes « mixins » proposées par Ruby ou les « traits » propres au langage PHP. Si vous souhaitez dé­com­po­ser la fonc­tion­na­lité en in­ter­faces et en classes abs­traites, pos­si­bi­lité offerte par Java, vous pouvez pour cela utiliser l’héritage multiple dans le langage Python.

Reprenons l’exemple de nos ré­ci­pients pour voir comment fonc­tionne l’héritage multiple avec le langage Python. Certains ré­ci­pients peuvent se fermer, et nous sou­hai­tons spé­cia­li­ser notre classe « Container » à cet égard. Nous devons définir une nouvelle classe « SealableContainer », qui hérite de la classe « Container ». À cela, nous ajoutons également une nouvelle classe « Sealable », contenant des méthodes per­met­tant d’apposer et de retirer un bouchon. Comme la classe « Sealable » a pour but de fournir des im­plé­men­ta­tions de méthodes sup­plé­men­taires à une autre classe, il s’agit en réalité d’une classe « 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
Python

Notre classe « SealableContainer » hérite donc de la classe « Container » et de la classe « mixin » « Sealable ». Il est donc temps d’écraser la méthode « __init__() » et de définir deux nouveaux pa­ra­mètres, lesquels vont permettre de définir le contenu et le type de fermeture du « SealableContainer » concerné lors de son ins­tan­cia­tion. Cette opération est né­ces­saire pour créer des ré­ci­pients avec des contenus, et qui ferment. Au sein même de la méthode « __init__() », nous utilisons « super() » pour appeler l’ini­tia­li­sa­tion de la classe parente :

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")
Python

Comme pour la méthode « __init__() », il convient d’écraser d’autres méthodes de manière ciblée, afin de pouvoir dif­fé­ren­cier notre « SealableContainer » du récipient qui lui ne ferme pas. Nous écrasons ensuite « __repr__() », de manière à ce que l’état du récipient (ouvert ou fermé) soit lui aussi affiché. N’oublions pas non plus d’écraser les méthodes « empty() » et «_add() », qui ne sont per­ti­nentes que si le récipient est ouvert. En procédant ainsi, nous pouvons forcer l’ouverture d’un récipient fermé avant de le vider ou de le remplir. Une fois encore, nous utilisons « super() » pour accéder à la fonc­tion­na­lité de la classe parente déjà existante. Imaginons par exemple que nous sou­hai­tions réaliser un Cuba Libre. Il nous faudrait pour cela un verre, une petite bouteille de Coca-Cola et un verre à shot contenant 20 cl de rhum :

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

Com­men­çons par mettre un peu de glace dans le verre, et ajoutons-y le rhum. La bouteille de Coca-Cola étant fermée, nous devons l’ouvrir avant de verser son contenu dans le verre :

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
Python
Aller au menu principal