# --- # jupyter: # jupytext: # formats: ipynb,py:percent # text_representation: # extension: .py # format_name: percent # format_version: '1.3' # jupytext_version: 1.14.0 # kernelspec: # display_name: Python 3 (ipykernel) # language: python # name: python3 # --- # %% [markdown] toc=true slideshow={"slide_type": "notes"} #

Table of Contents

#
# %% [markdown] slideshow={"slide_type": "slide"} # # Fondamenti di Programmazione # # # **Andrea Sterbini** # # lezione 15 - 24 novembre 2022 # %% slideshow={"slide_type": "notes"} init_cell=true # %load_ext nb_mypy # %% RECAP [markdown] slideshow={"slide_type": "slide"} # # RECAP: CLASSI e oggetti # - classi: attributi (di classe) e metodi # - istanze: attributi di istanza e metodi # - information hiding e "responsabilità" delle operazioni # - ereditarietà come meccanismo per estendere un tipo di oggetti # %% RECAP [markdown] slideshow={"slide_type": "slide"} # ## Colori # - operazioni matematiche sui colori (somma, prodotto, ...) # - costruttore # - rappresentazione (**`__repr__`** e **`__str__`**) # - conversione in tripla RGB # %% RECAP slideshow={"slide_type": "slide"} code_folding=[14, 54] import images from random import randint class Colore: white : 'Colore' black : 'Colore' red : 'Colore' green : 'Colore' blue : 'Colore' cyan : 'Colore' purple: 'Colore' yellow: 'Colore' grey : 'Colore' def __init__(self, R : float, G : float, B : float): "un colore contiene i tre canali R,G,B" self._R = R self._G = G self._B = B # può far comodo avere un secondo costruttore che genera colori casuali @classmethod def random(cls, m : int =0, M : int = 255) -> 'Colore' : # -> Colore casuale "torna un colore casuale con i valori delle luminosità in [m .. M]" return cls(randint(m,M), randint(m,M), randint(m,M)) def luminosità(self) -> float: # luminosità del colore "calcolo la luminosità media di un pixel (senza badare se è un valore intero)" return (self._R + self._G + self._B)/3 def __add__(self, other : 'Colore') -> 'Colore': "somma tra due colori" if not isinstance(other, Colore): raise ValueError("Il secondo addendo non è un Colore") return Colore(self._R + other._R, self._G + other._G, self._B + other._B) def __sub__(self, other : 'Colore') -> 'Colore': "somma tra due colori" if not isinstance(other, Colore): raise ValueError("Il secondo addendo non è un Colore") return Colore(self._R - other._R, self._G - other._G, self._B - other._B) def __mul__(self, k : float) -> 'Colore': "moltiplicazione di un colore per una costante" # FIXME: controllare che k sia un numero return Colore(self._R*k, self._G*k, self._B*k) def __truediv__(self, k : float) -> 'Colore': "divisione di un colore per una costante" # FIXME: controllare che k sia un numero != 0 return Colore(self._R/k, self._G/k, self._B/k) def _asTriple(self) -> tuple[int, int, int]: "creo la tripla di interi tra 0 e 255 che serve per le immagini PNG" def bound(X): # solo quando devo creare un file PNG mi assicuro che il pixel sia intero nel range 0..255 return max(0, min(255, round(X))) return bound(self._R), bound(self._G), bound(self._B) def __repr__(self) -> str : "stringa che deve essere visualizzata per stampare il colore" # uso una f-stringa equivalente a # return "Color(" + str(self._R) + ", " + str(self._G) + ", " + str(self._B) + ")" return f"Colore({self._R}, {self._G}, {self._B})" # %% RECAP slideshow={"slide_type": "slide"} code_folding=[10, 17] # solo dopo aver definito Colore posso aggiungere attributi che contengono un Colore Colore.white = Colore(255, 255, 255) Colore.black = Colore( 0, 0, 0) Colore.red = Colore(255, 0, 0) Colore.green = Colore( 0, 255, 0) Colore.blue = Colore( 0, 0, 255) Colore.cyan = Colore( 0, 255, 255) Colore.purple= Colore(255, 0, 255) Colore.yellow= Colore(255, 255, 0) Colore.grey = Colore.white/2 # Esempi p1 = Colore(255, 0, 0) p2 = Colore( 0,255, 0) p3 = p2 + p1 # uso l'operatore somma tra due colori che ho definito sopra p4 = p3 * 0.5 # uso l'operatore prodotto per una costante che ho definito sopra p5 = Colore(120, 34, 200) p4 # %% [markdown] slideshow={"slide_type": "slide"} # # Semplifichiamo le immagini # Per spostare le operazioni di disegno in classi separate # ci servono solo le primitive di disegno minime: # - **set_pixel** e **get_pixel** # - **is_inside** # - **`__init__`**, **`__repr__`**, **save** e **visualizza** # %%% Immagini come oggetti slideshow={"slide_type": "slide"} code_folding=[] import images import math class Immagine: def __init__(self, larghezza=None, altezza=None, sfondo=None, filename=None): """posso creare una immagine in due modi: - leggendola da un file PNG se passo il parametro filename - fornendo dimensioni e colore di sfondo se non lo passo """ if filename: img = images.load(filename) # letta la immagine la converto in una matrice di Colore self._img = [ [ Colore(r,g,b) for r,g,b in riga ] for riga in img ] self._W = len(img[0]) self._H = len(img) else: # altrimenti creo una immagine monocolore # FIXME: dovrei controllare che ci siano tutti e 3 gli altri argomenti if sfondo is None: sfondo = Colore.black self._W = larghezza self._H = altezza self._img = [ [sfondo for _ in range(larghezza) ] for _ in range(altezza) ] def __repr__(self) -> str: "per stampare l'immagine ne mostro le dimensioni" return f"Immagine(larghezza={self._W},altezza={self._H}, ... )" def save(self, filename : str) -> None: "si salva l'immagine dopo averla convertita in matrice di triple" images.save(self._asTriples(), filename) def _asTriples(self) -> list[list[tuple[int,int,int]]] : "conversione della immagine da matrice di Color a matrice di triple" return [ [ pixel._asTriple() for pixel in riga ] for riga in self._img ] def is_inside(self, x : float, y : float) -> bool: "verifico se le coordinate x,y sono dentro l'immagine" return 0 <= x < self._W and 0 <= y < self._H def set_pixel(self, x : float, y : float, color) -> None: "cambio un pixel se è dentro l'immagine" x = round(x) y = round(y) if 0 <= x < self._W and 0 <= y < self._H: self._img[y][x] = color def get_pixel(self, x : float, y : float) -> Colore: "leggo un pixel se è dentro l'immagine oppure torno None" x = max(0, min(round(x), self._W-1)) y = max(0, min(round(y), self._H-1)) return self._img[y][x] def visualizza(self): "visualizzo l'immagine in Spyder" return images.visd(self._asTriples()) # fa comodo poter trovare i colori intorno ad un punto, fino a distanza k def vicini(self, x : int, y : int, k : int) -> list[Colore]: "torno i colori nei pixel 2k x 2k intorno al punto x,y" return [ self.get_pixel(X,Y) for X in range(x-k,x+k+1) for Y in range(y-k,y+k+1) if self.is_inside(X,Y)] # # copia? # %% [markdown] slideshow={"slide_type": "slide"} # # Filtri come oggetti # Notate come la vita di un oggetto (istanza) sia fatta di DUE fasi # - **creazione** con tutti i parametri e le info sue personali # - **uso** con i suoi metodi # # (in altri linguaggi dobbiamo anche "distruggere/disallocare" l'oggetto ma Python lo fa automaticamante) # # Nell'uso dei filtri abbiamo dovuto usare trucchi come una lambda per passare parametri # # Se li trasformiamo in oggetti i parametri li possiamo passare nella creazione e l'applicazione diventa più semplice # %% [markdown] slideshow={"slide_type": "slide"} # ## Definiamo il filtro generico e poi lo specializziamo aggiungendo funzionalità # # - nell'**`__init__`** riceve tutti i parametri che gli serviranno # - ha un metodo **nuovo_pixel(self, pixel)** per i filtri che non dipendono dalla posizione # - ha un metodo **nuovo_pixel(self, immagine, x, y)** per i filtri che dipendono dalla posizione # - ha un metodo **reset(self)** per azzerarlo ed usarlo su altre immagini # - ha il metodo **applica(self, immagine)** # %% FILTRI come oggetti (con parametri) slideshow={"slide_type": "slide"} code_folding=[] class GenericFilter: # tutti i filtri specializzano questa classe def nuovo_pixel(self, pixel : Colore) -> Colore : raise NotImplementedError() def nuovo_pixel_XY(self, immagine : Immagine, x : int, y : int) -> Colore : raise NotImplementedError() def reset(self) -> None : raise NotImplementedError() def applica(self, immagine : Immagine) -> Immagine: raise NotImplementedError() # %% FILTRI come oggetti (con parametri) slideshow={"slide_type": "slide"} code_folding=[] class FiltroPixel (GenericFilter): # tutti i filtri indipendenti dalla posizione "un filtro che NON dipende dalla posizione del pixel" # applicazione di un filtro per ottenere una nuova immagine def applica(self, immagine : Immagine) -> Immagine: "costruisco una nuova immagine con ciascun pixel trasformato tramite il filtro" assert isinstance(immagine, Immagine), f"l'oggetto {immagine} non è una Immagine" nuova_immagine = Immagine(larghezza=immagine._W, altezza=immagine._H) for y,riga in enumerate(immagine._img): for x,pixel in enumerate(riga): nuova_immagine._img[y][x] = self.nuovo_pixel(pixel) return nuova_immagine def nuovo_pixel(self, pixel : Colore ) -> Colore : "trasformazione nulla di un pixel" return pixel # per default ritorno lo stesso pixel # %% FILTRI come oggetti (con parametri) slideshow={"slide_type": "slide"} code_folding=[] class FiltroXY (GenericFilter): # tutti i filtri dipendenti dalla posizione "un filtro che DIPENDE dalla posizione del pixel" def applica(self, immagine : Immagine) -> Immagine: "applicazione di un filtro che conosce la posizione del pixel" assert isinstance(immagine, Immagine), f"{immagine} non è una Immagine" nuova_immagine = Immagine(larghezza=immagine._W, altezza=immagine._H) self.reset() # se il filtro contiene info da usi precedenti le azzero for y in range(immagine._H): for x in range(immagine._W): nuova_immagine._img[y][x] = self.nuovo_pixel_XY( immagine, x,y) return nuova_immagine def nuovo_pixel_XY(self, immagine : Immagine, x : int, y : int) -> Colore : "trasformazione nulla di un pixel a coordinate x, y" return immagine.get_pixel(x,y) # per default ritorno lo stesso pixel def reset(self) -> None: # per default non faccio nulla pass # %% slideshow={"slide_type": "notes"} code_folding=[2] init_cell=true run_control={"marked": false} import graphviz figura = graphviz.Digraph() figura.body.append(''' GenericFilter -> FiltroPixel GenericFilter -> FiltroXY FiltroPixel -> BiancoENero FiltroPixel -> Negativo FiltroPixel -> Luminosità FiltroPixel -> Contrasto FiltroPixel -> RandomNoise FiltroXY -> Blur FiltroXY -> Pixellato FiltroXY -> Lente FiltroXY -> RandomNoiseXY ''') # %% slideshow={"slide_type": "subslide"} code_folding=[] figura # %% [markdown] slideshow={"slide_type": "slide"} # ### BiancoENero (FiltroPixel) # - genera pixel grigi della stessa luminosità dell'originale # %% slideshow={"slide_type": "fragment"} code_folding=[] class BiancoENero(FiltroPixel): def nuovo_pixel(self, pixel : Colore ) -> Colore : L = pixel.luminosità() return Colore(L,L,L) # grigio di luminosità L trecime = Immagine(filename='3cime.png') BiancoENero().applica(trecime).visualizza() # %% [markdown] slideshow={"slide_type": "slide"} # ### Negativo (FiltroPixel) # - inverte la scala di luminosità # %% FILTRI come oggetti (con parametri) slideshow={"slide_type": "fragment"} class Negativo(FiltroPixel): def nuovo_pixel(self, pixel : Colore ) -> Colore : return Colore.white - pixel Negativo().applica(trecime).visualizza() # %% [markdown] slideshow={"slide_type": "slide"} # ### Cambio Luminosità (FiltroPixel) # - moltiplica il colore per un fattore k # %% code_folding=[] slideshow={"slide_type": "fragment"} class CambioLuminosità (FiltroPixel): def __init__(self, k : float ) -> None: self._k = k def nuovo_pixel(self, pixel : Colore ) -> Colore : return pixel * self._k CambioLuminosità(0.5).applica(trecime).visualizza() CambioLuminosità(1.5).applica(trecime).visualizza() # %% [markdown] slideshow={"slide_type": "slide"} # ### Contrasto (FiltroPixel) # - avvicina/allontana i colori scuri e chiari al/dal grigio # %% FILTRI come oggetti (con parametri) slideshow={"slide_type": "fragment"} class Contrasto(FiltroPixel): def __init__(self, k : float) -> None: "Contrasto(k)" self._k = k def nuovo_pixel(self, pixel : Colore ) -> Colore : return Colore.grey + (pixel-Colore.grey)*self._k Contrasto(0.8).applica(trecime).visualizza() Contrasto(1.2).applica(trecime).visualizza() # %% [markdown] slideshow={"slide_type": "slide"} # ### Sfocatura/Blur (FiltroXY) # - dà al pixel il colore medio dei vicini fino a distanza k # %% FILTRI come oggetti (con parametri) slideshow={"slide_type": "fragment"} code_folding=[] class Blur(FiltroXY): def __init__(self, k : int): "inizializzo il filtro col parametro k" self._k = k def nuovo_pixel_XY(self, immagine : Immagine, x : int, y : int) -> Colore : "ciascun pixel è la media del gruppo grande 2k*2k che lo circonda" vicini = immagine.vicini(x, y, self._k ) ''' somma = Colore.black for v in vicini: somma += v return somma / len(vicini) ''' return sum(vicini, Colore.black)/len(vicini) Blur(2).applica(trecime).visualizza() Blur(5).applica(trecime).visualizza() # %% [markdown] slideshow={"slide_type": "slide"} # ### Effetto pixellato (FiltroXY) # - per ogni pixel trova il quadretto che lo contiene # - calcola la media dei pxel nel quadretto # - si ricorda questo valore (per non doverlo ricalcolare tante volte) # - da a tutti i pixel del quadretto lo stesso valore medio # %% FILTRI come oggetti (con parametri) slideshow={"slide_type": "fragment"} code_folding=[] class Pixellato(FiltroXY): """filtro che pixella l'immagine con la MEDIA dei pixel del quadretto """ def __init__(self, size : int): "inizializzo il filtro con la dimensione del quadretto" self._size = size self._valori : dict[tuple[int,int], Colore ] = {} # per ricordare i quadratini già calcolati def reset(self) -> None : "per riusare lo stesso filtro su una diversa immagine lo devo resettare" self._valori = {} def nuovo_pixel_XY(self, immagine : Immagine, x : int, y : int) -> Colore: "ciascun pixel è la media del gruppo grande 2k*2k che lo circonda" X = x - x % self._size + self._size//2 # centro del quadrato Y = y - y % self._size + self._size//2 if not (X,Y) in self._valori: # ottimizzazione vicini = immagine.vicini(X, Y, self._size//2 ) self._valori[X,Y] = sum(vicini, Colore.black)/len(vicini) return self._valori[X,Y] Pixellato(5).applica(trecime).visualizza() Pixellato(10).applica(trecime).visualizza() Pixellato(15).applica(trecime).visualizza() # %% [markdown] slideshow={"slide_type": "slide"} # ### Effetto Lente (FiltroXY) # - all'esterno del cerchio della lente lascia l'immagine uguale # - all'interno legge i pixel che stanno ad una distanza dal centro della lente maggiorata/diminuita di un fattore k # %% FILTRI come oggetti (con parametri) slideshow={"slide_type": "fragment"} code_folding=[] class Lente(FiltroXY): def __init__(self, x : int, y : int, raggio : int, ingrandimento : float) -> None: "inizializzo il filtro con posizione e raggio della lente e fattore di ingrandimento" self._x = x self._y = y self._raggio2 = raggio*raggio self._ingrandimento = ingrandimento def nuovo_pixel_XY(self, immagine : Immagine , x : int, y : int): """ciascun pixel che sta dentro la lente è preso da quello che sta sulla retta dal centro della lente ad una distanza aumentata di ingrandimento """ dx = x - self._x dy = y - self._y if dx*dx + dy*dy <= self._raggio2: # cerca il pixel giusto X = self._x + dx * self._ingrandimento Y = self._y + dy * self._ingrandimento return immagine.get_pixel(X,Y) else: # altrimenti lascio il pixel così com'è return immagine.get_pixel(x,y) Lente(100,100,100,0.5).applica(trecime).visualizza() Lente(100,100,100,1.5).applica(trecime).visualizza() # %% [markdown] slideshow={"slide_type": "slide"} # ### Rumore sul pixel (FiltroPixel) # - RandomNoise: aggiunge al pixel una variazione di **colore** casuale da -k a +k per ogni canale # - RandomLight: aggiunge al pixel una variazione di **grigio** casuale da -k a +k uguale per tutti i canali # %%%% random noise slideshow={"slide_type": "fragment"} code_folding=[] class RandomNoise(FiltroPixel): def __init__(self, k : int) -> None : self._k = k def nuovo_pixel(self, colore : Colore) -> Colore : return colore + Colore.random(-self._k, +self._k) class RandomLight(RandomNoise): def nuovo_pixel(self, colore : Colore) -> Colore : L = randint(-self._k, +self._k) return colore + Colore(L,L,L) RandomNoise(50).applica(trecime).visualizza() RandomLight(50).applica(trecime).visualizza() # %% [markdown] slideshow={"slide_type": "slide"} # ### Rumore sulla posizione del pixel (FiltroXY) # - sceglie un pixel vicino entro distanza k # %%%% random noise code_folding=[] slideshow={"slide_type": "fragment"} from random import randint class RandomNoiseXY(FiltroXY): def __init__(self, k : int) -> None : self._k = k def nuovo_pixel_XY(self, immagine : Immagine, x : int, y : int) -> Colore: X = x + randint(-self._k, self._k) Y = y + randint(-self._k, self._k) return immagine.get_pixel(X,Y) RandomNoiseXY(5).applica(trecime).visualizza() # %% [markdown] slideshow={"slide_type": "slide"} # # CONCLUSIONI: grazie all' OOP # # - **INTERFACCIA UGUALE**: anche se i due tipi di filtro sono diversi si applicano allo stesso modo!!! # - nomi uguali dei metodi -> oggetti intercambiabili (ricordate il "Duck Typing"?) # - l'applicazione che li **usa** non deve preoccuparsi di come sono fatti
# (tranne in questo caso per la loro creazione) # - **RIUSO DEL CODICE**: ciascun filtro eredita le funzionalità comuni # dalla superclasse e la estende # - una unica realizzazione delle funzionalità comuni -> meno errori di copy/paste e di aggiornamento # # %% [markdown] slideshow={"slide_type": "slide"} # # Altro esempio: Disegno di figure sulle immagini # - una Figura ha: # - un colore # - eventuali altri parametri # - e può: # - essere disegnata su una immagine in una certa posizione # %% slideshow={"slide_type": "notes"} init_cell=true code_folding=[1] figura = graphviz.Digraph() figura.body.append(''' Figura [ label="Figura\nha posizione e colore"] Punto Linea Ellisse Cerchio Poligono [ label="Poligono\nha lati"] PoligonoRegolare [ label="PoligonoRegolare\nha lati uguali"] TriangoloEquilatero Quadrato Pentagono Rettangolo Triangolo Figura -> Punto Figura -> Linea Figura -> Ellisse -> Cerchio Poligono -> PoligonoRegolare -> TriangoloEquilatero PoligonoRegolare -> Quadrato PoligonoRegolare -> Pentagono Figura -> Poligono -> Rettangolo -> Quadrato Poligono -> Triangolo -> TriangoloEquilatero ''') # %% slideshow={"slide_type": "slide"} figura # %% [markdown] slideshow={"slide_type": "slide"} # ## Figura e Punto # - tutte le Figure hanno un colore ed una posizione # - il punto disegna un pixel di quel colore nella posizione # %% slideshow={"slide_type": "fragment"} code_folding=[] class Figura: def __init__(self, colore : Colore, x: float, y: float): self._colore = colore # ricordo il colore self._x = x # e la self._y = y # posizione def disegna(self, immagine : Immagine) -> None: pass # per default non faccio nulla class Punto (Figura): def disegna(self, immagine : Immagine) -> None: immagine.set_pixel(self._x, self._y, self._colore) # %% [markdown] # ## Linea (segmento) # - rappresentata da un punto iniziale (x,y) e da una lunghezza ed angolo # - mi memorizzo il seno e coseno dell'angolo (in radianti) per riusarli comodamente # ![](lavagna.png) # %% slideshow={"slide_type": "slide"} code_folding=[] import math, random class Linea (Figura): def __init__(self, colore : Colore, x0 : float, y0 : float, lunghezza : float, angolo : float): "una linea che parte da un punto, e ha lunghezza e direzione" super().__init__(colore, x0, y0) self._lunghezza = lunghezza self._angolo = angolo self._cos = math.cos(math.radians(angolo)) self._sin = math.sin(math.radians(angolo)) # creo un secondo costruttore (classmethod) che # dalle coordinate degli estremi calcola distanza ed angolo @classmethod def lato(cls, colore, x,y, x1, y1) -> 'Linea' : "costruttore che parte dagli estremi della linea" dx = x1 - x dy = y1 - y lunghezza = math.dist((x,y),(x1,y1)) angolo = math.degrees(math.atan2(dy, dx)) return cls(colore, x, y, lunghezza, angolo) # viceversa, data una linea fa comodo trovare l'altro estremo def estremo(self): "torna l'altro estremo" return (self._x + self._cos*self._lunghezza, self._y + self._sin*self._lunghezza) def disegna(self, immagine : Immagine) -> None: "disegna la linea usando trigonometria" # scandisco la lunghezza della linea per disegnarne i punti for i in range(round(self._lunghezza)+1): X = self._x + i*self._cos Y = self._y + i*self._sin immagine.set_pixel(X,Y,self._colore) canvas = Immagine(500, 200) for x in range(0,200,20): for y in range(0,200,20): Linea(Colore.random(), x, y, 20, random.randint(1,360)).disegna(canvas) Linea.lato(Colore.random(200,250), 300, 50, 400, 150).disegna(canvas) canvas.visualizza() # %% [markdown] # ## Poligono # - un qualsiasi poligono è fatto da un gruppo di linee # - (non verifichiamo che siano consecutive o che sia una superficie chiusa) # %% class Poligono(Figura): "Un poligono è una figura fatta di linee" def __init__(self, linee : list[Linea]): self._linee = linee def disegna(self, immagine: Immagine): for l in self._linee: l.disegna(immagine) Poligono([ Linea.lato(Colore.red, 300, 100, 330, 100), Linea.lato(Colore.green, 300, 100, 300, 140), Linea.lato(Colore.cyan, 330, 100, 300, 140), ]).disegna(canvas) canvas.visualizza() # %% [markdown] # ## Triangolo # - dati 3 punti, ne costruisce le tre linee # %% class Triangolo(Poligono): "Un Triangolo è un poligono con 3 lati" def __init__(self, colore : Colore, x : float, y : float, x1 : float, y1 : float, x2 : float, y2 : float): lato1 = Linea.lato(colore, x, y, x1,y1) lato2 = Linea.lato(colore, x1,y1,x2,y2) lato3 = Linea.lato(colore, x2,y2,x, y) super().__init__([lato1, lato2, lato3]) canvas = Immagine(500, 200) Triangolo(Colore.red, 250,50, 290, 100, 270, 150).disegna(canvas) canvas.visualizza() # %% [markdown] # ## Rettangolo # - dato l'angolo sopra a sinistra, larghezza, altezza e inclinazione # - costruisce le 4 linee # %% class Rettangolo(Poligono): "Un Rettangolo è un poligono con 4 lati perpendicolari" def __init__(self, colore : Colore, x : float, y : float, larghezza : float, altezza : float, angolo : float): self._larghezza = larghezza self._altezza = altezza self._angolo = angolo sopra = Linea(colore, x, y, larghezza, angolo) sinistra = Linea(colore, x, y, altezza, angolo+90) x1,y1 = sopra.estremo() # vertice in alto a destra destra = Linea(colore, x1, y1, altezza, angolo+90) x2,y2 = sinistra.estremo() # vertice in basso a sinistra sotto = Linea(colore, x2, y2, larghezza, angolo) super().__init__([sopra, sotto, sinistra, destra]) canvas = Immagine(500, 200) Rettangolo(Colore.green, 100, 50, 300, 100, -45).disegna(canvas) canvas.visualizza() # %% [markdown] # ## un Quadrato è un rettangolo con lati uguali # %% class Quadrato(Rettangolo): def __init__(self, colore : Colore, x: float, y: float, lato : int, direzione : float): super().__init__(colore, x, y, lato, lato, direzione) Quadrato(Colore.red, 200, 100, 100, 30).disegna(canvas) canvas.visualizza()