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:
# 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:
- Nessuna validazione: Nulla ti impedisce di impostare
gpasu un valore non valido come-5.0o"excellent" - Nessun comportamento correlato: Operazioni come calcolare lo stato di merito o formattare le informazioni dello studente sono funzioni separate sparse nel codice
- 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:
# 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: TrueQuesto 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.
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:
- Definire classi con la parola chiave
class - Creare istanze e accedere ai loro attributi
- Aggiungere metodi che operano sui dati dell’istanza
- Comprendere
selfe come i metodi accedono ai dati dell’istanza - Inizializzare istanze con il metodo
__init__ - Controllare le rappresentazioni in stringa con
__str__e__repr__ - 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:
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:
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 constantsQuesta 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:
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: TrueOgni 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:
-
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à.
-
Uso pratico: Anche le classi vuote possono essere utili come marcatori o segnaposto. Per esempio, potresti definire tipi di eccezione personalizzati:
class InvalidGradeError:
pass
class StudentNotFoundError:
pass
# Queste classi vuote fungono da tipi di errore distintiTuttavia, 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:
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.8L’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:
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:
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.comQuesta 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:
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:
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) # AttributeErrorQuesto è 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:
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.8Il 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:
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.8Quando 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:
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:
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.5Quando 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.
I metodi possono accettare parametri aggiuntivi
I metodi possono accettare parametri oltre a self:
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.9Quando 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:
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 StudentNota 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
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: CPattern comuni di metodi
Ecco alcuni pattern comuni che userai spesso:
Metodi getter (recuperano informazioni calcolate):
class Student:
def get_full_info(self):
return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"Metodi setter (modificano attributi con validazione):
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):
class Student:
def is_honors(self):
return self.gpa >= 3.5
def is_failing(self):
return self.gpa < 2.0Metodi di azione (eseguono operazioni):
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:
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.5Quando scrivi Student("Alice Johnson", "S12345", 3.8), Python:
- Crea una nuova istanza
Studentvuota - Chiama
__init__con quell’istanza comeselfe con i tuoi argomenti - 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.
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:
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 JohnsonI parametri dopo self (name, student_id, gpa) diventano argomenti obbligatori quando crei un’istanza. Se non li fornisci, Python solleva un TypeError:
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) # CorrectQuesto è 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:
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.0Questo è 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:
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.0Questo 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:
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():
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.8Il 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():
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:
>>> 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:
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:
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice # Usa __repr__
Student('Alice Johnson', 'S12345', 3.8)
>>> print(alice) # Usa __str__
Alice Johnson - GPA: 3.8Quando 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 perrepr(), estr()ricade sull’uso di__repr__(quindi ancheprint()lo usa). - Se definisci solo
__str__:print()usa__str__, marepr()e il REPL usano il__repr__predefinito (che mostra l’indirizzo di memoria). Per questo definire__repr__è di solito più importante.
# 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__:
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.9Ecco 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:
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.00Questa 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:
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.58Questo è 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:
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 DevelopmentNota 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:
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: FalsePer 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.