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.

Tabella 9 Proprietà di alcuni tipi di dati strutturati in Pyton.#

Tipo strutturato

Classe

Posizionale

Dinamico

Mutabile

Omogeneo

stringa

str

✓

✕

✕

✓

array

np.array

✓

✕

✓

✓

lista

list

✓

✓

✓

✕

tupla

tuple

✓

✕

✕

✕

insieme

set

✕

✓

✓

✕

dizionario

dict

✕

✓

✓

✕

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 i quando 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 len restituisce 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 in consente di verificare se un elemento appartiene o meno a una struttura, mentre l’operatore del può 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 s per un numero intero i produce una nuova stringa ottenuta concatenando s con se stessa i volte;

  • s.find(t) restituisce la posizione iniziale della prima occorrenza della sottostringa t all’interno di s, se presente, e -1 altrimenti.

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))
\[\displaystyle \Phi(x) = \frac{1}{2 \pi} \int_{-\infty}^{+\infty} \mathrm e^{-\frac{x^2}{2}} \, \mathrm dx\]

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);

  • align definisce 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);

  • sign specifica 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;

  • width imposta 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;

  • precision definisce il numero di cifre decimali (per i float) o di caratteri (per le stringhe) da mostrare;

  • type specifica la modalità di rappresentazione del valore, scegliendo uno dei caratteri elencati nella 10; meritano un po' più di dettaglio gli specificatori e, E e g: 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.

Tabella 10 Modalità di formattazione dei letterali stringa (f-string) e relativi simboli.#

Carattere

Tipo di visualizzazione

s

Stringa (default)

d

Intero

f

Decimale (notzione a virgola fissa)

e, E

Decimale (notazione scientifica)

g

Forma compatta

%

Percentuale

x, X

Esadecimale

b

Binario

o

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 da 0 a m − 1;

  • range(m, n) rappresenta la sequenza degli interi da m a n − 1;

  • range(m, n, s) descrive la sequenza ottenuta partendo da m e applicando successivamente degli incrementi di s unità, fino a includere il più grande tra i valori che non superano n.

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, se l e n sono rispettivamente una lista e un intero, sia l * n che n * l producono una nuova lista formata da n ripetizioni dei contenuti di l, 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 set specificando 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].

Tabella 11 Sintassi delle principali relazioni tra insiemi utilizzabili in Python, indicando con \(S\) e \(T\) due insiemi e con s e t i corrispondenti oggetti.#

Relazione/operazione

Sintassi

\(S = T\)

s == t

\(S \neq T\)

s != t

\(S \subseteq T\)

s <= t

\(S \subset T\)

s < t

\(S \supseteq T\)

s >= t

\(S \supset T\)

s > t

\(S \cup T\)

s | t

\(S \cap T\)

s & t

\(S \backslash T\)

s - t

\(S \ominus T\)

s ^ t

\(e \in S\)

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