Dalla versione 3 in poi, Python ha spostato l’at­ten­zio­ne sulla pro­gram­ma­zio­ne orientata agli oggetti. Il lin­guag­gio segue la filosofia “eve­ry­thing is an object”, ovvero “tutto è un oggetto”.

A dif­fe­ren­za di Java, C++ e Python 2.x, Python 3 non distingue fra tipi primitivi e oggetti. Pertanto, cifre, stringhe, liste e ad­di­rit­tu­ra funzioni e classi sono trattate come oggetti.

Rispetto ad altri linguaggi, la pro­gram­ma­zio­ne orientata agli oggetti in Python usa le classi in modo molto fles­si­bi­le, con poche li­mi­ta­zio­ni. Da questo punto di vista, Python può con­si­de­rar­si dia­me­tral­men­te opposto a Java, il cui sistema è molto rigido. Di seguito vi spie­ghia­mo in modo semplice e chiaro come funziona la pro­gram­ma­zio­ne orientata agli oggetti con Python.

A cosa serve la pro­gram­ma­zio­ne orientata agli oggetti con Python?

La pro­gram­ma­zio­ne orientata agli oggetti è una forma di pro­gram­ma­zio­ne im­pe­ra­ti­va. Gli oggetti coniugano dati e fun­zio­na­li­tà. Un oggetto incapsula (contiene) il suo stato; l’accesso avviene tramite un’in­ter­fac­cia pubblica, ovvero l’in­ter­fac­cia dell’oggetto. Questa è definita dai suoi metodi. Gli oggetti in­te­ra­gi­sco­no fra loro mediante messaggi generati tramite la chiamata dei metodi.

Consiglio

Se de­si­de­ra­te com­pren­de­re meglio le basi di questo argomento, leggete gli articoli “Cos’è la pro­gram­ma­zio­ne orientata agli oggetti”, “Paradigmi di pro­gram­ma­zion” e “Tutorial su Python”.

In­cap­su­la­re oggetti in Python con la pro­gram­ma­zio­ne orientata agli oggetti

Nell’esempio riportato di seguito vedremo come è possibile usare la OOP in Python per in­cap­su­la­re oggetti. Im­ma­gi­nia­mo di scrivere del codice per una cucina, un bar o un la­bo­ra­to­rio. Mo­del­lia­mo i con­te­ni­to­ri, ad esempio bottiglie, bicchieri, tazze: in sostanza, qualsiasi oggetto che presenta un volume e che può essere riempito. A questo proposito una categoria di cose è definita “classe”.

Gli oggetti che sono dei con­te­ni­to­ri pre­sen­ta­no uno stato che può essere mo­di­fi­ca­to. I con­te­ni­to­ri possono essere riempiti, svuotati e molto altro. Se il con­te­ni­to­re è dotato di un coperchio, lo possiamo aprire e chiudere. Per logica, invece, una volta definito il volume del con­te­ni­to­re questo non può più essere mo­di­fi­ca­to. In relazione allo stato di un con­te­ni­to­re è possibile fare diverse con­si­de­ra­zio­ni che ri­spon­do­no alle seguenti domande:

  • “Il bicchiere è pieno?”
  • “Qual è il volume della bottiglia?”
  • “Il con­te­ni­to­re ha un coperchio?”

Inoltre, può essere utile fare in­te­ra­gi­re gli oggetti l’uno con l’altro. Ad esempio, dovrebbe essere possibile versare il contenuto di un bicchiere in una bottiglia. Di seguito esa­mi­ne­re­mo come mo­di­fi­ca­re lo stato di un oggetto in Python mediante la pro­gram­ma­zio­ne orientata agli oggetti. Le modifiche dello stato il­lu­stra­te di seguito (ovvero le relative domande), si rea­liz­za­no chiamando i vari metodi:

# 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

Definire i tipi in Python con la pro­gram­ma­zio­ne orientata agli oggetti

I tipi di dati rap­pre­sen­ta­no un concetto fon­da­men­ta­le della pro­gram­ma­zio­ne. Diversi dati possono essere usati in vari modi. I numeri si elaborano mediante ope­ra­zio­ni arit­me­ti­che, le catene di caratteri (string) possono essere ispe­zio­na­te:

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

I tentativi di sommare un numero e un carattere oppure di cercare una lettera all’interno di un numero causano un errore di tipo:

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

I tipi contenuti in Python sono astratti; un numero può rap­pre­sen­ta­re qualsiasi cosa: distanza, tempo, denaro. Il nome della variabile non è rap­pre­sen­ta­ti­vo del valore assegnato:

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

Cosa succede però quando de­si­de­ria­mo modellare dei concetti spe­cia­li­sti­ci? In Python anche in questo caso si usa la pro­gram­ma­zio­ne orientata agli oggetti. Gli oggetti sono strutture di dati dotate di un tipo iden­ti­fi­ca­bi­le, il quale risulta visibile mediante la funzione type() integrata:

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

Rea­liz­za­re astra­zio­ni in Python mediante la pro­gram­ma­zio­ne orientata agli oggetti

La pro­gram­ma­zio­ne si serve di astra­zio­ni per ridurre la com­ples­si­tà del codice. Questo consente al pro­gram­ma­to­re di operare a un più alto livello. Ad esempio, la domanda “il bicchiere è pieno?” equivale alla domanda “il volume di ciò che è contenuto nel bicchiere è pari al volume del bicchiere stesso”? La prima versione presenta un maggior grado di astra­zio­ne, è più corta e incisiva e pertanto da pre­fe­rir­si alla seconda. Le astra­zio­ni con­sen­to­no di creare e valutare sistemi più complessi:

# 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

In Python, con la OOP è possibile tra­sfe­ri­re concetti astratti a nuove idee. Il­lu­stria­mo questo aspetto con l’esempio dell’operatore addizione in Python. Il segno “più” (+) somma due numeri ma può anche essere usato per unire i contenuti di una lista:

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

Ora tra­sfe­ria­mo il concetto di addizione al nostro modello. Definiamo l’operatore addizione per i con­te­ni­to­ri. Questo ci consente di scrivere un codice che as­so­mi­glia mol­tis­si­mo al lin­guag­gio naturale. Più avanti il­lu­stre­re­mo l’im­ple­men­ta­zio­ne del codice, ora ci limitiamo a mostrare un esempio chiaro della sua ap­pli­ca­zio­ne:

# 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

Come funziona la pro­gram­ma­zio­ne orientata agli oggetti in Python?

Gli oggetti uniscono dati e fun­zio­na­li­tà, entrambi definiti come attributi. A dif­fe­ren­za di Java, PHP e C++, la pro­gram­ma­zio­ne orientata agli oggetti in Python non presenta parole chiave come private e protected volte a limitare l’accesso agli attributi. Al loro posto si usa una con­ven­zio­ne: gli attributi che iniziano con un trattino basso sono con­si­de­ra­ti non pubblici. Può trattarsi di attributi di dati secondo lo schema _internal_attr oppure di metodi secondo lo schema _internal_method().

In Python i metodi si de­fi­ni­sco­no con la variabile self come primo parametro. Tutti gli accessi agli attributi di un oggetto dall’interno dell’oggetto stesso avvengono facendo un ri­fe­ri­men­to a self. In Python, self funge da se­gna­po­sto per un’istanza concreta e quindi svolge lo stesso ruolo della parola chiave this, nor­mal­men­te usata in Java, PHP, Ja­va­Script e C++.

In con­co­mi­tan­za con la con­ven­zio­ne spiegata sopra, si ottiene un semplice schema per l’in­cap­su­la­men­to: l’accesso a un attributo interno come ri­fe­ri­men­to self._internal è corretto, poiché avviene all’interno dell’oggetto stesso. Gli accessi dall’esterno del tipo obj._internal in­fran­go­no la regola dell’in­cap­su­la­men­to e devono essere evitati:

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

Classi

Una classe funge da modello per un oggetto. Si dice che un oggetto è un’istanza di una classe, ovvero viene creato secondo il modello. La con­ven­zio­ne vuole che i nomi delle classi definite dall’utente inizino con la lettera maiuscola.

Di­ver­sa­men­te da Java, C++, PHP e Ja­va­Script, nella pro­gram­ma­zio­ne orientata agli oggetti in Python non esiste la parola chiave new. Invece, il nome della classe viene ri­chia­ma­to sot­to­for­ma di funzione e funge da metodo co­strut­to­re, il quale re­sti­tui­sce una nuova istanza. In modo implicito, il co­strut­to­re esegue una chiamata al metodo di ini­zia­liz­za­zio­ne __init__().

Ora esa­mi­nia­mo gli schemi men­zio­na­ti sopra sulla base di un esempio concreto. Mo­del­lia­mo il concetto di con­te­ni­to­re sotto forma di classe con il nome Container e definiamo i metodi relativi alle prin­ci­pa­li in­te­ra­zio­ni:

Metodo Spie­ga­zio­ne
__init__ Ini­zia­liz­za un nuovo con­te­ni­to­re con valori iniziali.
__repr__ Indica la rap­pre­sen­ta­zio­ne del con­te­ni­to­re (so­li­ta­men­te sotto forma di testo).
volume Indica la capienza del con­te­ni­to­re.
volume_filled Indica il volume di riem­pi­men­to del con­te­ni­to­re.
volume_available Indica il volume restante all’interno del con­te­ni­to­re.
is_empty Indica se il con­te­ni­to­re è vuoto.
is_full Indica se il con­te­ni­to­re è pieno.
empty Svuota il con­te­ni­to­re e re­sti­tui­sce il suo contenuto.
_add Metodo interno che aggiunge una sostanza, senza eseguire alcun tipo di controllo.
add Metodo pubblico che aggiunge la quantità di sostanza definita in base al volume di­spo­ni­bi­le.
fill Riempie lo spazio libero del con­te­ni­to­re con una sostanza.
pour_into Versa tutto il contenuto di un con­te­ni­to­re in un altro con­te­ni­to­re.
__add__ Im­ple­men­ta l’operatore addizione per i con­te­ni­to­ri; richiama il metodo pour_into.

Di seguito trovate il codice riferito alla classe Container. Dopo aver eseguito questo snippet nel vostro terminale locale REPL, potrete testare anche gli altri esempi presenti nell’articolo:

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

Ora vediamo alcuni esempi d’im­ple­men­ta­zio­ne del nostro con­te­ni­to­re: creiamo l’istanza bicchiere e lo riempiamo d’acqua. Come previsto, dopo l’im­ple­men­ta­zio­ne, il bicchiere sarà pieno:

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

Suc­ces­si­va­men­te svuotiamo il bicchiere e quindi otteniamo la quantità di acqua contenuta. La nostra im­ple­men­ta­zio­ne sembra fun­zio­na­re, infatti ora il bicchiere è vuoto:

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

Ora passiamo a un esempio più complesso. In una brocca (pitcher) mi­sce­lia­mo vino e succo d’arancia. Per farlo creiamo prima i con­te­ni­to­ri di cui abbiamo bisogno e ne riempiamo due con gli in­gre­dien­ti:

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

Ora usiamo l’operatore addizione/as­se­gna­zio­ne += per versare il contenuto dei due con­te­ni­to­ri (bottle e carton) nella brocca (pitcher).

# 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

L’esempio funziona perché la nostra classe Container ha im­ple­men­ta­to il metodo __add__(). Sotto la scocca l’as­se­gna­zio­ne pitcher += bottle diventa pitcher = pitcher + bottle. Inoltre pitcher + bottle di Python si traduce nella chiamata di metodo pitcher. __add__(bottle). Il nostro metodo __add__() re­sti­tui­sce il Receiver, in questo caso la brocca (pitcher) e quindi l’as­se­gna­zio­ne funziona.

Attributi statici

Finora abbiamo esaminato come è possibile accedere agli attributi di oggetti: dall’esterno mediante metodi pubblici, dall’interno mediante un ri­fe­ri­men­to a self. La con­di­zio­ne interna degli oggetti si realizza mediante gli attributi che ap­par­ten­go­no al relativo oggetto. Anche i metodi degli oggetti sono legati a una specifica istanza. Tuttavia, esistono anche attributi che ap­par­ten­go­no a classi, il che ha senso perché in Python anche le classi sono oggetti.

Gli attributi delle classi si de­fi­ni­sco­no anche attributi “statici”, perché esistono fin da prima dell’istan­zia­zio­ne di un oggetto. Può trattarsi sia di attributi che di metodi. Questo è utile per le costanti che sono uguali per tutte le istanze di una classe, nonché per i metodi che non operano su self. L’im­ple­men­ta­zio­ne delle routine di con­ver­sio­ne avviene spesso sotto forma di metodi statici.

A dif­fe­ren­za di linguaggi come Java e C++, Python non si serve della parola chiave static per operare una di­stin­zio­ne esplicita fra attributi e classi di un oggetto. Al suo posto viene invece usato il de­co­ra­to­re @sta­tic­me­thod. Ad esempio, esa­mi­nia­mo come potrebbe essere un metodo statico per la classe Container. Im­ple­men­tia­mo una routine di con­ver­sio­ne, per passare da mil­li­li­tri a once:

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

L’accesso agli attributi statici avviene come sempre mediante un ri­fe­ri­men­to all’attributo secondo lo schema obj.attr. L’unica dif­fe­ren­za rispetto agli esempi pre­ce­den­ti è che ora è il nome della classe a trovarsi a sinistra del punto: ClassName.static_method(). Tale an­no­ta­zio­ne risulta con­si­sten­te, dato che nella pro­gram­ma­zio­ne orientata agli oggetti in Python anche le classi sono oggetti. Per ri­chia­ma­re quindi la routine di con­ver­sio­ne della nostra classe Container, scriviamo:

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

In­ter­fac­ce

Si definisce Interface (in italiano: “in­ter­fac­cia”) l**’insieme di tutti i metodi pubblici di un oggetto**. L’in­ter­fac­cia definisce e documenta il com­por­ta­men­to di un oggetto e funge da API. A dif­fe­ren­za di C++, Python con­ce­pi­sce l’in­ter­fac­cia (file dell’header) e l’im­ple­men­ta­zio­ne sullo stesso livello. Allo stesso modo, a dif­fe­ren­za di Java e PHP, non esiste la parola chiave interface. In questi linguaggi, le in­ter­fac­ce con­ten­go­no firme di metodi e servono a de­scri­ve­re fun­zio­na­li­tà in relazione fra loro.

In Python l’in­for­ma­zio­ne relativa ai metodi di­spo­ni­bi­li per un oggetto e alla classe di cui l’oggetto è istanza, è de­ter­mi­na­ta di­na­mi­ca­men­te in fase di ese­cu­zio­ne. Pertanto, il lin­guag­gio non ha bisogno di in­ter­fac­ce esplicite. Invece, Python si serve del principio del “Duck Typing”:

Citazione

“If it walks like a duck and it quacks like a duck, then it must be a duck” — Fonte: https://docs.python.org/3/glossary.html#term-duck-typing Tra­du­zio­ne: “Se cammina come un’anatra e starnazza come un’anatra, allora si tratterà di un’anatra.” (tradotto da IONOS)

Ma cosa si intende esat­ta­men­te con Duck Typing? In breve, in Python l’oggetto di una classe può essere usato come un’altra classe, ammesso che contenga il metodo ne­ces­sa­rio. A titolo esem­pli­fi­ca­ti­vo, im­ma­gi­nia­mo un’anatra finta: questa emette suoni da anatra, nuota come un’anatra e anche le anatre la per­ce­pi­sco­no proprio come un’anatra.

Ere­di­ta­rie­tà

Come nella maggior parte dei linguaggi orientati agli oggetti, anche la OOP in Python usa il concetto di ere­di­ta­rie­tà: una classe può definirsi una spe­cia­liz­za­zio­ne di una classe genitore. Con­ti­nuan­do il processo si ottiene una gerarchia di classi ad albero, con la classe pre­de­fi­ni­ta Object come radice. Come in C++ (ma a dif­fe­ren­za di Java e PHP), Python consente l’ere­di­ta­rie­tà multipla: una classe può ereditare proprietà e com­por­ta­men­to da più di un genitore.

L’ere­di­ta­rie­tà multipla può essere usata in modo fles­si­bi­le. In questo modo è possibile rea­liz­za­re i co­sid­det­ti “mixins” in Ruby o “traits” in PHP. In Python l’ere­di­ta­rie­tà multipla può essere usata anche al posto della sud­di­vi­sio­ne delle fun­zio­na­li­tà in in­ter­fac­ce e classi astratte, tipica di Java.

Vediamo ora come funziona l’ere­di­ta­rie­tà multipla in Python ri­pren­den­do il nostro esempio del con­te­ni­to­re. Alcuni con­te­ni­to­ri possono essere dotati di un coperchio. A tal proposito de­si­de­ria­mo spe­cia­liz­za­re il com­por­ta­men­to della nostra classe Container. Di con­se­guen­za definiamo una nuova classe Sea­la­ble­Con­tai­ner, che eredita dalla classe Container. Suc­ces­si­va­men­te definiamo la nuova classe Sealable, che contiene i metodi per mettere e togliere un coperchio. Poiché la classe Sealable serve solo a dotare un’altra classe di ulteriori im­ple­men­ta­zio­ni di metodo, si tratta di un “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

La nostra classe Sea­la­ble­Con­tai­ner eredita dalla classe Container e dal mixin Sealable. So­vra­scri­via­mo il metodo __init__() e definiamo due nuovi parametri che con­sen­to­no di impostare nell’istanza il contenuto e lo stato di chiusura del Sea­la­ble­Con­tai­ner. Questo è ne­ces­sa­rio al fine di generare con­te­ni­to­ri chiusi con contenuto. All’interno del metodo __init__() ri­chia­mia­mo l’ini­zia­liz­za­zio­ne della classe genitore mediante super():

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

Ana­lo­ga­men­te al metodo __init__(), so­vra­scri­via­mo altri metodi in modo mirato al fine di dif­fe­ren­zia­re il nostro Sea­la­ble­Con­tai­ner dal con­te­ni­to­re non chiuso. So­vra­scri­via­mo __repr__() in modo da creare un output anche dello stato aperto/chiuso. Inoltre, so­vra­scri­via­mo i metodi empty() e _add(), che hanno senso solo quando il con­te­ni­to­re è aperto. In questo modo forziamo l’apertura di un con­te­ni­to­re chiuso prima che questo possa essere svuotato o riempito. Anche in questo caso, usiamo super() per accedere alle fun­zio­na­li­tà esistenti della classe genitore.

Prendiamo un esempio: vogliamo miscelare gli in­gre­dien­ti di un Cuba Libre. Per questo abbiamo bisogno di un bicchiere, di una bottiglia di Coca Cola e di un bic­chie­ri­no da liquore con­te­nen­te 20 cl di rum:

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

Versiamo un po’ di ghiaccio nel bicchiere e ag­giun­gia­mo il rum. Dato che la bottiglia di Coca Cola è chiusa, prima la apriamo e solo dopo versiamo il contenuto nel bicchiere:

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
Vai al menu prin­ci­pa­le