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:
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:
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 totalPer 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.
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 totalUna 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():
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__":):
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: , 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à ; find_price(2, 20) dà . 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 , 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_quantitysenza 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 scrivereif quantity <= self.discount_quantity: return total. - Prodotto assente.
find_pricedella superclasse restituisceNone, eNone * quantitysollevaTypeError(verificato confind_price(99)). copycondivide gli oggetti. Aggiunge al listino gli stessi oggettiProductdell'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 bastacopy.copy(product).find_pricedella 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:
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.
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 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.
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,selfl'istanza; ereditarietàclass Sotto(Sopra),super().__init__()esuper().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) ePriceList(productslista,add,find_price(id)che restituisceNonese assente,totalsomma dei prezzi). Provando con pasta (1) e riso (2): prezzo 1.0, totale 3.0. Cart: classe di supportoCartItem(product, quantity)contotal_price = price * quantity;Cart.totalsomma le righe: 2 paste a 1 e 3 di riso a 2 danno .PremiumPriceList(PriceList): attributidiscount_quantity,discount;find_price(id, quantity)faprice * quantitye applica(1 - discount)sequantity >= 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 conidassente: totale .- Difetti della soluzione: sconto anche a quantità uguale alla soglia (il testo dice "superiore"); prodotto assente dà
TypeError(None * q);copycondivide gli stessi oggettiProduct(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'assegnazionealias = oldnon 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 prezzo; prenotazioni = lista di tavoli; ordini = lista di dizionari{'table_number', 'order'}(o dizionario tavolo lista di piatti); metodiadd_item_to_menu,book_tables,customer_ordere tre stampe. - Errore in
free_table:del self.booked[table]cancella per posizione;[1, 9]confree_table(9)dàIndexError; giustoself.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)).