Questa è la continuazione della precedente Parte 1 su Python dove avevo illustrato alcune funzioni base tramite l'uso diretto della console interattiva (py.exe / python3).
In questa parte illustro la gestione delle eccezioni, l'uso delle funzioni, i moduli e le classi.

Uso dei file


L'uso della console interattiva va bene per piccole prove, appena si deve creare qualche cosa di più complesso conviene creare un file, con estensione .py dove scrivere il proprio codice.
L'esecuzione del codice avviene con la sintassi
# windows
py .\nome_file.py

# linux
python3 .\nome_file.py

Il modo migliore per gestire i file è usare visual Studio Code
Visual Studio CodeVisual Studio Code

Eccezioni


Una parte importante di ogni programma è la corretta gestione delle eccezioni.
Python, per questo scopo, mette a disposizione le istruzioni try except else finally nella forma
try:
    # codice che potrebbe generare un eccezione
except:
    # gestione dell'eccezione
else:
    # opzionale
    # eseguita solo se NON vengono sollevate eccezioni
finally:
    # opzionale
    # istruzioni eseguite sempre
    # sia quando va tutto bene che dopo la gestione dell'eccezione
nella parte except si possono gestire vari tipi di eccezioni, ad esempio
#x = 1  # prova a togliere il commento
y = 0  # prova con y = 0
try:
  print(x / y)
except ZeroDivisionError as e:
  # qui gestisco solo le eccezioni di tipo ZeroDivisionError   
  print("div by zero")
except:
  # quella generica deve essere l'ultima
  print("error")
else:
  print("else")
finally:
  print("finally")
questo codice stampa a video
  • con x non definita => error, finally
  • con x=1 e y=1 => 1.0, else, finally
  • con x=1 e y=0 => div by zero, finally
Non vanno definite tutte le keyword, è possibile usare solo try except
tabellina = input("Tabellina: ")
try:
    t = int(tabellina)
    for n in range(1, 11):
        print(f"{n} x {t} = {n * t}")
except:
    print('Numero non valido')

Volendo posso anche sollevare un eccezione da codice tramite il comando raise nome_eccezione
# sollevo un eccezione specifica
raise ValueError

# all'interno di except: rilancio l'eccezione corrente
try:
    # eventuale errore
except:
    print("errore")
    # rilancio l'eccezione attuale
    raise 

Funzioni


Le funzioni in Python vengono definite tramite la keywork def
def nome_funzione(parametro1, parametro2, ...):
    # corpo della funzione, con eventuale valore di ritorno
    return valore
ad esempio la funzione
from datetime import datetime

def print_time(message, show_message = True):
    if show_message == True:
        print(f"{datetime.now()} {message}")
    else:
        print(datetime.now())
    print()
Da notare il parametro show_message a cui è stato assegnato un valore di default.
Per invocare una funzione si usa la sintassi nome_funzione(eventuali_parametri)
# ometto il parametro con il valore di default
print_time('Ora')    

# richiamo la funzione passando i parametri per nome
print_time(message='test', show_message=False)
# in questo caso l'ordine dei parametri non è importante
print_time(message='test', show_message=False)
l'esempio produce questo risultato
2019-10-10 22:04:30.746470 Ora

2019-10-10 22:04:30.747469

2019-10-10 22:04:30.748468
Attenzione, la funzione deve essere definita nel file prima che venga richiamata, altrimenti viene sollevata un eccezione di funzione non definita:

Traceback (most recent call last):
File ".\app.py", line 13, in <module>
print_time('Ora')
NameError: name 'print_time' is not defined
Argomenti infiiti

E' possibile definire una funzione per rifare in modo che accetti un numero variabile di parametri tramite *argv
def fn_multi(p1, *argv):
    print("p1", p1)
    for p in argv:
        print(p)

fn_multi("ciao", "come", "va", "altri parametri", "...")
da come risultato
p1 ciao
come
va
altri parametri
...
Oppure posso usare **kwargs per passare dei parametri in numero variabile per nome
def fn_multi(**kwargs):
    for key, value in kwargs.items():
        print(key, value)
 
fn_multi(primo="ciao", par1="come", parN="va")
da come risultato
primo ciao
par1 come
parN va
Da notare la sintassi del for per ciclare sia sul nome del parametro (key) che sul valore (value)

Moduli


Quando il codice diventa complesso, è impensabile gestire tutto in un unico file, conviene suddividere in codice in più parti, dette moduli, da richiamare quando necessario.
Ogni modulo è un diverso file con estensione .py.

Ad esempio possiamo creare un modulo di nome helpers.py con una libreria di nostre funzioni di utilità generale
# helpers.py

def display(message, is_warning=False):
    if is_warning:
        print(f"Warning!! {message}")
    else:
        print(message)

def somma(op1, op2):
    return op1 + op2
in questo caso una funzione per stampare un log nella console e una per sommare due numeri.
La keyword return, nella funzione somma, viene usata per restituire il risultato della funzione.

Per usarlo in un altro file, dobbiamo prima importarlo con import nome_modulo
# app.py
import helpers

# richiamo la funzione con la sintassi nome_modulo.nome_funzione
helpers.display("Funziona :-)", True)
# Warning!! Funziona :-)

print(helpers.somma(3, 5))
# 8
se non servono tutte le funzioni, è inutile importare tutto il modulo, si può importare solo quella che serve con le keyword from ... import ...
# app.py
from helpers import display

# ho importato solo la funzione "display", la richiamo senza il prefisso del modulo
display("Ciao")
# Ciao
posso importare tutte le funzioni con l'asterisco *
from helpers import *
nel caso il nome della funzione andasse in conflitto con un'altra già esistente, posso rinominarla con la keyword as
# app.py
from helpers import display as prt

# non esiste la funzione "display" è diventata "prt"
prt("Ciao")
infine, nel caso avessi un conflitto a livello di nome del modulo, posso rinominare tutto il modulo
# app.py
import helpers as mia_lib

print(mia_lib.somma(2,5))
# 7

Classi


Come dicevo nella Parte 1, Python è un linguaggio orientato agli oggetti.
La sintassi per definire una classe è la seguente che fa uso della keyword class
class NomeClasse:
   proprieta_condivisa = valore

    def __init__(self, parametro_1, parametro_2, ...):
        # corpo del costruttore + proprietà
        self.proprieta_1 = parametro_1
        self.proprieta_2 = parametro_2
        self.proprieta_N = parametro_N

    def nome_metodo(self, parametro_a, parametro_b, ...):
        # corpo del metodo
La convenzione per i nomi delle classi è quella di usare la notazione CamelCase
Gli aspetti su cui focalizzarsi sono:
  • la classe si definisce tramite la keyword class
  • il costruttore è una funzione con il nome riservato __init__
  • il primo parametro del costruttore e degli altri metodi deve sempre essere self
  • le proprietà si definiscono con la sintassi self.nome_proprieta e sono valide a livello di istanza (es.: self.proprieta_1)
  • le proprietà definite a livello di classe son condivise tra tutte le istanze (es.: proprieta_condivisa), vanno modificate con la sintassi NomeClasse.proprieta
per istanziare una classe la sintassi è
variabile = NomeClasse(eventuali_parametri)
Da nota la mancanza di una keyword prima del nome della classe, ad esempio new usata da JavaScript o C#
Per capire come funziona, creiamo una classe di esempio Macchina
class Macchina:
    # definisco una proprietà a livello di classe
    modello = "Ford"

    # quando istanzio la classe devo passare un identificativo e una velocità iniziale
    def __init__(self, id_macchina, velocita):
        self.velocita_attuale = velocita
        self.id = id_macchina

    # con questo metodo spengo il motore, velocità = 0
    def spegni_motore(self):
        self.velocita_attuale = 0

    # con questo lo accendo ad un valore di default, velocità = 1
    def accendi_motore(self):
        self.velocita_attuale = 1

    # con questo metodo posso impostare una velocità specifica
    def imposta_velocita(self, velocita):
        if velocita >= 0 and velocita < 10:
            self.velocita_attuale = velocita

    # questo metodo lo uso per cambiare la proprietà a livello di classe condivisa tra le istanze
    def imposta_modello(self, modello):
        # accesso alla proprietà a livello di classe
        Macchina.modello = modello

    # infine, un metodo che mi restituisce lo stato delle macchina
    def info(self):
        return f"Veicolo: {self.id}, velocità: {self.velocita_attuale}, modello: {Macchina.modello}"
istanziamole, macchina_uno e macchina_due,' invocando metodi' (il risultato è nei commenti)
# creo due macchine
macchina_uno = Macchina(1, 3)
macchina_due = Macchina(2, 1)

print("Start:", macchina_uno.info(), "|",  macchina_due.info())
# Start: Veicolo: 1, velocità: 3, modello: Ford | Veicolo: 2, velocità: 1, modello: Ford

macchina_uno.spegni_motore()
print("Spegni  1 =>", macchina_uno.info(), "|",  macchina_due.info())
# Spegni  1 => Veicolo: 1, velocità: 0, modello: Ford | Veicolo: 2, velocità: 1, modello: Ford

macchina_uno.accendi_motore()
print("Accendi 1 =>", macchina_uno.info(), "|",  macchina_due.info())
# Accendi 1 => Veicolo: 1, velocità: 1, modello: Ford | Veicolo: 2, velocità: 1, modello: Ford

macchina_uno.imposta_velocita(7)
print("Imposta 1 =>", macchina_uno.info(), "|",  macchina_due.info())
# Imposta 1 => Veicolo: 1, velocità: 7, modello: Ford | Veicolo: 2, velocità: 1, modello: Ford

macchina_due.imposta_velocita(9)
print("Imposta 2 =>", macchina_uno.info(), "|",  macchina_due.info())
# Imposta 2 => Veicolo: 1, velocità: 7, modello: Ford | Veicolo: 2, velocità: 9, modello: Ford

macchina_due.imposta_modello("Fiat")
print("Modello 2 =>", macchina_uno.info(), "|",  macchina_due.info())
# Modello 2 => Veicolo: 1, velocità: 7, modello: Fiat | Veicolo: 2, velocità: 9, modello: Fiat

Rivedi la Parte 1 oppure prosegui con la Parte 3