Classi e oggetti

Python è un linguaggio di programmazione orientato agli oggetti (OOP - Object Oriented Programming).

Un oggetto è semplicemente una raccolta di dati (variabili) e metodi (funzioni) che agiscono su questi dati.

Allo stesso modo, una classe è un progetto, un modello, per quell'oggetto.
Possiamo pensare ad una classe come ad uno schizzo (prototipo) di una casa. Contiene tutti i dettagli relativi a pavimenti, porte, finestre, ecc. Sulla base di queste descrizioni costruiamo la casa.
La casa è l'oggetto.

Come le definizioni delle funzioni iniziano con la parola chiave def in Python, le definizioni delle classi iniziano la parola chiave class.

class Mia_classe:
  '''Questo è una docstring. Serve per documentare la classe'''
  pass

Come creare un oggetto (istanziazione)

In Python, una classe deve essere istanziata prima dell'uso.

Per analogia, una classe può essere pensata come un progetto (Auto) e un'istanza è un'effettiva implementazione del progetto (Ferrari).

Possiamo creare nuove istanze di oggetti (istanziazione) di una determinata classe. La procedura per creare un oggetto è simile a una chiamata di funzione.

nome_oggetto = Nome_classe(eventuali_argomenti)

Possiamo accedere agli attributi degli oggetti utilizzando come prefisso il nome dell'oggetto. Gli attributi possono essere dati o metodi. I metodi di un oggetto sono le funzioni corrispondenti di quella classe.

class Animale:
  "Classe vuota"
  pass

gatto = Animale() # Istanziazione

Variabili della classe

In Python, le variabili di classe sono definite al di fuori di tutti i metodi e hanno lo stesso valore per ogni istanza della classe.

Le variabili di classe sono accessibili con la sintassi istanza.variable o nome_classe.variabile.

class Mia_classe:
  variabile = "Sono una variabile della classe."

a = Mia_classe()
b = Mia_classe()

print (a.variabile)
print (b.variabile)

Metodi

I metodi sono funzioni che sono definite come parte di una classe.

Il primo argomento di qualsiasi metodo che fa parte di una classe è il richiamo dell'oggetto che chiama il metodo. Questo argomento è solitamente definito con la parola self.

class Cane: 
  def abbaia(self):
     print("Woff-Woff!")

bob = Cane()

bob.abbaia()
Woff-Woff!

Metodo .__init__()

In Python, il metodo .__init__() è usato per inizializzare un oggetto appena creato. Viene chiamato ogni volta che la classe viene istanziata.

Questo tipo di metodo è anche chiamato costruttore in Object Oriented Programming (OOP). Normalmente lo usiamo per inizializzare tutte le variabili.

class Animale:
   def __init__(self, verso):
     self.verso = verso

gatto = Animale("Miao!")
cane = Animale("Bau!")

print (gatto.verso)
print (cane.verso)
Miao!
Bau!

Metodi Dunder in Python

I metodi Dunder, che sta per "Double Under" (underscore), sono metodi speciali che hanno una doppia sottolineatura all'inizio e alla fine del nome.

Li usiamo per creare funzionalità che non possono essere rappresentate come un metodo normale.

Alcuni esempi per i metodi dunder sono: __init__, __add__, __len__, e __iter__.

__main__ in Python

In Python, __main__ è un identificatore usato per fare riferimento al contesto del file corrente.

Quando un modulo viene letto da un input standard, da uno script o da un prompt interattivo, il suo __nome__ è impostato uguale a __main__.

Supponiamo di creare un'istanza di una classe chiamata Nome_Completo.
Il risultato della stampa a video di type() dell'istanza risulterà:
<classe '__main__.Nome_Completo'>

Ciò significa che la classe Nome_Completo è stata definita nel file di script corrente.

Ereditarietà in Python

L'ereditarietà è una potente caratteristica nella programmazione orientata agli oggetti.

Si riferisce alla definizione di una nuova classe con poche o nessuna modifica a una classe esistente.

La nuova classe è chiamata classe derivata (o figlia o sottoclasse) e quella da cui eredita è chiamata classe base (o genitore o superclasse).

L'ereditarietà, sintatticamente, può essere realizzata mettendo il nome della superclasse (classe genitore) tra parentesi dopo il nome della sottoclasse.

class Animale:
  def __init__(self, nome, zampe):
     self.nome = nome
     self.zampe = zampe

class Cane(Animale):
  def sound(self):
     print("Woof!")

fido = Cane("fido", 4)
print (fido.nome)
print (fido.zampe)
fido.sound()
fido
4
Woof!

Polimorfismo in Python

Quando due classi offrono lo stesso insieme di metodi con implementazioni diverse, le classi sono polimorfiche e si dice che abbiano la stessa interfaccia.

Questo permette di usare i due oggetti nello stesso modo, indipendentemente dal loro tipo di dato.

Quando una classe figlia sovrascrive un metodo di una classe genitore, allora il tipo dell'oggetto determina la versione del metodo da chiamare.

Se l'oggetto è un'istanza della classe figlia, allora la versione della classe figlia del metodo sovrascritto verrà chiamata.

Se invece l'oggetto è un'istanza della classe genitore, allora viene richiamata la versione della classe genitore del metodo.

class Genitore:
  def print_self(self):
     print ("a")

class Figlio(Genitore):
  def print_self(self):
     print ("b")

ogg_a = Genitore()
ogg_b = Figlio()

ogg_a.print_self()
ogg_b.print_self()