4.3. Dati semplici#
Come spiegato nel paragrafo precedente, i dati semplici rappresentano entità che non ha senso suddividere ulteriormente, vuoi perché non è possibile farlo, vuoi perché non è rilevante per i nostri scopi. I tipi di dati semplici sui quali mi concentrerò nel seguito sono quelli numerici — interi, decimali e booleani — e quello che permette di lavorare con le stringhe, pur menzionando brevemente anche altri tipi di dati.
4.3.1. Tipi numerici#
Tra i tipi numerici disponibili in Python, i più utilizzati sono int e
float: il primo serve a memorizzare numeri interi, il secondo numeri
decimali. Come già accennato, int e float rappresentano due classi, e i
valori numerici sono oggetti di queste classi. Poiché l’uso di costanti
numeriche è molto frequente, per esempio per riferirsi al numero 42[42],
Python permette di scrivere direttamente i valori senza dover creare
esplicitamente gli oggetti corrispondenti, tramite i letterali di una
particolare grammatica.
Letterali
Un letterale è un valore costante di un dato tipo, espresso secondo una
notazione prefissata. Ad esempio, 42 e 3.14 sono letterali interi e
decimali rispettivamente in quasi tutti i linguaggi di programmazione, mentre
'foo' è un letterale di tipo stringa in Python, ma non in Java o Go. Non
tutti i tipi hanno letterali: ad esempio, file o date non dispongono di un
letterale associato in Python — né probabilmente in alcun altro
linguaggio.
Il modo più semplice di ottenere un letterale intero è scrivere una sequenza di
cifre, eventualmente preceduta da un segno: il risultato rappresenta un numero
in base dieci[1]. Gli interi in Python hanno precisione arbitraria,
quindi non esiste un valore massimo o minimo definito, fatta eccezione per la
memoria disponibile[2]. Per migliorare la leggibilità (non è facile
comprendere a prima vista l'ordine di grandezza dei numeri già a partire dalle
decine di migliaia), è possibile inserire un carattere di underscore (_)
tra le cifre, separando ad esempio le migliaia o evidenziando componenti
logiche diverse (come in un numero di telefono).
Per i numeri decimali (float), il letterale è formato da un eventuale segno,
dalle cifre della parte intera, dal punto decimale . e dalle cifre della
parte frazionaria. È obbligatorio specificare almeno una delle due parti,
intera o decimale. Anche qui, è possibile usare l’underscore tra cifre. Per
numeri molto grandi o piccoli è più pratico usare la notazione scientifica: si
scrive la mantissa (un numero, eventualmente con segno o virgola) seguita da
E o e e da un esponente intero; il valore rappresentato è la mantissa
moltiplicata per 10 elevato all’esponente. Ad esempio, 1E9 e 1E-9
corrispondono rispettivamente a un miliardo e a un miliardesimo. È interessante
sottolineare che, quando deve visualizzare un float, Python decide
automaticamente quale tra le notazioni tradizionale e scientifica ottimizza la
leggibilità del risultato.
I valori decimali sono memorizzati secondo lo standard a virgola mobile IEEE
754, usando tipicamente 8
byte[3]. Questo formato non garantisce precisione arbitraria: numeri al
di fuori dell’intervallo i cui estremi sono circa ±1.8E308 diventano inf o
-inf. Valori molto vicini a zero (approssimativamente tra 5E-324 e il suo
opposto) vengono codificati come 0.0. Inoltre, esiste il valore speciale
nan («not a number»), utilizzato per codificare il risultato di un'operazione
aritmetica quando questo risulta non definita (come nel calcolo del logaritmo
di un numero negativo, o per indicare valori mancanti in un dataset. La
divisione per zero invece genera un’eccezione.
Stranezze in virgola mobile
La rappresentazione dei numeri decimali usando il formato in virgola mobile richiede spesso di effettuare delle approssimazioni. Questo fatto è ovvio se pensiamo per esempio ai numeri irrazionali, mentre è meno intuitivo quando coinvolge numeri con parte decimale finita. Nella cella che segue si può vedere un esempio eclatante.
0.1
0.1
print(0.1 + 0.2)
0.30000000000000004
Il risultato non è, come sarebbe logico aspettarsi, \(0.3\), ma un numero
leggermente più alto. Questo è dovuto alla differenza tra la base decimale che
ho usato per indicare i due addendi e la base binaria che viene utilizzata per
rappresentare i corrispondenti valori float. Se usiamo la notazione \((n)_b\)
per indicare la rappresentazione del numero \(n\) in base \(b\), si ha infatti che
a significare che, a destra dell'uguale, la prima cifra binaria dopo il punto
decimale va moltiplicata per \(2^{-1}\), la seconda per \(2^{-2}\) e così via, e
sommando tutti i prodotti si ottiene il numero indicato a sinistra (per il
quale si potrebbe svolgere un processo analogo, ma l'uso della base decimale fa
sì che il risultato sarebbe esattamente il numero indicato tra parentesi). La
sovralineatura sta a indicare il periodo nella rappresentazione: in altre
parole, la sequenza \(1100\) si ripete all'infinito, così come succederebbe con
l'equivalente decimale della frazione \(1/3\) espresso in base \(10\). Ora, il
formato usato per rappresentare i valori float memorizza la mantissa (e anche
l'esponente) esprimendola in base \(2\) e utilizzando un numero fisso di bit.
Dunque non è possibile rappresentare \(0.1\) in modo preciso. Lo stesso vale per
gli altri due valori coinvolti nell'operazione, perché
mentre
Se ipotizziamo per semplicità che vengano utilizzati \(11\) bit dopo la virgola decimale, calcolando la somma delle approssimazioni di \(0.1\) e \(0.2\) nel sistema binario otteniamo
0.00011001100 +
0.00110011001 =
-------------
0.01001100101
che differisce dall'espansione binaria di \(0.3\) a partire dalla nona cifra dopo la virgola, e che corrisponde al valore \(0.29931640625\) in base dieci. Tenendo conto del fatto che Python utilizza \(52\) bit per memorizzare il valore assoluto della mantissa (e di altre peculiarità dello standard IEEE per la virgola mobile che non approfondisco per brevità), il risultato ottenuto eseguendo il codice — pur non essendo corretto — presenta un errore di approssimazione estremamente minore, dell'ordine di \(10^{-17}\).
In ogni caso, le stranezze non finiscono qui, perché valutando il letterale
0.1 si ottiene un risultato incoerente con quanto detto finora:
0.1
0.1
e si può verificare che lo stesso accade per \(0.2\) e \(0.3\). Ciò è dovuto al
fatto che la rappresentazione in virgola mobile è univoca ma non biunivoca:
fissato un numero, la sua rappresentazione è unica, ma a partire da una
rappresentazione esistono infiniti numeri decimali che le corrispondono.
Pertanto, quando Python deve visualizzare un valore float, deve anche
scegliere uno tra questi infiniti numeri. La scelta che viene fatta è quella
ragionevole di mostrare il numero decimale più conciso tra tutti quelli compatibili con la rappresentazione interna.
Riassumendo, quando viene valutata l'espressione 0.1 + 0.2, nell'ordine:
vengono creati i valori
floatper gli addendi, e in entrambi i casi si introduce un'approssimazione rispetto ai valori originali;le due approssimazioni vengono sommate, ottenendo la corrispondente rappresentazione in virgola mobile;
viene visualizzato il numero decimale più corto tra tutti quelli che corrispondono alla rappresentazione binaria ottenuta, e questo numero non è \(0.3\), bensì Caricamento....
Il fatto che ogni letterale identifichi l'oggetto di una classe significa che
su questi valori è possibile in teoria invocare dei metodi della classe stessa.
Per esempio, float include un metodo as_integer_ratio che restituisce una
coppia di valori interi il cui rapporto è uguale al valore sul quale il metodo
è stato invocato:
3.14.as_integer_ratio()
(7070651414971679, 2251799813685248)
Per questioni di compatibilità, lo stesso metodo è presente anche in
int[4]:
(3).as_integer_ratio()
(3, 1)
Nella pratica comune, però, non succede quasi mai di invocare dei metodi
direttamente su un letterale, e nemmeno di invocare dei metodi su oggetti di
tipo int o float. Analogamente, la praticità dell'utilizzo del letterali fa
sì che i costruttori di queste due classi non vengano di norma utilizzati per
creare degli oggetti. Vi è però un'importante eccezione: tutti i costruttori
delle classi che corrispondono ai tipi semplici accettano un parametro il cui
tipo può differire da quello che essi implementano: per esempio, int può
ricevere un valore di tipo float e viceversa. Durante la loro esecuzione,
questi costruttori tentano di convertire il valore dell'argomento nel tipo
che rappresentano: se la conversione è possibile viene creato un nuovo oggetto,
altrimenti viene lanciata un'eccezione. Pertanto, la conversione tra i tipi
semplici si effettua — ove possibile — utilizzando il nome del
tipo di destinazione come funzione di conversione.
Esempio 2
L'invocazione
int(3.14)
3
effettua una conversione da float a int, troncando all'intero più vicino da
sinistra e dunque causando una (necessaria) perdita di informazione. Ciò non
capita invece nella conversione in senso inverso
float(3)
3.0
che semplicemente introduce una parte frazionaria nulla. Procedendo in modo analogo è possibile estrarre il valore numerico contenuto in una stringa, a patto che i suoi contenuti siano interpretabili in tal senso:
float('3.14')
3.14
Altrimenti, viene sollevata un'eccezione:
float('Python')
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[10], line 1
----> 1 float('Python')
ValueError: could not convert string to float: 'Python'
Operatori per i tipi numerici#
A partire da riferimenti a oggetti, espressi usando variabili o letterali, è possibile costruire espressioni arbitrariamente complesse utilizzando degli operatori. Per operatore si intende un tipo particolare di funzione che viene espressa tramite la notazione infissa: gli argomenti (che in questo contesto prendono il nome di operandi) non sono racchiusi tra parentesi, ma compaiono prima o dopo il simbolo che denota l'operatore stesso. L'uso degli operatori consente di scrivere espressioni più concise e, in genere, più leggibili rispetto all'equivalente ottenuto con delle funzioni, perché segue la notazione aritmetica comunemente utilizzata.
Operazione |
Operatore |
|---|---|
addizione |
|
sottrazione |
|
moltiplicazione |
|
divisione (decimale) |
|
divisione (troncata) |
|
resto (modulo) |
|
elevamento a potenza |
|
La 5 elenca i principali operatori per valori numerici. Su questi operatori è opportuno fare alcune osservazioni, perché il loro comportamento non è sempre banale.
Gli operatori di addizione e sottrazione possono essere usati sia in forma binaria, sia in forma unaria. Nel primo caso, che è anche il più comune, l'operatore denota la somma o la differenza tra due operandi e viene scritto tra di essi. Nel secondo caso l'operatore precede un unico operando e serve a indicarne il segno. Il suo uso tipico è quello di cambiare il segno del risultato di un'epressione senza ricorrere a un'esplicita moltiplicazione per
-1. Per esempio,-valoreviene valutata come l'opposto del contenuto della variabilevalore. In linea di principio anche l'operatoreèpuò essere usato in forma unaria, ma questo suo impiego non è particolarmente informativo.Addizione, sottrazione, moltiplicazione ed elevamento a potenza si applicano a tutti gli operandi di tipo numerico. Se gli operandi sono dello stesso tipo, questo sarà anche il tipo del risultato, altrimenti avverrà una promozione, cioè la conversione del tipo di uno dei due operandi. Più precisamente, verrà applicata la conversione che non comporta perdita di informazione: per i tipi numerici che abbiamo visto finora, la sola promozione possibile è quindi quella da
intafloat, nella quale un valore intero diventa l'equivalente decimale con parte frazionaria nulla.La divisione decimale
a / brestituisce il valorefloat, che moltiplicato per il divisoreb, fornisce come risultato il dividendoa. È importante notare che questa divisione restituisce sempre unfloat, anche quando entrambi gli operandi sono interi eaè un multiplo esatto dib.La divisione troncata
a // brestituisce il più grande valore intero minore o uguale ada / b. Se gli operandi hanno lo stesso segno, il risultato è analogo al calcolo della divisione decimale arrotondato per difetto. Se i segni sono diversi, il comportamento può risultare meno intuitivo. Per esempio, dividendo \(-10\) per \(3\) il risultato è \(-\frac{10}{3} = -3.\overline{3}\), pertanto-10 // 3restituisce \(-4\), che è il più grande tra gli interi che sono minori o uguali della precedente frazione. Lo stesso vale per10 // -3. Il risultato è un intero quando entrambi gli operandi sono interi, altrimenti viene restituito unfloat. Ma anche in questo caso il valore rappresenta comunque un intero, nel senso che la sua parte decimale è nulla.L'operatore
%denota il modulo, definito nel modo seguente: il modulo tra due valori \(a\) e \(b\) è il valore \(r \in \mathbb N\) compreso tra zero e \(b\) tale che \(a = b \cdot q + r\), dove \(q\) è il valore della divisone troncata tra \(a\) e \(b\). Seaebsono entrambi positivi, alloraa % bequivale al resto della divisione intera traaeb, altrimenti il risultato va calcolato con attenzione. Riprendendo l'esempio del punto precedente,-10 % 3è uguale a \(2\), mentre10 % -3è uguale a \(-2\). Anche in questo caso, l'operatore restituisce un intero quando lo sono anche gli operandi coinvolti, altrimenti restituisce un valorefloatcon parte decimale nulla.L’operatore
%denota il modulo. In Python, dati due valoriaeb, il moduloa % bè definito come il valorertale che
[ a = b \cdot q + r, ] doveqè il risultato della divisione troncataa // b. Seaebsono entrambi positivi, questo coincide con il resto della divisione intera tradizionale; in presenza di valori negativi, invece, è necessario prestare attenzione alla definizione adottata dal linguaggio. Riprendendo gli esempi precedenti,-10 % 3vale2, mentre10 % -3vale-2. Anche in questo caso, se gli operandi sono interi il risultato è un intero; altrimenti è unfloatcon parte decimale nulla.L'elevamento a potenza accetta operandi sia interi che decimali, e restituisce:
un valore intero se sia la base che l'esponente sono
int,un valore decimale se la base o l'esponente sono
floate la base è positiva,un valore complesso (vedi Paragrafo 4.3.3) se la base è negativa e non intera.
Va inoltre rimarcato che a ognuna delle operazioni in
5 è associata una specifica variante
dell'operatore di assegnamento, espressa aggiungendo il carattere = a destra
del simbolo dell'operazione. La loro semantica è la seguente: dapprima viene
applicata l'operazione ai due operandi, e il risultato viene assegnato alla
variabile che compare a sinistra dell'assegnamento. Pertanto a += 1 equivale
ad a = a + 1, b %= 2 a b = b % 2, c **= 3 a c = c ** 3 e così via.
L’operatore di elevamento a potenza accetta operandi sia interi sia decimali e restituisce:
un valore intero se sia la base sia l’esponente sono di tipo
int;un valore decimale se almeno uno tra base ed esponente è
floate la base è positiva;un valore complesso (si veda il Paragrafo 4.3.3) se la base è negativa e l’esponente non è intero.
Vale infine la pena ricordare che a ciascuna delle operazioni elencate in
5 è associata una variante composta
dell’operatore di assegnamento, ottenuta aggiungendo il simbolo =. La
semantica è sempre la stessa: l’operazione viene prima applicata agli operandi
e il risultato viene poi assegnato alla variabile che compare a sinistra.
Pertanto, a += 1 equivale a a = a + 1, b %= 2 a b = b % 2, c **= 3 a
c = c ** 3, e così via.
Esempio 3
Captain Atom è un supereroe con una caratteristica peculiare: il suo corpo è in grado sia di emettere enormi quantità di energia, sia di assorbirla da fonti esterne. Dal punto di vista fisico, l’energia non può assumere valori negativi. Tuttavia, per modellare questo comportamento in modo semplice, adotto una rappresentazione di tipo contabile: valori positivi indicano che si è emessa più energia di quanta ne sia stata assorbita, e valori negativi indicano il contrario. Consideriamo una situazione di partenza, in cui Captain Atom dispone di una quantità positiva di energia che intende utilizzare per sferrare più attacchi durante una battaglia, perdendo quantità intere di energia a ogni attacco.
energy = 100
planned_strikes = 6
La divisione decimale permette di calcolare l’energia media da usare per ciascun attacco (nell'idea di attaccare sempre con la stessa forza), mentre quella troncata indica quanta energia si può spendere in ognuno di essi senza andare sotto zero. Tramite il modulo otteniamo invece l’energia residua al termine degli attacchi.
# Distribuzione dell'energia
mean_energy = energy / planned_strikes
energy_per_strike = energy // planned_strikes
residual_energy = energy % planned_strikes
print('energia a ogni attacco:', energy_per_strike)
print('unità di energia rimaste:', residual_energy)
energia a ogni attacco: 16
unità di energia rimaste: 4
Immaginiamo che, dopo aver portato a termine gli attacchi, a seguito di uno scontro particolarmente intenso, Captain Atom perda dieci unità di energia:
energy = residual_energy
energy -= 10
print('energia a seguito dello scontro: ', energy)
energia a seguito dello scontro: -6
Dopo lo scontro, Captain Atom si trova dunque in debito energetico. Supponiamo che in questa situazione egli possa assorbire energia solo in pacchetti minimi di quattro unità. La divisione troncata indica quanti di questi pacchetti dovrà ancora assorbire per tornare in bilancio positivo, mentre il modulo mostra l’energia che avrà al termine di queste operazioni:
min_energy = 4
num_absorbtions = energy // min_energy
residual_energy = energy % min_energy
print('assorbimenti necessari:', num_absorbtions)
print('energia finale:', residual_energy)
assorbimenti necessari: -2
energia finale: 2
Il risultato negativo di // indica che Captain Atom parte da una situazione
di debito: il numero rappresenta quante volte è necessario assorbire la
quantità minima di energia per avere un bilancio positivo.
Anche in presenza di valori negativi, le operazioni restano coerenti: la divisione troncata arrotonda verso meno infinito, e il modulo preserva la relazione fondamentale
energy == min_energy * (energy // min_energy) + (energy % min_energy)
True
4.3.2. Il tipo booleano#
Python implementa attraverso la classe bool un tipo booleano, dedicato a
esprimere i due valori di verità nella logica classica: vero e falso, ai quali
corrispondono i letterali True e False. Tecnicamente, True e False sono
l'equivalente di due costanti i cui valori sono rispettivamente 1 e 0, come
si può facilmente verificare per esempio sommando True a sé stesso. In altre
parole, bool è un sottotipo di int, ed è possibile usare operandi di tipo
booleano per tutti gli operatori della
5.
È possibile convertire qualsiasi oggetto in un valore di verità: questo si può
fare esplicitamente passando per il costruttore bool, o implicitamente in
tutti i punti nei quali è richiesto di specificare un valore booleano, come per
esempio all'interno di un'operazione di selezione (vedi il
Paragrafo 4.5). Diventano False i letterali False
(ovviamente) e None (vedi il Paragrafo L'oggetto speciale None), i valori nulli dei
tipi numerici (l'intero 0, il valore decimale 0.0, ma anche i loro
equivalenti per gli altri tipi numerici di Python) e, in generale, tutti gli
oggetti che si possono pensare come «vuoti» perché non contengono alcunché,
come la stringa vuota o la lista vuota (vedi il Paragrafo 4.4.2 e
il Paragrafo 4.4).
Operatori logici#
La 6 illustra i principali operatori associati
ai valori booleani. I primi sette accettano operandi di tipo differente e
restituiscono un valore bool: in particolare, la differenza e gli operatori
di confronto ordinale hanno una sintassi intuitiva, che è identica a quella di
molti linguaggi di programmazione moderni. Gli ultimi tre operatori
corrispondono ai classici connettivi logici, dunque anche i loro operandi sono
valori di verità. Va notato che questi connettivi presentano una sintassi che
utilizza i termini and, or e not (dunque in modo esplicito rispetto alla
sintassi di linguaggi come Java o C, che usano invece i simboli &&, || e
!). I connettivi logici vengono valutati sfruttando il meccanismo del corto
circuito: il secondo operando di and e or viene valutato solo quando il
primo operando è stato valutato, rispettivamente, come True e False. Negli
altri casi, l'operatore restituisce il valore del primo operando.
Operazione |
Operatore |
|---|---|
uguale (per contenuto) |
|
uguale (per riferimento) |
|
diverso (per contenuto) |
|
diverso (per riferimento) |
|
minore |
|
minore o uguale |
|
maggiore |
|
maggiore o uguale |
|
congiunzione |
|
disgiunzione |
|
negazione |
|
L'uguaglianza tra due operandi può essere valutata in due modi differenti:
per contenuto e per riferimento. L'uguaglianza per contenuto, che corrisponde
all'operatore ==, coinvolge un confronto tra i valori di due operandi,
mentre is restituisce True solo quando i riferimenti indicati come operandi
puntano al medesimo oggetto. Per esempio, nel codice seguente
a = int("10_000")
b = int("10_000")
il valore 2.71 rappresenta un letterale di tipo float, e per ognuna delle
due righe nelle quali occorre è stato creato l'oggetto corrispondente.
Esisteranno dunque due oggetti float distinti, pertanto i riferimenti
memorizzati in a e b saranno diversi, ma questi due oggetti corrispondono
allo stesso numero decimale. Concludendo, i confronti fatti usando is e ==
daranno risultati diversi:
print('Il confronto per riferimento vale', a is b)
print('Il confronto per valore vale', a == b)
Il confronto per riferimento vale False
Il confronto per valore vale True
In generale, ogni volta che viene invocato un costruttore viene dunque creato
un nuovo oggetto, anche se già esiste in memoria centrale un altro oggetto
equivalente. Il motivo dietro questa scelta è legato all'efficienza: il tempo
di esecuzione diventerebbe rapidamente inaccettabile se si operasse altrimenti.
Ci sono però delle eccezioni: semplificando non poco la
questione[5], queste riguardano gli oggetti di bool e int. Per
quanto riguarda la prima classe, esistono solo due possibili letterali (True
e False), quindi è molto semplice pre-istanziare i due oggetti corrispondenti
e farli restituire dal costruttore, così che non possano mai esistere due
oggetti bool diversi che contengono il medesimo valore di verità[6].
Per int può succedere una cosa simile. Chiaramente, non è possibile creare
preventivamente «tutti» i possibili oggetti (ricordate che Python implementa
gli interi con precisione arbitraria). Però, alcuni particolari letterali
interi tendono a essere usati frequentemente nel codice (indipendentemente dal
linguaggio di programmazione usato): questo capita tipicamente per valori
positivi non troppo grandi e per i primi valori a sinistra dello zero.
Nell'implementazione di Cpython, che è attualmente quella più usata dalla
comunità degli sviluppatori e delle sviluppatrici, viene per esempio garantita
l'esistenza di un unico oggetto per tutti gli interi compresi tra \(-5\) e \(256\).
Pertanto
a = 256
b = 256
print(a is b)
a = 257
b = 257
print(a is b)
True
False
Regole di precedenza#
Quando un'espressione contiene più operatori, può diventare significativo
l'ordine nel quale le operazioni vengono eseguite. Quando gli operatori
coinvolti sono associativi, come per esempio in a + b + c, il valore
dell'espressione è indipendente da questo ordine, ma in generale questo non è
vero. Considerate per esempio a / b / c: se si esegue la prima divisione e
poi la seconda si ottiene il rapporto tra a e b * c, ma se si inverte
l'ordine si ottiene il rapporto tra a * c e b. Lo stesso vale per la
situazione più generale nella quale l'espressione contiene operatori
differenti. Ogni linguaggio stabilisce dunque delle regole di precedenza che
indicano l'ordine preciso nel quale i vari operatori devono essere valutati. La
7 elenca gli operatori che ho introdotto finora,
ordinati per precedenza decrescente: quando deve essere valutata
un'espressione, si eseguono prima tutti gli elevamenti a potenza, se presenti,
per poi passare agli operatori di segno, sempre se sono presenti, e così via.
Notate che alcuni operatori appaiono su una stessa riga della tabella, a
indicare che hanno la stessa precedenza. Rimane però il problema di stabilire
l'ordine con il quale devono venire applicate più occorrenze di un medesimo
operatore, o operatori diversi che però hanno la stessa precedenza. In questo
caso, la regola è semplice: si considera l'ordine nel quale gli operatori
compaiono nell'espressione, procedendo da sinistra verso destra. Vi è però
un'eccezione: questa regola non vale per l'elevamento a potenza, le cui
occorrenze vengono valutate da destra verso sinistra.
Operatore |
Descrizione |
|---|---|
|
Elevamento a potenza |
|
Segno unario |
|
Moltiplicazione, divisione, divisione troncata, modulo |
|
Addizione e sottrazione |
|
Confronto |
|
Negazione |
|
Congiunzione |
|
Disgiunzione |
Chiaramente, può capitare di voler valutare un'espressione seguendo un ordine che non è quello previsto dalle regole di precedenza. Come negli altri linguaggi, In casi come questi si utilizzano le parentesi tonde per delimitare le parti dell'espressione che vanno valutate per prime. L'uso delle parentesi è comunque consigliato anche quando non strettamente necessario, se questo contribuisce ad aumentare la leggibilità del codice (cosa che tipicamente avviene per le espressioni non particolarmente semplici). Sempre al fine di produrre codice facile da leggere, è consigliabile anche inserire gli spazi tra operandi e operatori in un modo ragionato e coerente. La Style Guide for Python Code (introdotta nel Paragrafo 4.2) contiene un paragrafo dedicato proprio a questo argomento, e io mi atterrò alle indicazioni ivi contenute.
Infine, vale la pena ricordare che la 7 fa riferimento solo agli operatori che abbiamo visto fino a questo momento (per la precisione, mancano quelli che operano componente per componente, ai quali ho solamente accennato). Per una lista completa, come sempre, il punto migliore al quale fare riferimento è la documentazione ufficiale. Notate che questa fonte riporta le parentesi tonde come un particolare operatore che ha precedenza su tutto il resto.
4.3.3. Altri tipi#
Come già accennato, Python supporta un elevato numero di tipi di dati, alcuni automaticamente disponibili e altri importabili da moduli di sistema o di terze parti. In questo paragrafo farò alcuni accenni a dei tipi che si possono facilmente incontrare quando si lavora con questo linguaggio, vuoi perché si riferiscono a concetti utili per l'analisi dei dati, vuoi perché potrebbero essere chiamati in causa da eventuali bug[7] nel software che scrivete.
Numeri complessi#
Python supporta in modo nativo i numeri complessi, implementati come oggetti
della classe complex. I letterali corrispondenti si ottengono in un modo che
ricalca la rappresentazione cartesiana dei numeri complessi: si scrive un
letterale di tipo intero o decimale seguito dal simbolo j (o J), ed
eventualmente si aggiunge (o si sottrae) un altro letterale intero o decimale.
Sono dunque esempi di letterali complessi 3j, 5.J-4 e così via. È
importante notare che deve essere sempre presente un letterale prima del
simbolo, altrimenti quest'ultimo viene scambiato dal parser del linguaggio
come il nome di una variabile. Ciò significa che l'unità
immaginaria[8] va sempre scritta come 1j o 1J.
Gli operatori elencati nella 5 e nella 6 si possono applicare a operandi complessi, eccezion fatta per le operazioni che non sono definite in tal senso (il confronto, la divisione troncata e il modulo).
Non tratterò i numeri complessi nel seguito, ma è utile conoscerli perché alcuni bug non producono errori immediati: invece di sollevare un’eccezione, possono generare numeri complessi che si propagano silenziosamente fino all’output finale. In questi casi è importante saperli riconoscere, per evitare di interpretarli come valori reali. Per esempio, l’operazione di elevamento a potenza con base negativa ed esponente frazionario produce un risultato complesso:
x = (-4) ** 0.5
print(x)
(1.2246467991473532e-16+2j)
Immaginiamo ora un codice più articolato, nel quale la base della
potenza è il valore di una variabile. Se, in certe situazioni, questo valore
dovesse risultare per errore negativo, l'esecuzione non genererebbe errori:
a un primoo sguardo, il risultato potrebbe sembrare valido, ma porterebbe con
sé una parte immaginaria inattesa. Un segnale che permette di accorgersi
di una siffatta situazione è la presenza della lettera j nell’output: se
compare accanto a un numero, significa che una componente immaginaria è entrata
nei calcoli.
L'oggetto speciale None#
Molti linguaggi hanno un costrutto speciale per indicare una sorta di «valore
nullo», come ad esempio la parola chiave null in Java o la costante NULL in
C che rappresentano un riferimento che non è associato ad alcunché. Python
utilizza invece la costante predefinita None, che viene tipicamente usata
nelle situazioni in cui non si può (o non avrebbe senso) fare riferimento a uno
specifico valore. Questo può capitare, per esempio,
quando si inizializza una variabile,
quando si vuole utilizzare un valore «speciale» all'interno di una sequenza, che deve dunque essere diverso da qualsiasi possibile altro valore,
quando una particolare procedura può terminare dando un esito negativo, senza che questo implichi l'emissione di un'eccezione: in questi casi, il risultato della procedura è
Nonequando l'esito è negativo, e un valore specifico in tutti gli altri casi,quando una funzione o un metodo non restituiscono un valore.
Nei primi due casi, None verrà dunque utilizzato all'interno di un
assegnamento o per creare la sequenza, mentre nel terzo sarà necessario
verificare se l'esito della procedura sia stato None oppure no. Questo
aspetto è particolarmente delicato, perché tutti i valori in Python
rappresentano oggetti di una classe, e None non fa eccezione: è un oggetto
della classe NoneType, o, meglio, è l'unico oggetto di questa classe che
può esistere (nel senso che non è nemmeno possibile invocarne il costruttore
per creare altri oggetti[9]). Questa peculiarità permette di valutare
se una variabile, diciamo, a contiene None tramite l'espressione
a is None, che è inerentemente più sicura di a == None[10]. Una
cosa simile non sarebbe possibile, per esempio, con a is 42, perché possono
esistere più oggetti della classe int uguali a 42.
Numeri in base dieci#
Il modulo decimal mette a disposizione la classe Decimal che implementa
numeri decimali memorizzati in base dieci. Questa classe non incorre dunque nei
comportamenti controintuitivi illustrati nel riquadro Stranezze in virgola
mobile. Rappresenta dunque una soluzione efficace quando è
necessario fare riferimento a quantità monetarie, anche perché le espressioni
che coinvolgono oggetti di questa classe venogno valutate preservando tutte le
cifre significative degli operandi. Per esempio, nella cella seguente
from decimal import Decimal
print(Decimal('15.45') + Decimal('0.05'))
15.50
il risultato è stampato indicando comunque due cifre decimali, anche se la seconda è uno zero. La documentazione ufficiale descrive in dettaglio questa classe.
Date e istanti#
Quando si analizzano dei dati, può capitare che alcuni di questi indichino
delle misurazioni temporali, indicate con una precisione più o meno
elevata: un particolare giorno, o uno specifico secondo all'interno
di una giornata, o di un anno, o ancora la durata di un lasso di tempo
espressa in minuti, e così via. Per far fronte a questa situazione, si può
utilizzare il modulo datetime, che mette a disposizione le classi descritte
qui di seguito.
date, i cui oggetti indicano un giorno dell'anno, espressi attraverso le variabili di istanzayear,montheday, va notato come l'informazione contenuta in uno di questi oggetti non possa fare riferimento a uno specifico fuso orario, e assuma il calendario Gregoriano come riferimento, indipendentemente dal fatto che questo fosse in vigore nella data specificata.time, per indicare un istante all'interno di un giorno non specificato, nei termini delle variabili di istanzahour,minute,second,microsecondetzinfo. L'ultima variabile di istanza permette di specificare il fuso orario nel quale l'istante viene indicato.datetime, che combina insieme le informazioni contenute negli oggetti di tipodateetime. La creazione di un oggetto di questa classe richiede di specificare sempre anno, mese e giorno come argomenti al costruttore, e assume l'orario di mezzanotte quando non vengono specificati valori per gli altri argomenti.timedelta, le cui istanze indicano un intervallo di tempo, espresso usando le variaili di istanzaweeks,days,hours,minutes,seconds,microsecondsemilliseconds.
Direttiva |
Significato |
|---|---|
|
Nome del giorno della settimana |
|
Numero del giorno |
|
Nome del mese |
|
Numero del mese |
|
Anno (due cifre) |
|
Anno (quattro cifre) |
|
Ora (per un periodo di 24 ore) |
|
Ora (per un periodo di 12 ore) |
|
AM/PM |
|
Minuti |
|
Secondi |
La creazione degli oggetti di queste classi permette in alcuni casi di non
indicare tutti o alcuni dei valori per le variabili di istanza. In particolare,
per date bisogna invocare il costruttore specificando tutti i valori,
rispettando l'ordine nel quale le variabili di istanza sono state introdotte.
Invece time è più flessibile: seppure anche in questo caso l'ordine degli
argomenti sia fissato, è possibile specificarne solo alcuni, e le rimanenti
variabili di istanza verranno impostate a zero, e lo stesso vale per
timedelta. Infine, per quanto riguarda datetime è sempre necessario
specificare anno, mese e giorno. Per esempio
import datetime
birth = datetime.date(1912, 6, 23)
memorizza in birth l'oggetto che corrisponde alla data di nascita di Alan
Turing, mentre
free = datetime.timedelta(14526)
salva in free l'oggetto che corrisponde a 14526 giorni, che equivale al
periodo che è trascorso tra la nascita di Alan Turing e l'inizio del suo
processo per «indecenza grave» da parte delle autorità britanniche. È
interessante sottolineare che si possono scrivere espressioni matematiche che
hanno oggetti di questo tipo come operandi. Per esempio,
birth + free
datetime.date(1952, 3, 31)
corrisponde alla data dell'udienza di Turing al suo processo. Analogamente, si
può ottenere un oggetto di tipo timedelta sottraendo due date, o sommare tra
loro oggetti di tipo timedelta o ancora moltiplicarli per un numero. Più in
generale, le classi del modulo datetime permettono di eseguire compiti
particolarmente articolati, che non vale la pena di approfondire qui. Anche in
questo caso, la documentazione
ufficiale è un
ottimo punto di partenza per studiare le funzionalità che esse introducono. Mi
limito qui a parlare solamente di come gli oggetti di queste classi si possano
visualizzare con un notevole grado di flessibilità. L'uso di print permette
di stampare date e istanti temporali usando un formato predefinito, che segue
gli standard del mondo anglofono, come si verifica per esempio nella cella
seguente:
print(birth)
1912-06-23
Si vede come l'output ottenuto indichi la sequenza anno, mese e giorno. Il
metodo strftime permette di specificare un formato personalizzato per la
stampa, come nella cella seguente:
print(birth.strftime('%d/%m/%Y'))
23/06/1912
Il funzionamento del metoodo si basa sull'indicazione di una stringa di
formato, che contiene sia caratteri da includere nella stampa (nel nostro
caso, i separatori /), sia direttive che specificano tanto un particolare
componente dell'istante temporale, quanto il modo di stamparlo. La
8 riporta un elenco delle principali
direttive che si possono utilizzare. Si può pertanto verificare che Alan
Turing è nato di domenica:
print(birth.strftime('%A'))
Sunday
Infine, le direttive di strftime si possono utilizzare anche per effettuare
l'operazione inversa: costruire cioè un oggetto della classe datetime
partendo da una stringa che contiene una data espressa in un certo formato,
utilizzando il metodo strptime[11]:
datetime.datetime.strptime('10/05/2024', '%d/%m/%Y')
datetime.datetime(2024, 5, 10, 0, 0)
4.3.4. Esercizi#
Esercizio 20 (•)
La funzione type accetta un riferimento e restituisce il tipo corrispondente.
Invocatela passando le seguenti espressioni come argomento, osservando quello
che viene restituito e analizzando il risultato:
8,0.1,'0.1',True,None,int,print,math(dopo avere importato il modulo omonimo).
Esercizio 21 (•)
Considerate ognuna delle seguenti sequenze di carattere, e, per ognuna, dire se viene interpretata da Python come il letterale di un tipo, specificando in caso affermativo quale sia questo tipo e quale sia il valore ottenuto. Sforzatevi di svolgere questo esercizio ragionando per conto vostro, e valutando le espressioni in Python solo per avere una conferma.
536,000,+5,-5,--5,0_0_0,0____________________0,__0,_,3 5,3_5,0xF,E4,1E4,0X1E4,0X1H4,'0X1H4',0b100_001,1.3,-1.3E3,1.3E-3,1.3E0xAA,True,'True',Tr__ue,inf,nan,j,j-1,3j,1j,1j-1,1j - 1,1912-06-23.
Esercizio 22 (•)
Determinate una coppia di valori float il più possibile vicini tra loro,
ma caratterizzati dal fatto che uno viene stampato da Python usando la
notazione scientifica, mentre l'altro no.
Esercizio 23 (•)
Quali degli operatori introdotti finora sono unari?
Esercizio 24 (••)
Determinate tre espressioni che quando vengono valutate producono
rispettivamente come risultato inf, -inf e nan.
Esercizio 25 (•••)
Verificate che l'assegnamento big = 2 ** 40000 sia eseguibile senza che
venga prodotto alcun errore. Provate poi a valutare la semplice espressione
big, o a stampare il contenuto della variabile e commentate il risultato
ottenuto (suggerimento: rileggete attentamente la nota [2]).
Esercizio 26 (••)
Utilizzando la definizione di divisione troncata, calcolate manualmente il
valore delle espressioni -10 // 3 e 10 // -3. Verificate poi che il
risultato ottenuto sia corretto, valutando le stesse espressioni con Python.
Esercizio 27 (••)
Ripetete l'esercizio precedente, considerando ora le espressioni 10 % 3,
-10 % -3, -10 % 3 e 10 % -3.
Esercizio 28 (•)
Convertite mentalmente, qualora sia possibile, le seguenti espressioni al tipo
int:
6,-6,3.1,3.9,-3.1,-3.9,2e3,2g3,2.432354e3,0x2e3,0x2g3,inf,nan.
Verificate poi che i risultati che avete ottenuto siano in linea con i valori effettivamente convertiti con Python.
Esercizio 29 (•)
Ripetete l'esercizio precedente, convertendo però le espressioni in valori
float.
Esercizio 30 (•)
Verificate come sia possibile usare i costruttori di int e float per
convertire una stringa rispettivamente in un intero o un decimale. Quali
sono i vincoli che la stringa da convertire deve rispettare? Che cosa succede
se questi vincoli non sono rispettati?
Esercizio 31 (••)
È possibile usare il costruttore di bool per convertire una stringa in valore
booleano? (Suggerimento: provate a valutare l'espressione bool('False').)
Esercizio 32 (••)
Sulla base del risultato ottenuto risolvendo l'esercizio precedente, cercate
di spiegare come viene elaborato l'argomento passato al costruttore di bool.
Esercizio 33 (•••)
Ricordando che l'elevamento a potenza con base irrazionale restituisce un
valore float solo quando questa base è positiva, valutate con Python
l'espressione -2.5 ** 3.5. Il risultato dovrebbe sorprendervi: perché? Come
potete spiegare questo comportamento? E come dovete modificare l'espressione
affinché il risultato della valutazione sia coerente con la definizione di
elevamento a potenza?
Esercizio 34 (••)
Scrivete un'espressione che coinvolge un operatore di confronto che è ben
formata ma che genera un'eccezione di tipo TypeError, dovuta al fatto che
gli operandi non sono compatibili con l'operazione calcolata.
Esercizio 35 (••)
Indicate qual è l'ordine nel quale vengono valutati gli operatori in oguna
delle seguenti espressioni, assumendo che di volta in volta a, b e c
siano variabili contenenti valori che hanno senso per i corrispondenti
operandi:
a - b - c,-a - b - c,a ** b ** c,-a ** b,a ** -b,a + b * c,a % b // c,a or b and c,not a != b,a and not b,not a and b,not a is not b.
Esercizio 36 (••)
Implementate le seguenti funzioni, copiandone i contenuti nella cella di un notebook e valutandola.
def true_function():
print('in true_function')
return True
def false_function():
print('in false_function')
return False
Valutate poi ognuna delle seguenti espressioni, e leggendo l'output prodotto indicate in quali casi sia stato sfruttato il meccanismo di corto circuito nella valutazione degli operatori logici.
true_function() and true_function(),true_function() or true_function(),false_function() and false_function(),false_function() or false_function().
Esercizio 37 (••)
Valutate mentalmente il tipo e il valore delle seguenti espressioni, indicando l'ordine nel quale gli operatori vengono valutati e specificando se intervengono delle conversioni implicite tra tipi di dati:
bool(False) is False,bool('False') is False,3 + (4 and 2),'3' + 3,3 + 3.,3 % True,3 % False,'3' and 3,'3' or 3,not 3,3 + 4 < 2 - 8,3 + (4 < 2) - 8,2 ** 2 ** 2,2 ** 2 * 2 ** 2,2 == 3 is int(4.0),type(4.0) is int,1j / 1j.
Valutate poi le stesse espressioni usando Python e verificando la validità dei risultati precedentemente ottenuti.
Esercizio 38 (••)
Dovreste sapere che \(\sqrt{-4} = 2 \mathrm{i}\), ma questo sembrerebbe essere incoerente con il risultato della stessa estrazione di radice fatta in Python:
(-4)**0.5
Descrivete in che cosa consiste questa incorerenza, e cercate di spiegarla.
Esercizio 39
Valutate l'espressione -4**0.5 e spiegate il processo che porta al
corrispondente valore restituito, confrontandolo con l'analogo processo
dell'espressione indicata nell'esercizio precedente.
Esercizio 40 (•)
Implemetate una funzione second_grade_roots(a, b, c) che interpreta i suoi
parametri come i coefficienti \(a\), \(b\) e \(c\) dell'equazione
\(a x^2 + b x + c = 0\) e restituisce le due radici (eventualmente complesse) di
questa equazione. (Suggerimento: l'istruzione return m, n permette di
restituire due valori, aggregandoli in una tupla—un tipo di dati
strutturato che vedremo nel prossimo capitolo.)
Esercizio 41 (•••)
Riconsiderate l'espressione print(Decimal('15.45') + Decimal('0.05')) che
abbiamo valutato nel Paragrafo Numeri in base dieci, e notate come
gli argomenti del costruttore Decimal siano delle stringhe. Se eliminate gli
apici dall'espressione a la rivalutate, otterrete un risultato diverso.
Descrivete questo risultato e indicate perché, a prima vista, può sembrare
inatteso. Trovate poi una motivazione di questo comportamento.
Esercizio 42 (•••)
Come è possibile modificare l'espressione nell'esercizio precedente in modo che non vi compaiano stringhe e che venga valutata nello stesso modo?
Esercizio 43 (••)
Impratichitevi a scrivere e valutare delle espressioni che coinvolgono oggetti
di classi definiti nel modulo datetime. Per ogni operazione che considerate,
verificate quale deve essere la combinazione di operandi affinché non venga
generata un'eccezione. Quando questo accade, provate a giustificare come mai
l'operazione non possa essere eseguita.
Esercizio 44 (•••)
Dopo aver letto la
documentazione del
modulo locale, usate il metodo setlocale per impostare il locale sulla
lingua italiana parlata in Italia (la stringa che descrive questo locale è
'it_IT') e verificate come il comportamento di strftime e strptime viene
influenzato.