Esercizio 15archivio di libri con eccezioni
In questa pagina 5
Testo (appello del 2 settembre 2025, canale C, di Fondamenti di Informatica, Ingegneria dell'Informazione UniPD; adattato da un tema d'esame in Java, qui in Python; i formati dei file sono ricostruiti perché il testo non li fissa).
Si gestisce un archivio di libri con un dizionario a chiave unica (mappa) realizzato con un array. La classe Libro (da non modificare) ha il titolo e il numero di copie presenti, con numcopie(), titololib(), setquantita(q) e una __str__ del tipo "del libro titolo ci sono n copie".
- Completare la classe
ArchivioLibri: costruttore e metodiaggiungi(codice, titolo, copie),ricerca(codice),cancella(codice),modifica(codice, quantita)e__str__. L'elemento dell'array è unaCoppiacon ilcodice(chiave) e ilLibro(attributo), confrontabile per codice.ricerca,cancellaemodificadevono lanciare un'eccezione (NoSuchElementExceptionin Java) se il codice non è presente. - Programma di prova: costruisce l'archivio dal file
libri.date lo stampa; effettua le modifiche contenute inlibrivar.date stampa di nuovo l'archivio. Inlibrivar.datogni riga contiene un codice: se compare solo il codice il libro va cancellato; se compare il codice e un numero intero, il numero è la variazione delle copie (numero < 0: copie richieste in prestito;numero > 0: copie restituite).cancellaemodificapossono lanciare l'eccezione: il programma di prova deve intercettarla e stampare il codice errato. Per provarlo c'èlibrierr.dat, con codici non presenti oppure con più copie richieste di quelle presenti.
Teoria: Eccezioni in PythonEccezioni e traceback, eccezioni predefinite più comuni, try/except/else/finally, raise per segnalare errori, propagazione lungo le chiamate.Eccezioni in Python →, Classi ed ereditarietà in PythonAttributi di istanza e di classe, metodi di istanza, di classe e statici; confronto e ordinamento con eq e lt; proprietà; ereditarietà, super(), override e polimorfismo; classi astratte come interfacce; eccezioni personalizzate; overloading e shadowing in Python.Classi ed ereditarietà in Python →, Realizzare contenitori su array e listeCome si realizza un ADT contenitore partendo da un array: lunghezza logica e capacità con raddoppio (costo ammortizzato), dizionario su array ordinato con ricerca binaria, coda doppia su array circolare, coda con priorità a livelli, ADT costruiti sopra altri ADT (pila di code, pila reversibile); tabella dei costi.Realizzare contenitori su array e liste →, File di record e controllo degli errori riga per rigaSchema per leggere un file (o lo standard input) di record, uno per riga: formati con separatore, controllo di ogni riga, messaggi di errore con il numero di riga su standard error, righe da saltare, fine dell'input su riga vuota; versione in Python con split, int, float e eccezioni; versione in C con fgets, sscanf e strtol.File di record e controllo degli errori riga per riga →.
I file usati qui (formato scelto: codice;titolo;copie e codice [variazione]):
libri.dat
B003;Neuromante;5
B001;Il nome della rosa;3
B004;I promessi sposi;1
B002;Dune;2librivar.dat librierr.dat
B002 -1 B999
B004 B002 -5
B003 2 B777 2
B001 -3 xx
B003 abcStruttura dei dati
L'array contiene le Coppia ordinate per codice: l'ordine fa trovare un codice con la ricerca binaria in (bisect_left con il parametro key) e rende __str__ ordinato senza altro lavoro; l'inserimento e la cancellazione costano per gli spostamenti (Realizzare contenitori su array e listeCome si realizza un ADT contenitore partendo da un array: lunghezza logica e capacità con raddoppio (costo ammortizzato), dizionario su array ordinato con ricerca binaria, coda doppia su array circolare, coda con priorità a livelli, ADT costruiti sopra altri ADT (pila di code, pila reversibile); tabella dei costi.Realizzare contenitori su array e liste →). La Coppia è una classe interna, definita dentro ArchivioLibri perché serve solo a lei, con __lt__ sul codice.
Una funzione privata _posto(codice) restituisce (indice, trovato); la usano tutti i metodi:
aggiungi: se il codice c'è già, sostituisce il libro (chiave unica); altrimentiinsert(i, ...);ricerca,cancella: se non trovato,raise KeyError(codice);modifica: usaricerca(che solleva già l'eccezione) e verifica che le copie restino non negative; se se ne chiedono più di quelle presenti si solleva una seconda eccezione, personalizzata:CopieInsufficienti, sottoclasse diValueError(un valore inammissibile), così il chiamante distingue "codice inesistente" da "copie insufficienti".
Codice
import io
from bisect import bisect_left
class Libro: # NON MODIFICARE
def __init__(self, titolo, numero):
self._titolo, self._numero = titolo, numero
def numcopie(self):
return self._numero
def titololib(self):
return self._titolo
def setquantita(self, q):
self._numero = q
def __str__(self):
return f"del libro {self._titolo} ci sono {self._numero} copie"
class CopieInsufficienti(ValueError):
"""si chiedono più copie di quelle presenti"""
class ArchivioLibri:
"""Dizionario a chiave unica (codice) realizzato con un array ordinato di Coppia."""
class Coppia:
def __init__(self, codice, libro):
self.codice, self.libro = codice, libro
def __lt__(self, altra):
return self.codice < altra.codice
def __init__(self):
self._a = [] # Coppia ordinate per codice
def _posto(self, codice): # O(log n)
i = bisect_left(self._a, codice, key=lambda c: c.codice)
return i, i < len(self._a) and self._a[i].codice == codice
def aggiungi(self, codice, titolo, copie):
i, trovato = self._posto(codice)
if trovato:
self._a[i].libro = Libro(titolo, copie) # chiave unica: si sostituisce
else:
self._a.insert(i, self.Coppia(codice, Libro(titolo, copie)))
def ricerca(self, codice):
i, trovato = self._posto(codice)
if not trovato:
raise KeyError(codice)
return self._a[i].libro
def cancella(self, codice):
i, trovato = self._posto(codice)
if not trovato:
raise KeyError(codice)
del self._a[i]
def modifica(self, codice, quantita):
libro = self.ricerca(codice) # KeyError se manca
if libro.numcopie() + quantita < 0:
raise CopieInsufficienti(codice)
libro.setquantita(libro.numcopie() + quantita)
def __str__(self):
return "\n".join(f"{c.codice}: {c.libro}" for c in self._a)Programma di prova
La lettura è per righe (File di record e controllo degli errori riga per rigaSchema per leggere un file (o lo standard input) di record, uno per riga: formati con separatore, controllo di ogni riga, messaggi di errore con il numero di riga su standard error, righe da saltare, fine dell'input su riga vuota; versione in Python con split, int, float e eccezioni; versione in C con fgets, sscanf e strtol.File di record e controllo degli errori riga per riga →); per ogni riga di librivar.dat si decide l'azione dal numero di campi (split()): uno solo → cancella, due → modifica. Le eccezioni si intercettano per tipo, dal più specifico al più generale:
def carica(f):
a = ArchivioLibri()
for riga in f:
riga = riga.strip()
if riga:
codice, titolo, copie = riga.split(";")
a.aggiungi(codice, titolo, int(copie))
return a
def applica(a, f, out):
for n, riga in enumerate(f, start=1):
campi = riga.split()
if not campi:
continue
try:
if len(campi) == 1:
a.cancella(campi[0])
else:
a.modifica(campi[0], int(campi[1]))
except KeyError as e: # codice non presente
print(f"codice errato: {e.args[0]}", file=out)
except CopieInsufficienti as e: # PRIMA di ValueError: ne è una sottoclasse
print(f"copie insufficienti per {e}", file=out)
except ValueError: # int("abc")
print(f"riga {n}: numero non valido", file=out)
LIBRI = "B003;Neuromante;5\nB001;Il nome della rosa;3\nB004;I promessi sposi;1\nB002;Dune;2\n"
VAR = "B002 -1\nB004\nB003 2\nB001 -3\n"
ERR = "B999\nB002 -5\nB777 2\nxx\nB003 abc\n"
a = carica(io.StringIO(LIBRI))
print(a)
errori = io.StringIO()
applica(a, io.StringIO(VAR), errori)
print("--- dopo librivar.dat")
print(a)
applica(a, io.StringIO(ERR), errori)
print("--- errori intercettati da librierr.dat")
print(errori.getvalue())Primo print(a) (ordinato per codice anche se il file non lo era):
B001: del libro Il nome della rosa ci sono 3 copie
B002: del libro Dune ci sono 2 copie
B003: del libro Neuromante ci sono 5 copie
B004: del libro I promessi sposi ci sono 1 copieDopo librivar.dat: B002 -1 lascia 1 copia di Dune; B004 cancella I promessi sposi; B003 2 porta Neuromante a 7; B001 -3 porta Il nome della rosa a 0 copie (il libro resta in archivio con 0 copie: il testo non chiede di cancellarlo).
B001: del libro Il nome della rosa ci sono 0 copie
B002: del libro Dune ci sono 1 copie
B003: del libro Neuromante ci sono 7 copielibrierr.dat produce, senza interrompere il programma:
codice errato: B999 (cancella: codice assente)
copie insufficienti per B002 (B002 ha 1 copia, se ne chiedono 5)
codice errato: B777 (modifica: codice assente)
codice errato: xx (una riga con il solo "xx" è una cancellazione di un codice assente)
riga 5: numero non valido (B003 abc: la variazione non è un intero)Lo stato dell'archivio non cambia: la modifica con copie insufficienti viene rifiutata prima di toccare Libro.
Verifica automatica
a = carica(io.StringIO(LIBRI))
applica(a, io.StringIO(VAR), io.StringIO())
assert [(c.codice, c.libro.numcopie()) for c in a._a] == [("B001", 0), ("B002", 1), ("B003", 7)]
for codice in ("B999", "B004"):
for azione in (a.ricerca, a.cancella):
try:
azione(codice)
except KeyError:
pass
else:
raise AssertionError(azione.__name__ + " doveva fallire")
try:
a.modifica("B002", -2)
except CopieInsufficienti:
pass
assert a.ricerca("B002").numcopie() == 1 # invariato
a.aggiungi("B002", "Dune II", 9) # chiave unica: sostituisce
assert len(a._a) == 3 and a.ricerca("B002").titololib() == "Dune II"
print("ok")Errori comuni
- Mettere
except ValueErrorprima diexcept CopieInsufficienti: la prima clausola cattura anche la sottoclasse e la seconda non si raggiunge mai. - Cercare con una scansione lineare quando l'array è ordinato (funziona, ma costa invece di ); oppure cercare con
bisectsu un array non ordinato. - Inserire un codice già presente creando un duplicato (viola la chiave unica).
- Aggiornare le copie prima di controllare che il risultato non sia negativo.
- Stampare un messaggio generico: il testo chiede di stampare il codice errato.
- Confondere cancellazione (riga con un solo campo) e modifica (due campi): decidere dal numero di campi, non dal contenuto.
Versione ripasso
Appello del 2 settembre 2025 (UniPD, in Java; adattato a Python, formati dei file ricostruiti). Teoria: Eccezioni in PythonEccezioni e traceback, eccezioni predefinite più comuni, try/except/else/finally, raise per segnalare errori, propagazione lungo le chiamate.Eccezioni in Python →, Realizzare contenitori su array e listeCome si realizza un ADT contenitore partendo da un array: lunghezza logica e capacità con raddoppio (costo ammortizzato), dizionario su array ordinato con ricerca binaria, coda doppia su array circolare, coda con priorità a livelli, ADT costruiti sopra altri ADT (pila di code, pila reversibile); tabella dei costi.Realizzare contenitori su array e liste →.
Struttura. Array di Coppia (codice, Libro) ordinate per codice (__lt__; classe interna), _posto(codice) → (indice, trovato) con bisect_left(..., key=...) ; inserimento e cancellazione .
aggiungi: codice presente → sostituisce (chiave unica), altrimentiinsert(i, ...).ricerca/cancella: assente →raise KeyError(codice).modifica:ricerca(solleva giàKeyError); secopie + quantita < 0→CopieInsufficienti(ValueError); poisetquantita.
Prova. Per ogni riga di librivar.dat: un campo → cancella, due → modifica(codice, int(campo)). except KeyError (stampa il codice errato), except CopieInsufficienti, poi except ValueError (sottoclasse prima della classe base). Nessuna interruzione, archivio invariato in caso di errore.
Esempio: dopo librivar.dat: B001 0 copie, B002 1, B003 7, B004 cancellato.
Errori comuni: except ValueError prima della sottoclasse; scansione lineare o bisect su array non ordinato; duplicati; copie aggiornate prima del controllo; messaggio senza codice; cancellazione e modifica confuse.