Previous Up Next

Buy this book at Amazon.com

Chapter 19  Ulteriori strumenti

Uno degli obiettivi di questo libro è di illustrarvi il minimo indispensabile di Python. Quando esistono due modi diversi di fare qualcosa, preferisco sceglierne uno ed evitare di citare l’altro, oppure inserire il secondo all’interno di un esercizio.

Ora vorrei tornare a recuperare alcune chicche che avevo tralasciato. Python è dotato di parecchie funzionalità che non sono indispensabili—potete scrivere del buon codice anche senza usarle—ma che in certi casi vi permettono di scrivere del codice più conciso, leggibile, efficiente, o anche tutte e tre le cose insieme.

19.1  Espressioni condizionali

Abbiamo visto nel Paragrafo 5.4. le istruzioni condizionali, che vengono usate di frequente per scegliere uno tra due valori alternativi, per esempio:

if x > 0:
    y = math.log(x)
else:
    y = float('nan')

Questa istruzione controlla se x è positivo. Se lo è, calcola math.log, altrimenti math.log scatenerebbe un ValueError. Per evitare che il programma si arresti, generiamo un “NaN”, che è un valore a virgola mobile speciale che rappresenta “Not a Number” (Non è un Numero).

Possiamo scrivere questa condizione in modo più conciso utilizzando un’espressione condizionale:

y = math.log(x) if x > 0 else float('nan')

Si può quasi leggere questa riga come fosse: “y diventa log-x se x è maggiore di 0; altrimenti diventa NaN”.

A volte, le funzioni ricorsive possono essere riscritte utilizzando le espressioni condizionali. Prendiamo ad esempio una versione ricorsiva di fattoriale:

def fattoriale(n):
    if n == 0:
        return 1
    else:
        return n * fattoriale(n-1)

Si può riscrivere così:

def fattoriale(n):
    return 1 if n == 0 else n * fattoriale(n-1)

Un altro utilizzo delle espressioni condizionali è la gestione degli argomenti opzionali. Per esempio, ecco il metodo init di GoodKangaroo (vedere Esercizio 2):

    def __init__(self, nome, contenuti=None):
        self.nome = nome
        if contenuti == None:
        contenuti = []
        self.contenuto_tasca = contenuti

Si può riscrivere così:

    def __init__(self, nome, contenuti=None):
        self.nome = nome
        self.contenuto_tasca = [] if contenuti == None else contenuti 

In generale, si può sostituire un’istruzione condizionale con un’espressione condizionale se entrambe le ramificazioni contengono semplici espressioni che vengono o ritornate o assegnate alla stessa variabile.

19.2  List comprehension

Nel Paragrafo 10.7 abbiamo visto gli schemi di mappa e filtro. Per esempio, questa funzione prende una lista di stringhe, mappa il metodo delle stringhe capitalize negli elementi e restituisce una nuova lista di stringhe:

def tutte_maiuscole(t):
    res = []
    for s in t:
        res.append(s.capitalize())
    return res

Si può scrivere in modo più conciso utilizzando una list comprehension:

def tutte_maiuscole(t):
    return [s.capitalize() for s in t]

Gli operatori parentesi quadre indicano che stiamo costruendo una nuova lista. L’espressione all’interno delle parentesi specifica gli elementi della lista, e il costrutto for specifica la sequenza che stiamo attraversando.

La sintassi di una list comprehension è un po’ sgraziata, perché la variabile del ciclo, s in questo esempio, compare nell’espressione prima di ottenerne la definizione.

Si può usare la list comprehension anche per filtrare. Per esempio, questa funzione seleziona solo gli elementi di t che sono composti di lettere maiuscole, e restituisce una nuova lista:

def solo_maiuscole(t):
    res = []
    for s in t:
        if s.isupper():
            res.append(s)
    return res

Riscriviamola usando una list comprehension:

def solo_maiuscole(t):
    return [s for s in t if s.isupper()]

Le list comprehension sono concise e leggibili, almeno per le espressioni semplici. E di solito sono più veloci dei cicli for equivalenti, a volte molto più veloci. Capisco quindi se mi state biasimando per non avervene parlato prima.

La giustificazione è che il debug delle list comprehension è più difficile, perché non potete inserire delle istruzioni di stampa nel ciclo. Vi consiglio di usarle solo se i calcoli sono abbastanza semplici da avere buone probabilità di azzeccarci al primo colpo. Che per un principiante, vuol dire quasi mai.

19.3  Generator expression

Le generator expression assomigliano sintatticamente a delle list comprehension, ma con parentesi tonde anziché quadre:

>>> g = (x**2 for x in range(5))
>>> g
<generator object <genexpr> at 0x7f4c45a786c0>

Il risultato è un oggetto generatore che è in grado di iterare attraverso una sequenza di valori. Ma a differenza di una list comprehension, non calcola i valori tutti in una volta: attende che gli venga chiesto di farlo. Con la funzione predefinita next, si ottiene dal generatore il valore successivo:

>>> next(g)
0
>>> next(g)
1

Arrivati alla fine della sequenza, next solleva un’eccezione StopIteration. Si può anche usare un ciclo for per iterare attraverso i valori:

>>> for val in g:
...     print(val)
4
9
16

L’oggetto generatore mantiene traccia del punto in cui si trova all’interno della sequenza, quindi il ciclo for riprende da dove next era rimasto. Una volta che il generatore è esaurito, continua sollevando delle StopException:

>>> next(g)
StopIteration

Le generator expression vengono usate spesso con funzioni come sum, max, e min:

>>> sum(x**2 for x in range(5))
30

19.4  any e all

Python dispone di una funzione predefinita di nome any, che prende una sequenza di valori booleani e restituisce True se almeno uno dei valori è True. Funziona sulle liste:

>>> any([False, False, True])
True

Ma viene usata spesso con le generator expression:

>>> any(lettera == 't' for lettera in 'monty')
True

Questo esempio non è granché utile, perché fa la stessa cosa dell’operatore in. Ma possiamo usare any per riscrivere alcune delle funzioni di ricerca che avevamo scritto nel Paragrafo 9.3. Per esempio, avremmo potuto scrivere evita così:

def evita(parola, vietate):
    return not any(lettera in vietate for lettera in parola)

La funzione si legge quasi come fosse: “la parola evita le vietate se non c’è alcuna lettera in vietate per ogni lettera in parola.”

L’uso di any con una generator expression è efficiente, perché si ferma immediatamente se trova un valore True, senza dover necessariamente verificare tutta la sequenza.

Python contiene poi un’altra funzione predefinita, all, che restituisce True se ogni elemento di una sequenza è True. Come esercizio, usate all per riscrivere la funzione usa_tutte del Paragrafo 9.3.

19.5  Insiemi (set)

Nel Paragrafo 13.6 avevo utilizzato dei dizionari per trovare le parole che comparivano in un testo, ma non in un elenco di parole. La funzione che avevo scritto prendeva d1, contenente le parole del testo come chiavi, e d2, contenente l’elenco di parole. Essa restituiva un dizionario contenente le chiavi di d1 che non comparivano in d2.

def sottrai(d1, d2):
    res = dict()
    for chiave in d1:
        if chiave not in d2:
            res[chiave] = None
    return res

In tutti questi dizionari, i valori sono None perché non sono necessari e non vengono usati. Ma questo spreca dello spazio di memoria.

Python dispone di un altro tipo predefinito chiamato insieme o set, che si comporta come una raccolta di chiavi di dizionario prive di valori. Aggiungere elementi ad un insieme è rapido, come pure controllare se un elemento appartiene all’insieme. Vengono poi forniti metodi e operatori per eseguire le comuni operazioni sugli insiemi.

Per esempio, la sottrazione di insiemi è disponibile sotto forma di metodo chiamato difference oppure come operatore, -. Possiamo allora riscrivere sottrai in questo modo:

def sottrai(d1, d2):
    return set(d1) - set(d2)

Il risultato è un insieme anziché un dizionario, ma per operazioni come l’iterazione il comportamento è identico.

Alcuni esercizi di questo libro possono essere svolti in modo conciso ed efficiente usando gli insiemi. Per esempio, questa è una soluzione di ha_duplicati, dall’Esercizio 7, che utilizza un dizionario:

def ha_duplicati(t):
    d = {}
    for x in t:
        if x in d:
            return True
        d[x] = True
    return False

Quando un elemento compare per la prima volta, viene aggiunto al dizionario. Se il medesimo elemento ricompare, la funzione restituisce True.

Usando gli insiemi, si può riscrivere la funzione così:

def ha_duplicati(t):
    return len(set(t)) < len(t)

Siccome un elemento può comparire in un insieme solo una volta, se in t esiste qualche elemento che compare più volte, l’insieme risulterà più piccolo di t. Se invece non ci sono duplicati, l’insieme avrà la stessa dimensione di t.

Si possono usare gli insiemi anche per risolvere alcuni esercizi del Capitolo ??. Per esempio, questa è la versione di usa_solo con un ciclo:

def usa_solo(parola, valide):
    for lettera in parola: 
        if lettera not in valide:
            return False
    return True

usa_solo controlla se tutte le lettere in una word sono tra quelle valide. La possiamo riscrivere così:

def usa_solo(parola, valide):
    return set(parola) <= set(valide)

L’operatore <= controlla se un insieme è un sottoinsieme di un altro, compresa la possibilità che siano uguali, il che è vero se tutte le lettere nella parola fanno parte delle valide.

Per esercizio, riscrivete la funzione evita usando gli insiemi.

19.6  Contatori

Un contatore è una specie di insieme, tranne per il fatto che se un elemento compare più di una volta, il contatore prende nota di quante volte compare. Se vi è noto il concetto matematico di multiinsieme, un contatore è un modo immediato per rappresentarlo.

Il contatore è definito all’interno di un modulo standard chiamato collections, quindi dovete innanzitutto importarlo. Potete inizializzare un contatore con una stringa, lista o qualsiasi altro oggetto che sia iterabile:

>>> from collections import Counter
>>> conta = Counter('parrot')
>>> conta
Counter({'r': 2, 't': 1, 'o': 1, 'p': 1, 'a': 1})

I contatori si comportano per molti versi come i dizionari: fanno corrispondere ciascuna chiave al numero di volte in cui essa compare. Come per i dizionari, le chiavi devono essere idonee all’hashing.

A differenza dei dizionari, i contatori non sollevano eccezioni in caso di accesso a un elemento che non esiste; invece, restituiscono 0:

>>> conta['d']
0

Possiamo usare i contatori per riscrivere anagramma dell’Esercizio 6:

def anagramma(parola1, parola2):
    return Counter(parola1) == Counter(parola2)

Se due parole sono anagrammi, contengono le stesse lettere, lo stesso numero di volte: pertanto i loro contatori sono equivalenti.

Anche i contatori sono dotati dei metodi e degli operatori per eseguire le operazioni tipiche degli insiemi, incluse addizione, sottrazione e intersezione. Ed espongono un metodo molto utile, most_common, che restituisce una lista di coppie valore-frequenza, in ordine di frequenza decrescente:

>>> conta = Counter('parrot')
>>> for valore, frequenza in count.most_common(3):
...     print(valore, frequenza)
r 2
p 1
a 1

19.7  defaultdict

Il modulo collections contiene anche defaultdict, che è simile a un dizionario, con la differenza che quando si tenta di accedere ad una chiave inesistente, può generare al volo un nuovo valore.

Nel creare un defaultdict, dovete fornire una funzione che viene usata per creare i nuovi valori. Una funzione usata per creare oggetti viene detta da alcuni factory. Le funzioni predefinite che creano liste, insiemi e altri tipi, possono essere usate come factory:

>>> from collections import defaultdict
>>> d = defaultdict(list)

Notate che l’argomento è list, che è un oggetto classe, non list(), che è una nuova lista. La funzione che avete creato viene chiamata soltanto quando tentate di accedere ad una chiave che non esiste.

>>> t = d['nuova chiave']
>>> t
[]

La nuova lista, che chiamiamo t, viene aggiunta al dizionario. Quindi se modifichiamo t, i cambiamenti compaiono in d:

>>> t.append('nuovo valore')
>>> d
defaultdict(<class 'list'>, {'nuova chiave': ['nuovo valore']})

Se state creando un dizionario di liste, usare defaultdict permette spesso di scrivere codice più semplice. Nella mia risoluzione dell’Esercizio 2, che potete scaricare da http://thinkpython2.com/code/anagram_sets.py, ho creato un dizionario che mappa da una stringa ordinata di lettere nella lista di parole che si possono comporre con quelle lettere. Per esempio, ’opst’ corrisponde alla lista [’opts’, ’post’, ’pots’, ’spot’, ’stop’, ’tops’].

Questo è il codice di partenza:

def tutti_anagrammi(nomefile):
    d = {}
    for riga in open(nomefile):
        parola = riga.strip().lower()
        t = signature(parola)
        if t not in d:
            d[t] = [parola]
        else:
            d[t].append(parola)
    return d

Si può semplificare usando setdefault, che potreste avere usato nell’Esercizio 2:

def tutti_anagrammi(nomefile):
    d = {}
    for riga in open(nomefile):
        parola = riga.strip().lower()
        t = signature(parola)
        d.setdefault(t, []).append(parola)
    return d

Questa soluzione ha il difetto di creare ogni volta una nuova lista, anche se non è necessario. Non è un grande problema se si tratta di liste, ma se la funzione factory è complessa, può diventarlo.

Ma si può evitare il problema e semplificare il codice con un defaultdict:

def tutti_anagrammi(nomefile):
    d = defaultdict(list)
    for riga in open(nomefile):
        parola = riga.strip().lower()
        t = signature(parola)
        d[t].append(parola)
    return d

La mia risoluzione dell’Esercizio 3, scaricabile da http://thinkpython2.com/code/PokerHandSoln.py, usa setdefault nella funzione has_straightflush. Ha il difetto di creare un oggetto Mano ad ogni ripetizione del ciclo, anche se non serve. Come esercizio, riscrivetela usando un defaultdict.

19.8  Tuple con nome (namedtuple)

Molti oggetti semplici sono, fondamentalmente, delle raccolte di valori tra loro correlati. Ad esempio, l’oggetto Punto che abbiamo definito nel Capitolo 15 contiene due numeri, x e y. Nella definizione di una classe come questa, si comincia di solito con un metodo init e un metodo str:

class Punto:

    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __str__(self):
        return '(%g, %g)' % (self.x, self.y)

Qui serve molto codice per trasportare una piccola quantità di informazione. Python dispone di un modo più conciso per dire la stessa cosa:

from collections import namedtuple
Punto = namedtuple('Punto', ['x', 'y'])

Il primo argomento è il nome della classe che volete creare. Il secondo è una lista degli attributi, come stringhe, che l’oggetto Punto deve avere. Il valore di ritorno da namedtuple è un oggetto classe:

>>> Punto
<class '__main__.Punto'>

Punto dispone automaticamente dei metodi __init__ e __str__, pertanto non occorre scriverli.

Per creare un oggetto Punto, usate la classe Punto come fosse una funzione:

>>> p = Punto(1, 2)
>>> p
Punto(x=1, y=2)

Il metodo init assegna gli argomenti agli attributi usando i nomi che avete specificato. Il metodo str mostra una rappresentazione dell’oggetto Punto e dei suoi attributi.

Potete accedere agli elementi della namedtuple usando il loro nome:

>>> p.x, p.y
(1, 2)

Ma potete anche trattare una namedtuple come una tupla, e usare gli indici:

>>> p[0], p[1]
(1, 2)

>>> x, y = p
>>> x, y
(1, 2)

Le tuple con nome offrono un modo rapido per definire delle classi semplici. Per contro, le classi semplici non sempre rimangono tali. Si potrebbe decidere in un secondo tempo di voler aggiungere dei metodi a una tupla con nome. In quel caso, occorrerebbe definire una nuova classe che erediti dalla tupla con nome:

class IperPunto(Punto):
    # aggiungere qui altri metodi

Oppure si può passare ad una definizione di classe tradizionale.

19.9  Raccolta di argomenti con nome

Nel Paragrafo 12.4, avevamo visto come scrivere una funzione che raccoglie in una tupla i suoi argomenti:

def stampatutti(*args):
    print(args)

Questa funzione può essere chiamata con un numero qualunque di argomenti posizionali (cioè, argomenti che non hanno nome):

>>> stampatutti(1, 2.0, '3')
(1, 2.0, '3')

Tuttavia, l’operatore di raccolta * non funziona con gli argomenti con nome:

>>> stampatutti(1, 2.0, terzo='3')
TypeError: stampatutti() got an unexpected keyword argument 'terzo'

Per raccogliere gli argomenti con nome, si usa invece l’operatore **:

def stampatutti(*args, **kwargs):
    print(args, kwargs)

Si può chiamare il parametro di raccolta come si vuole, ma per prassi si usa kwargs. Il risultato è un dizionario che mappa i nomi nei valori:

>>> stampatutti(1, 2.0, terzo='3')
(1, 2.0) {'terzo': '3'}

Se disponete di un dizionario di nomi e valori, potete usare l’operatore di spacchettamento, ** , per chiamare una funzione:

>>> d = dict(x=1, y=2)
>>> Punto(**d)
Punto(x=1, y=2)

Senza l’operatore di spacchettamento, la funzione interpreterebbe d come un singolo argomento posizionale, assegnandolo a x e lamentando l’assenza di qualcosa da assegnare a y:

>>> d = dict(x=1, y=2)
>>> Punto(d)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __new__() missing 1 required positional argument: 'y'

Quando si lavora con funzioni che hanno parecchi parametri, è una buona idea creare e passare loro dei dizionari che contengono le opzioni di uso più frequente.

19.10  Glossario

espressione condizionale:
Un’espressione che contiene uno tra due alternativi valori, a seconda di una data condizione.
list comprehension:
Espressione con un ciclo for tra parentesi quadre che genera una nuova lista.
generator expression:
Espressione con un ciclo for tra parentesi tonde che produce un oggetto generatore.
multiinsieme:
Ente matematico che rappresenta una corrispondenza tra gli elementi di un insieme e il numero di volte in cui compaiono.
factory:
Funzione, di solito passata come argomento, usata per creare oggetti.

19.11  Esercizi

Esercizio 1  

La seguente funzione calcola ricorsivamente il coefficiente binomiale:

def coeff_binomiale(n, k):
    """Calcola il coefficiente binomiale "n sopra k".

    n: numero di prove
    k: numero di successi

    ritorna: int
    """
    if k == 0:
        return 1
    if n == 0:
        return 0

    res = coeff_binomiale(n-1, k) + coeff_binomiale(n-1, k-1)
    return res

Riscrivete il corpo della funzione usando delle espressioni condizionali nidificate.

Nota: questa funzione non è molto efficiente, perché finisce per calcolare continuamente gli stessi valori. Potreste renderla più efficiente con la memoizzazione (vedere Paragrafo 11.6). Riscontrerete però che è difficile farlo, scrivendola con le espressioni condizionali.

Buy this book at Amazon.com

Contribute

If you would like to make a contribution to support my books, you can use the button below. Thank you!
Pay what you want:

Are you using one of our books in a class?

We'd like to know about it. Please consider filling out this short survey.



Previous Up Next