4.4. Dati strutturati#
Come anticipato nel Paragrafo 4.2, considero strutturato un tipo di dati quando i suoi valori rappresentano un'aggregazione di più elementi, e quando è possibile iterare automaticamente sui suoi contenuti. Dopo aver chiarito meglio questo concetto, in questo paragrafo presenterò i principali tipi strutturati disponibili in Python. Di ciascuno descriverò la classe corrispondente e alcuni aspetti della sintassi che consente di accedere agli elementi in esso contenuti.
Nomenclatura
Il valore associato a un tipo di dati strutturato viene memorizzato attraverso una struttura dati, cioè un modo per organizzare e gestire un insieme di informazioni all’interno di un computer. Questa vicinanza terminologica può generare confusione: talvolta i due concetti coincidono (come nel caso degli array), ma nella maggior parte delle situazioni restano distinti. In effetti, uno stesso tipo di dati strutturato può essere implementato utilizzando strutture dati diverse. Per esempio, un insieme può basarsi tanto su un array quanto su un albero, anche se la scelta dell’una o dell’altra soluzione influisce sull’efficienza delle operazioni eseguibili. Poiché in questo libro non entrerò nel merito delle strutture dati, eviterò ove possibile di usare l’espressione «valore di un tipo di dati strutturato», che risulta piuttosto macchinosa. Adotterò occasionalmente il termine struttura come abbreviazione, confidando che il contesto renda chiaro, di volta in volta, a che cosa mi sto riferendo.
I tipi di dati strutturati che presenterò nei prossimi paragrafi — elencati nella 9 — sono i seguenti:
le stringhe, che abbiamo già utilizzato e che rappresentano sequenze di caratteri;
gli array, che contengono sequenze omogenee di oggetti;
le liste e le tuple, che implementano sequenze di generici oggetti;
gli insiemi, che corrispondono all'omonimo concetto in matematica;
i dizionari, che consentono di costruire associazioni — o mappe — tra oggetti.
Classificherò questi tipi di dati secondo alcuni criteri, illustrati di seguito.
La modalità di accesso: quando l'aggregazione è di tipo sequenziale, il modo più naturale per riferirsi a un elemento è tramite un indice che ne indica la posizione. Questo tipo di accesso, detto posizionale, è supportato dastringhe, liste, array e tuple. Insiemi e dizionari, invece, organizzano i dati in modo non necessariamente ordinato, e l'accesso ai loro elementi segue regole diverse.
La staticità o dinamicità della struttura: alcuni tipi di dati strutturati (come stringhe e tuple) non consentono di aggiungere o rimuovere elementi dopo la creazione della struttura, mentre altri (come liste, insiemi e dizionari) lo permettono.
La mutabilità degli elementi: alcuni tipi sono immutabili, nel senso che gli elementi che contengono non possono essere modificati, mentre altri sono mutabili. Tra i tipi che considererò, stringhe e tuple sono immutabili, mentre tutti gli altri sono mutabili. I tipi immutabili sono necessariamente statici, ma non sempre vale il contrario.
L'omogeneità o eterogeneità dei contenuti: alcune strutture richiedono che tutti gli elementi siano dello stesso tipo, mentre altre accettano elementi eterogenei.
La 9 riassume i tipi di dati sui quali mi soffermerò nei prossimi paragrafi, indicando per ciascuno la classe Python corrispondente e specificando quali delle proprietà appena elencate sono soddisfatte e quali no.
Tipo strutturato |
Classe |
Posizionale |
Dinamico |
Mutabile |
Omogeneo |
|---|---|---|---|---|---|
|
✓ |
✕ |
✕ |
✓ |
|
|
✓ |
✕ |
✓ |
✓ |
|
|
✓ |
✓ |
✓ |
✕ |
|
|
✓ |
✕ |
✕ |
✕ |
|
|
✕ |
✓ |
✓ |
✕ |
|
|
✕ |
✓ |
✓ |
✕ |
4.4.1. Accedere a una struttura#
Sebbene facciano riferimento a concetti anche molto diversi tra loro, i tipi di dati strutturati che prenderò in esame condividono alcune modalità di accesso comuni. Python fornisce per queste operazioni una sintassi coerente e relativamente uniforme. La prima modalità riguarda proprio l’aspetto che ho scelto per definire i dati strutturati: la possibilità di iterare automaticamente sui contenuti di una struttura.
Molti linguaggi di programmazione di tipo imperativo includono, tra le
strutture di controllo, il cosiddetto ciclo enumerativo, noto ai più come
«ciclo for», dal nome della parola chiave che lo introduce. Nella sua versione
originale[1], questa forma di iterazione implica un conteggio
esplicito, nel senso che viene definita una variabile di ciclo[2]
che, durante l'esecuzione assume tutti valori in una sequenza predefinita
— normalmente, ma non sempre, gli interi da \(0\) fino a un valore
prefissato[3]. Python, invece, adotta un approccio del tutto diverso. La sintassi più semplice
per questo ciclo è for <variabile> in <struttura>:, dove <variabile> è il
nome assegnato alla variabile di ciclo e <struttura> è un'espressione che
restituisce il valore di un tipo di dati strutturato. Il corpo del ciclo segue
immediatamente dopo, con un livello di indentazione maggiore. L’esecuzione
procede in modo intuitivo: ciascun elemento contenuto in <struttura> viene
considerato a turno, una sola volta, assegnato alla variabile di ciclo ed
elaborato dal suo corpo.
Esempio 4
L’unico tipo di dati strutturato di cui ho parlato finora sono le stringhe: eseguire un ciclo for su una di esse significa scorrere tutti i suoi caratteri, nell’ordine naturale che va dal primo all’ultimo:
s = 'Wasp'
for c in s:
print(c)
W
a
s
p
Il nome della variabile di ciclo viene di solito scelto rispettando due criteri:
deve essere breve e deve richiamare il significato dei valori che assumerà
durante l’iterazione. Nell’esempio precedente ho usato c, poiché il ciclo si
svolge sui caratteri di una stringa.
Nonostante la differenza semantica rispetto ai linguaggi imperativi
tradizionali, il costrutto for di Python appartiene comunque alla categoria
dei cicli enumerativi. Enumerare ha infatti il significato di elencare in
modo completo gli elementi in un gruppo, senza che questo debba necessariamente
coinvolgere esplicitamente una numerazione. Effettivamente, la variabile di
ciclo assume, uno dopo l’altro, i valori di una sequenza, che corrispondono
agli elementi contenuti nella struttura su cui si sta iterando. È però
importante sottolineare che la semantica di for non specifica in quale
ordine vengano considerati gli elementi: ciò dipende dal tipo di dati
utilizzato. In alcuni casi — come per le stringhe o le liste —
l’ordine è determinato in modo prevedibile; in altri, come per gli insiemi o i
dizionari, potrebbe non esserlo.
Pattern e antipattern
Quando si studia un linguaggio di programmazione, si impara rapidamente a riconoscere degli schemi ricorrenti nel codice sorgente: modalità tipiche di combinare istruzioni che si presentano in contesti diversi, perché offrono una soluzione efficace per problemi anche diversi tra loro, ma che condividono la medesima logica di fondo. Questi schemi vengono chiamati pattern del linguaggio, anche se spesso si preferisce parlare di codice idiomatico o, nel caso specifico di Python, pythonico[4]. L’uso del ciclo for per iterare su una stringa, mostrato negli esempi precedenti, è un esempio di codice pythonico: rappresenta una soluzione generale che va oltre la semplice iterazione sui caratteri di una stringa, perché si può applicare ogni qual volta sia necessario considerare tutti gli elementi in un tipo di dato strutturato. Infatti, lo stesso approccio permetterebbe di iterare su altre strutture dati come le liste o i dizionari, aiutando a produrre codice Python generale e a individuare rapidamente strategie di soluzione anche quando non si conosce in dettaglio un componente del linguaggio.
Va però sottolineato che esistono schemi che possono emergere facilmente senza rappresentare codice idiomatico. Sono gli antipattern: schemi che, appresi autonomamente, adattati da altri linguaggi, oppure reperiti su fonti non sempre affidabili (come alcuni forum di discussione), producono codice poco efficiente o difficilmente adattabile ad altri problemi. Chi conosce già Java, per esempio, potrebbe iterare su una stringa ricalcando l'approccio tradizionale che si basa su una variabile di ciclo associata alle posizioni dei tcarateri, scrivendo il codice che segue.
for i in range(len(s)):
print(s[i])
W
a
s
p
Il risultato è un antipattern che introduce complessità inutile, obbligando a:
definire una variabile intera
iquando ciò che interessa sono i singoli caratteri e non le loro posizioni;invocare
range, che genera un oggetto sul quale è possibile iterare per ottenere le posizioni della stringa;calcolare esplicitamente la lunghezza della stringa.
Inoltre, questo metodo non si generalizza facilmente ad altri tipi di dati
strutturati, come insiemi, dizionari o file.
L’approccio idiomatico, invece, è più semplice da scrivere e dunque anche più
leggibile. Naturalmente, ci sono situazioni in cui è utile conoscere sia i
caratteri, sia la loro posizione: in questi casi, il modo pythonico di
procedere è utilizzare la funzione enumerate:
for i, c in enumerate(s):
print('Il carattere in posizione ', i, 'è', c)
Il carattere in posizione 0 è W
Il carattere in posizione 1 è a
Il carattere in posizione 2 è s
Il carattere in posizione 3 è p
che permette di iterare su delle coppie che includono la posizione e l'elemento della struttura, e che ha il vantaggio di essere applicabile anche a dati strutturati che non supportano l'accesso posizionale[5].
Le altre modalità di accesso comuni ai tipi di dati strutturati, che approfondirò nei prossimi paragrafi, sono riassunte nel seguente elenco.
La funzione
lenrestituisce sempre il numero di elementi contenuti nella struttura[6]. Nel gergo informatico, questo numero è chiamato lunghezza, anche se in alcuni casi la denominazione può risultare impropria (per esempio, per gli insiemi si dovrebbe parlare più correttamente di cardinalità).Sono quasi sempre disponibili uno o più operatori di accesso che permettono di ottenere il valore di un elemento specifico contenuto nella struttura e, se questa è mutabile, di modificarlo. La loro sintassi si basa sull’uso delle parentesi quadre, estendendo l’operatore di indicizzazione degli array presente nella maggior parte dei linguaggi di programmazione.
L’operatore
inconsente di verificare se un elemento appartiene o meno a una struttura, mentre l’operatoredelpuò essere utilizzato per rimuovere elementi da strutture dinamiche.
4.4.2. Le stringhe#
A differenza di altri linguaggi, come per esempio C, in Python non esiste
un tipo di dato dedicato ai singoli caratteri. Le stringhe, intese come
sequenze di zero o più caratteri, sono implementate attraverso la classe str,
che mette a disposizione un tipo omogeneo e ad accesso posizionale, i cui
oggetti sono immutabili. Nella versione più semplice, i letterali di questa
classe si ottengono racchiudendo i caratteri della stringa tra due delimitatori.
Esistono tre tipi di delimitatori: gli apici singoli, doppi e tripli. L’uso
degli apici singoli (') o doppi (") ricalca la sintassi classica dei
linguaggi di programmazione più diffusi, con la differenza che in Python è
possibile scegliere tra due delimitatori diversi: in altre parole, 'Robin' e
"Robin" identificano la stessa stringa. Questa possibilità facilita la
creazione di stringhe che contengono apici doppi o singoli, come negli esempi
seguenti:
'Superman proviene da un pianeta chiamato "Krypton".'
"L'altro nome di Superman è Kal-El."
È però necessario chiudere la stringa con lo stesso tipo di delimitatore con cui è stata aperta: sempre due apici singoli o due doppi apici. Gli apici tripli hanno un ruolo analogo, ma in più permettono di utilizzare stringhe che si estendono su più righe, senza dover utilizzare sequenze di escape per i caratteri di «a capo». Per esempio:
'''Il primo fumetto che ha Storm come protagonista
compare nel numero uno di "Giant-size X-men"
ed è stato pubblicato nel 1977.
'''
definisce una stringa di tre righe. I tre apici possono essere sia singoli sia doppi, ma anche in questo caso è necessaria coerenza tra il delimitatore iniziale e quello finale.
Abbiamo già visto che l’iterazione su una stringa avviene carattere per
carattere, nell’ordine in cui questi compaiono. Non sorprende quindi che
len(s) restituisca il numero di caratteri della stringa s, né che i
letterali '' e "" rappresentino la stringa vuota (la cui lunghezza è zero).
L’operatore di accesso merita qualche parola in più. È naturale voler accedere
ai singoli caratteri di una stringa in base alla loro posizione: dopo il nome
della variabile che la referenzia (o il corrispondente letterale), basta
specificare tra parentesi quadre l’indice del carattere desiderato, a partire
da 0[7]. In altre parole, l’operatore di accesso ha la stessa
semantica degli array. Tuttavia, Python ne estende l’uso: se si indica un
valore negativo come indice, a questo viene automaticamente sommata la
lunghezza della stringa, così che -1 identifica l’ultimo carattere, -2 il
penultimo e così via. È inoltre possibile indicare un intervallo di posizioni
consecutive per ottenere una sottostringa. Questa modalità, detta slicing, si
esprime indicando tra parentesi quadre la posizione del primo carattere da
includere, seguita da due punti e dalla posizione del primo carattere da
escludere[8]. Gli slicing possono combinare indici positivi e
negativi, purché la posizione iniziale preceda quella finale; in caso
contrario, l’operazione restituisce la stringa vuota. Come vedremo nei
prossimi paragrafi, indici negativi e slicing si utilizzano in modo analogo
anche in altri tipi di dati strutturati. Infine, l’operatore in permette di verificare se una stringa è contenuta
all’interno di un’altra stringa. In particolare,
s in t restituisce True se la stringa s è una sottostringa di t, e
False altrimenti. Chiaramente, questo operatore permette anche di verificare
facilmente se un carattere occorre o meno in una stringa.
Esempio 5
Consideriamo la stringa definita nella cella seguente.
s = '''- Spider-Man: Dev'esserci qualcun altro da chiamare... Per esempio Thor?
- Nick Fury: Fuori portata.
- Spider-Man: Captain Marvel.
- Maria Hill: Indisponibile.
- Spider-Man: Io sono solo un amichevole Spider-Man di quartiere.
- Nick Fury: Ma per favore! Sei stato nello spazio!'''
Le istruzioni che seguono mostrano come:
calcolare la lunghezza della stringa;
estrarne il primo e l’ultimo carattere, utilizzando l’operatore di accesso;
ottenere una sottostringa, combinando operatore di accesso e slicing;
verificare se una sottostringa è presente all’interno della stringa.
print('La stringa contiene', len(s), 'caratteri')
print('inizia per', s[0])
print('e finisce per', s[-1])
start = 67
end = 70
print(s[start:end+1], 'occorre dalla posizione', start, 'alla posizione', end)
hero = 'Aquaman'
print(hero, end=' ')
if hero not in s:
print('non ', end='')
print('occorre nella stringa')
Come ho già precedentemente accennato, in Python le stringhe sono immutabili, cioè non è possibile modificarne il contenuto, ad esempio sostituendo un carattere con un altro o aggiungendo uno o più caratteri. Per effettuare operazioni di questo tipo è sempre necessario creare una nuova stringa che sostituisca quella di partenza.
La classe str implementa vari metodi e operatori specifici per le stringhe;
in particolare vale la pena evidenziare che:
l'operatore
+concatena due stringhe (e solo due stringhe: è necessario convertire esplicitamente in stringa eventuali operandi di tipo diverso);la moltiplicazione di una stringa
sper un numero interoiproduce una nuova stringa ottenuta concatenandoscon se stessaivolte;s.find(t)restituisce la posizione iniziale della prima occorrenza della sottostringatall’interno dis, se presente, e-1altrimenti.
Per un elenco completo dei metodi disponibili si può consultare la sezione relativa alle stringhe nella documentazione ufficiale.
Letterali stringa grezzi#
Come ho già detto, si può generare una stringa usando sequenze di escape per
inserire caratteri difficili o impossibili da rappresentare direttamente in un
letterale. Nei casi più frequenti, ciò riguarda i caratteri di «a capo»
(newline, \n), di tabulazione (\t) e lo stesso backslash
(\\)[9], che altrimenti non potrebbe comparire all’interno di
un letterale stringa. In alcuni contesti è necessario creare stringhe
contenenti molti caratteri di backslash: in questi casi, l’uso delle sequenze
di escape renderebbe difficile la lettura dei letterali che si otterrebbero.
Per risolvere questo problema, si utilizzano i cosiddetti letterali grezzi
(in inglese, raw string literals, o, per brevità, r-string). In questi
letterali, che si ottengono prependendo r al delimitatore iniziale della
stringa, le sequenze di escape non vengono interpretate.
Esempio 6 (\(\LaTeX\))
\(\LaTeX\) è un linguaggio di marcatura che permette di descrivere testi
formattati, ampiamente utilizzato in ambito
scientifico per produrre documentazione e che ho usato anche per generare tutte
le formule matematiche di questo libro. In \(\LaTeX\), i simboli matematici
vengono descritti tramite una particolare sintassi basata sul carattere di
backslash: ad esempio, \pi e \int producono rispettivamente \(\pi\) e
l’operatore integrale. Nella cella seguente, gli oggetti display e Math
del package IPython.display vengono utilizzati per visualizzare nella cella
di output una formula relativamente complessa, il cui sorgente \(\LaTeX\) è
memorizzato nella stringa s, definita tramite un letterale grezzo.
from IPython.display import display, Math
s = r'\Phi(x) = \frac{1}{2 \pi} '
s += r'\int_{-\infty}^{+\infty} \mathrm e^{-\frac{x^2}{2}} \, \mathrm dx'
display(Math(s))
Questa formula descrive una particolare funzione che studieremo nel
Paragrafo 21.4. Notate come abbia costruito la stringa
giustapponendo due letterali grezzi tramite l'operatore +=, così da evitare
righe di codice più lunghe di ottanta caratteri[10].
Esempio 7 (Espressioni regolari)
Le espressioni regolari sono un formalismo che permette di definire particolari insiemi di stringhe che soddisfano specifiche regole. Il punto di partenza di questo formalismo sono dei costrutti fondamentali che esprimono proprietà relativamente semplici di una stringa: ad esempio, se ne può fissare la lunghezza, oppure richiedere che essa contenga esclusivamente determinati caratteri. Componendo questi costrutti è possibile definire regole complesse, in grado di individuare famiglie articolate di stringhe — come tutti gli indirizzi di posta elettronica in un certo dominio, i numeri di telefono con prefisso internazionale o tutte le password di almeno dieci caratteri che contengono una cifra e lettere sia minuscole che maiuscole. Tipicamente, data un'espressione regolare, si vuole valutare se una stringa la soddisfi oppure no, o estrarre tutte le sue sottostringhe che la rispettano.
Il modulo re permette di utilizzare le espressioni regolari in Python,
definendole tramite una sintassi che fa ampio uso dei caratteri di
backslash. Per esempio, \d indica una cifra e + descrive una sequenza di
una o più occorrenze della regola precedente, così che \d+ corrisponde a una
sequenza di una o più cifre, e dunque a una stringa che rappresenta un numero
intero. La funzione compile, definita nel modulo, restituisce un oggetto che
rappresenta un’espressione regolare \(e\), costruita a partire da una stringa che
ne descrive la sintassi. Su questo oggetto si possono invocare vari metodi,
come ad esempio findall, che accetta una stringa e ne estrae tutte le
sottostringhe che soddisfano \(e\). Per esempio, il codice seguente elenca tutti
i numeri che compaiono in una descrizione dei superpoteri di Elastigirl, tratta
dalla relativa pagina su Wikipedia:
import re
s = "Il superpotere principale di Elastigirl è l'elasticità, che le "
s += "consente di allungare varie parti del suo corpo fino a 300 piedi "
s += "(90 metri) e può essere sottile 1 millimetro."
p = re.compile(r'\d+')
p.findall(s)
['300', '90', '1']
Dovreste notare qualcosa di nuovo nel valore restituito dal metodo findall:
si tratta di un dato strutturato che aggrega le varie occorrenze individuate
nella stringa di partenza. Studieremo tra breve anche il tipo corrispondente,
nel Paragrafo 4.4.3.
Letterali stringa formattati#
Abbiamo visto che è possibile stampare un output mescolando stringhe fisse
e contenuti di variabili, semplicemente passandoli come argomenti alla funzione
print.
name = 'Brainiac'
month = 'luglio'
year = 1958
magazine = 'Action Comics'
num = 242
print(name, 'ha debuttato nel numero', num, 'di', magazine, 'nel', month, year)
Brainiac ha debuttato nel numero 242 di Action Comics nel luglio 1958
È importante sottolineare che in molti linguaggi di programmazione
l'equivalente della funzione print accetta un unico argomento, e per stampare
un output analogo a quello precedente si segue un pattern preciso: si
utilizza l'operatore di concatenazione delle stringhe, che in alcuni casi
promuove automaticamente gli operandi che non sono di tipo stringa. Python,
invece, effettua un type checking di tipo forte, il che significa che non
ammette conversioni implicite: la concatenazione può avvenire solo tra due o
più stringhe. Pertanto, per applicare il pattern appena descritto sarebbe
necessario convertire esplicitamente in stringa tutti gli operandi che non sono
già di questo tipo, pena l'emissione di un'eccezione. Ciò renderebbe il codice
inutilmente complesso, ed è per questo motivo che print accetta più
argomenti.
Nella pratica comune, tuttavia, quando si desidera generare un output complesso
che combini stringhe fisse con il contenuto di variabili, si preferisce
produrre un output formattato, utilizzando una tecnica nota come «string
interpolation». In Python ciò può essere fatto in tre modi: utilizzando
l'operatore %, invocando il metodo format su una stringa oppure ricorrendo
ai letterali stringa formattati — spesso indicati come f-string
nella documentazione, per brevità. Sebbene le prime due modalità
siano tuttora disponibili, l'ultima — introdotta più di recente — è
quella che fornisce più controllo sul risultato finale e, al tempo stesso,
semplifica il codice. Un letterale stringa formattato si riconosce perché il
suo delimitatore iniziale è preceduto da un carattere f. Al suo interno
è possibile inserire arbitrarie espressioni Python racchiuse tra parentesi
graffe: durante l'esecuzione, tali espressioni vengono valutate e il loro
risultato è inserito nel punto della stringa che corrisponde a queste parentesi
quadre.
print(f'{name} ha debuttato nel numero {num} di {magazine} nel {month} {year}.')
Brainiac ha debuttato nel numero 242 di Action Comics nel luglio 1958.
Il vantaggio principale dei letterali formattati consiste nella possibilità di specificare con notevole precisione il modo in cui i valori devono essere visualizzati. Per esempio, terminare la stringa nell'ultima cella con un punto è immediato, mentre ottenere lo stesso risultato nella cella precedente richiede un po’ di attenzione. Ma la cosa più importante è la possibilità di utilizzare un mini linguaggio che definisce alcune regole di formattazione. Più precisamente, prima di chiudere le parentesi graffe è possibile inserire un carattere di due punti e indicare il tipo di valore mostrato e la modalità di visualizzazione. Per esempio, nella cella seguente:
a = 0.1
b = 0.2
print(f'{a+b:.1f}')
0.3
l’espressione .1f indica che il valore calcolato è di tipo float e deve
essere mostrato usando una sola cifra decimale dopo la virgola.
Una versione semplificata della sintassi utilizzata per specificare il formato
di visualizzazione di un valore è la seguente:
[[fill]align][sign][width][,|_][.precision][type]
dove:
fillè il carattere utilizzato per assicurare che l'espressione occupi un determinato numero di caratteri (per default, uno spazio);aligndefinisce l’allineamento del valore:<,>,^e=indicano, rispettivamente, l’allineamento a sinistra, a destra, centrato e quello in cui il segno è a sinistra e il valore a destra (valido solo per numeri);signspecifica come rappresentare l’eventuale segno:+lo mostra sempre, anche per i numeri positivi;-lo mostra solo per i negativi;␣visualizza uno spazio per i numeri positivi e-per quelli negativi, in modo da mantenere un corretto allineamento verticale;widthimposta il numero totale di caratteri da utilizzare per la visualizzazione;,o_indicano il carattere da usare per separare i gruppi di tre cifre nei valori numerici;precisiondefinisce il numero di cifre decimali (per ifloat) o di caratteri (per le stringhe) da mostrare;typespecifica la modalità di rappresentazione del valore, scegliendo uno dei caratteri elencati nella 10; meritano un po' più di dettaglio gli specificatorie,Eeg: i primi due forzano l'utilizzo della notazione scientifica per indicare i valori decimali, mentre il terzo adotta quella tra le notazioni scientifica e a virgola fissa che garantisce l'output più corto.
La sintassi completa del mini linguaggio di formattazione è leggermente più complessa, ed è descritta in dettaglio nella documentazione ufficiale.
Carattere |
Tipo di visualizzazione |
|---|---|
|
Stringa (default) |
|
Intero |
|
Decimale (notzione a virgola fissa) |
|
Decimale (notazione scientifica) |
|
Forma compatta |
|
Percentuale |
|
Esadecimale |
|
Binario |
|
Ottale |
4.4.3. Le liste#
Le liste sono strutture dati dinamiche ad accesso posizionale, utilizzate per
memorizzare sequenze di riferimenti a oggetti di tipo eterogeneo. Quando una
lista contiene pochi elementi, è pratico definirla in modo estensivo,
racchiudendo tra parentesi quadre l’elenco dei suoi elementi, indicati
nell’ordine desiderato e separati da virgole. Il risultato è un letterale di
tipo list, cioè un’istanza della classe che in Python implementa le liste.
Per esempio, la lista che contiene i primi quattro quadrati perfetti, partendo
da zero, corrisponde al letterale [0, 1, 4, 9], mentre quella composta dai
nomi dei membri fondatori della Justice League nella sua formazione originale
si può esprimere come ['Superman', 'Batman', 'Wonder Woman', 'Flash', 'Green Lantern', 'Aquaman', 'Martian Manhunter']. Analogamente, [] indica la lista
vuota.
Vale la pena sottolineare che quello che viene memorizzato in una lista sono
solamente i riferimenti agli oggetti, e non gli oggetti stessi. Pertanto, il
letterale [0, 1, 4, 9] corrisponde a una lista che nella prima posizione
contiene il riferimento a un oggetto int che corrisponde a zero, nella
seconda contiene il riferimento a un altro oggetto int, e così via, come
illustrato nella Figura 10. Ciascuno di questi oggetti
risulterà memorizzato in una porzione di memoria diversa da quella dedicata
alla lista. Anche se tecnicamente sarebbe più corretto parlare di «liste di
riferimenti a oggetti», per semplicità — o anche solo per evitare di
appesantire il discorso — si parla comunemente di «liste di oggetti» o
«liste di valori», e anche io quasi sempre utilizzerò questa dicitura
sempliicata.
graph LR
subgraph lista[" "]
direction LR
A0["l[0]"]
A1["l[1]"]
A2["l[2]"]
A3["l[3]"]
end
A0 --> B0["0"]
A1 --> B1["1"]
A2 --> B2["4"]
A3 --> B3["9"]
l --> lista
classDef list fill:#fff8e1,stroke:#ffb300,stroke-width:2px,color:#333;
classDef obj fill:#e3f2fd,stroke:#1e88e5,stroke-width:2px,color:#333;
class A0,A1,A2,A3 list;
class B0,B1,B2,B3 obj;
Figura 10 Rappresentazione grafica dei riferimenti contenuti nella lista.
l = [0, 1, 4, 9].#
L'iterazione svolta su una lista dai cicli for è quella che, intuitivamente, rispetta l'ordine degli elementi:
l = [0, 1, 4, 9]
for i in l:
print(i)
0
1
4
9
Nomenclatura
Il termine «lista» potrebbe trarvi in inganno, dato che in informatica viene a volte usato come abbreviazione di «lista collegata» (traduzione dall'inglese «linked list»), che è una struttura dati ben precisa. Nella forma più semplice di questa struttura dati, ogni elemento è collegato a quello successivo, e per accedere a una specifica posizione bisogna attraversare la struttura, partendo dall'inizio e passando per tutte le posizioni precedenti. Questo implica che il tempo di accesso ai contenuti non sia costante: gli elementi alla fine della lista richiedono più tempo rispetto a quelli iniziali. D'altro canto, questo tipo di organizzazione rende più semplice eseguire operazioni di altro tipo, come l'inserimento o la cancellazione di un elemento.
Le liste collegate rappresentano uno dei modi possibili di implementare
il concetto astratto che corrisponde a una sequenza ordinata di elementi,
che prende appunto il nome di lista e che è fondamentalmente corrisponde a
ciò che è memorizzato in un oggetto di tipo list — a patto di
ragionare nei termini di quello a cui puntano i riferimenti memorizzati nella
lista.
Le specifiche di Python non impongono alcuna rappresentazione interna per gli
oggetti di tipo list. Nella pratica, però, tutte le principali
implementazioni adottano una soluzione completamente diversa da quella delle
liste collegate: i riferimenti agli oggetti sono memorizzati in un array
dinamico, cioè un’area di memoria contigua che può essere ampliata o ridotta
quando necessario. Questo approccio consente un tempo di accesso costante agli
elementi.
È comunque importante ricordare che una lista in Python non è un array nel senso tradizionale del termine: infatti, può contenere oggetti eterogenei e cresce o si restringe a seconda delle operazioni svolte. Si tratta quindi di una struttura più flessibile rispetto agli array statici di molti altri linguaggi.
L'approccio estensivo tende a diventare poco pratico per liste che contengono
più di una decina di elementi. In questi casi le liste vengono costruite
dinamicamente, ad esempio usando un ciclo che genera gli elementi uno alla
volta e li aggiunge via via alla lista. Esiste poi la possibilità di creare
liste invocando metodi o funzioni: per esempio il metodo split, invocato su
una stringa, restituisce la lista delle parole in essa contenute.
Vi è poi un terzo modo di procedere, che se ben congegnato risulta più
efficiente rispetto all'uso esplicito di cicli: la list comprehension.
Fondamentalmente, applica una trasformazione a tutti gli elementi di una
struttura di partenza e raccoglie i risultati in una nuova lista. In un certo
senso, opera un approccio intensivo, perché si basa sulla specificazione di
una proprietà che caratterizza gli elementi della lista risultante. Nella sua
forma più semplice, la sintassi per introdurre una list comprehension è
[f(e) for e in l], dovef(e) è un’espressione nella quale occorre la
variabile muta e, mentre l è la struttura di partenza. Durante la
valutazione della comprehension, gli elementi di l vengono enumerati usando
l'equivalente di un ciclo for; ciascuno, a turno, viene temporaneamente
referenziato da e, così che risulta possibile valutare f(e). I risultati
ottenuti vengono raccolti in una nuova lista, mantenendo l’ordine nel quale
sono stati generati. In altre parole, il primo elemento della lista è il
risultato dell’applicazione di f al primo elemento di l che è stato
considerato, il secondo è il risultato di f sul secondo elemento, e così via.
Se la struttura di partenza è una lista, questa verrà attraversata dalla prima
alla ultima posizione, e quindi gli elementi di l e quelli del risultato
ottenuto saranno allineati. Per esempio, la lista dei primi quattro quadrati
perfetti può essere espressa anche come:
[i**2 for i in range(4)]
[0, 1, 4, 9]
dove range(4) restituisce un oggetto che, in questo contesto, equivale alla
lista [0, 1, 2, 3] (ma che non è una lista, come spiego nel riquadro
Range qui sotto): i suoi elementi verranno considerati quindi
nell'ordine nel quale occorrono ed elevati al quadrato.
In una list comprehension è possibile non solo trasformare gli elementi di
una struttura, ma anche scartarne una parte, selezionando solo quelli che
soddisfano una certa condizione. La sintassi estesa [f(e) for e in l if g(e)]
indica che un elemento e della struttura l viene considerato (e
trasformato) se e solo se l’espressione logica g(e) risulta vera. Ad esempio,
la lista dei primi tre numeri dispari si può ottenere nel modo seguente:
[i for i in range(1, 6) if i % 2 == 1]
[1, 3, 5]
Range
In alcuni esempi precedenti ho utilizzato range per costruire sequenze di
numeri interi equispaziati tra due estremi. Più precisamente, dati tre valori
interi memorizzati nelle variabili m, n e s:
range(m)individua la sequenza degli interi da0am − 1;range(m, n)rappresenta la sequenza degli interi daman − 1;range(m, n, s)descrive la sequenza ottenuta partendo dame applicando successivamente degli incrementi disunità, fino a includere il più grande tra i valori che non superanon.
Nelle prime versioni di Python, range era una funzione che restituiva
esplicitamente la lista contenente tutti gli elementi della sequenza.
Questo approccio diventava però rapidamente impraticabile quando la sequenza
era molto lunga, perché richiedeva di mantenere tutti i suoi valori in memoria
centrale.
Anche quando le sequenze non erano particolarmente grandi, questo modo di
procedere risultava spesso inefficiente. Un uso molto comune di range, per
esempio, era quello di imitare il comportamento del ciclo for presente in
altri linguaggi, iterando su una sequenza di indici usati per accedere agli
elementi di una struttura dati. Anche ignorando per un momento il fatto che
questo stile di programmazione rappresenta un antipattern (come discusso nel
riquadro corrispondente), per svolgere questo compito non è affatto
necessario memorizzare l'intera sequenza: è sufficiente conoscere il valore
corrente, l'incremento e il limite superiore.
Per questo motivo, a partire da Python 3, range non è più una funzione che
produce una lista, ma una classe: i suoi oggetti rappresentano la sequenza in
modo implicito, memorizzando soltanto i parametri necessari per generarla. I
valori vengono prodotti solo quando servono, seguendo una strategia nota come
lazy evaluation. Gli oggetti restituiti da range si comportano quindi come
sequenze sulle quali è possibile iterare con un ciclo for o con una
list comprehension --- ma anche effettuare altre operazioni come il calcolo
della lunghezza o la verifica di appartenenza --- ma con un consumo di memoria
estremamente ridotto.
L’eterogeneità delle liste consente di usarle anche per rappresentare un record, cioè un'aggregazione di informazioni che descrive un’entità complessa, come ad esempio un supereroe, nei termini degli attributi elencati nella 2:
iron_man = ['Iron Man', 'Anthony Edward Stark', 'Tony Stark', 'Good',
'Long Island, New York', 'Marvel Comics',
'Earth-616 - Prime Marvel Universe', 1963, 'Blue', 'Black',
198.1, 192.8, 100, 'High', 100, 85, 80, 90]
L’operatore di accesso alle liste segue la stessa semantica già vista per le stringhe: tra parentesi quadre si specifica una posizione, che individua il corrispondente elemento della sequenza (più precisamente, il riferimento a quell’oggetto), oppure uno slicing per ottenere una sotto-sequenza.
Esempio 8
Se consideriamo la lista memorizzata in iron_man, valutando le espressioni
iron_man[2] e iron_man[-2] si ottengono rispettivamente il terzo e il
penultimo dei valori presenti nella lista, ossia la stringa 'Tony Stark' e
l'intero 80. Allo stesso modo, si può estrarre la sotto-lista che contiene i
colori di occhi e capelli usando uno slicing come nella cella che segue.
iron_man[8:10]
['Blue', 'Black']
Sono disponibili diverse operazioni che agiscono sulle liste, implementate tramite elementi distinti del linguaggio: operatori, funzioni e metodi. Di seguito, senza alcuna pretesa di completezza, introduco alcuni esempi basandomi sulla seguente lista che contiene i nomi di alcuni supereroi.
names = ['Aquaman', 'Ant-Man', 'Batman', 'Black Widow',
'Captain America', 'Daredevil', 'Elektra', 'Flash',
'Green Arrow', 'Human Torch', 'Hancock', 'Iron Man',
'Mystique', 'Professor X', 'Rogue', 'Superman',
'Spider-Man', 'Thor', 'Northstar']
Come già indicato in precedenza, la funzione len può essere utilizzata per
calcolare la lunghezza di un oggetto: nel caso delle liste, tale valore
corrisponde al numero di elementi contenuti.
len(names)
19
Alcuni degli operatori descritti nel Paragrafo 4.3 mantengono la
stessa semantica anche quando vengono applicati a delle liste. Per esempio,
== consente di verificare se due liste contengano gli stessi
elementi, nel medesimo ordine. Altri operatori, invece, assumono un significato
diverso. In particolare:
L’operatore
+consente di concatenare due liste, creando una nuova lista che contiene tutti gli elementi della prima seguiti da quelli della seconda, lasciando inalterato il loro ordine;L’operatore
*permette di concatenare una lista con se stessa più volte; più precisamente, selensono rispettivamente una lista e un intero, sial * nchen * lproducono una nuova lista formata danripetizioni dei contenuti dil, sempre mantenendone l'ordine.
Nel Paragrafo 4.4 ho introdotto gli operatori in e del,
che si applicano in modo naturale anche alle liste. Il primo implementa la
relazione di appartenenza: se e è un’espressione e l una lista,
l’espressione e in l viene valutata come logicamente vera se il valore
dell’espressione occorre in una qualunque posizione della lista, e come
logicamente falsa altrimenti:
print('Thing' in names)
print('Human Torch' in names)
False
True
L’operatore del consente invece di eliminare un riferimento da una lista
specificandone la posizione. Per esempio, eseguendo la cella seguente viene
eliminato dalla lista memorizzata in names il riferimento contenuto nella
sua prima posizione:
del names[0]
Va sottolineato che del è un operatore dal comportamento peculiare, in quanto
non restituisce alcun valore; la sua esecuzione ha però l’effetto collaterale
di eliminare il riferimento nella posizione specificata della lista usata come
operando[11]. Nell’esempio precedente, la conseguenza di questo
effetto è quella di eliminare l'elemento all'inizio della lista, così che
quello che prima era il secondo elemento diventa ora il primo, e così via, come
si può facilmente verificare:
names[0]
'Ant-Man'
Nota
L'operatore del può essere applicato a una vasta gamma di operandi, al fine
di eliminare un riferimento a un oggetto. Se dopo questa operazione non
esistono più riferimenti che puntano all’oggetto, questo verrà rimosso dalla
memoria, liberando spazio[12]. Il fatto che la maggior parte
delle componenti di Python accessibili durante l’esecuzione di un programma
siano, in modo più o meno esplicito, associate a degli oggetti, rende possibile
eliminare non solo variabili o parti di strutture dati, ma anche altri elementi
del linguaggio che vedremo più avanti, come funzioni e moduli.
Infine, è possibile usare oggetti della classe list invocando su di essi dei
metodi. Prendiamo ad esempio sort, che permette di ordinare gli elementi
sulla base di una relazione predefinita. Per quanto riguarda le stringhe,
questa relazione corrisponde all'ordine lessicografico, che è una
generalizzazione dell'ordine alfabetico a parole su un qualsiasi alfabeto di
simboli ordinati. In Python, la comparazione tra due stringhe viene fatta
scandendole da da sinistra verso destra, fermandosi alla prima posizione in cui
si trovano due caratteri diversi, che vengono confrontati secondo la loro
codifica Unicode: quello che ha un codice più basso individua la stringa «più
piccola». Se una stringa è un prefisso dell'altra, allora «viene prima»
nell'ordine. Supponiamo per esempio di voler mettere in ordine i nomi dei
supereroi contenuti in names (la lista è quasi in ordine, l'unico elemento
fuori posto è l'ultimo): uno dei modi in cui è possibile eseguire questa
operazione è quella di invocare sulla lista il metodo sort.
names.sort()
Così come l'operatore del, questo metodo non restituisce alcun valore, in
quanto l'ordinamento è eseguito in place [13]: l'invocazione di sort
ha come effetto collaterale il riposizionamento degli elementi all'interno
della lista in modo da riflettere l'ordinamento [14].
Possiamo convincercene facilmente visualizzando per esempio gli ultimi cinque
elementi di names:
names[-5:]
['Professor X', 'Rogue', 'Spider-Man', 'Superman', 'Thor']
In Python è possibile specificare valori per argomenti opzionali quando si
invoca un metodo (o, come vedremo più avanti, una funzione): si tratta di
argomenti identificati da un nome, che possono essere omessi, e in tal caso
assumono un valore predefinito [15]. Per poter specificare un
valore diverso da quello predefinito è necessario indicare, dopo tutti gli
eventuali argomenti non opzionali, un'espressione del tipo <nome>=<valore>,
dove <nome> indica il nome dell'argomento opzionale in questione e <valore>
è il relativo valore. Per esempio, l'argomento opzionale reversed del metodo
sort permette di invertire il verso dell'ordinamento:
names.sort(reverse=True)
Un'altra caratteristica di Python è quella di poter specificare una funzione
come argomento di un metodo (o di un'altra funzione) [16]; ciò si può
fare indicando il nome della funzione, oppure usando una funzione anonima
(vedi il Paragrafo 4.6.2). Esemplifico anche questa
caratteristica usando il metodo sort: il suo argomento opzionale key
permette di specificare criteri alternativi per eseguire l'ordinamento,
indicando una funzione che trasforma gli elementi della lista in quantità
numeriche su cui basare l'ordinamento (nel senso che elementi trasformati in
numeri più bassi compariranno prima nella lista ordinata rispetto ad altri
elementi trasformati in numeri più alti). Nell'invocazione seguente
names.sort(key=len)
l'ordinamento viene fatto:
associando il nome di ogni supereroe nella lista alla sua lunghezza,
ordinando le coppie ottenute in base a questa lunghezza,
considerando solo i nomi, mantenendo questo nuovo ordinamento,
che corrisponde a riordinare i nomi dal più breve al più lungo, come si può facilmente verificare:
names
['Thor',
'Rogue',
'Flash',
'Batman',
'Hancock',
'Elektra',
'Ant-Man',
'Superman',
'Mystique',
'Iron Man',
'Northstar',
'Daredevil',
'Spider-Man',
'Professor X',
'Human Torch',
'Green Arrow',
'Black Widow',
'Captain America']
Nel caso in cui si volessero specificare due o più argomenti opzionali, basta separarli usando le virgole esattamente come si procede per gli argomenti non opzionali. Per esempio
names.sort(key=lambda n:len(n), reverse=True)
riordina le stringhe contenute in names dalla più lunga alla più corta. Va
anche notato che essendo gli argomenti opzionali univocamente individuati dal
loro nome, non è necessario specificarli seguendo un ordine prefissato:
pertanto si potrebbero scambiare le posizioni di key e reverse
nell'invocazione precedente senza modificare la semantica dell'istruzione.
Per degli approfondimenti è possibile consultare la documentazione ufficiale, che contiene un documento introduttivo e uno più dettagliato sull'uso delle liste.
4.4.4. Le tuple#
Una tupla è in tutto e per tutto una lista immutabile, nel senso che una volta che essa è stata creata non è possibile modificare i suoi contenuti. Un letterale di tipo tupla viene indicato con una sintassi analoga a quella per le liste, con l'unica differenza che i suoi contenuti sono delimitati da parentesi tonde.
rogue = ('Rogue',
'Anna Marie',
'Caldecott County, Mississippi',
'Marvel Comics',
173.1,
54.39,
'F',
1981,
'Green',
'Brown / White',
10,
'good')
L'accesso a un elemento di una tupla viene fatto in modo posizionale usando la medesima sintassi introdotta per le liste, e anche in questo caso è possibile estrarre delle sotto-tuple utilizzando degli slicing. Quando però si tenta di modificare un elemento contenuto in una tupla, l'esecuzione verrà bloccata emettendo un errore:
rogue[-2] = 70
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[29], line 1
----> 1 rogue[-2] = 70
TypeError: 'tuple' object does not support item assignment
A seguito di questo errore, la tupla manterrà i suoi valori originali, restando quindi effettivamente invariata:
rogue
('Rogue',
'Anna Marie',
'Caldecott County, Mississippi',
'Marvel Comics',
173.1,
54.39,
'F',
1981,
'Green',
'Brown / White',
10,
'good')
Una tupla può essere utilizzata facendo ri ferimento agli stessi operatori e
alle stesse funzioni messi a disposizione per le liste (come per esempio in e
len), escludendo ovviamente le operazioni che modificano la tupla stessa
(come sort).
L'immutabilità delle tuple le rende da preferire rispetto alle liste in tutti i casi in cui si vuole impedire che dei dati vengano modificati, per esempio a causa di un bug; inoltre la loro elaborazione è in molti casi più efficiente di quella delle liste.
4.4.5. Gli array#
Un array, o vettore, è una struttura dati statica ad accesso posizionale che permette di memorizzare una sequenza di valori omogenei.
4.4.6. Gli insiemi#
Python mette a disposizione il tipo set per rappresentare insiemi finiti.
Questi possono essere descritti
tramite dei letterali di tipo insieme, delimitando tramite parentesi graffe una sequenza degli elementi dell'insieme da descrivere separati tramite virgole, oppure
invocando il costruttore della classe
setspecificando come argomento un iterabile contenente gli elementi dell'insieme.
Pertanto le espressioni {1, 3, 5}, set([1, 3, 5]) e set((1, 3, 5))
corrispondono allo stesso oggetto di tipo insieme. Chiaramente tutte le tre
espressioni precedenti individuano lo stesso insieme:
{1, 3, 5} == set([3, 1, 5])
True
L'espressione nella cella precedente e il relativo valore permettono di
sottolineare due cose: la prima è che, coerentemente con la definizione di
insieme, l'ordine con cui si specificano gli elementi è indifferente, e la
seconda è che l'operatore di uguaglianza si può utilizzare mantenendone
invariata la semantica quando i suoi argomenti sono oggetti di tipo insieme.
La 11 elenca la sintassi dei principali operatori
che Python mette a disposizione per gli insiemi, e come si può vedere alcuni
degli operatori introdotti per i tipi di dati semplici si estendon
naturalmente agli insiemi, mantenendo dunque la loro semantica e la sintassi
infissa [17]: è questo il caso delle relazioni di
uguaglianza, differenza e appartenenza, così come dell'operazione di
differenza. In altri casi viene modificata la semantica: per esempio <=
rappresenta la relazione di sottoinsieme. Vi sono poi degli operatori specifici
legati a intersezione, unione e differenza simmetrica [18].
Relazione/operazione |
Sintassi |
|---|---|
\(S = T\) |
|
\(S \neq T\) |
|
\(S \subseteq T\) |
|
\(S \subset T\) |
|
\(S \supseteq T\) |
|
\(S \supset T\) |
|
\(S \cup T\) |
|
\(S \cap T\) |
|
\(S \backslash T\) |
|
\(S \ominus T\) |
|
\(e \in S\) |
|
Oltre agli operatori, anche la funzione len mantiene inalterato il suo
signifcato, restituendo la cardinalità dell'insieme, intesa come il numero di
elementi in esso contenuti. Ovviamente l'accesso posizionale perde di
significato per questo tipo di dato: le operazioni fondamentali per accedere a
un insieme sono infatti:
la verifica dell'appartenenza o meno di un elemento a un insieme, utilizzando l'operatore
in,l'inserimento di un elemento in un insieme, invocando il metodo
add[19],l'eliminazione di un elemento in un insieme, invocando il metodo
discard[20].
Come gli altri tipi visti nel Paragrafo @dati-semplici, anche per gli insiemi il linguaggio prevede un supporto molto più completo di quanto descritto in questo paragrafo. La documentazione ufficiale descrive in modo estensivo operatori, funzioni e metodi a disposizione.
4.4.7. I dizionari#
I dizionari servono a memorizzare delle associazioni tra oggetti, in analogia con il concetto matematico di funzione. È quindi possibile pensare a essi come a insiemi di coppie (chiave, valore), dove una data chiave non occorre più di una volta, e a ogni chiave corrisponde un unico valore.
Un letterale di tipo dizionario viene descritto indicando ogni coppia separando chiave e valore con il carattere di due punti, separando le coppie con delle virgole e racchiudendo l'intero letterale tra parentesi graffe. Possiamo per esempio usare un dizionario per rappresentare un record in modo più elegante rispetto a quanto fatto nel Paragrafo 4.4.3 utilizzando le liste:
rogue = {'name': 'Rogue',
'identity': 'Anna Marie',
'birth_place': 'Caldecott County, Mississippi',
'publisher': 'Marvel Comics',
'height': 173.1,
'weight': 54.39,
'gender': 'F',
'first_appearance': 1981,
'eye_color': 'Green',
'hair_color': 'Brown / White',
'strength': 10,
'intelligence': 'good'}
L'accesso, in lettura o scrittura, agli elementi di un dizionario viene fatto con una notazione che ricorda quella di liste e tuple: si specifica all'interno di parentesi quadre la chiave per ottenere o modificare il valore corrispondente:
rogue['identity']
'Anna Marie'
È proprio questa modalità di accesso che fa sì che i dizionari rappresentino
una scelta più elegante per memorizzare un record: rogue['identity'] è
sicuramente più leggibile di rogue[1]. Va notato che il prezzo da pagare per
la leggibilità è un'efficienza potenzialmente minore nelle operazioni di
accesso (normalmente le liste sono implementate con una logica simile a quella
degli array e dunque hanno un tempo di accesso costante ai loro elementi,
mentre i dizionari sono implementati tramite tabelle di hash, pertanto
l'accesso è a tempo costante solo se non avvengono collisioni).
Se si tenta di accedere in lettura a un dizionario specificando una chiave
inesistente viene lanciata un'eccezione (KeyError), mentre accedendovi in
scrittura la specificazione di una chiave inesistente comporterà l'aggiunta
della corrispondente coppia (chiave, valore) al dizionario.
L'operatore in introdotto per le liste può anche essere utilizzato per i
dizionari: più precisamente, l'espressione k in d restituisce True se k è
una chiave valida per il dizionario d.
Anche nel caso dei dizionari il linguaggio mette a disposizione una serie di funzioni specifiche, e si può fare riferimento alla documentazione ufficiale di python per approfondire l'argomento.
4.4.8. Esercizi#
Provare a modificare un carattere in una stringa e vedere che cosa succede
Doppio for in list comprehension