Salta al contenuto
Note per Studenti Esercizio - Programmazione a oggetti in Python - listino, carrello, copie e ristorante (laboratorio 2)

Esercizio - Programmazione a oggetti in Python - listino, carrello, copie e ristorante (laboratorio 2)

In questa pagina 4

Quattro esercizi del laboratorio 2 (programmazione a oggetti in Python), con le soluzioni del corso di Intelligenza Artificiale, Ing. dell'Informazione UniPD. Il laboratorio prepara la struttura a classi (Problem, Node, Map) del laboratorio 3 (Esercizio - Algoritmi di ricerca in Python con Problem, Node e best-first search (laboratorio 3)). Gli argomenti sono in 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 → e Variabili, oggetti e riferimentiIn Python una variabile è un nome legato a un oggetto; tipo, identità e valore; assegnamento, aliasing, oggetti mutabili e immutabili, None.Variabili, oggetti e riferimenti →. Tutto il codice è stato eseguito e gli output sono quelli veri.

Esercizio 1 - e-commerce: Product, PriceList, Cart, PremiumPriceList

Testo. Si deve programmare il software per un sito di e-commerce, creando gli oggetti (e le classi) che rappresentano le entità necessarie: una classe Product, una classe PriceList, una classe Cart, una classe PremiumPriceList.

Product. Un prodotto ha: id (identificatore numerico univoco), name (nome), price (prezzo). Aggiungere un metodo print che stampa gli attributi del prodotto.

PriceList. Gestisce il listino di un negozio e contiene istanze di Product. Deve permettere di: aggiungere un Product al listino; trovare il prezzo di un prodotto dal suo id; calcolare il valore totale del listino (somma dei prezzi di ogni prodotto).

Cart. Oltre a PriceList, anche Cart usa Product. Deve permettere di: aggiungere un Product con la relativa quantità; calcolare il valore totale del carrello (somma dei prezzi dei prodotti contenuti, tenendo conto delle quantità).

PremiumPriceList. Eredita attributi e metodi da PriceList e aggiunge: (a) il calcolo del prezzo totale indicando il prodotto (tramite id) e la quantità; per una quantità superiore a quella definita nel listino premium si applica uno sconto (valore definito nel listino) al prezzo totale; (b) la copia dei prezzi da un altro PriceList: dati L1 e un L2 passato come parametro, tutti gli articoli di L2 con i relativi prezzi sono copiati in L1, tranne quelli già presenti in L1 (nessuna sovrascrittura). Per la prima funzione il listino premium avrà due variabili per gestire quantità e sconto.

Soluzione.

Una classe raggruppa dati (attributi) e operazioni (metodi); __init__ è il costruttore e self l'istanza su cui si agisce. Product ha solo attributi e un metodo di stampa:

python
class Product:
    def __init__(self, id: int, name: str, price: float):
        self.id = id
        self.name = name
        self.price = price

    def print(self):
        print("ID:" + str(self.id) + " PRODUCT:" + self.name + " PRICE:" + str(self.price))

PriceList tiene una lista di prodotti. find_price scorre la lista e restituisce None se l'id non c'è; total somma i prezzi:

python
class PriceList:
    def __init__(self):
        self.products = []

    def add(self, product: Product):
        self.products.append(product)

    def find_price(self, productId: int):
        for product in self.products:
            if product.id == productId:
                return product.price
        return None

    def total(self):
        total = 0
        for product in self.products:
            total += product.price
        return total

Per il carrello serve la quantità, che non è un attributo del prodotto ma della riga del carrello: la soluzione introduce una classe di supporto CartItem(product, quantity) con il prezzo di riga prezzo * quantità; il totale del carrello è la somma dei prezzi di riga.

python
class CartItem:
    def __init__(self, product, quantity):
        self.product = product
        self.quantity = quantity

    def total_price(self):
        return self.product.price * self.quantity

class Cart:
    def __init__(self):
        self.products = []

    def add(self, product, quantity):
        self.products.append(CartItem(product, quantity))

    def total(self):
        total = 0
        for cart_item in self.products:
            total += cart_item.total_price()
        return total

Una soluzione alternativa del corso inserisce lo stesso prodotto quantity volte in una lista e somma i prezzi: più corta, ma spreca memoria per quantità grandi.

PremiumPriceList eredita da PriceList (class PremiumPriceList(PriceList)): ha già add, total, products. Il suo costruttore chiama super().__init__() per creare la lista di prodotti e aggiunge i due attributi discount_quantity (soglia) e discount (sconto in frazione). find_price viene ridefinita (override) con un parametro in più; per i prezzi di un solo prodotto usa quella della superclasse tramite super():

python
class PremiumPriceList(PriceList):
    def __init__(self, discount_quantity: int, discount: float):
        super().__init__()
        self.discount_quantity = discount_quantity
        self.discount = discount

    def find_price(self, productId: int, quantity: int = 1):
        if quantity < 0:
            print("Only positive quantities")
            return
        price = super().find_price(productId)
        total = price * quantity
        if quantity < self.discount_quantity:
            return total
        else:
            return total * (1 - self.discount)

    def contains(self, productId):
        for product in self.products:
            if product.id == productId:
                return True
        return False

    def copy(self, pricelist):
        for product in pricelist.products:
            if not self.contains(product.id):
                self.add(product)

Prova (il main del corso, dentro if __name__ == "__main__":):

python
price_list_test = PriceList()
price_list_test.add(Product(1, 'pasta', 1.))
price_list_test.add(Product(2, 'rice', 2.))
print(f"The price of the article with id 1 is: {price_list_test.find_price(1)}")
print(f"The total value of the pricelist is : {price_list_test.total()}")

Output: The price of the article with id 1 is: 1.0 e The total value of the pricelist is : 3.0. Per il carrello con 2 paste a 1 e 3 di riso a 2: 2⋅1+3⋅2=82 \cdot 1 + 3 \cdot 2 = 8, output The total value of the cart is 8.0.

Per il listino premium con soglia 10 e sconto 0,1: find_price(1) dà 1.0; total() dà 3.0 (eredita quello di PriceList, senza quantità); find_price(1, 10) dà 1⋅10⋅(1−0,1)=9,01 \cdot 10 \cdot (1 - 0{,}1) = 9{,}0; find_price(2, 20) dà 2⋅20⋅0,9=36,02 \cdot 20 \cdot 0{,}9 = 36{,}0. Se si copia nel primo listino un secondo con i prodotti 1 ("pasta") e 3 ("hamburgers", prezzo 3) il prodotto 1 non viene ricopiato (già presente), il 3 sì: il totale diventa 1+2+3=6,01 + 2 + 3 = 6{,}0, come nell'output del corso.

Osservazioni sulla soluzione del corso.

  • Soglia. Il testo parla di quantità superiore alla soglia, ma il codice (if quantity < discount_quantity senza sconto) applica lo sconto anche a quantità uguale alla soglia: lo conferma il suo stesso esempio, in cui 10 pezzi con soglia 10 danno 9,0, cioè scontato. Per "strettamente maggiore" bisogna scrivere if quantity <= self.discount_quantity: return total.
  • Prodotto assente. find_price della superclasse restituisce None, e None * quantity solleva TypeError (verificato con find_price(99)).
  • copy condivide gli oggetti. Aggiunge al listino gli stessi oggetti Product dell'altro, non copie: se dopo la copia si cambia il prezzo nel secondo listino (p2.products[1].price = 10), cambia anche nel primo (find_price(3) dà 10). Per evitarlo basta copy.copy(product).
  • find_price della sottoclasse ha una firma diversa da quella della superclasse (un parametro in più): in Python va bene (l'argomento ha un valore predefinito), ma non è un override "puro".

Esercizio 2 - copia superficiale di una lista di liste

Testo. Partendo dalla lista old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]], creare una copia superficiale (shallow copy) e poi modificare la lista in modo da ottenere [[1, 1, 1], [2, 2, 'BB'], [3, 3, 3]].

Soluzione. Una copia superficiale crea una nuova lista esterna ma i suoi elementi (le tre liste interne) sono gli stessi oggetti dell'originale: l'indirizzo cambia solo al primo livello. Il modulo copy offre copy.copy:

python
import copy

old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.copy(old_list)
new_list[1][2] = 'BB'

print(old_list)   # [[1, 1, 1], [2, 2, 'BB'], [3, 3, 3]]
print(new_list)   # [[1, 1, 1], [2, 2, 'BB'], [3, 3, 3]]

Modificare new_list[1][2] cambia entrambe le liste, perché new_list[1] e old_list[1] sono lo stesso oggetto (old_list[1] is new_list[1] è True, mentre old_list is new_list è False). Lo stesso succede con la semplice assegnazione alias = old_list, che non copia niente: c'è un solo oggetto con due nomi (aliasing), vedi Variabili, oggetti e riferimentiIn Python una variabile è un nome legato a un oggetto; tipo, identità e valore; assegnamento, aliasing, oggetti mutabili e immutabili, None.Variabili, oggetti e riferimenti →.

Esercizio 3 - copia profonda di una lista di liste

Testo. Partendo dalla stessa lista, creare una copia profonda (deep copy) e poi modificarla in modo da ottenere [[1, 1, 1], [2, 2, 'BB'], [3, 3, 3]].

Soluzione. La copia profonda duplica ricorsivamente anche gli elementi interni, quindi new_list non condivide niente con old_list: la modifica non tocca l'originale.

python
import copy

old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.deepcopy(old_list)
new_list[1][2] = 'BB'

print(old_list)   # [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
print(new_list)   # [[1, 1, 1], [2, 2, 'BB'], [3, 3, 3]]

La differenza tra le due copie conta solo per gli oggetti composti (liste, dizionari, istanze di classi): per una lista di numeri o di stringhe, immutabili, le due coincidono. Nella ricerca (Ricerca nello spazio degli stati - formulazione dei problemi e ricerca non informataUn agente risolutore di problemi è un agente basato su obiettivi con rappresentazione atomica: formula un problema (stato iniziale, azioni, modello di transizione, test dell'obiettivo, costo dei passi) e cerca una sequenza di azioni che porta a uno stato obiettivo costruendo un albero di ricerca. Le strategie non informate usano solo la definizione del problema e si distinguono per l'ordine di espansione della frontiera: ampiezza (coda FIFO), costo uniforme (coda con priorità sul costo g), profondità (pila LIFO), profondità limitata, approfondimento iterativo e ricerca bidirezionale. Si confrontano per completezza, ottimalità, tempo e spazio in funzione di b, d, m; tutte sono casi di best-first search con una diversa funzione di valutazione f.Ricerca nello spazio degli stati - formulazione dei problemi e ricerca non informata →) il problema si presenta se uno stato è una lista modificata sul posto: i nodi dell'albero condividerebbero lo stato. Per questo gli stati del laboratorio 3 sono tuple (immutabili).

Esercizio 4 - la classe Restaurant

Testo. Scrivere una classe Python Restaurant con gli attributi menu_items, booked_table, customer_orders e i metodi add_item_to_menu(item, price), book_tables(table), customer_order(table, item). Nel main: aggiungere piatti al menu; prenotare dei tavoli; registrare ordini dei clienti; stampare il menu; stampare le prenotazioni; stampare gli ordini. Nota: usare dizionari e liste per memorizzare i dati.

Soluzione. Le scelte dei contenitori: il menu è un dizionario piatto →\to prezzo (accesso per nome); le prenotazioni sono una lista di numeri di tavolo; gli ordini sono una lista di dizionari {'table_number': ..., 'order': ...}, uno per piatto ordinato.

python
class Restaurant:
    def __init__(self):
        self.menu_items = {}
        self.booked_tables = []
        self.customer_orders = []

    def add_item_to_menu(self, item, price):
        self.menu_items[item] = price

    def book_tables(self, table):
        self.booked_tables.append(table)

    def customer_order(self, table, item):
        self.customer_orders.append({'table_number': table, 'order': item})

    def print_menu_items(self):
        for item, price in self.menu_items.items():
            print("{}: {}".format(item, price))

    def print_table_reservations(self):
        for table in self.booked_tables:
            print("Table {}".format(table))

    def print_customer_orders(self):
        for order in self.customer_orders:
            print("Table {}: {}".format(order['table_number'], order['order']))

restaurant = Restaurant()
for item, price in [("Cheeseburger", 9.99), ("Caesar Salad", 8), ("Grilled Salmon", 19.99),
                    ("French Fries", 3.99), ("Fish & Chips", 15)]:
    restaurant.add_item_to_menu(item, price)
for table in (1, 2, 3):
    restaurant.book_tables(table)
for table, item in [(1, "Cheeseburger"), (1, "Grilled Salmon"), (2, "Fish & Chips"), (2, "Grilled Salmon")]:
    restaurant.customer_order(table, item)

restaurant.print_menu_items()
restaurant.print_table_reservations()
restaurant.print_customer_orders()

Output:

Cheeseburger: 9.99
Caesar Salad: 8
Grilled Salmon: 19.99
French Fries: 3.99
Fish & Chips: 15
Table 1
Table 2
Table 3
Table 1: Cheeseburger
Table 1: Grilled Salmon
Table 2: Fish & Chips
Table 2: Grilled Salmon

(Nel codice del corso la voce del menu è "Fish & Chips:" con i due punti, mentre l'ordine è "Fish & Chips": l'ordine non corrisponde a nessuna voce del menu e il programma non se ne accorge. Qui il nome è lo stesso. Nel testo l'attributo è booked_table, nella soluzione book_table: qui booked_tables.)

Seconda soluzione del corso. Gli ordini sono un dizionario tavolo -> lista di piatti, il che raggruppa gli ordini per tavolo; e i metodi controllano che il piatto o il tavolo non siano già presenti (if item not in self.menu). Con due tavoli prenotati (1 e 9) e gli ordini del corso:

{'Pasta al pesto': 10, 'Pasta al pomodoro': 9, 'Tagliata di manzo': 15, 'Petto di pollo': 12, 'Gelato': 5, 'Torta di mele': 5}
[1, 9]
{1: ['Pasta al pesto', 'Tagliata di manzo', 'Gelato'], 9: ['Pasta al pomodoro', 'Petto di pollo']}

Il metodo aggiuntivo free_table(table) di quella soluzione ha un errore: fa del self.booked[table], che cancella l'elemento in posizione table della lista e non quello di valore table. Con booked = [1, 9], free_table(9) dà IndexError: list assignment index out of range; con booked = [1, 0], free_table(1) cancella lo 0 e lascia [1]. La forma corretta è self.booked.remove(table).

Versione ripasso

  • Classe: attributi più metodi; __init__ costruttore, self l'istanza; ereditarietà class Sotto(Sopra), super().__init__() e super().metodo(), override (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 →).
  • Es. 1, Product (id, name, price, print) e PriceList (products lista, add, find_price(id) che restituisce None se assente, total somma dei prezzi). Provando con pasta (1) e riso (2): prezzo 1.0, totale 3.0.
  • Cart: classe di supporto CartItem(product, quantity) con total_price = price * quantity; Cart.total somma le righe: 2 paste a 1 e 3 di riso a 2 danno 2+6=8,02 + 6 = 8{,}0.
  • PremiumPriceList(PriceList): attributi discount_quantity, discount; find_price(id, quantity) fa price * quantity e applica (1 - discount) se quantity >= soglia; soglia 10 e sconto 0,1: find_price(1, 10) = 9.0, find_price(2, 20) = 36.0; total() ignora le quantità. copy(L2) aggiunge solo i prodotti con id assente: totale 1+2+3=6,01 + 2 + 3 = 6{,}0.
  • Difetti della soluzione: sconto anche a quantità uguale alla soglia (il testo dice "superiore"); prodotto assente dà TypeError (None * q); copy condivide gli stessi oggetti Product (cambiare un prezzo li cambia in entrambi i listini).
  • Es. 2, copia superficiale copy.copy(old): lista esterna nuova, liste interne condivise; new[1][2] = 'BB' cambia entrambe; old[1] is new[1] è True, old is new è False. L'assegnazione alias = old non copia nulla (Variabili, oggetti e riferimentiIn Python una variabile è un nome legato a un oggetto; tipo, identità e valore; assegnamento, aliasing, oggetti mutabili e immutabili, None.Variabili, oggetti e riferimenti →).
  • Es. 3, copia profonda copy.deepcopy(old): copia ricorsiva, l'originale resta [[1,1,1],[2,2,2],[3,3,3]]. La differenza conta solo per oggetti composti. Negli stati di ricerca si usano tuple (immutabili).
  • Es. 4, Restaurant: menu = dizionario piatto →\to prezzo; prenotazioni = lista di tavoli; ordini = lista di dizionari {'table_number', 'order'} (o dizionario tavolo →\to lista di piatti); metodi add_item_to_menu, book_tables, customer_order e tre stampe.
  • Errore in free_table: del self.booked[table] cancella per posizione; [1, 9] con free_table(9) dà IndexError; giusto self.booked.remove(table).
  • Errori tipici: credere che la copia superficiale protegga le liste interne; dimenticare super().__init__() nel costruttore della sottoclasse; confondere la rimozione per posizione (del l[i]) con quella per valore (l.remove(v)).

Esercizi su questo argomento

Teoria collegata