Python Base¶

Python Biella Group¶


Terza serata, 17/10/2022¶

Speaker:

  • James Luca Bosotti (Assistente e Studente @ Università di Trento)
  • bosottiluca@gmail.com

Repository github per questi tutorial:

  • https://github.com/PythonBiellaGroup/PythonBase/tree/main/teoria

(Per ripassare link al libro SoftPython )

  • (Per anteprima slide premi Esc)

Programmazione a Oggetti¶

OOP per gli amici¶

Cambia il focus:

Il codice non è più una linea più o meno dritta in cui dobbiamo esplicitare tutte le istruzioni

Ma il compito del programmatore diventa quello di descrivere un ecosistema di oggetti e relazioni

Il modo in cui queste relazioni si intersecano definirà il comportamento degli oggetti

In [22]:
# NO OOP

animali = []

animali.append( ("Gatto", "miao") )
animali.append( ("Cane", "bau") )
animali.append( ("Coccodrillo", "??") )

def parla(animale):
    print(animale[1])
    
for animale in animali:
    parla(animale)
miao
bau
??
In [23]:
# OOP!

class Animale:
    def __init__(self, specie, verso):
        self.specie = specie
        self.verso = verso
    
    def parla(self):
        print(f"Il verso di {self.specie} è: {self.verso}")
        
animali = []

animali.append(Animale("Gatto", "miao"))
animali.append(Animale("Cane", "bau"))
animali.append(Animale("Coccodrillo", "??"))

for animale in animali:
    animale.parla()
Il verso di Gatto è: miao
Il verso di Cane è: bau
Il verso di Coccodrillo è: ??

Beh ma quindi dove sta il vantaggio?

  • La quantità di codice è simile se non addirittura superiore (in questo esempio)
  • Sembra che il codice sia lo stesso, una funzione, due variabili

Il vantaggio sta nella creazione di uno "standard"

Il codice ora sa cos'è un Animale, sa che ha due variabili e un metodo

Sa che non ha senso esista un animale senza specie o senza verso

Sa che tutti gli animali hanno un modo per parlare

Per tutti gli amici biologi, questa è ovviamente una semplificazione :P¶

Oggetto o classe?¶

In [24]:
class Animale:
    pass

mio_gatto = Animale()

Animale è una classe, è una rappresentazione di tutti i possibili animali che possono esistere dal mio gatto ai temibili Vermi delle sabbie di Dune.

Aggiungere un metodo¶

I metodi sono le "funzioni" degli oggetti. Descrivono i suoi comportamenti

In [25]:
class Animale:
    def parla(self):
        print(f"Il verso di {self.specie} è: {self.verso}")

Ok, ma cos'è "self"?

Se Animale è una classe, al contrario mio_gatto, Verme delle sabbie o ogni altro animale reale (Cioè che esiste e ti più mordere) è un'istanza della classe Animale.

self rappresenta quindi l'istanza stessa.

quando si legge print(self.verso) si deve intendere "stampa il verso proprio della mia istanza"

Horror Vacui¶

Aggiungiamo pezzi alla nostra classe Animale

In [26]:
class Animale:
    def __init__(self, specie, verso):             # <--
        self.specie = specie                       # <--
        self.verso = verso                         # <--
        
    def parla(self):
        print(f"Il verso di {self.specie} è: {self.verso}")

Prima avevamo spiegato a Python che i nostri animali potevano parlare tramite la variabile verso. Ma non avevamo mica detto che valore avesse o come si dovesse assegnare.

__init__ risolve questo problema. E' un metodo che si chiama costruttore e spiega come vada creata un istanza della nostra classe.

Notate come il suo nome inizia e finisca con due underscores. Questo è un segnale per Python che si tratta di un metodo speciale

self.specie = specie Questa linea sta dicendo di assegnare il parametro del metodo alla variabile d'istanza specie.

Sembra rindondante per i nomi usati, ma è in realtà una buona pratica per non perdere di vista cosa vada dove

Variabili di istanza e di classe¶

  • Una variabile d'istanza è una proprietà che ogni rappresentante di quella classe possiede ma che è diversa tra ciascun individuo.

  • Una variabile di classe invece è una proprietà che tutti gli appartenendi a quella classe possiedono nella stessa e ugual misura.

Per esempio

  • Istanza: Peso, altezza, propensione al rischio, numero di scarpe

  • Classe: classificazione regno animale,

In [27]:
class Animale:
    
    regno_della_vita = "ANIMALE"           # <--
    fotosintesi = False                    # <--
    
    
    def __init__(self, specie, verso):
        self.specie = specie
        self.verso = verso
        
    def parla(self):
        print(f"Il verso di {self.specie} è: {self.verso}")

regno_della_vita è una variabile di classe, non è necessario inizializzarla

self.specie è una variabile di istanza, assume un valore potenzialmente diverso per ogni animale

In [28]:
zanzara = Animale("zanzara", "bzzz")
elefante_confuso = Animale("elefante", "bzzz")

print(zanzara.regno_della_vita)
print(elefante_confuso.regno_della_vita)
zanzara.parla()
elefante_confuso.parla()
ANIMALE
ANIMALE
Il verso di zanzara è: bzzz
Il verso di elefante è: bzzz

Modificare le variabili di classe¶

In [33]:
pinguino = Animale("Pinguino", "skee")
batterio = Animale("Batterio", "...")

batterio.fotosintesi = True
batterio.regno_della_vita += "...Forse"

print("Batterio")
print(batterio.fotosintesi)
print(batterio.regno_della_vita)
Batterio
True
ANIMALE...Forse
In [35]:
print("Pinguino")
print(pinguino.fotosintesi)
print(pinguino.regno_della_vita)
Pinguino
False
ANIMALE
In [37]:
Animale.fotosintesi = True

delfino = Animale("Delfino", "kekeke")
print("Delfino")
print(delfino.fotosintesi)
print(delfino.regno_della_vita)
Delfino
True
ANIMALE

E' possibile modificare una variabile di classe MA la modifica sarà valida solo per l'istanza per cui è stata modificata

Overloading¶

Ma il mio animale però fa tanti versi diversi!

In [11]:
class Animale:
    
    regno_della_vita = "ANIMALE"           
    fotosintesi = False                    
    
    def __init__(self, specie, verso):
        self.specie = specie
        self.verso = verso
        
    def parla(self):
        print(f"Il verso di {self.specie} è: {self.verso}")
    
    def parla(self, ritmo):                                                  # <---
        print(f"Il verso di {self.specie} è: {(self.verso+' ') * ritmo}")    # <---
        
cucu = Animale("Cucù", "cucù!")

cucu.parla(5)
cucu.parla()
Il verso di Cucù è: cucù! cucù! cucù! cucù! cucù! 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-11-f46403af23a4> in <module>
     17 
     18 cucu.parla(5)
---> 19 cucu.parla()

TypeError: parla() missing 1 required positional argument: 'ritmo'

Purtroppo, in Python, non è possibile fare l'overloading¶

Ovvero, definire più volte un metodo usando firme diverse e scegliere a runtime quello corretto da utilizzare.

In caso di "tentato overloading" infatti, solo l'ultima funzione definita avrà effetto.

Methods overriding¶

Diverso è il caso dell'overriding.

In [15]:
print(cucu)
print(batterio)
<__main__.Animale object at 0x7f57802cfd30>
<__main__.Animale object at 0x7f57802912b0>

Qui vedete come Python stampi una cosa strana alla print dell'animale.

Si tratta della print di default di tutti gli oggetti e indica la cella di memoria in cui è contenuta l'istanza dell'oggetto.

Non sempre è molto informativa e quindi sarebbe interessante cambiarla.

In [20]:
class Animale:
    
    regno_della_vita = "ANIMALE"           
    fotosintesi = False                    
    
    def __init__(self, specie, verso):
        self.specie = specie
        self.verso = verso
    
    def __str__(self):                                         # <---
        return f"{self.regno_della_vita}: {self.specie}"       # <---
        
    def parla(self):
        print(f"Il verso di {self.specie} è: {self.verso}")

piccione = Animale("Piccione Comune", "grugrugru")
print(piccione)
ANIMALE: Piccione Comune

Per modificarla è sufficiente ridefinire il metodo __str__

In questo modo cambia il modo con l'oggetto verrà trasformato in stringa in ogni contesto in cui questo verrà richiesto.

Ma perchè proprio __str__?

Perchè è uno dei cosiddetti Special Methods (Docs Ufficiale) che una volta sovrascritti cambiano le funzionalità degli oggetti.

Altri metodi speciali sono:

  • __repr__ che restituisce la cosa più vicina al codice necessario per rigenerare l'oggetto
  • __lt__ __le__ __eq__ __ne__ __gt__ __ge__ che determinao il comportamento coi booleani
  • __getitem__ che fa ridefinire [ ]

L'ereditarietà¶

Una delle feature fondamentali dell'OOP permette di usare il pensiero di analogia

"Questa cosa funziona come quella ma..."

In [66]:
class Animale:
    
    regno_della_vita = "ANIMALE"           
    fotosintesi = False                    
    
    def __init__(self, specie, verso):
        self.specie = specie
        self.verso = verso
    
    def __str__(self):                                         
        return f"{self.regno_della_vita}: {self.specie}"       
        
    def parla(self):
        print(f"Il verso di {self.specie} è: {self.verso}")
        
    def muovi(self):                                           # <---
        print("Questo animale si muove su terra")              # <---

Aggiungiamo il metodo per muoversi

Ora aggiungiamo una nuova classe di animali

In [68]:
class Animale_Anfibio(Animale):
    def nuota(self):
        print("Questo animale nuota")
        
polpo = Animale_Anfibio("Polpo", "blub")
polpo.nuota()
polpo.muovi()
Questo animale nuota
Questo animale si muove su terra

Quindi vedete che il nostro polpo ha un mezzo in più

In [71]:
class Animale_Acquatico(Animale_Anfibio):
    def muovi(self):
        print("Questo animale NON può andare sulla terraferma")
        self.nuota()
        
pesce_chirurgo = Animale_Acquatico("Pesce Chirurgo", "blub")
pesce_chirurgo.muovi()
Questo animale NON può andare sulla terraferma
Questo animale nuota

Qui invece vedete come è possibile usare l'ereditarietà anche per "togliere" delle funzionalità.

Semplicemente facendo un override di un dato metodo impediamo il suo utilizzo.

Limitare l'accesso¶

Come fare per avere delle variabili private?

In [76]:
class Animale_Anfibio(Animale):
    def __init__(self, specie, verso, pinne):
        super().__init__(specie, verso)
        self.__pinne = pinne
        
    def nuota(self):
        print("Questo animale nuota")
        
    def get_pinne(self):
        print(self.__pinne)
        
salmone = Animale_Anfibio("Salmone", "sushi", 5)
salmone.get_pinne()
5
In [77]:
print(salmone.__pinne)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-77-157e0ebf4cdb> in <module>
----> 1 print(salmone.__pinne)

AttributeError: 'Animale_Anfibio' object has no attribute '__pinne'

Come vedete è proprio impossibile accedere dall'esterno al valore di pinne

Per scrivere i getter e i setter ci sono vari modi, questo che segue usa i decoratori standard

Ovvero @property e @name.setter

In [81]:
class Portal:
 
    def __init__(self):
        self.__name =''
     
    # Using @property decorator
    @property
     
    # Getter method
    def name(self):
        print("Hai usato il getter!")
        return self.__name
     
    # Setter method
    @name.setter
    def name(self, val):
        print("Hai usato il setter!")
        self.__name = val

        
p = Portal();

p.name = 'GeeksforGeeks'

print (p.name)
Hai usato il setter!
Hai usato il getter!
GeeksforGeeks

Sfide!¶

Passiamo ora alle sfide!

Nuka Cola Recipe 1/2¶

Siete assunti per scrivere il software che gestisce il mixing e le ricette della famosa e mirabolante NukaCola Corporation.

Dovete scrivere il software che rappresenta ogni ricetta a partire dalla BasicCola.

La BasicCola è, appunto, la base di ogni altra ricetta. Traccia l'ammontare di acqua, zucchero e caffeina. L'acqua normalmente è sempre 300g (per le dosi da laboratorio). La ricetta deve essere in grado di stampare il proprio peso totale con un metodo appropriato.

Nuka Cola Recipe 2/2¶

La ricetta pià famosa ovviamente è quella della NukaCola, derivata dalla base grazie all'aggiunta dell'ingredienti segreto: il cesio. E' un additivo dal sapore energico vagamente radioattivo ma la gente ne va matta e quindi... \spallucce :D

Aggiungete al software un pezzo che permetta di modellare anche la ricetta della NukaCola che quindi tenga traccia del cesio.

Per calcolare il peso totale della NukaCola però bisogna tenere a mente il decadimento radioattivo del cesio, perchè una bottiglia creata oggi o una creata 4 anni fa hanno un peso ben diverso. Per calcolare il peso occorre fare cesio * 0.97716^anni

Scrivete il codice più breve possibile (con creanza!) che vi faccia avvantaggiare della struttura a classi!¶

La Caffetteria 1/2¶

Scrivi una classe Caffetteria che abbia 3 variabili:

  • nome (stringa)
  • menu (lista di MenuItem)
  • ordini (array vuoto)

Ogni MenuItem è composto da

  • nome
  • tipo ("cibo"/"bevanda")
  • prezzo

La Caffetteria 2/2¶

Aggiungi alla Caffetteria questi sette metodi:

  • aggiungiOrdine, prende una tupla (MenuItem, numero_tavolo) e l'aggiunge agli ordini
  • completaOrdine, che prende il primo ordine nella lista e annuncia il suo completamento e poi lo toglie (se ordini è vuota lo scrive)
  • elencaOrdini, ritorna la lista degli ordini
  • totale, restituisce il conto di tutti i MenuItem nell'ordine
  • prezzoMinore, restituisce il MenuItem ordinato dal prezzo minore
  • bevandeOrdinate, ritorna i nomi delle bevande ordinate
  • cibiOrdinati, ritorna i nomi dei cibi ordinati

Grazie mille¶

Photo by Andre Mouton from Pexels: https://www.pexels.com/photo/closeup-photo-of-primate-1207875/

Photo by Torsten Dettlaff: https://www.pexels.com/photo/lighting-strike-67102/