1: LA SLICE NOTATION

LE LISTE E GLI INDICI

Sappiamo già che Python (come tutti i linguaggi di programmazione) permette di aggregare più dati semplici formando dei tipi di dati più complessi che sono detti genericamente containers o collections (in Python abbiamo le liste, tuple, ecc.) Un container permette di accedere ai suoi elementi per mezzo degli indici racchiusi dalle parentesi quadre (ad esempio, se ls è una lista, ls[0] è il suo primo elemento, ls[1] il secondo e così via).

Python ha però enormemente sviluppato questa possibilità, dando all'utente la possibilità di selezionare non solo singoli elementi, ma intere sottosequenze della sequenza principale, tramite una serie di regole sintattiche che sono state ribattezzate slice notation (cioè "notazione a fettine"). Vediamo in questa lezione le possibilità che ci offre.

INDICI NEGATIVI

Possiamo usare come indice un numero negativo: in questo caso gli indici partono dalla fine della lista: l'elemento -1 è l'ultimo, l'elemento -2 il penultimo e così via. Proviamo questi comandi in IDLE:


>>> citta = ["Roma", "Milano", "Napoli", "Firenze", "Bologna"]
>>> citta[-1]

'Bologna'

>>> citta[-2]

'Firenze'

>>> citta[-10]

Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    citta[-10]
IndexError: list index out of range

>>> nome = "Massimiliano"
>>> nome[-1]

'o'

>>> nome[-2]

'n'

>>> nome[-len(nome)]

'M'

Nella terza istruzione ho volutamente provocato un IndexError, mentre l'ultima è un modo più complicato di indicare il primo elemento.

SOTTOSEQUENZE

Possiamo selezionare un'intera sottosequenza di una lista indicando tra parentesi quadre due indici separati da un carattere ":"; come al solito il primo indice è quello iniziale (incluso), mentre il secondo è quello finale (escluso). Continuate così:


>>> citta[1:3]

['Milano', 'Napoli']

>>> nome[3:9]

'simili'

>>> nome[5:5]

''

>>> nome[5:4]

''

Notate le ultime due istruzioni: se il secondo indice è uguale o minore del primo ottengo una sequenza vuota. La cosa funziona anche con gli indici negativi, che si possono anche usare insieme a quelli positivi.


>>> citta[-3:-1]    # gli elementi dal terzultimo all'ultimo

['Napoli', 'Firenze']

>>> citta[-1:-3]    # il contrario (lista vuota)

[]

>>> nome[3:-2]      # le lettere di 'Massimiliano' dalla terza alla penultima

'similia'

>>> nome[5:-2]      # dalla quinta alla penultima

'milia'

>>> nome[10:-2]     # stringa vuota

''

Vi ripeto che se il secondo elemento (in qualunque modo esso sia indicato) occupa una posizione uguale o minore del primo si ottiene una sottosequenza vuota). E' il caso dell'ultimo esempio, dove il primo indice (10) rappresenta la nona lettera di "Massimiliano" (cioè la "a") e il secondo (-2) la terzultima (la stessa "a").

Per includere nella selezione l'ultimo elemento della sequenza si deve omettere il secondo indice, lasciando però i due punti; similmente, anche il primo indice può essere omesso, indicando il primo elemento della sequenza.


>>> citta[:3]

['Roma', 'Milano', 'Napoli']

>>> nome[-5:]

'liano'

Ed infine possiamo aggiungere un terzo numero, sempre separato da ":", che indica lo "step" con cui dobbiamo selezionare gli elementi. Per ricordare queste regole basta notare che sono identiche a quelle che si usano per i parametri della funzione range() (vedi qui).


>>> nome[2:8:2]        # dalla terza lettera alla settima ogni due lettere

'sii'

>>> nome[::2]          # tutta la stringa ogni due lettere

'Msiiin'

>>> nome[::-3]         # dall'ultima lettera alla prima ogni tre lettere

'oims'

Insomma, potete capire che le possibilità sono davvero molte e forse è anche difficile capire l'utilità di tutte combinazioni possibili. Nel seguito cercherò di mostrarvene alcune.

ESERCIZIO 1.1: Scrivete in IDLE l'istruzione:

  >>> numeri = list(range(100))
  
che ci da la lista di tutti i numeri da 0 a 99. Ora provate ad immettere queste istruzioni, cercando, prima di dare invio per ognuna, di indovinare il loro risultato (che qui ometto).

  >>> numeri[5:20]
  >>> numeri[:50]
  >>> numeri[-5:-1]
  >>> numeri[50:-3]
  >>> numeri[-99:5]
  >>> numeri[10:30:3]
  >>> numeri[10::4]
  
ESERCIZIO 1.2: Ora facciamo l'esercizio inverso: vi propongo delle sottosequenze della lista numeri e voi dovete scrivere delle istruzioni che restituiscano le sottosequenze date:

  [25, 26, 27, 28, 29]
  [30, 32, 34, 35]
  [90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
  [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 5', 55, 60, 65, 70, 75, 80, 85, 90, 95]
  [0, 2, 4, 6, 8]
  [99, 98, 97, 96, 95, 94, 93, 92, 91, 90]
  

MANIPOLARE LE SEQUENZE

Ricordiamo che per Python stringhe e tuple sono oggetti immutable (cioè che non si possono più modificare una volta che è stato loro assegnato un valore): questo vuol dire che per mezzo degli indici possiamo solo leggere i dati ma non modificarli. Una tecnica molto usata per aggirare questa limitazione è quella di eseguire delle operazioni sull'oggetto e poi riassegnare il risultato a sè stesso. Ad esempio, proviamo a togliere qualche carattere ad una stringa:


>>> s = "Luigi"
>>> s = s[:-1]
>>> s

'Luig'

>>> s = s[1:]
>>> s

'uig'

nella prima istruzione abbiamo riassegnato ad s tutti i suoi caratteri dal primo al penultimo, nella seconda i suoi caratteri dal secondo in poi. Vediamo ora come cambiare una lettera in una stringa:


>>> s = "mastino"
>>> s = s[:2] + "t" + s[3:]
>>> s

'mattino'

analizzate l'esempio e cercate di capire come ho fatto ad ottenere il risultato. Infine vediamo un programmino che toglie una per una tutte le lettere aad una stringa:

NUOVO PROGRAMMA: prova_slice.py

import random

alfabeto = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
random.seed()
while len(alfabeto) > 0:
    print("La stringa", alfabeto, "contiene", len(alfabeto), "caratteri")
    input("Premi INVIO per togliere una lettera")
    ind = random.randrange(len(alfabeto))
    print("Ho tolto la", alfabeto[ind])
    alfabeto = alfabeto[:ind] + alfabeto[ind + 1:]
print("Fine")

Dovreste ricordare che il modulo random importato nella #1 contiene alcune funzioni per la generazione di numeri casuali. Notate che ho usato la sintassi import random che ci obbliga a chiamare tali funzioni anteponendo il nome del modulo (come nella #4 e nella #8). Questo è un po' più scomodo rispetto all'istruzione from random import * che ho usato nel primo tutorial, ma è la prassi comune nei programmi "seri" e quindi conviene abituarcisi (vedi qui). Se non ricordate a cosa servono le funzioni seed() e randrange() potete ripassarlo qui. Le righe che ci interessano sono la #8 che genera un indice casuale (corrispondente ad una lettera della stringa alfabeto) e la #10 che toglie quella lettera dalla stringa.

NUOVO PROGRAMMA: coniugazioni.py

Usiamo ora le nostre nuove conoscenze per implementare un programma capace di coniugare i verbi regolari: vogliamo quindi ottenere un I/O di questo tipo (ho scritto in blu l'input dell'utente):


Immetti un verbo regolare della prima coniugazione all'infinito: amare
Io amo
Tu ami
Egli ama
Noi amiamo
Voi amate
Essi amano
ESERCIZIO 1.3 PROGRAMMA PASSO PASSO: Scrivete il programma a partire da questo schema:
  1. Per prima cosa copiate ed incollate queste due tuple nel vostro programma:
    
    pronomi = ("Io", "Tu", "Egli", "Noi", "Voi", "Essi")
    desinenze = ("o", "i", "a", "iamo", "ate", "ano")
    La prima contiene i pronomi delle sei persone del verbo, mentre la seconda contiene le desinenze corrispondenti
  2. Ora aggiungete un'istruzione input() che chieda all'utente di scrivere un verbo regolare della prima coniugazione, assegnando la stringa digitata alla variabile verbo;
  3. Ora bisogna coniugare il verbo nelle sei persone: aggiungete un ciclo for con il contatore i che varia da 0 a 5 (dovete usare un range() ...);
  4. Per ognuna delle sei iterazioni dovete stampare il pronome ed il verbo coniugato; la coniugazione si ottiene appunto concatenando la radice del verbo (cioè tutte le lettere della variabile verbo meno le ultime tre) con la corrispondente desinenza nella nostra tuple. Dovrebbe essere abbastanza facile ottenere questo usando la slice notation.
ESERCIZIO 1.4 Se siete riusciti a scrivere il programma potete modificarlo in modo che coniughi tutte e tre le coniugazioni italiane. Dovete fare queste modifiche:
  1. Aggiungete una nuova tupla con le tre coniugazioni:
    
    coniugazioni = ("are", "ere", "ire")
  2. Modificate anche la tuple desinenze, in modo da avere le desinenze per tutte e tre le coniugazioni. ATTENZIONE! In questo modo desinenze diventa una tuple che contiene a sua volta altre tuple, quindi le singole desinenze andranno chiamate con un doppio indice...
    
    desinenze  = (("o", "i", "a", "iamo", "ate", "ano"),   # prima coniugazione
                  ("o", "i", "e", "iamo", "ete", "ono"),   # seconda coniugazione
                  ("o", "i", "e", "iamo", "ite", "ono"))   # terza coniugazione
    
  3. Modificate il prompt della input(), togliendo il riferimento alla prima coniugazione;
  4. Dopo avere immesso il verbo, dobbiamo capire a quale coniugazione appartiene. Per fare questo dobbiamo cercare la sua desinenza (le ultime tre lettere del verbo, dovete di nuovo usare la slice notation) all'interno della tuple coniugazioni: utilizzate la funzione index() (vedi qui) che resituisce l'indice della desinenza all'interno della tuple, assegnandola alla variabile num_con (0 = prima, 1 = seconda, 2 = terza).
  5. Infine dobbiamo modificare l'istruzione per stampare all'interno del ciclo, tenendo presente che per indicare la nostra desinenza dobbiamo utilizzare due volte le parentesi quadre: la prima per indicare la coniugazione e la seconda la desinenza della perona del verbo.

Questi esercizi dovrebbero darvi un'idea delle difficoltà cui vanno incontro i programmatori che vogliono scrivere software che trattano il linguaggio umano (come ad esempio i traduttori automatici). Infatti notiamo che, anche all'interno dei verbi regolari, ci sono alcune eccezioni. Provate ad inserire i verbi "giocare" o "mangiare" e vedrete che il nostro programmino fallirà miseramente.

ESERCIZIO 1.5 (FACOLTATIVO) Se volete rimediare a questa situazione dovete complicare un poco il programma; i verbi problematici sono quelli che finiscono in "care", "gare" e "iare" (tutti della prima coniugazione: nei primi due casi si mantiene il suono della "c" o "g" dura (quindi "tu giochi" e non "tu gioci"), nel terzo in alcune persone c'è una "i" di troppo ("tu mangi" e non "tu mangii"). Procedete così:
  1. Aggiungete altre due "coniugazioni" alla tuple desinenze: la quarta verrà usata per i verbi in "care" e "gare", la quinta per i verbi in "iare";
    
    desinenze  = (("o", "i", "a", "iamo", "ate", "ano"),
                  ("o", "i", "e", "iamo", "ete", "ono"),
                  ("o", "i", "e", "iamo", "ite", "ono"),
    			  ("o", "hi", "a", "hiamo", "ate", "ano"),
    			  ("o", "", "a", "amo", "ate", "ano"))
    
  2. Dopo l'istruzione che determina la coniugazione del verbo, aggiungiamo due if per controllare se siamo in uno dei casi particolari;
  3. Se la coniugazione è la prima e la quartultima lettera è "c" o "g" (potete usare l'operatore in per controllare la seconda ipotesi) siamo nel primo caso particolare e dobbiamo porre num_con = 3 per usare la nostra "quarta coniugazione";
  4. Altrimenti, se la coniugazione è la prima e la quartultima lettera è "i" ponete num_con = 4.
Queste modifiche dovrebbero rendere il programma preciso anche con questi verbi più ostici. Pensate adesso alla difficoltà di coniugare tutti i tempi e modi verbali italiani e di tener conto dei verbi irregolari ...

Fine della lezione