Salta al contenuto
Note per Studenti Esercizio 15 · archivio di libri con eccezioni

Esercizio 15archivio di libri con eccezioni

Esame
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".

  1. Completare la classe ArchivioLibri: costruttore e metodi aggiungi(codice, titolo, copie), ricerca(codice), cancella(codice), modifica(codice, quantita) e __str__. L'elemento dell'array è una Coppia con il codice (chiave) e il Libro (attributo), confrontabile per codice. ricerca, cancella e modifica devono lanciare un'eccezione (NoSuchElementException in Java) se il codice non è presente.
  2. Programma di prova: costruisce l'archivio dal file libri.dat e lo stampa; effettua le modifiche contenute in librivar.dat e stampa di nuovo l'archivio. In librivar.dat ogni 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). cancella e modifica possono 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;2
librivar.dat        librierr.dat
B002 -1             B999
B004                B002 -5
B003 2              B777 2
B001 -3             xx
                    B003 abc

Struttura dei dati

L'array contiene le Coppia ordinate per codice: l'ordine fa trovare un codice con la ricerca binaria in O(log⁡n)O(\log n) (bisect_left con il parametro key) e rende __str__ ordinato senza altro lavoro; l'inserimento e la cancellazione costano O(n)O(n) 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); altrimenti insert(i, ...);
  • ricerca, cancella: se non trovato, raise KeyError(codice);
  • modifica: usa ricerca (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 di ValueError (un valore inammissibile), così il chiamante distingue "codice inesistente" da "copie insufficienti".

Codice

python
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:

python
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 copie

Dopo 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 copie

librierr.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

python
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 ValueError prima di except 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 O(n)O(n) invece di O(log⁡n)O(\log n)); oppure cercare con bisect su 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=...) O(log⁡n)O(\log n); inserimento e cancellazione O(n)O(n).

  • aggiungi: codice presente → sostituisce (chiave unica), altrimenti insert(i, ...).
  • ricerca/cancella: assente → raise KeyError(codice).
  • modifica: ricerca (solleva già KeyError); se copie + quantita < 0 → CopieInsufficienti(ValueError); poi setquantita.

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.

Teoria collegata