Python Challenges: mutable objects

Spread the love
Python logo
Python challenges

Più che una challenge, questo è un’approfondimento su una peculiarità di Python: la mutabilità nei tipi di variabili.

In Python i tipi di dato si dividono in mutabili e immutabili.

Immutabile non significa che non si può cambiare, come suggerirebbe la parola (a mio modestissimo parere la scelta del termine è molto infelice), significa invece che posso cambiare il dato e che in realtà il compilatore, nel sottobosco, lascerà immutata la variabile precedentemente definita e creerà un’altra variabile con il nuovo valore!

Breve iter per exempla

Vediamo una rassegna con esempi per alcuni tipi di dato.

Variabili

Le variabili sono immutabili. Per verificarlo stampiamo l’indirizzo della locazione di memoria che contiene la variabile.

a = 1
print("a=", a)
print("address=", id(a))

a = 2
print("a=", a)
print("address=", id(a))
Output
a= 1
address= 140031306432752
a= 2
address= 140031306432784

Come si vede, assegnando un nuovo valore alla stessa variabile, in realtà Python ne ha creata un’altra e se mi riferisco alla variabile “a” il puntatore ad essa sarà l’ultimo ad essere stato definito.

La variabile precedente non viene cambiata (questo è il senso di immutabile) e va direttamente a fare parte del garbage.

Quindi: per cambiare valore ad una variabile, Python la ridefinisce.

Anche se modifico il valore di una variabile tramite un’operazione, il runtime occupa una seconda locazione di memoria:

a = a + 1
print("a=a+1=",a,"address=",id(a))
Output:
a=a+1= 3 address= 140660793311536

Un curioso effetto che si ha è il seguente:

>>> a=3
>>> q=[1, 2, a]
>>> q
[1, 2, 3]
>>> a=4
>>> q
[1, 2, 3]
>>> 

Qui la a che viene ridefinita è in effetti un’altra variabile rispetto a quella contenuta nella lista q che, a sua volta, è diversa dalla prima a che avevo definito:

>>> a = 2
>>> q=[0, 1, a]
>>> q
[0, 1, 2]
>>> id(a)
140628132561168
>>> id(l[2])
140628131172080
>>> 

Oggetti

Gli oggetti invece sono mutabili (posso modificarne direttamente il valore senza doverne creare una copia)

class Intero():
   def __init__(self, a):
      self.intero = a
   
   def print_intero(self):
   	print(self.intero)
   	return True
   	
   def scrivi_intero(self, a):
      self.intero = a
      return True
      
   def leggi_intero(self):
   	return self.intero

i = Intero(2)
print("i=", i.leggi_intero())
print("address=", id(i))

i.scrivi_intero(3)
print("i=", i.leggi_intero())
print("address=", id(i))
Output:
i= 2
address(i)= 140354601254816 - address(i.intero)= 140354603745552
i= 3
address(i)= 140354601254816 - address(i.intero)= 140354603745584

Come si vede l’indirizzo dell’oggetto (l’istanza della classe) non cambia ma, all’interno della classe,
le sue proprietà sono “variabili” e sono quindi immutabili, tant’è che se ne cambio il valore, cambia anche l’identificativo.

Attenzione a non confondersi, perché l’indirizzo di una variabile di un tipo immutabile cambia, mentre l’indirizzo di una variabile del tipo mutabile non cambia 🙂

Tuple

Le tuple sono simili alle liste: a differenza di queste si definiscono con le parentesi tonde e, diversamente dagli altri oggetti, sono immutabili:

t1 = ('A', 42)
print("t1=",t1)
print("address=",id(t1))

t1 = ('A', 43)
print("t1=",t1)
print("address=",id(t1))
Output:
t1= ('A', 42)
address= 140031305289920
t1= ('A', 43)
address= 140031305961280

Attenzione che c’è un’altra differenza sostanziale con le liste: non posso manipolare i singoli elementi della tupla, allo stesso modo di quanto accade con le stringhe:

try:
   t1[1] = 44
   print("t1=",t1)
   print("address=",id(t1))
except TypeError as e:
   print("Errore!", e)
Output:
Errore! 'tuple' object does not support item assignment
s = "ciao"

try:
   s[0] = "m"
except TypeError as e:
   print("Errore!", e)

Uno pensa che il programma stampi miao invece di ciao ma si sbaglia:

Output:
Errore! 'str' object does not support item assignment

Attenzione: l’utilizzo delle parentesi tonde per definire una tupla espone Python ad una pericolosa ambiguità: come faccio a definire una tupla con un solo elemento senza confondere la notazione con quella di una funzione?

Risposta: una tupla con un solo elemento (un singoletto) si scrive con largomento e la virgola:

>>> t=(1,)
>>> t
(1,)

Tuple packing

Le tuple sono formidabili perché consentono cose molto performanti come questa:

>>> a, b, c = 1, ['a', 0], 3.14
>>> a
1
>>> b
['a', 0]
>>> c
3.14
>>> 

Alla fine rimangono solo le variabili, la tupla viene distrutta subito dopo l’assegnazione. Questo consente di fare questa operazione molto elegante:

>>> a, c = 2, 3.14
>>> a, c
(2, 3.14)
>>> a, c = c, a
>>> a, c
(3.14, 2)

Il swap di variabili è stato fatto senza utilizzare una terza variabile, come si fa di solito.

Liste

Le liste sono mutabili:

l1 = ['A', 42]
print("l1=",l1)
print("address=",id(l1))

l1[1] = 43  # modifico un *singolo elemento*
print("l1=",l1)
print("address=",id(l1))
Output:
l1= ['A', 42]
address= 140031305244544
l1= ['A', 43]
address= 140031305244544

Attenzione perché questo non è mai scritto chiaramente: sono mutabili nel senso che posso modificare un singolo elemento. Se invece ridefinisco l’intera lista, anch’essa si comporta come un oggetto immutabile:

l2 = ['A', 42]
print("l2=",l2)
print("address=",id(l2))

l2 = ['A', 43]
print("l3=",l2)
print("address=",id(l2))
Output:
l2= ['A', 42]
address= 140031303811840
l3= ['A', 43]
address= 140031303811648

Riassumendo

Immutabilità:

  • Gli oggetti immutabili sono quelli i cui valori non possono essere modificati dopo la creazione.
  • Esempi di tipi di dati immutabili in Python includono tuple, stringhe e interi.
  • Quando si modifica un oggetto immutabile, in realtà si crea un nuovo oggetto con il valore modificato.

Mutabilità:

  • Gli oggetti mutabili sono quelli i cui valori possono essere modificati dopo la creazione.
  • Esempi di tipi di dati mutabili in Python includono liste, dizionari e insiemi.
  • La modifica di un oggetto mutabile influisce direttamente sull’oggetto stesso, senza crearne uno nuovo.

In modo abbastanza elementare si potrebbe dire che le variabili contengono riferimenti agli oggetti, mentre gli oggetti vivono in posizioni ben definite della memoria [2].

La ratio dietro a questa partizione.

Ma qual è la ragione che sta dietro alla mutabilità delle variabili di Python? Perché alcuni tipi di dato sono mutabili e altri no?

La ragione per avere oggetti mutabili e immutabili in Python è legata a diverse considerazioni di progettazione e prestazioni. In particolare mi sono chiesto perché l’utilizzo di tipi immutabili sia considerato più efficiente e in effetti non è esattamente così:

  1. Assegnazione diretta: Gli oggetti immutabili possono essere assegnati direttamente in memoria, poiché il loro stato non cambia. Questo elimina la necessità di allocare e deallocare memoria dinamicamente, rendendo il processo più veloce.
  2. Condivisione sicura: Poiché gli oggetti immutabili non possono essere modificati dopo la creazione, possono essere condivisi in modo sicuro tra diverse parti del codice senza preoccuparsi delle modifiche accidentali da parte di altre parti. In particolare sono thread-safe. Questo riduce il rischio di condizioni di conflitto e semplifica la gestione della concorrenza.
  3. Hashing efficiente: Gli oggetti immutabili sono hashabili, il che significa che possono essere utilizzati come chiavi in strutture dati come dizionari e insiemi. Questo è possibile perché l’hash di un oggetto immutabile sarà costante durante il suo ciclo di vita.
  4. Ottimizzazioni del compilatore: La natura immutabile dei dati consente al compilatore di eseguire ottimizzazioni più aggressive. Ad esempio, il compilatore può decidere di memorizzare una singola copia di un valore immutabile utilizzato in più parti del codice anziché allocarne una copia separata per ciascuna occorrenza.
  5. Semplifica il debug: Gli oggetti immutabili semplificano il processo di debug perché il loro stato non può essere modificato. Questo rende più facile ragionare sul comportamento del programma in fasi diverse dell’esecuzione.

Tuttavia, gli oggetti immutabili hanno anche degli svantaggi, come ad esempio il costo di creare nuovi oggetti ogni volta che si vuole modificare il valore di una variabile. Questo può portare a un overhead di memoria e a una maggiore pressione sul garbage collector [3] (per una spiegazione sul funzionamento del garbage collector si può fare rifermiento ad un mio articolo ,sempre in questo blog; l’articolo spiega come funziona il GC in Java, ma il concetto è simile anche per Python). Inoltre, gli oggetti immutabili possono essere meno flessibili e intuitivi da usare rispetto agli oggetti mutabili, soprattutto quando si lavora con strutture dati complesse.

In sintesi, la scelta tra mutabilità e immutabilità dipende dalle esigenze specifiche del programma. Python offre entrambe le opzioni per fornire ai programmatori flessibilità e controllo sulla gestione dei dati.

Spariti tutti i dubbi sulle proprietà di mutabilità e la differenza tra tuple e liste in Python? 😉

Ma allora già che ci siamo, dai… aggiungiamo la challenge. Non ha molto a che fare con l’argomento tratttato. Diciamo che studiando questa challenge poi sono approdato al problema della mutabilità, per questo lo presento qua.

Challenge di oggi

La funzione round trasforma un numero a virgola mobile in un intero. È curioso analizzarne il comportamento della funzione nei punti discriminanti come i seminteri: arrotondo per eccesso o per difetto?

Esempio:

print("round(5.5) = ", round(5.5))
Output:
round(5.5) =  6

Quindi mi aspetto che round(6.5)=7. Controlliamo:

print("round(6.5) = ", round(6.5))
Output:
round(6.5) =  6

Com’è possibile!!?!

Risposta

Il rounding operato dalla funzione built-in di Python si chiama arrotondamento di Banker: si tratta di un algoritmo per arrotondare le quantità a numeri interi, in cui i seminteri vengono arrotondati all’intero pari più vicino. Pertanto, 0,5 viene arrotondato per difetto a 0; 1,5 arrotonda a 2. Un algoritmo simile può essere costruito per arrotondare ad altri insiemi oltre agli interi (in particolare, insiemi che hanno un intervallo costante tra membri adiacenti) [3].

Riferimenti

  1. Christian Hill, Learning Scientific Programming with Python, 2020, Cambridge University Press
  2. Leodanis Pozo Ramos, Python’s Mutable vs Immutable Types: What’s the Difference?, 2023, Real Python
  3. https://wiki.c2.com/?BankersRounding
  4. https://ichi.pro/it/mutabile-vs-inmutabile-in-python-25884837532864

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.