18. Modello dei dati e degli oggetti in Python: riferimenti, confronti e copie
Comprendere come Python memorizza e gestisce i dati è fondamentale per scrivere programmi corretti. In questo capitolo esploreremo il modello a oggetti di Python—il sistema fondamentale che governa il funzionamento di tutti i dati in Python. Imparerai perché alcune assegnazioni creano copie indipendenti mentre altre creano riferimenti condivisi, come confrontare correttamente gli oggetti e come evitare errori comuni quando lavori con le collezioni(collections).
Questa conoscenza ti aiuterà a capire comportamenti sorprendenti che potresti aver incontrato, come perché modificare una lista(list) a volte influisce su un’altra, o perché confrontare due liste con == dà risultati diversi rispetto a confrontarle con is.
18.1) In Python tutto è un oggetto
In Python, ogni dato è un oggetto. Non è solo un concetto teorico—ha implicazioni pratiche su come funzionano i tuoi programmi.
Quando crei un numero, una stringa, una lista o qualsiasi altro valore, Python crea un oggetto in memoria. Un oggetto è un contenitore che include:
- I dati effettivi (il valore)
- Informazioni su che tipo di dato è (il tipo)
- Un identificatore univoco (l’identità)
Vediamolo in pratica:
# Creazione di diversi tipi di oggetti
number = 42
text = "Hello"
items = [1, 2, 3]
# Ognuna di queste variabili fa riferimento a un oggetto in memoria
print(number) # Output: 42
print(text) # Output: Hello
print(items) # Output: [1, 2, 3]Persino valori semplici come gli interi sono oggetti. Questo significa che hanno capacità oltre al semplice memorizzare un numero:
# Gli interi sono oggetti con metodi
number = 42
print(number.bit_length()) # Output: 6
# Le stringhe sono oggetti con metodi
text = "hello"
print(text.upper()) # Output: HELLO
# Le liste sono oggetti con metodi
items = [3, 1, 2]
items.sort()
print(items) # Output: [1, 2, 3]Perché è importante? Perché quando assegni una variabile o passi dei dati a una funzione(function), non stai copiando l’oggetto—stai creando un riferimento(reference) allo stesso oggetto. Questo è fondamentalmente diverso da come funzionano alcuni altri linguaggi di programmazione, e capire questa distinzione previene molti bug confusi.
# Creazione di un oggetto lista
original = [1, 2, 3]
# Questo non crea una nuova lista: crea un altro riferimento
# alla STESSA lista
another_name = original
# La modifica tramite un riferimento influisce anche sull'altro
another_name.append(4)
print(original) # Output: [1, 2, 3, 4]
print(another_name) # Output: [1, 2, 3, 4]Sia original sia another_name fanno riferimento allo stesso oggetto lista in memoria. Quando modifichiamo la lista tramite another_name, vediamo la modifica tramite original perché entrambi stanno guardando lo stesso oggetto.
Questo comportamento si chiama semantica dei riferimenti(reference semantics) ed è uno dei concetti più importanti della programmazione in Python. Lo esploreremo in profondità nel corso di questo capitolo.
18.2) Identità, tipo e valore degli oggetti
Ogni oggetto in Python ha tre caratteristiche fondamentali che lo definiscono: identità, tipo e valore. Comprendere queste caratteristiche ti aiuta a ragionare su come si comportano gli oggetti e su come confrontarli correttamente.
18.2.1) Identità dell’oggetto con id()
L’identità di un oggetto è un numero univoco che Python assegna quando l’oggetto viene creato. Questa identità non cambia mai durante la vita dell’oggetto—è come un indirizzo permanente in memoria.
Puoi recuperare l’identità di un oggetto usando la funzione id():
# Creazione di oggetti e controllo delle loro identità
x = [1, 2, 3]
y = [1, 2, 3]
z = x
print(id(x)) # Output: 140234567890123 (example - actual number varies)
print(id(y)) # Output: 140234567890456 (different from x)
print(id(z)) # Output: 140234567890123 (same as x)I numeri effettivi che vedrai saranno diversi ogni volta che esegui il programma, ma lo schema resta: x e y hanno identità diverse perché sono oggetti diversi, anche se contengono gli stessi valori. Nel frattempo, z ha la stessa identità di x perché z è solo un altro nome per lo stesso oggetto.
Ecco un esempio pratico che mostra perché l’identità conta:
# Due studenti con gli stessi voti
student1_grades = [85, 90, 92]
student2_grades = [85, 90, 92]
# Questi sono oggetti diversi (identità diverse)
print(id(student1_grades)) # Output: 140234567890123 (example)
print(id(student2_grades)) # Output: 140234567890456 (different)
# Modificare uno non influisce sull'altro
student1_grades.append(88)
print(student1_grades) # Output: [85, 90, 92, 88]
print(student2_grades) # Output: [85, 90, 92]Ora considera uno scenario diverso:
# I voti di uno studente tracciati da due variabili
original_grades = [85, 90, 92]
backup_reference = original_grades
# Questi fanno riferimento allo STESSO oggetto (stessa identità)
print(id(original_grades)) # Output: 140234567890123 (example)
print(id(backup_reference)) # Output: 140234567890123 (same!)
# Modificare tramite uno qualsiasi dei nomi influisce su entrambi
backup_reference.append(88)
print(original_grades) # Output: [85, 90, 92, 88]
print(backup_reference) # Output: [85, 90, 92, 88]Intuizione chiave: Quando due variabili hanno la stessa identità, fanno riferimento esattamente allo stesso oggetto in memoria. Le modifiche fatte tramite una variabile sono visibili tramite l’altra perché c’è un solo oggetto che viene modificato.
18.2.2) Tipo dell’oggetto con type()
Il tipo di un oggetto determina che tipo di dati contiene e quali operazioni puoi eseguire su di esso. Come abbiamo imparato nel Capitolo 3, puoi controllare il tipo di un oggetto usando la funzione type():
# Diversi tipi di oggetti
number = 42
text = "Hello"
items = [1, 2, 3]
mapping = {"name": "Alice"}
print(type(number)) # Output: <class 'int'>
print(type(text)) # Output: <class 'str'>
print(type(items)) # Output: <class 'list'>
print(type(mapping)) # Output: <class 'dict'>Il tipo di un oggetto non cambia mai dopo la creazione. Non puoi trasformare un intero in una stringa—puoi solo creare un nuovo oggetto stringa basato sul valore dell’intero:
# Il tipo è fissato alla creazione
x = 42
print(type(x)) # Output: <class 'int'>
# Questo non cambia il tipo di x: crea un NUOVO oggetto stringa
# e fa sì che x faccia riferimento a quell'oggetto nuovo
x = str(x)
# L'oggetto intero originale (42) esiste ancora in memoria finché non viene garbage collected
# x ora punta a un oggetto completamente diverso: la stringa "42"
print(type(x)) # Output: <class 'str'>
print(x) # Output: 42 (ora una stringa, non un intero)Comprendere i tipi è fondamentale perché tipi diversi supportano operazioni diverse:
# Le liste supportano append
grades = [85, 90]
grades.append(92)
print(grades) # Output: [85, 90, 92]
# Le stringhe non hanno append: sono immutabili
text = "Hello"
# text.append(" World") # AttributeError: 'str' object has no attribute 'append'
# Ma le stringhe supportano la concatenazione
text = text + " World"
print(text) # Output: Hello World18.2.3) Valore dell’oggetto
Il valore di un oggetto è il dato effettivo che contiene. A differenza di identità e tipo, il valore può cambiare per oggetti mutabili(mutable) (come liste e dizionari) ma non può cambiare per oggetti immutabili(immutable) (come interi e stringhe).
# Per gli oggetti mutabili, il valore può cambiare
shopping_cart = ["milk", "bread"]
print(shopping_cart) # Output: ['milk', 'bread']
shopping_cart.append("eggs")
print(shopping_cart) # Output: ['milk', 'bread', 'eggs']
# Stesso oggetto (stessa identità), valore diverso
# Per gli oggetti immutabili, il valore non può cambiare
count = 5
print(count) # Output: 5
count = count + 1
print(count) # Output: 6
# Questo ha creato un NUOVO oggetto con una nuova identitàEcco un esempio completo che mostra tutte e tre le caratteristiche:
# Creazione di un oggetto lista
data = [10, 20, 30]
print("Identity:", id(data)) # Output: Identity: 140234567890123 (example)
print("Type:", type(data)) # Output: Type: <class 'list'>
print("Value:", data) # Output: Value: [10, 20, 30]
# Modifica del valore (identità e tipo restano gli stessi)
data.append(40)
print("Identity:", id(data)) # Output: Identity: 140234567890123 (unchanged)
print("Type:", type(data)) # Output: Type: <class 'list'> (unchanged)
print("Value:", data) # Output: Value: [10, 20, 30, 40] (changed)Comprendere queste tre caratteristiche ti aiuta a prevedere come si comporteranno gli oggetti nei tuoi programmi. L’identità ti dice se due variabili fanno riferimento allo stesso oggetto, il tipo ti dice quali operazioni sono consentite e il valore ti dice quali dati l’oggetto contiene in quel momento.
18.3) Tipi mutabili e immutabili
Una delle distinzioni più importanti in Python è tra tipi mutabili(mutable) e immutabili(immutable). Questa distinzione influisce su come si comportano gli oggetti quando provi a cambiarli, e comprenderla previene molti errori di programmazione comuni.
18.3.1) Tipi immutabili: valori che non possono cambiare
Un oggetto immutabile è un oggetto il cui valore non può essere cambiato dopo la creazione. Quando esegui un’operazione che sembra modificare un oggetto immutabile, Python in realtà crea un nuovo oggetto con il valore modificato.
I tipi immutabili di Python includono:
- Interi (
int) - Numeri in virgola mobile (
float) - Stringhe (
str) - Tuple (
tuple) - Booleani (
bool) - None (
NoneType)
Vediamo l’immutabilità in azione con gli interi:
# Creazione di un intero
x = 100
print("Original x:", x) # Output: Original x: 100
print("Identity of x:", id(x)) # Output: Identity of x: 140234567890123 (example)
# Sembra che stiamo modificando x, ma in realtà stiamo creando un nuovo oggetto
x = x + 1
print("Modified x:", x) # Output: Modified x: 101
print("Identity of x:", id(x)) # Output: Identity of x: 140234567890456 (different!)L’identità è cambiata perché x = x + 1 ha creato un oggetto intero completamente nuovo con valore 101. L’oggetto originale con valore 100 esiste ancora (finché il garbage collector di Python non lo rimuove), ma x ora fa riferimento a un oggetto diverso.
Le stringhe mostrano l’immutabilità ancora più chiaramente:
# Creazione di una stringa
message = "Hello"
print("Original:", message) # Output: Original: Hello
print("Identity:", id(message)) # Output: Identity: 140234567890789 (example)
# I metodi delle stringhe non modificano l'originale: restituiscono nuove stringhe
uppercase = message.upper()
print("Original:", message) # Output: Original: Hello (unchanged)
print("Uppercase:", uppercase) # Output: Uppercase: HELLO
print("Identity of original:", id(message)) # Output: Identity of original: 140234567890789 (same)
print("Identity of uppercase:", id(uppercase)) # Output: Identity of uppercase: 140234567891012 (different)Anche operazioni che sembrano modificare una stringa in realtà creano nuovi oggetti stringa:
# Costruire una stringa tramite concatenazione
text = "Python"
print("Before:", text, "- ID:", id(text)) # Output: Before: Python - ID: 140234567891234 (example)
text = text + " Programming"
print("After:", text, "- ID:", id(text)) # Output: After: Python Programming - ID: 140234567891567 (different)Perché l’immutabilità conta: gli oggetti immutabili sono sicuri da condividere tra parti diverse del programma perché nessuna parte può modificarli accidentalmente. Questo rende il codice più prevedibile e più facile da comprendere.
18.3.2) Tipi mutabili: valori che possono cambiare
Un oggetto mutabile è un oggetto il cui valore può essere cambiato dopo la creazione senza creare un nuovo oggetto. L’identità dell’oggetto resta la stessa, ma i suoi contenuti possono essere modificati.
I tipi mutabili di Python includono:
- Liste (
list) - Dizionari (
dict) - Insiemi (
set)
Vediamo la mutabilità con le liste:
# Creazione di una lista
numbers = [1, 2, 3]
print("Original:", numbers) # Output: Original: [1, 2, 3]
print("Identity:", id(numbers)) # Output: Identity: 140234567892345 (example)
# Modifica della lista - stesso oggetto, valore diverso
numbers.append(4)
print("Modified:", numbers) # Output: Modified: [1, 2, 3, 4]
print("Identity:", id(numbers)) # Output: Identity: 140234567892345 (same!)L’identità non è cambiata perché abbiamo modificato l’oggetto lista esistente invece di crearne uno nuovo. Questo è fondamentalmente diverso da come funzionano i tipi immutabili.
Anche dizionari e insiemi sono mutabili:
# Esempio con dizionario
student = {"name": "Alice", "grade": 85}
print("Before:", student, "- ID:", id(student)) # Output: Before: {'name': 'Alice', 'grade': 85} - ID: 140234567893012 (example)
student["grade"] = 90 # Modifica del dizionario
print("After:", student, "- ID:", id(student)) # Output: After: {'name': 'Alice', 'grade': 90} - ID: 140234567893012 (same)
# Esempio con set
unique_numbers = {1, 2, 3}
print("Before:", unique_numbers, "- ID:", id(unique_numbers)) # Output: Before: {1, 2, 3} - ID: 140234567893345 (example)
unique_numbers.add(4) # Modifica del set
print("After:", unique_numbers, "- ID:", id(unique_numbers)) # Output: After: {1, 2, 3, 4} - ID: 140234567893345 (same)18.3.3) Perché la mutabilità conta nella pratica
La differenza tra tipi mutabili e immutabili diventa critica quando più variabili fanno riferimento allo stesso oggetto:
# Esempio immutabile - condivisione sicura
x = "Hello"
y = x # y fa riferimento allo stesso oggetto stringa
# "Modificare" x crea un nuovo oggetto
x = x + " World"
print(x) # Output: Hello World
print(y) # Output: Hello (unchanged - y still refers to the original)# Esempio mutabile - modifiche condivise
list1 = [1, 2, 3]
list2 = list1 # list2 fa riferimento allo STESSO oggetto lista
# Modificare tramite list1 influisce su list2
list1.append(4)
print(list1) # Output: [1, 2, 3, 4]
print(list2) # Output: [1, 2, 3, 4] (also changed!)Comprendere la mutabilità è essenziale per:
- Prevedere il comportamento: sapere se un’operazione crea un nuovo oggetto o modifica un oggetto esistente
- Evitare bug: prevenire modifiche involontarie quando gli oggetti sono condivisi
- Scrivere codice efficiente: scegliere il tipo giusto per il tuo caso d’uso
- Comprendere il comportamento delle funzioni: sapere quando i parametri di una funzione possono essere modificati
Nelle prossime sezioni esploreremo come funziona l’assegnazione con questi tipi diversi e come creare copie indipendenti quando necessario.
18.4) Come funziona l’assegnazione con gli oggetti
L’assegnazione in Python non copia gli oggetti—crea riferimenti(references) agli oggetti. Comprendere questa distinzione è fondamentale per scrivere programmi corretti, soprattutto quando lavori con tipi mutabili.
18.4.1) L’assegnazione crea riferimenti, non copie
Quando scrivi x = y, Python non crea una copia dell’oggetto a cui y fa riferimento. Invece, fa sì che x faccia riferimento allo stesso oggetto a cui y fa riferimento. Entrambe le variabili diventano nomi per lo stesso oggetto in memoria.
Vediamolo prima con oggetti immutabili:
# Assegnazione con interi (immutabili)
a = 100
b = a # b ora fa riferimento allo stesso oggetto intero di a
print("a:", a) # Output: a: 100
print("b:", b) # Output: b: 100
print("Same object?", id(a) == id(b)) # Output: Same object? True
# "Modificare" a crea un nuovo oggetto
a = a + 1
print("a:", a) # Output: a: 101
print("b:", b) # Output: b: 100 (unchanged)
print("Same object?", id(a) == id(b)) # Output: Same object? FalseCon gli oggetti immutabili, questo comportamento di solito è sicuro perché non puoi modificare l’oggetto originale. Quando esegui un’operazione che cambia il valore, Python crea un nuovo oggetto.
Tuttavia, con gli oggetti mutabili, il comportamento è molto diverso:
# Assegnazione con liste (mutabili)
list1 = [1, 2, 3]
list2 = list1 # list2 fa riferimento allo STESSO oggetto lista di list1
print("list1:", list1) # Output: list1: [1, 2, 3]
print("list2:", list2) # Output: list2: [1, 2, 3]
print("Same object?", id(list1) == id(list2)) # Output: Same object? True
# Modificare tramite list1 influisce su list2
list1.append(4)
print("list1:", list1) # Output: list1: [1, 2, 3, 4]
print("list2:", list2) # Output: list2: [1, 2, 3, 4] (also changed!)
print("Same object?", id(list1) == id(list2)) # Output: Same object? TrueSia list1 sia list2 sono nomi per lo stesso oggetto lista. Quando modifichi la lista tramite uno dei due nomi, vedi la modifica tramite entrambi perché c’è una sola lista.
Ecco un esempio pratico che mostra perché questo conta:
# Gestire i voti degli studenti
alice_grades = [85, 90, 92]
backup_grades = alice_grades # Tentativo di creare un backup
print("Original:", alice_grades) # Output: Original: [85, 90, 92]
print("Backup:", backup_grades) # Output: Backup: [85, 90, 92]
# Aggiungere un nuovo voto
alice_grades.append(88)
# Anche il "backup" è stato modificato!
print("Original:", alice_grades) # Output: Original: [85, 90, 92, 88]
print("Backup:", backup_grades) # Output: Backup: [85, 90, 92, 88]Questo non è affatto un backup—entrambe le variabili fanno riferimento alla stessa lista. Per creare un vero backup, devi creare una copia (che tratteremo nella Sezione 18.8).
18.4.2) Assegnazione nelle chiamate di funzione
Quando passi un argomento a una funzione, Python usa la stessa semantica dei riferimenti. Il parametro diventa un altro nome per lo stesso oggetto:
# Funzione con parametro immutabile
def increment(number):
number = number + 1 # Crea un nuovo oggetto
return number
value = 5
result = increment(value)
print("Original value:", value) # Output: Original value: 5 (unchanged)
print("Returned result:", result) # Output: Returned result: 6Il parametro number inizialmente fa riferimento allo stesso oggetto intero di value. Quando facciamo number = number + 1, creiamo un nuovo oggetto intero e facciamo sì che number lo referenzi. L’oggetto originale (e value) restano invariati.
Con gli oggetti mutabili, il comportamento è diverso:
# Funzione con parametro mutabile
def add_item(items, new_item):
items.append(new_item) # Modifica la lista originale
shopping_list = ["milk", "bread"]
add_item(shopping_list, "eggs")
print("Original list:", shopping_list) # Output: Original list: ['milk', 'bread', 'eggs']Il parametro items fa riferimento allo stesso oggetto lista di shopping_list. Quando modifichiamo la lista tramite items, stiamo modificando la lista originale.
Ecco un errore comune e come evitarlo:
# ERRORE: modifica involontaria dell'originale
def process_grades(grades):
grades.append(100) # Modifica l'originale!
return grades
student_grades = [85, 90, 92]
processed = process_grades(student_grades)
print("Original:", student_grades) # Output: Original: [85, 90, 92, 100] (modified!)
print("Processed:", processed) # Output: Processed: [85, 90, 92, 100]
# CORRETTO: crea una copia se non vuoi modificare l'originale
def process_grades_safely(grades):
# Crea una nuova lista con gli stessi elementi
result = grades + [100] # La concatenazione crea una nuova lista
return result
student_grades = [85, 90, 92]
processed = process_grades_safely(student_grades)
print("Original:", student_grades) # Output: Original: [85, 90, 92] (unchanged)
print("Processed:", processed) # Output: Processed: [85, 90, 92, 100]Nota importante sugli argomenti predefiniti mutabili: un’insidia comune correlata riguarda l’uso di oggetti mutabili come valori predefiniti dei parametri (come def func(items=[]):). I parametri predefiniti vengono creati una sola volta quando la funzione è definita, non ogni volta che viene chiamata, il che può portare a un comportamento inatteso in cui la lista predefinita accumula valori attraverso più chiamate della funzione. Lo esploreremo in dettaglio nel Capitolo 20, ma tieni presente che questa è una fonte frequente di bug quando si lavora con parametri mutabili.
18.5) Semantica dei riferimenti e aliasing degli oggetti
La semantica dei riferimenti(reference semantics) significa che le variabili in Python sono nomi che fanno riferimento agli oggetti, non contenitori che conservano valori. Quando più variabili fanno riferimento allo stesso oggetto, lo chiamiamo aliasing(aliasing). Comprendere l’aliasing è essenziale per prevedere come si comportano i tuoi programmi.
18.5.1) Che cos’è l’aliasing?
L’aliasing si verifica quando due o più variabili fanno riferimento allo stesso oggetto in memoria. Le variabili sono “alias” l’una dell’altra—nomi diversi per la stessa cosa.
Vediamo l’aliasing con un esempio semplice:
# Creazione di una lista e di un alias
original = [1, 2, 3]
alias = original # alias fa riferimento alla stessa lista di original
print("Original:", original) # Output: Original: [1, 2, 3]
print("Alias:", alias) # Output: Alias: [1, 2, 3]
print("Same object?", id(original) == id(alias)) # Output: Same object? True
# Modifica tramite l'alias
alias.append(4)
# La modifica è visibile tramite entrambi i nomi
print("Original:", original) # Output: Original: [1, 2, 3, 4]
print("Alias:", alias) # Output: Alias: [1, 2, 3, 4]C’è un solo oggetto lista in memoria, ma ha due nomi: original e alias. Qualsiasi modifica fatta tramite uno dei due nomi influisce sullo stesso oggetto sottostante.
Ecco un esempio più realistico con record di studenti:
# Database studenti con aliasing
students = {
"alice": {"name": "Alice", "grade": 85},
"bob": {"name": "Bob", "grade": 90}
}
# Creazione di un alias al record di Alice
alice_record = students["alice"]
print("Alice's grade:", alice_record["grade"]) # Output: Alice's grade: 85
# Modifica tramite l'alias
alice_record["grade"] = 95
# La modifica è visibile nel dizionario originale
print("Updated grade:", students["alice"]["grade"]) # Output: Updated grade: 95La variabile alice_record è un alias del dizionario memorizzato in students["alice"]. Quando modifichiamo alice_record, stiamo modificando lo stesso dizionario che è memorizzato nel dizionario students.
18.5.2) Rilevare l’aliasing con l’operatore is
Puoi verificare se due variabili sono alias (fanno riferimento allo stesso oggetto) usando l’operatore is:
# Controllare l'aliasing
list1 = [1, 2, 3]
list2 = list1 # Alias
list3 = [1, 2, 3] # Oggetto diverso con lo stesso valore
print("list1 is list2:", list1 is list2) # Output: list1 is list2: True (aliases)
print("list1 is list3:", list1 is list3) # Output: list1 is list3: False (different objects)
print("list1 == list3:", list1 == list3) # Output: list1 == list3: True (same value)L’operatore is controlla l’identità (se due variabili fanno riferimento allo stesso oggetto), mentre == controlla il valore (se due oggetti hanno gli stessi contenuti). Esploreremo questa distinzione nel dettaglio nella Sezione 18.6.
18.5.3) Aliasing nelle collezioni
L’aliasing diventa più complesso quando gli oggetti sono memorizzati nelle collezioni:
# Creazione di una lista di liste
row = [0, 0, 0]
grid = [row, row, row] # Tutti e tre gli elementi sono alias della stessa lista!
print("Grid:")
for r in grid:
print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
# Modificare un elemento influisce su tutte le righe
grid[0][0] = 1
print("\nAfter modification:")
for r in grid:
print(r)
# Output:
# [1, 0, 0]
# [1, 0, 0]
# [1, 0, 0]Questo è un errore comune quando si cerca di creare una griglia 2D. Tutte e tre le righe sono alias della stessa lista, quindi modificare una riga le modifica tutte.
Il modo corretto per creare righe indipendenti:
# Creazione di righe indipendenti
grid = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] # Ogni riga è una lista separata
print("Grid:")
for r in grid:
print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
# Ora modificare un elemento influisce solo su quella riga
grid[0][0] = 1
print("\nAfter modification:")
for r in grid:
print(r)
# Output:
# [1, 0, 0]
# [0, 0, 0]
# [0, 0, 0]18.6) Uguaglianza, identità e appartenenza (==, is e in) tra tipi
Python fornisce tre operatori fondamentali per confrontare e verificare relazioni tra oggetti: == per l’uguaglianza, is per l’identità e in per l’appartenenza. Capire quando usare ciascun operatore è essenziale per scrivere programmi corretti.
18.6.1) Uguaglianza con == (confronto dei valori)
L’operatore == controlla se due oggetti hanno lo stesso valore. Non importa se sono lo stesso oggetto in memoria—conta solo se i loro contenuti sono uguali.
# Confronto dei valori con ==
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
print(list1 == list2) # Output: True (same values)
print(list1 == list3) # Output: True (same values)Anche se list1 e list2 sono oggetti diversi in memoria, hanno lo stesso valore, quindi == restituisce True.
Ecco come funziona == con tipi diversi:
# Uguaglianza tra tipi diversi
print(42 == 42) # Output: True (same integer value)
print(42 == 42.0) # Output: True (integer equals float with same value)
print("hello" == "hello") # Output: True (same string value)
print([1, 2] == [1, 2]) # Output: True (same list contents)
print({"a": 1} == {"a": 1}) # Output: True (same dictionary contents)
# Valori diversi
print(42 == 43) # Output: False
print("hello" == "Hello") # Output: False (case-sensitive)
print([1, 2] == [2, 1]) # Output: False (order matters)Per le collezioni, == esegue un confronto profondo(deep comparison)—controlla se tutti gli elementi sono uguali:
# Confronto profondo con strutture annidate
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
print(list1 == list2) # Output: True (all nested elements are equal)
# Anche se le liste interne sono oggetti diversi
print(id(list1[0]) == id(list2[0])) # Output: False (different objects)
print(list1[0] == list2[0]) # Output: True (same values)18.6.2) Identità con is (confronto dell’identità dell’oggetto)
L’operatore is controlla se due variabili fanno riferimento allo stesso oggetto in memoria. Confronta le identità, non i valori.
# Confronto delle identità con is
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
print(list1 is list2) # Output: False (different objects)
print(list1 is list3) # Output: True (same object)
# Conferma con id()
print(id(list1) == id(list2)) # Output: False
print(id(list1) == id(list3)) # Output: TrueQuando usare is: l’uso più comune di is è controllare None:
# Controllare None (nel modo corretto)
def find_student(name, students):
"""Return student record or None if not found."""
for student in students:
if student["name"] == name:
return student
return None
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 90}
]
result = find_student("Charlie", students)
# Usa 'is' per controllare None
if result is None:
print("Student not found") # Output: Student not found
else:
print(f"Found: {result}")18.6.3) Appartenenza con in (controllo del contenimento)
L’operatore in controlla se un valore è contenuto in una collezione. Funziona con stringhe, liste, tuple, set e dizionari:
# Appartenenza in tipi diversi
print(2 in [1, 2, 3]) # Output: True
print("hello" in "hello world") # Output: True
print("x" in {"x": 10, "y": 20}) # Output: True (checks keys)
print(5 in {1, 2, 3, 4, 5}) # Output: TruePer i dizionari, in controlla se esiste una chiave:
# Controllo dell'appartenenza in un dizionario
student = {"name": "Alice", "grade": 85, "age": 20}
print("name" in student) # Output: True (key exists)
print("Alice" in student) # Output: False (value, not key)
print("grade" in student) # Output: True (key exists)
# Controllare i valori richiede l'accesso a .values()
print("Alice" in student.values()) # Output: TrueL’operatore not in controlla l’assenza:
# Controllo dell'assenza
shopping_list = ["milk", "bread", "eggs"]
if "butter" not in shopping_list:
print("Don't forget to buy butter!") # Output: Don't forget to buy butter!Riepilogo di quando usare ciascun operatore:
- Usa
==quando vuoi controllare se due oggetti hanno lo stesso valore - Usa
isquando vuoi controllare se due variabili fanno riferimento allo stesso oggetto (più comunemente conNone, o quando fai debugging dell’aliasing) - Usa
inquando vuoi controllare se un valore è contenuto in una collezione
Comprendere queste distinzioni ti aiuta a scrivere confronti più precisi e corretti nei tuoi programmi.
18.7) Confrontare oggetti che contengono altri oggetti
Quando gli oggetti contengono altri oggetti (come liste dentro liste, o dizionari che contengono liste), i confronti diventano più sfumati. Capire come Python confronta le strutture annidate è essenziale per lavorare con dati complessi.
18.7.1) Come funziona == con strutture annidate
L’operatore == esegue un confronto ricorsivo(recursive comparison) per le strutture annidate. Confronta non solo il contenitore esterno, ma anche tutti gli oggetti annidati:
# Confronto di liste annidate
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
print(list1 == list2) # Output: True
# Anche se le liste interne sono oggetti diversi
print(id(list1[0]) == id(list2[0])) # Output: False
print(list1[0] == list2[0]) # Output: TruePython confronta ricorsivamente ogni elemento. Perché list1 == list2 sia True, ogni elemento corrispondente deve essere uguale, inclusi gli elementi annidati.
Ecco un esempio più complesso:
# Struttura annidata con più livelli
data1 = {
"students": [
{"name": "Alice", "grades": [85, 90, 92]},
{"name": "Bob", "grades": [88, 91, 87]}
],
"class": "Python 101"
}
data2 = {
"students": [
{"name": "Alice", "grades": [85, 90, 92]},
{"name": "Bob", "grades": [88, 91, 87]}
],
"class": "Python 101"
}
print(data1 == data2) # Output: TruePython confronta:
- Le chiavi e i valori del dizionario al livello superiore ("students" e "class")
- La lista degli studenti
- Ogni dizionario studente (con le chiavi "name" e "grades")
- La lista dei voti per ciascuno studente
- Ogni singolo numero di voto
Tutti i livelli devono coincidere perché il confronto restituisca True.
18.7.2) L’ordine conta per le sequenze
Per le sequenze (liste e tuple), l’ordine degli elementi conta:
# L'ordine conta nelle liste
list1 = [[1, 2], [3, 4]]
list2 = [[3, 4], [1, 2]]
print(list1 == list2) # Output: False (different order)
# Ma l'ordine non conta per i set
set1 = {frozenset([1, 2]), frozenset([3, 4])}
set2 = {frozenset([3, 4]), frozenset([1, 2])}
print(set1 == set2) # Output: True (sets are unordered)18.7.3) Confrontare collezioni di tipi diversi
Tipi di collezione diversi (list, tuple, set) non sono mai uguali tra loro, anche se contengono gli stessi elementi:
# Confronto di tipi diversi
print([1, 2, 3] == (1, 2, 3)) # Output: False (list vs tuple)
print([1, 2, 3] == {1, 2, 3}) # Output: False (list vs set)
# Anche con gli stessi elementi
list_version = [1, 2, 3]
tuple_version = (1, 2, 3)
set_version = {1, 2, 3}
print(list_version == tuple_version) # Output: False
print(list_version == set_version) # Output: False
print(tuple_version == set_version) # Output: False18.8) Copie superficiali di liste, dizionari e set
Quando lavori con oggetti mutabili, spesso devi creare copie indipendenti per evitare modifiche indesiderate. Per esempio, quando fai il backup dei dati prima di elaborarli, crei scenari di test senza influire sui dati di produzione, o passi dati a funzioni che non dovrebbero modificare l’originale. Comprendere come funzionano i meccanismi di copia di Python ti aiuta a creare copie veramente indipendenti quando necessario.
Tuttavia, non tutti i metodi di copia creano copie completamente indipendenti. Comprendere la differenza tra copie superficiali(shallow copies) e copie profonde(deep copies) è fondamentale per evitare bug sottili.
18.8.1) Che cos’è una copia superficiale?
Una copia superficiale crea un nuovo oggetto, ma non crea copie degli oggetti contenuti al suo interno. Invece, il nuovo oggetto contiene riferimenti agli stessi oggetti annidati dell’originale.
Vediamolo con una lista semplice:
# Creazione di una copia superficiale di una lista semplice
original = [1, 2, 3]
copy = original.copy() # Crea una copia superficiale
print("Original:", original) # Output: Original: [1, 2, 3]
print("Copy:", copy) # Output: Copy: [1, 2, 3]
# Sono oggetti diversi
print("Same object?", original is copy) # Output: Same object? False
# Modificare la copia non influisce sull'originale
copy.append(4)
print("Original:", original) # Output: Original: [1, 2, 3]
print("Copy:", copy) # Output: Copy: [1, 2, 3, 4]Per liste semplici che contengono oggetti immutabili (come gli interi), una copia superficiale funziona perfettamente. La copia è indipendente dall’originale.
Ma cosa succede con strutture annidate? Vediamo dove le copie superficiali mostrano i loro limiti:
# Copia superficiale con liste annidate
original = [[1, 2], [3, 4]]
copy = original.copy()
print("Original:", original) # Output: Original: [[1, 2], [3, 4]]
print("Copy:", copy) # Output: Copy: [[1, 2], [3, 4]]
# Le liste esterne sono oggetti diversi
print("Same outer list?", original is copy) # Output: Same outer list? False
# Ma le liste annidate sono gli STESSI oggetti
print("Same nested list?", original[0] is copy[0]) # Output: Same nested list? True
# Modificare una lista annidata influisce su entrambe
copy[0].append(99)
print("Original:", original) # Output: Original: [[1, 2, 99], [3, 4]]
print("Copy:", copy) # Output: Copy: [[1, 2, 99], [3, 4]]18.8.2) Creare copie superficiali di liste
Ci sono diversi modi per creare una copia superficiale di una lista:
# Metodo 1: usare il metodo copy()
original = [[1, 2], [3, 4]]
copy1 = original.copy()
# Metodo 2: usare lo slicing della lista
copy2 = original[:]
# Metodo 3: usare il costruttore list()
copy3 = list(original)
# Tutti e tre creano copie superficiali
print(copy1) # Output: [[1, 2], [3, 4]]
print(copy2) # Output: [[1, 2], [3, 4]]
print(copy3) # Output: [[1, 2], [3, 4]]
# La lista esterna è diversa
print(original is copy1) # Output: False
print(original is copy2) # Output: False
print(original is copy3) # Output: False
# Ma le liste interne sono CONDIVISE
print(original[0] is copy1[0]) # Output: True
print(original[0] is copy2[0]) # Output: True
print(original[0] is copy3[0]) # Output: True18.8.3) Creare copie superficiali di dizionari
Anche i dizionari supportano la copia superficiale:
# Metodo 1: usare il metodo copy()
original = {"name": "Alice", "grade": 85}
copy1 = original.copy()
# Metodo 2: usare il costruttore dict()
copy2 = dict(original)
# Entrambi creano copie superficiali
print(copy1) # Output: {'name': 'Alice', 'grade': 85}
print(copy2) # Output: {'name': 'Alice', 'grade': 85}
# Sono oggetti diversi
print(original is copy1) # Output: False
print(original is copy2) # Output: False
# Modificare la copia non influisce sull'originale
copy1["grade"] = 90
print("Original:", original) # Output: Original: {'name': 'Alice', 'grade': 85}
print("Copy:", copy1) # Output: Copy: {'name': 'Alice', 'grade': 90}Tuttavia, con strutture annidate, si applica la stessa limitazione della copia superficiale:
# Copia superficiale con dizionario annidato
original = {
"name": "Alice",
"grades": [85, 90, 92]
}
copy = original.copy()
print("Original:", original) # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92]}
print("Copy:", copy) # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92]}
# I dizionari sono oggetti diversi
print("Same dict?", original is copy) # Output: Same dict? False
# Ma la lista grades è lo STESSO oggetto
print("Same grades list?", original["grades"] is copy["grades"]) # Output: Same grades list? True
# Modificare la lista grades influisce su entrambi
copy["grades"].append(88)
print("Original:", original) # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92, 88]}
print("Copy:", copy) # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92, 88]}