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

\[(0.1)_{10} = (0.000\overline{1100})_2 \enspace,\]

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é

\[(0.2)_{10} = (0.00 \overline{1100})_2 \enspace,\]

mentre

\[(0.3)_{10} = (0.0100 \overline{1100})_2 \enspace.\]

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 float per 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.

Tabella 5 Elenco dei principali operatori per i tipi numerici#

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, -valore viene valutata come l'opposto del contenuto della variabile valore. 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 int a float, nella quale un valore intero diventa l'equivalente decimale con parte frazionaria nulla.

  • La divisione decimale a / b restituisce il valore float, che moltiplicato per il divisore b, fornisce come risultato il dividendo a. È importante notare che questa divisione restituisce sempre un float, anche quando entrambi gli operandi sono interi e a è un multiplo esatto di b.

  • La divisione troncata a // b restituisce il più grande valore intero minore o uguale ad a / 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 // 3 restituisce \(-4\), che è il più grande tra gli interi che sono minori o uguali della precedente frazione. Lo stesso vale per 10 // -3. Il risultato è un intero quando entrambi gli operandi sono interi, altrimenti viene restituito un float. 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\). Se a e b sono entrambi positivi, allora a % b equivale al resto della divisione intera tra a e b, altrimenti il risultato va calcolato con attenzione. Riprendendo l'esempio del punto precedente, -10 % 3 è uguale a \(2\), mentre 10 % -3 è uguale a \(-2\). Anche in questo caso, l'operatore restituisce un intero quando lo sono anche gli operandi coinvolti, altrimenti restituisce un valore float con parte decimale nulla.

  • L’operatore % denota il modulo. In Python, dati due valori a e b, il modulo a % b è definito come il valore r tale che
    [ a = b \cdot q + r, ] dove q è il risultato della divisione troncata a // b. Se a e b sono 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 % 3 vale 2, mentre 10 % -3 vale -2. Anche in questo caso, se gli operandi sono interi il risultato è un intero; altrimenti è un float con 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 float e 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 è float e 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.

Tabella 6 Elenco dei principali operatori logici#

Operazione

Operatore

uguale (per contenuto)

==

uguale (per riferimento)

is

diverso (per contenuto)

!=

diverso (per riferimento)

is not

minore

<

minore o uguale

<=

maggiore

>

maggiore o uguale

>=

congiunzione

and

disgiunzione

or

negazione

not

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.

Tabella 7 Regole di precedenza per gli operatori#

Operatore

Descrizione

**

Elevamento a potenza

+x, -x

Segno unario

*, /, //, %

Moltiplicazione, divisione, divisione troncata, modulo

+, -

Addizione e sottrazione

is, is not, <, <=, >, >=, !=, ==

Confronto

not

Negazione

and

Congiunzione

or

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,

  1. quando si inizializza una variabile,

  2. quando si vuole utilizzare un valore «speciale» all'interno di una sequenza, che deve dunque essere diverso da qualsiasi possibile altro valore,

  3. 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 è None quando l'esito è negativo, e un valore specifico in tutti gli altri casi,

  4. 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 istanza year, month e day, 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 istanza hour, minute, second, microsecond e tzinfo. 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 tipo date e time. 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 istanza weeks, days, hours, minutes, seconds, microseconds e milliseconds.

Tabella 8 Significato delle principali direttive per la formattazione delle date.#

Direttiva

Significato

%A

Nome del giorno della settimana

%d

Numero del giorno

%B

Nome del mese

%m

Numero del mese

%y

Anno (due cifre)

%Y

Anno (quattro cifre)

%H

Ora (per un periodo di 24 ore)

%I

Ora (per un periodo di 12 ore)

%p

AM/PM

%M

Minuti

%S

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.