Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

La programmazione ad oggetti

Introduzione alla programmazione ad oggetti e suo utilizzo in Python: cos'è, a cosa serve ed in quali casi è meglio utilizzare questo paradigma.
Introduzione alla programmazione ad oggetti e suo utilizzo in Python: cos'è, a cosa serve ed in quali casi è meglio utilizzare questo paradigma.
Link copiato negli appunti

In uno dei capitoli introduttivi di questa guida abbiamo visto che Python è un linguaggio multi-paradigma, che supporta cioè sia la programmazione procedurale (che fa uso delle funzioni), sia la programmazione funzionale (includendo iteratori e generatori), sia la programmazione ad oggetti (includendo funzionalità come l'ereditarietà singola e multipla, l'overloading degli operatori, e il duck typing).

In questo capitolo introdurremo i concetti della programmazione orientata agli oggetti, e vedremo alcuni semplici esempi. Nei capitoli successivi vedremo più in dettaglio come usare gli oggetti in Python, la sintassi necessaria per crearli, e le operazioni che supportano.

Gli oggetti

Mentre nella programmazione procedurale le funzioni (o procedure) sono l'elemento organizzativo principale, nella programmazione ad oggetti (anche conosciuta come OOP, ovvero object-Oriented Programming) l'elemento organizzativo principale sono gli oggetti.

Nella programmazione procedurale, i dati e le funzioni sono separate, e questo può creare una serie di problemi, tra cui:

  • è necessario gestire dati e funzioni separatamente;
  • è necessario importare le funzioni che vogliamo usare;
  • è necessario passare i dati alle funzioni;
  • è necessario verificare che i dati e le funzioni siano compatibili;
  • è più difficile estendere e modificare le funzionalità;
  • il codice è più difficile da mantenere;
  • è più facile introdurre bug.

Nella programmazione ad oggetti, gli oggetti svolgono la funzione di racchiudere in un'unica unità organizzativa sia i dati che il comportamento. Questo ha diversi vantaggi:

  • dati e funzioni sono raggruppati;
  • è facile sapere quali operazioni possono essere eseguite sui dati;
  • non è necessario importare funzioni per eseguire queste operazioni;
  • non è necessario passare i dati alle funzioni;
  • le funzioni sono compatibili con i dati;
  • è più facile estendere e modificare le funzionalità;
  • il codice è più semplice da mantenere;
  • è più difficile introdurre bug.

Vediamo un semplice esempio: abbiamo la base e l'altezza di 100 diversi rettangoli e vogliamo sapere area e perimetro di ogni rettangolo. Usando un approccio procedurale, possiamo risolvere il problema creando due funzioni separate che accettano base e altezza:

>>> # definiamo due funzioni per calcolare area e perimetro
>>> def calc_rectangle_area(base, height):
...     """Calculate and return the area of a rectangle."""
...     return base * height
...
>>> def calc_rectangle_perimeter(base, height):
...     """Calculate and return the perimeter of a rectangle."""
...     return (base + height) * 2
...

Possiamo poi creare una lista di tuple casuali (base, altezza), iterarla con un for e passare i valori alle funzioni:

>>> from random import randrange
>>> # creiamo una lista di 100 tuple (base, altezza) con valori casuali
>>> rects = [(randrange(100), randrange(100)) for x in range(100)]
>>> rects
[(16, 39), (92, 96), (60, 72), (99, 32), (39, 5), (43, 6), (51, 28), ...]
>>> # iteriamo la lista di rettangoli e printiamo
>>> # base, altezza, area, perimetro di ogni rettangolo
>>> for base, height in rects:
...     print('Rect:', base, height)
...     print('  Area:', calc_rectangle_area(base, height))
...     print('  Perimeter:', calc_rectangle_perimeter(base, height))
...
Rect: 16 39
  Area: 624
  Perimeter: 110
Rect: 92 96
  Area: 8832
  Perimeter: 376
Rect: 60 72
  Area: 4320
  Perimeter: 264
...

Usando la programmazione orientata agli oggetti, possiamo invece creare una classe che rappresenta l'oggetto rettangolo. Invece che rappresentare i rettangoli come una lista di tuple, usiamo la classe per creare 100 istanze della classe Rettangolo, e invece che chiamare le funzioni passando la base e l'altezza, chiamiamo i metodi dell'istanza:

>>> # definiamo una classe che rappresenta un rettangolo generico
>>> class Rectangle:
...     def __init__(self, base, height):
...         """Initialize the base and height attributes."""
...         self.base = base
...         self.height = height
...     def calc_area(self):
...         """Calculate and return the area of the rectangle."""
...         return self.base * self.height
...     def calc_perimeter(self):
...         """Calculate and return the perimeter of a rectangle."""
...         return (self.base + self.height) * 2
...

Nella prossima lezione vedremo piu in dettaglio come definire le classi, ma come possiamo vedere dal prossimo esempio, creare e usare istanze è abbastanza intuitivo:

>>> # creiamo un'istanza della classe Rectangle con base 3 e altezza 5
>>> myrect = Rectangle(3, 5)
>>> myrect.base  # l'istanza ha una base
3
>>> myrect.height  # l'istanza ha un'altezza
5
>>> myrect.calc_area()  # è possibile calcolare l'area direttamente
15
>>> myrect.calc_perimeter()  # e anche il perimetro
16

Ora che abbiamo un'idea di base del funzionamento delle classi, possiamo creare i 100 rettangoli e calcolare area e perimetro:

>>> from random import randrange
>>> # creiamo una lista di 100 istanze di Rectangle con valori casuali
>>> rects = [Rectangle(randrange(100), randrange(100)) for x in range(100)]
>>> # iteriamo la lista di rettangoli e printiamo
>>> # base, altezza, area, perimetro di ogni rettangolo
>>> for rect in rects:
...     print('Rect:', rect.base, rect.height)
...     print('  Area:', rect.calc_area())
...     print('  Perimeter:', rect.calc_perimeter())
...
Rect: 78 44
  Area: 3432
  Perimeter: 244
Rect: 0 85
  Area: 0
  Perimeter: 170
Rect: 32 2
  Area: 64
  Perimeter: 68

Come possiamo vedere confrontando i due esempi, usando la programmazione ad oggetti possiamo lavorare direttamente con oggetti singoli (le istanze di Rectangle). La lista non contiene più tuple, ma rettangoli, e per calcolare area e perimetro non è più necessario passare la base e l'altezza esplicitamente. Inoltre, calc_area() e calc_perimeter() sono associati all'istanza, e quindi non è necessario importare le funzioni, non si rischia di usare la funzione sbagliata (ad esempio una funzione che calcola l'area di un triangolo), non si rischia di passare la base o l'altezza del rettangolo sbagliato o passarli nell'ordine sbagliato.

Termini e concetti

Nei precedendi esempi abbiamo introdotto alcuni termini e concetti nuovi che sono usati comunemente nella programmazione ad oggetti.

Classi

Le classi sono usate per definire le caratteristiche di un oggetto, i suoi attributi (ad esempio la base e l'altezza) e i suoi metodi (ad esempio calc_area() e calc_perimeter()). Le classi sono "astratte" - non si riferiscono a nessun oggetto specifico, ma rappresentano un modello che può essere usato per creare istanze. Ad esempio la classe Rectangle specifica che i rettangoli hanno una base, un'altezza, un'area e un perimetro, ma la classe non si riferisce a nessun rettangolo in particolare.

Istanze

Le istanze sono oggetti creati a partire da una classe. Ad esempio Rectangle(3, 5) ci restituisce un'istanza della classe Rectangle che si riferisce a uno specifico rettangolo che ha base 3 e altezza 5. Una classe può essere usata per creare diverse istanze dello stesso tipo ma con attributi diversi, come i 100 diversi rettangoli che abbiamo visto nell'esempio precedente. È possibile usare i metodi definiti dalla classe con ogni istanza, semplicemente facendo istanza.metodo() (ad esempio myrect.calc_area()).

Attributi

Gli attributi sono dei valori associati all'istanza, come ad esempio la base e l'altezza del rettangolo. Gli attributi di ogni istanza sono separati: ogni istanza di Rectangle ha una base e un'altezza diversa. Per accedere a un attributo basta fare istanza.attributo (ad esempio myrect.base).

Metodi

I metodi descrivono il comportamento dell'oggetto, sono simili alle funzioni, e sono specifici per ogni classe. Ad esempio sia la classe Rectangle che la classe Triangle possono definire un metodo chiamato calc_area(), che ritornerà risultati diversi in base al tipo dell'istanza. I metodi possono accedere altri attributi e metodi dell'istanza: questo ci permette ad esempio di chiamare myrect.calc_area() senza dover passare la base e l'altezza esplicitamente. Per chiamare un metodo basta fare istanza.metodo() (ad esempio myrect.calc_area()).

Ereditarietà

Un altro concetto importante della programmazione è l'ereditarietà. L'ereditarietà ci permette di creare una nuova classe a partire da una classe esistente e di estenderla o modificarla.

Per esempio possiamo creare una classe Square che eredita dalla classe Rectangle. Dato che i 4 lati di un quadrato hanno la stessa lunghezza, non è più necessario richiedere base e altezza separatamente, quindi nella classe Square possiamo modificare l'inizializzazione in modo da richiedere la lunghezza di un singolo lato. Così facendo possiamo definire una nuova classe che invece di accettare e definire i due attributi base e height definisce e accetta un singolo attributo side. Dato che il quadrato è un tipo particolare di rettangolo, i metodi per calcolare area e perimetro funzionano senza modifiche e possiamo quindi utilizzare calc_area() e calc_perimeter() ereditati automaticamente dalla classe Rectangle senza doverli ridefinire.

È inoltre possibile definire gerarchie di classi, ad esempio si può definire la clase Husky che eredita dalla classe Cane che eredita dalla classe Mammifero che eredita dalla classe Animale. Ognuna di queste classi può definire attributi e comportamenti comuni a tutti gli oggetti di quella classe, e le sottoclassi possono aggiungerne di nuovi.

Python supporta anche l'ereditarietà multipla: è possibile definire nuovi classi che ereditano metodi e attributi da diverse altre classi, combinandoli.

Nella prossima lezione vedremo esempi concreti di utilizzo dell'ereditarietà.

Superclassi e sottoclassi

Se la classe Square eredita dalla classe Rectangle, possiamo dire che Rectangle è la superclasse (o classe base - base class in inglese) mentre Square è la sottoclasse (o subclasse). L'operazione di creare una sottoclasse a partire da una classe esistente è a volte chiamata subclassare.

Overloading degli operatori

In Python, le classi ci permettono anche di ridefinire il comportamento degli operatori: questa operazione è chiamata overloading degli operatori. È possibile definire metodi speciali che vengono invocati automaticamente quando un operatore viene usato con un'istanza della classe.

Ad esempio possiamo definire che myrect1 < myrect2 ritorni True quando l'area di myrect1 è inferiore a quella di myrect2, oppure possiamo definire che myrect1 + myrect2 ritorni un nuova istanza di Rectangle creata dalla combinazione di myrect1 e myrect2.

Quando usare la programmazione ad oggetti

Anche se la programmazione ad oggetti è uno strumento molto utile e potente, non è la soluzione a tutti i problemi. Spesso creare una semplice funzione è sufficiente e non è necessario definire una classe.

In genere la programmazione ad oggetti può essere la soluzione adatta se:

  • la classe che vogliamo creare rappresenta un oggetto (es. Rectangle, Person, Student, Window, Widget, Connection, ecc.);
  • vogliamo associare all'oggetto sia dati che comportamenti;
  • vogliamo creare diverse istanze della stessa classe.

La programmazione ad oggetti potrebbe non essere la soluzione migliore se:

  • la classe che vogliamo creare non rappresenta un oggetto, ma ad esempio un verbo (es. Find, Connect, ecc.);
  • vogliamo solo rappresentare dati (meglio usare una struttura dati come list, dict, namedtuple, ecc.) o solo comportamenti (meglio usare funzioni, eventualmente raggruppate in un modulo separato);
  • vogliamo creare una sola istanza della stessa classe (meglio usare un modulo per raggruppare dati e funzioni).

Ovviamente ci sono anche delle eccezioni (ad esempio il pattern singleton, che definisce una classe che prevede una sola istanza). Python è un linguaggio multiparadigma, ed è quindi importante scegliere il paradigma che meglio si adatta alla situazione.

Nella prossima lezione vedremo in dettaglio come definire e usare classi in Python.

Ti consigliamo anche