Il codice originale del nostro piccolo programma, con le correzione della pillola precedente, dovrebbe avere quest’aspetto:
from datetime import datetime
def is_leap(year):
return year % 4 == 0 and year % 100 != 0
def __increase(start, stop, items):
c = 0
for a in range(start, stop):
c += items[a]
return c
def int_input(msg, min, max):
try:
r = int(input(f"{msg} [{min}-{max}]? "))
if r < min or r > max:
raise Exception()
return r
except Exception:
print(f"Error: Please insert an integer from {min} to {max}")
return int_input(msg, min, max)
def calc_gap(source_year, source_month, source_day, target_month, target_day):
month_days = [ 31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]
if source_month == target_month:
# Sto lavorando su un intervallo breve dello stesso mese di partenza e di arrivo
if source_day < target_day:
return target_day - source_day
month_days[2] = 29 if is_leap(year) else 28
gap = target_day
if source_month < target_month:
gap += __increase(source_month, target_month, month_days)
else:
gap += __increase(source_month, len(month_days), month_days)
month_days[2] = 29 if is_leap(year + 1) else 28
gap += __increase(1, target_month, month_days)
gap -= source_day
return gap
if __name__ == '__main__':
year = int(datetime.today().strftime('%Y'))
month = int(datetime.today().strftime('%m'))
day = int(datetime.today().strftime('%d'))
target_month = int_input("Target month", 1, 12)
target_day = int_input("Target day", 1, 31)
print(f"{calc_gap(year, month, day, target_month, target_day)} days to go")
calc_gap fa piuttosto paura. cinque parametri! Facile scordarsi l’ordine con cui vanno inseriti. Possiamo chiamarli per nome, ma tutto diventa ancora più prolisso!
calc_gap(source_year = year, source_month = month, target_month=tgt_month, source_day = day, target_day = tgt_day)
Faticoso! Oltretutto in questo codice ci sono funzioni e variabili che hanno senso soltanto qui. Sarebbe notevole dare una struttura più solida e circoscritta! Possiamo farlo definendo una classe.
class DaysGap:
month_days = [ 31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]
def __init__(self):
self.source_year = datetime.today().year
self.source_month = datetime.today().month
self.source_day = datetime.today().day
Questa non è la classe completa ma definisce molti concetti chiave.
class DaysGap: definisce una classe con nome DaysGap per convenzione in Python le classi usano al notazione CamelCase. In cui tutte le parole hanno l’iniziale maiuscola e GliSpaziSpariscono. Come vedete nomi composti, come il nostro, restano abbastanza leggibili.
In Python tutte le classi hanno un def __init__(self, ...) se non c’è è implicito. Si tratta del costruttore della classe, grazie a lui possiamo istanziare oggetti. Si noti la presenza di self, all’interno di una classe consente di accedere espressamente a metodi (le funzioni) e proprietà (le variabili) definite nella classe. Complicato? Prendiamo per esempio self.source_year = datetime.today().year associa un valore alla property e lo rende accessibile da tutti i metodi della classe.
Notare anche che abbiamo modificato il modo di caricare il valore dell’anno. Così è più immediato e corretto (non ho bisogno di passare da una stringa per ottenere il valore intero) l’approccio usato prima però ci ha fatto imparare diverse cosette.
from datetime import datetime
class DaysGap:
month_days = [ 31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]
def __init__(self):
self.source_year = datetime.today().year
self.source_month = datetime.today().month
self.source_day = datetime.today().day
def is_leap(self):
return self.source_year % 4 == 0 and self.source_year % 100 != 0
def __increase(self, start, stop, items):
c = 0
for a in range(start, stop):
c += items[a]
return c
def calc_gap(self, target_month, target_day):
if self.source_month == target_month:
# Sto lavorando su un intervallo breve dello stesso mese di partenza e di arrivo
if self.source_day < target_day:
return target_day - self.source_day
self.month_days[2] = 29 if self.is_leap() else 28
gap = target_day
if self.source_month < target_month:
gap += self.__increase(self.source_month, target_month, self.month_days)
else:
gap += self.__increase(self.source_month, len(self.month_days), self.month_days)
self.source_year += 1
self.month_days[2] = 29 if self.is_leap() else 28
gap += self.__increase(1, target_month, self.month_days)
gap -= self.source_day
return gap
Questa è la nostra classe completa. Siete invitati a confrontare con la versione precedente alla ricerca di tutte le differenze. Intanto abbiamo dovuto indentare tutto il codice, che deve stare all’interno della definizione della classe class DaysGap:e poi ci sono self un po’ dappertutto.
Guardate soprattutto la definizione di is_leap. In origine prendeva il parametro year, ora prende unicamente self e all’interno usa self.source_year. Il risultato però è che altrove nel codice della classe abbiamo dovuto scrivere:
self.source_year += 1
self.month_days[2] = 29 if self.is_leap() else 28
Potrebbe migliorare? Direi di sì
def is_leap(self, year=None):
if year is None:
year = self.source_year
return year % 4 == 0 and year % 100 != 0
Ora funziona con o senza l’anno assegnato esplicitamente!
Notare il (non) valore None, per quasi tutti gli altri linguaggi è null. Significa che la variabile (o il parametro) non è assegnato. E’ diverso da False o da 0 (zero), e notate che viene confrontato con l’operatore is.
Manca un pezzo di codice, eccolo.
def int_input(msg, min, max):
try:
r = int(input(f"{msg} [{min}-{max}]? "))
if r < min or r > max:
raise Exception()
return r
except Exception:
print(f"Error: Please insert an integer from {min} to {max}")
return int_input(msg, min, max)
if __name__ == '__main__':
target_month = int_input("Target month", 1, 12)
target_day = int_input("Target day", 1, 31)
days_gap = DaysGap()
print(f"{days_gap.calc_gap(target_month, target_day)} days to go")
Notate che int_input(...) non è stato trasformato in un metodo della nostra classe. Il motivo è che non fa parte del contesto di cui si occupa la classe, non ha nulla a che fare con le date, esegue unicamente un controllo sul dato in input.
La parte più importante è però alla fine: days_gap = DaysGap() rende days_gap l’istanza della nostra classe DaysGap sotto il cofano DaysGap() altro non fa che chiamare il costruttore __init__(self). Si noti tuttavia che DaysGap() non ha alcun parametro! e che self, di fatto, viene restituito dal costruttore e per noi diventa days_gap. Anche days_gap.calc_gap(target_month, target_day) non considera affatto self. Tutto nasce però sempre da days_gap.calc_gap(...) è come se avessimo chiamato DaysGap.calc_gap(days_gap, ...) ed è vero. Provate! Ma poi usate la sintassi originale molto più chiara e stringata.
Parecchi miglioramenti ma anche un problema insidioso che vedremo prossimamente.