Python & AI Tutorials Logo
Programmazione Python

30. Introduzione a classi e oggetti

30.1) L’idea della programmazione orientata agli oggetti (creare i tuoi tipi)

In tutto questo libro, hai lavorato con i tipi integrati di Python: interi, stringhe, liste, dizionari e altro. Ogni tipo raggruppa insieme dati (come i caratteri in una stringa) con operazioni che puoi eseguire su quei dati (come .upper() o .split()). Questa combinazione di dati e comportamento è potente: ti permette di pensare alle stringhe come entità complete con capacità proprie, non solo come sequenze grezze di caratteri.

La programmazione orientata agli oggetti (OOP) estende questa idea: ti permette di creare i tuoi tipi personalizzati, chiamati classi, che raggruppano insieme dati e comportamento specifici per il tuo dominio del problema. Proprio come Python fornisce un tipo str per lavorare con il testo e un tipo list per lavorare con le sequenze, puoi creare un tipo BankAccount per gestire transazioni finanziarie, un tipo Student per tracciare registri accademici, o un tipo Product per un sistema di inventario.

Perché creare i tuoi tipi?

Considera la gestione delle informazioni sugli studenti in un sistema scolastico. Senza classi, potresti usare variabili separate o dizionari:

python
# Usare variabili separate: diventa complicato rapidamente
student1_name = "Alice Johnson"
student1_id = "S12345"
student1_gpa = 3.8
 
student2_name = "Bob Smith"
student2_id = "S12346"
student2_gpa = 3.5
 
# Oppure usare dizionari: meglio, ma comunque limitato
student1 = {"name": "Alice Johnson", "id": "S12345", "gpa": 3.8}
student2 = {"name": "Bob Smith", "id": "S12346", "gpa": 3.5}

Questo approccio funziona per casi semplici, ma ha delle limitazioni:

  1. Nessuna validazione: Nulla ti impedisce di impostare gpa su un valore non valido come -5.0 o "excellent"
  2. Nessun comportamento correlato: Operazioni come calcolare lo stato di merito o formattare le informazioni dello studente sono funzioni separate sparse nel codice
  3. Nessun controllo di tipo: Un dizionario che rappresenta uno studente sembra identico a qualsiasi altro dizionario: Python non può aiutarti a intercettare errori in cui usi accidentalmente un dizionario di prodotto quando era atteso un dizionario di studente

Le classi risolvono questi problemi permettendoti di definire un nuovo tipo che rappresenta esattamente che cos’è uno studente e quali operazioni hanno senso per gli studenti:

python
# Ci costruiremo sopra: una classe Student che raggruppa dati e comportamento
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def is_honors(self):
        return self.gpa >= 3.5
    
    def display_info(self):
        status = "Honors" if self.is_honors() else "Regular"
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa} [{status}]"
 
# Ora possiamo creare oggetti student
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
 
print(alice.display_info())  # Output: Alice Johnson (S12345) - GPA: 3.8 [Honors]
print(bob.is_honors())       # Output: True

Questo capitolo ti insegnerà come costruire classi come questa partendo da zero. Inizieremo con le classi più semplici possibili e aggiungeremo gradualmente funzionalità finché non sarai in grado di creare tipi personalizzati ricchi e utili.

Classi vs istanze: l’analogia del progetto

Comprendere la distinzione tra una classe e un’istanza è fondamentale nella programmazione orientata agli oggetti:

  • Una classe è come un progetto o un modello. Definisce quali dati conterrà un tipo di oggetto e quali operazioni potrà eseguire. La classe in sé non è uno studente specifico: è la definizione di ciò che significa essere uno studente.

  • Un’istanza (chiamata anche oggetto) è un esempio specifico creato a partire da quel progetto. Quando crei alice = Student("Alice Johnson", "S12345", 3.8), stai creando una specifica istanza di studente con i dati particolari di Alice.

Classe Student
Progetto

istanza alice
Nome: Alice Johnson
ID: S12345
GPA: 3.8

istanza bob
Nome: Bob Smith
ID: S12346
GPA: 3.5

istanza carol
Nome: Carol Davis
ID: S12347
GPA: 3.9

Puoi creare tutte le istanze di cui hai bisogno da una singola classe, proprio come un architetto può usare un solo progetto per costruire molte case. Ogni istanza ha i propri dati (il GPA di Alice è diverso da quello di Bob), ma condividono tutte la stessa struttura e le stesse capacità definite dalla classe.

Cosa imparerai in questo capitolo

Questo capitolo introduce i concetti fondamentali della programmazione orientata agli oggetti in Python:

  1. Definire classi con la parola chiave class
  2. Creare istanze e accedere ai loro attributi
  3. Aggiungere metodi che operano sui dati dell’istanza
  4. Comprendere self e come i metodi accedono ai dati dell’istanza
  5. Inizializzare istanze con il metodo __init__
  6. Controllare le rappresentazioni in stringa con __str__ e __repr__
  7. Creare più istanze indipendenti dalla stessa classe

Alla fine di questo capitolo, sarai in grado di progettare e implementare i tuoi tipi personalizzati che rendono i tuoi programmi più organizzati, manutenibili ed espressivi. Costruiremo su queste basi nel Capitolo 31 con funzionalità di classe più avanzate e nel Capitolo 32 con ereditarietà e polimorfismo.

30.2) Definire classi semplici con class

Iniziamo creando la classe più semplice possibile: una che definisce solo un nuovo tipo senza dati o comportamento, per ora.

La parola chiave class

Definisci una classe usando la parola chiave class, seguita dal nome della classe e dai due punti:

python
class Student:
    pass  # Classe vuota per ora
 
# Crea un'istanza
alice = Student()
print(alice)  # Output: <__main__.Student object at 0x...>
print(type(alice))  # Output: <class '__main__.Student'>

Anche questa classe minimale è utile: crea un nuovo tipo chiamato Student. Quando crei un’istanza con alice = Student(), Python crea un nuovo oggetto di tipo Student. L’output mostra che alice è effettivamente un oggetto Student, anche se non fa ancora nulla di interessante.

Convenzioni di denominazione delle classi

I nomi delle classi in Python seguono una convenzione specifica chiamata CapWords o PascalCase: ogni parola inizia con una lettera maiuscola, senza underscore tra le parole:

python
class BankAccount:      # Bene: CapWords
    pass
 
class ProductInventory:  # Bene: CapWords
    pass
 
class HTTPRequest:      # Bene: gli acronimi sono tutti in maiuscolo
    pass
 
# Evita questi stili per le classi:
# class bank_account:   # Wrong: snake_case is for functions/variables
# class bankaccount:    # Wrong: hard to read
# class BANKACCOUNT:    # Wrong: ALL_CAPS is for constants

Questa convenzione aiuta a distinguere le classi dalle funzioni e dalle variabili (che usano snake_case) quando leggi il codice.

Creare istanze

Creare un’istanza da una classe assomiglia a chiamare una funzione: usi il nome della classe seguito da parentesi:

python
class Product:
    pass
 
# Crea tre istanze di prodotto diverse
item1 = Product()
item2 = Product()
item3 = Product()
 
# Ogni istanza è un oggetto separato
print(item1)  # Output: <__main__.Product object at 0x...>
print(item2)  # Output: <__main__.Product object at 0x...>
print(item3)  # Output: <__main__.Product object at 0x...>
 
# Sono oggetti diversi, anche se sono dello stesso tipo
print(item1 is item2)  # Output: False
print(type(item1) is type(item2))  # Output: True

Ogni chiamata a Product() crea una nuova istanza indipendente. Gli indirizzi di memoria (la parte 0x...) sono diversi, confermando che si tratta di oggetti separati in memoria.

Perché iniziare con classi vuote?

Potresti chiederti perché stiamo iniziando con classi che non fanno nulla. Ci sono due ragioni:

  1. Chiarezza concettuale: Capire che una classe è semplicemente un nuovo tipo, separato dai suoi dati e dal suo comportamento, ti aiuta a cogliere il concetto fondamentale prima di aggiungere complessità.

  2. Uso pratico: Anche le classi vuote possono essere utili come marcatori o segnaposto. Per esempio, potresti definire tipi di eccezione personalizzati:

python
class InvalidGradeError:
    pass
 
class StudentNotFoundError:
    pass
 
# Queste classi vuote fungono da tipi di errore distinti

Tuttavia, le classi vuote sono rare nel codice reale. Aggiungiamo qualche dato per rendere utili le nostre classi.

30.3) Creare istanze e accedere agli attributi

Le classi diventano utili quando contengono dati. In Python, puoi aggiungere attributi (dati associati a un’istanza) in qualsiasi momento semplicemente assegnandoli.

Aggiungere attributi alle istanze

Puoi aggiungere attributi a un’istanza usando la notazione con punto:

python
class Student:
    pass
 
# Crea un'istanza
alice = Student()
 
# Aggiungi attributi
alice.name = "Alice Johnson"
alice.student_id = "S12345"
alice.gpa = 3.8
 
# Accedi agli attributi
print(alice.name)        # Output: Alice Johnson
print(alice.student_id)  # Output: S12345
print(alice.gpa)         # Output: 3.8

L’operatore punto (.) accede agli attributi: alice.name significa “ottieni l’attributo name dell’oggetto alice”. È la stessa sintassi che hai usato con le stringhe (come text.upper()) e le liste (come numbers.append(5)): in quei casi stai accedendo a metodi e attributi di quegli oggetti.

Ogni istanza ha i propri attributi

Istanze diverse della stessa classe hanno attributi indipendenti:

python
class Student:
    pass
 
# Crea due studenti
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
 
# Ogni istanza ha i propri dati
print(alice.name)  # Output: Alice Johnson
print(bob.name)    # Output: Bob Smith
 
# Cambiare una non influisce sull'altra
alice.gpa = 3.9
print(alice.gpa)  # Output: 3.9
print(bob.gpa)    # Output: 3.5 (unchanged)

Questa indipendenza è cruciale: alice e bob sono oggetti separati con dati separati. Modificare alice.gpa non influisce su bob.gpa.

Gli attributi possono essere di qualsiasi tipo

Gli attributi non sono limitati a tipi semplici: possono contenere qualsiasi valore Python:

python
class Student:
    pass
 
student = Student()
student.name = "Carol Davis"
student.grades = [95, 88, 92, 90]  # Attributo lista
student.contact = {                 # Attributo dizionario
    "email": "carol@example.com",
    "phone": "555-0123"
}
student.is_active = True            # Attributo booleano
 
# Accedi a dati annidati
print(student.grades[0])           # Output: 95
print(student.contact["email"])    # Output: carol@example.com

Questa flessibilità ti permette di modellare entità complesse del mondo reale con strutture dati ricche.

Accedere ad attributi inesistenti

Tentare di accedere a un attributo che non esiste solleva un AttributeError:

python
class Student:
    pass
 
student = Student()
student.name = "David Lee"
 
print(student.name)  # Output: David Lee
# print(student.age)  # AttributeError: 'Student' object has no attribute 'age'

Questo errore è utile: intercetta refusi ed errori di logica in cui ti aspetti che un attributo esista ma non esiste.

Il problema dell’assegnazione manuale degli attributi

Anche se puoi aggiungere attributi manualmente dopo aver creato un’istanza, questo approccio ha seri svantaggi:

python
class Student:
    pass
 
# È facile dimenticare attributi o sbagliare a scriverli
alice = Student()
alice.name = "Alice Johnson"
alice.student_id = "S12345"
# Hai dimenticato di impostare gpa!
 
bob = Student()
bob.name = "Bob Smith"
bob.stuent_id = "S12346"  # Typo: stuent instead of student
bob.gpa = 3.5
 
# Ora ad alice manca gpa, e bob ha un refuso
# print(alice.gpa)  # AttributeError
# print(bob.student_id)  # AttributeError

Questo è soggetto a errori e noioso. Ti serve un modo per assicurarti che ogni istanza inizi con gli attributi corretti. È qui che entra in gioco il metodo __init__, che tratteremo nella sezione 30.5. Ma prima, impariamo i metodi: funzioni che appartengono a una classe.

30.4) Aggiungere metodi di istanza: comprendere self

I metodi sono funzioni definite dentro una classe che operano sui dati dell’istanza. Danno alle tue classi un comportamento, non solo dati.

Definire un metodo semplice

Aggiungiamo un metodo alla nostra classe Student:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
# Crea un'istanza e aggiungi attributi
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
# Chiama il metodo
alice.display_info()  # Output: Alice Johnson - GPA: 3.8

Il metodo display_info è definito dentro la classe usando def, proprio come le funzioni normali. La differenza principale è il primo parametro: self.

Comprendere self

Il parametro self è il modo in cui un metodo accede all’istanza specifica su cui sta operando. Quando chiami alice.display_info(), Python passa automaticamente alice come primo argomento al metodo. Dentro il metodo, self si riferisce a alice, quindi self.name accede a alice.name e self.gpa accede a alice.gpa.

Ecco cosa succede dietro le quinte:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
# Queste due chiamate sono equivalenti:
alice.display_info()           # Modo normale
Student.display_info(alice)    # Ciò che Python fa realmente
 
# Entrambe stampano: Alice Johnson - GPA: 3.8

Quando scrivi alice.display_info(), Python lo traduce in Student.display_info(alice). L’istanza (alice) diventa il parametro self dentro il metodo.

Perché "self"?

Il nome self è una convenzione, non una parola chiave. Tecnicamente potresti usare qualsiasi nome:

python
class Student:
    def display_info(this):  # Funziona, ma non farlo
        print(f"{this.name} - GPA: {this.gpa}")

Tuttavia, usa sempre self. È una convenzione universale in Python che rende il tuo codice leggibile per altri programmatori Python. Usare un nome diverso confonderà i lettori e violerà gli standard della community.

Metodi con più istanze

La potenza di self diventa chiara quando hai più istanze:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
# Crea due studenti
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
 
# Stesso metodo, dati diversi
alice.display_info()  # Output: Alice Johnson - GPA: 3.8
bob.display_info()    # Output: Bob Smith - GPA: 3.5

Quando chiami alice.display_info(), self è alice. Quando chiami bob.display_info(), self è bob. Lo stesso codice del metodo funziona per qualsiasi istanza perché self si adatta a qualunque istanza lo abbia chiamato.

alice.display_info

self = alice

bob.display_info

self = bob

Accede a alice.name
alice.gpa

Accede a bob.name
bob.gpa

I metodi possono accettare parametri aggiuntivi

I metodi possono accettare parametri oltre a self:

python
class Student:
    def update_gpa(self, new_gpa):
        self.gpa = new_gpa
        print(f"Updated {self.name}'s GPA to {self.gpa}")
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
alice.update_gpa(3.9)  # Output: Updated Alice Johnson's GPA to 3.9
print(alice.gpa)       # Output: 3.9

Quando chiami alice.update_gpa(3.9), Python passa alice come self e 3.9 come new_gpa. La firma del metodo è def update_gpa(self, new_gpa), ma quando lo chiami passi un solo argomento: Python gestisce self automaticamente.

I metodi possono restituire valori

I metodi possono restituire valori proprio come le funzioni normali:

python
class Student:
    def is_honors(self):
        return self.gpa >= 3.5
    
    def get_status(self):
        if self.is_honors():
            return "Honors Student"
        else:
            return "Regular Student"
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.2
 
print(alice.get_status())  # Output: Honors Student
print(bob.get_status())    # Output: Regular Student

Nota come get_status chiama un altro metodo (is_honors) usando self.is_honors(). I metodi possono chiamare altri metodi sulla stessa istanza.

Metodi vs funzioni: quando usare ciascuno

Potresti chiederti quando usare un metodo rispetto a una funzione autonoma. Ecco la regola guida:

Usa un metodo quando l’operazione:

  • Ha bisogno di accedere ai dati dell’istanza (self.name, self.gpa, ecc.)
  • Appartiene logicamente al tipo (è qualcosa che uno Student fa o è)
  • Modifica lo stato dell’istanza

Usa una funzione autonoma quando l’operazione:

  • Non ha bisogno dei dati dell’istanza
  • Funziona con più tipi
  • È un’utilità generale
python
class Student:
    # Metodo: ha bisogno dei dati dell'istanza
    def is_honors(self):
        return self.gpa >= 3.5
 
# Funzione: utilità generale, funziona con qualsiasi valore di GPA
def calculate_letter_grade(gpa):
    if gpa >= 3.7:
        return "A"
    elif gpa >= 3.0:
        return "B"
    elif gpa >= 2.0:
        return "C"
    else:
        return "D"
 
alice = Student()
alice.gpa = 3.8
 
# Usa il metodo per controlli specifici dell'istanza
print(alice.is_honors())  # Output: True
 
# Usa la funzione per calcoli generali
print(calculate_letter_grade(alice.gpa))  # Output: A
print(calculate_letter_grade(2.5))        # Output: C

Pattern comuni di metodi

Ecco alcuni pattern comuni che userai spesso:

Metodi getter (recuperano informazioni calcolate):

python
class Student:
    def get_full_info(self):
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"

Metodi setter (modificano attributi con validazione):

python
class Student:
    def set_gpa(self, new_gpa):
        if 0.0 <= new_gpa <= 4.0:
            self.gpa = new_gpa
        else:
            print("Invalid GPA: must be between 0.0 and 4.0")

Metodi di interrogazione (rispondono a domande sì/no):

python
class Student:
    def is_honors(self):
        return self.gpa >= 3.5
    
    def is_failing(self):
        return self.gpa < 2.0

Metodi di azione (eseguono operazioni):

python
class Student:
    def add_grade(self, grade):
        self.grades.append(grade)
        # Ricalcola il GPA in base a tutti i voti
        self.gpa = sum(self.grades) / len(self.grades)

30.5) Inizializzare istanze con __init__

Impostare manualmente gli attributi dopo aver creato un’istanza è noioso e soggetto a errori. Il metodo __init__ risolve questo problema permettendoti di inizializzare le istanze con dati nel momento in cui vengono create.

Il metodo __init__

Il metodo __init__ (pronunciato “dunder init” o “init”) è un metodo speciale che Python chiama automaticamente quando crei una nuova istanza:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# Crea istanze con dati iniziali
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
 
print(alice.name)  # Output: Alice Johnson
print(bob.gpa)     # Output: 3.5

Quando scrivi Student("Alice Johnson", "S12345", 3.8), Python:

  1. Crea una nuova istanza Student vuota
  2. Chiama __init__ con quell’istanza come self e con i tuoi argomenti
  3. Restituisce l’istanza inizializzata

Il metodo __init__ non restituisce esplicitamente un valore: modifica l’istanza sul posto impostando i suoi attributi. Se provi a restituire un valore da __init__, Python solleverà un TypeError.

python
class Student:
    def __init__(self, name):
        self.name = name
        # Non restituire nulla da __init__
        # return self  # Wrong! TypeError: __init__() should return None, not 'Student'

Come funziona __init__

Scomponiamo cosa succede passo dopo passo:

python
class Student:
    def __init__(self, name, student_id, gpa):
        print(f"Initializing student: {name}")
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
        print(f"Initialization complete")
 
alice = Student("Alice Johnson", "S12345", 3.8)
# Output:
# Initializing student: Alice Johnson
# Initialization complete
 
print(alice.name)  # Output: Alice Johnson

I parametri dopo self (name, student_id, gpa) diventano argomenti obbligatori quando crei un’istanza. Se non li fornisci, Python solleva un TypeError:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# student = Student()  # TypeError: __init__() missing 3 required positional arguments
# student = Student("Alice")  # TypeError: __init__() missing 2 required positional arguments
student = Student("Alice Johnson", "S12345", 3.8)  # Correct

Questo è molto meglio dell’assegnazione manuale degli attributi: Python garantisce che ogni istanza inizi con i dati richiesti.

Valori predefiniti dei parametri in __init__

Puoi usare valori predefiniti dei parametri in __init__, proprio come nelle funzioni normali:

python
class Student:
    def __init__(self, name, student_id, gpa=0.0):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# Il GPA è facoltativo, il valore predefinito è 0.0
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346")  # Usa il valore predefinito gpa=0.0
 
print(alice.gpa)  # Output: 3.8
print(bob.gpa)    # Output: 0.0

Questo è utile per attributi che hanno valori predefiniti sensati ma possono essere personalizzati quando necessario.

Validazione in __init__

Puoi validare l’input in __init__ per garantire che le istanze inizino in uno stato valido:

python
class Student:
    def __init__(self, name, student_id, gpa):
        if not name:
            print("Error: Name cannot be empty")
            self.name = "Unknown"
        else:
            self.name = name
        
        self.student_id = student_id
        
        if 0.0 <= gpa <= 4.0:
            self.gpa = gpa
        else:
            print(f"Warning: Invalid GPA {gpa}, setting to 0.0")
            self.gpa = 0.0
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice.gpa)  # Output: 3.8
 
bob = Student("", "S12346", 5.0)
# Output:
# Error: Name cannot be empty
# Warning: Invalid GPA 5.0, setting to 0.0
print(bob.name)  # Output: Unknown
print(bob.gpa)   # Output: 0.0

Questo garantisce che, anche se qualcuno passa dati non validi, l’istanza finisca in uno stato ragionevole.

30.6) Rappresentazioni in stringa con __str__ e __repr__

Quando stampi un’istanza con print() o la visualizzi nella shell interattiva, Python deve convertirla in una stringa. Per impostazione predefinita, ottieni qualcosa di poco utile:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice)  # Output: <__main__.Student object at 0x...>

L’output predefinito mostra il nome della classe e l’indirizzo di memoria, ma nulla sui dati reali di Alice. Puoi personalizzarlo con i metodi speciali __str__ e __repr__.

Il metodo __str__

Il metodo __str__ definisce come le tue istanze vengono convertite in stringhe da print() e str():

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __str__(self):
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice)  # Output: Alice Johnson (S12345) - GPA: 3.8
print(str(alice))  # Output: Alice Johnson (S12345) - GPA: 3.8

Il metodo __str__ dovrebbe restituire una stringa leggibile e informativa per gli utenti finali. Pensalo come la rappresentazione “amichevole”.

Il metodo __repr__

Il metodo __repr__ definisce la rappresentazione in stringa “ufficiale” delle tue istanze, usata dal REPL e da repr():

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __repr__(self):
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(repr(alice))  # Output: Student('Alice Johnson', 'S12345', 3.8)

Nel REPL:

python
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice
Student('Alice Johnson', 'S12345', 3.8)

Il metodo __repr__ dovrebbe restituire una stringa che assomiglia a codice Python valido per ricreare l’oggetto. Pensalo come la rappresentazione per gli sviluppatori: dovrebbe essere non ambigua e utile per il debugging.

Usare sia __str__ sia __repr__

Puoi definire entrambi i metodi per scopi diversi:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __str__(self):
        # Formato amichevole e leggibile
        return f"{self.name} - GPA: {self.gpa}"
    
    def __repr__(self):
        # Formato non ambiguo, simile a codice
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
alice = Student("Alice Johnson", "S12345", 3.8)
 
print(alice)        # Usa __str__
# Output: Alice Johnson - GPA: 3.8
 
print(repr(alice))  # Usa __repr__
# Output: Student('Alice Johnson', 'S12345', 3.8)

Nel REPL:

python
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice  # Usa __repr__
Student('Alice Johnson', 'S12345', 3.8)
>>> print(alice)  # Usa __str__
Alice Johnson - GPA: 3.8

Quando definire quale metodo

Ecco la linea guida:

  • Definisci sempre __repr__: è usato dal REPL e dagli strumenti di debugging. Se ne definisci uno solo, definisci questo.
  • Definisci __str__ quando ti serve un formato user-friendly: se la tua classe verrà stampata per utenti finali, fornisci un __str__ leggibile.
  • Se definisci solo __repr__: Python lo usa per repr(), e str() ricade sull’uso di __repr__ (quindi anche print() lo usa).
  • Se definisci solo __str__: print() usa __str__, ma repr() e il REPL usano il __repr__ predefinito (che mostra l’indirizzo di memoria). Per questo definire __repr__ è di solito più importante.
python
# Solo __repr__ definito
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    
    def __repr__(self):
        return f"Product('{self.name}', {self.price})"
 
item = Product("Laptop", 999.99)
print(item)        # Usa __repr__ come fallback
# Output: Product('Laptop', 999.99)
print(repr(item))  # Usa __repr__
# Output: Product('Laptop', 999.99)

Rappresentazione in stringa nelle collezioni

Quando le istanze sono dentro collezioni (liste, dizionari, ecc.), Python usa __repr__ per visualizzarle, non __str__:

python
class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.gpa = gpa
    
    def __str__(self):
        return f"{self.name}: {self.gpa}"
    
    def __repr__(self):
        return f"Student('{self.name}', {self.gpa})"
 
students = [
    Student("Alice", 3.8),
    Student("Bob", 3.5),
    Student("Carol", 3.9)
]
 
# Stampare la lista usa __repr__ per ogni studente
print(students)
# Output: [Student('Alice', 3.8), Student('Bob', 3.5), Student('Carol', 3.9)]
 
# Stampare i singoli studenti usa __str__
for student in students:
    print(student)
# Output:
# Alice: 3.8
# Bob: 3.5
# Carol: 3.9

Ecco perché __repr__ dovrebbe essere non ambiguo: ti aiuta a capire cosa c’è nelle strutture dati durante il debugging. Quando stampi una lista, Python in sostanza chiama repr() su ogni elemento per mostrare chiaramente la struttura.

30.7) Creare più istanze indipendenti

Uno degli aspetti più potenti delle classi è che puoi creare molte istanze indipendenti, ciascuna con i propri dati. Esploriamolo in profondità.

Ogni istanza ha i propri dati

Quando crei più istanze dalla stessa classe, ognuna mantiene i propri attributi separati:

python
class BankAccount:
    def __init__(self, account_number, holder_name, balance=0.0):
        self.account_number = account_number
        self.holder_name = holder_name
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        print(f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}")
    
    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            print(f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}")
            return True
        else:
            print(f"Insufficient funds. Balance: ${self.balance:.2f}")
            return False
    
    def __str__(self):
        return f"{self.holder_name}'s account ({self.account_number}): ${self.balance:.2f}"
 
# Crea tre conti indipendenti
alice_account = BankAccount("ACC-001", "Alice Johnson", 1000.0)
bob_account = BankAccount("ACC-002", "Bob Smith", 500.0)
carol_account = BankAccount("ACC-003", "Carol Davis", 2000.0)
 
# Le operazioni su un conto non influenzano gli altri
alice_account.deposit(500)
# Output: Deposited $500.00. New balance: $1500.00
 
bob_account.withdraw(200)
# Output: Withdrew $200.00. New balance: $300.00
 
# Ogni conto mantiene il proprio saldo
print(alice_account)  # Output: Alice Johnson's account (ACC-001): $1500.00
print(bob_account)    # Output: Bob Smith's account (ACC-002): $300.00
print(carol_account)  # Output: Carol Davis's account (ACC-003): $2000.00

Questa indipendenza è fondamentale nella programmazione orientata agli oggetti. Ogni istanza è un’entità separata con il proprio stato.

Istanze nelle collezioni

Puoi memorizzare istanze in liste, dizionari o qualsiasi altra collezione:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def is_honors(self):
        return self.gpa >= 3.5
    
    def __repr__(self):
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
# Crea una lista di studenti
students = [
    Student("Alice Johnson", "S12345", 3.8),
    Student("Bob Smith", "S12346", 3.2),
    Student("Carol Davis", "S12347", 3.9),
    Student("David Lee", "S12348", 3.4)
]
 
# Trova tutti gli studenti con lode
honors_students = []
for student in students:
    if student.is_honors():
        honors_students.append(student)
 
print("Honors students:")
for student in honors_students:
    print(f"  {student.name}: {student.gpa}")
# Output:
# Honors students:
#   Alice Johnson: 3.8
#   Carol Davis: 3.9
 
# Calcola la media del GPA
total_gpa = sum(student.gpa for student in students)
average_gpa = total_gpa / len(students)
print(f"Average GPA: {average_gpa:.2f}")  # Output: Average GPA: 3.58

Questo è un pattern comune: creare più istanze, memorizzarle in una collezione, poi elaborarle con cicli e comprensioni.

Le istanze possono riferirsi ad altre istanze

Le istanze possono avere attributi che fanno riferimento ad altre istanze, creando relazioni tra oggetti:

python
class Course:
    def __init__(self, course_code, course_name):
        self.course_code = course_code
        self.course_name = course_name
    
    def __str__(self):
        return f"{self.course_code}: {self.course_name}"
 
class Student:
    def __init__(self, name, student_id):
        self.name = name
        self.student_id = student_id
        self.courses = []  # Lista di istanze Course
    
    def enroll(self, course):
        self.courses.append(course)
        print(f"{self.name} enrolled in {course.course_name}")
    
    def list_courses(self):
        print(f"{self.name}'s courses:")
        for course in self.courses:
            print(f"  {course}")
 
# Crea corsi
python_course = Course("CS101", "Introduction to Python")
data_course = Course("CS102", "Data Structures")
web_course = Course("CS103", "Web Development")
 
# Crea studenti e iscrivili ai corsi
alice = Student("Alice Johnson", "S12345")
alice.enroll(python_course)
alice.enroll(data_course)
# Output:
# Alice Johnson enrolled in Introduction to Python
# Alice Johnson enrolled in Data Structures
 
bob = Student("Bob Smith", "S12346")
bob.enroll(python_course)
bob.enroll(web_course)
# Output:
# Bob Smith enrolled in Introduction to Python
# Bob Smith enrolled in Web Development
 
# Elenca i corsi di ciascuno studente
alice.list_courses()
# Output:
# Alice Johnson's courses:
#   CS101: Introduction to Python
#   CS102: Data Structures
 
bob.list_courses()
# Output:
# Bob Smith's courses:
#   CS101: Introduction to Python
#   CS103: Web Development

Nota che sia Alice sia Bob sono iscritti a python_course: stanno facendo riferimento alla stessa istanza di Course. Questo modella la relazione del mondo reale in cui più studenti possono seguire lo stesso corso.

Identità ed uguaglianza delle istanze

Ogni istanza è un oggetto unico, anche se ha gli stessi dati di un’altra istanza:

python
class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.gpa = gpa
 
alice1 = Student("Alice", 3.8)
alice2 = Student("Alice", 3.8)
 
# Oggetti diversi, anche con dati identici
print(alice1 is alice2)  # Output: False
print(id(alice1) == id(alice2))  # Output: False

Per impostazione predefinita, anche == controlla l’identità (se sono lo stesso oggetto), non se hanno gli stessi dati. Nel Capitolo 31, impareremo come personalizzare il confronto di uguaglianza con il metodo speciale __eq__.


Questo capitolo ti ha introdotto ai fondamenti della programmazione orientata agli oggetti in Python. Hai imparato come definire classi, creare istanze, aggiungere metodi, inizializzare istanze con __init__, controllare le rappresentazioni in stringa e lavorare con più istanze indipendenti. Questi concetti costituiscono la base per funzionalità OOP più avanzate che esploreremo nei Capitoli 31 e 32.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai