Introduzione L'inizio di tutto
Nomi e scatole
Le cose con
cui lavorare
Oggetti
Pile incluse, PEP & zen Visibilità Moduli Bignami
Assegnamenti
Numeri
Assegnameno incrementale
Operazioni sui bit
Espressioni condizionali elif, else, continue, pass Stringhe
Sequenze di escape
Stringhe raw
Encoding
Conversioni
Formattazione
Metodi principali
ljust,
center, rjust
lower, upper
startswith, endswith
split, join
find, index
inversione
appartenenza Liste
List comprehension,
costruzione
Liste come tabelle
Liste come strutture fifo, lifo
Metodi principali
extend
index
sort
reverse
del, pop,
append
appartenenza
copia Tuple Dizionari
Accesso per chiave,
appartenenza
Lista delle chiavi, dei
valori, degli elementi
Ricerca elemento get
Aggiunge e cancella elemento
Lunghezza e concatenazione
Dict comprehension,
costruzione
Dizionari come tabelle
Dizionari come strutture
Dizionari come switch
JSON Insiemi bytes e bytearray enumerate, range, zip Funzioni
Passaggio per posizione
Passaggio per keyword
Argomenti arbitrari
Forma lambda
map
Chiamate indirette Generatori Test di verità
and e or ritornano un oggetto
Esecuzione condizionata Programmazione funzionale
filter, reduce
INTRODUZIONE
La prima parte è stata scritta in modo lineare (con una sintassi
elementare simile a tutti i linguaggi) per parlare in modo
informale dei rudimenti della
programmazione e di alcuni concetti di programmazione strutturata, ma
in questo modo si è sorvolato sia su
aspetti
importanti dei linguaggi stessi (come l'ambito di
visibilità
delle variabili o le diverse modalità di passaggio degli argomenti alle
funzioni), sia sulle caratteristiche specifiche di Python, anzi si è scritto davvero "brutto" codice Python.
Anche questa seconda parte non è un corso, ma piuttosto una raccolta
"bignami" di argomenti, che ampliano quanto visto nella prima e
aggiungono quello che non poteva trovarvi posto, con una iniziale
attenzione alla
filosofia ad oggetti di Python che rappresenta forse
la maggiore "stranezza" per chi proviene da un
altro linguaggio, ma anche la sua vera potenza.
Infatti è frequente, per chi comincia a studiare Python, sentirsi dire
"non devi pensare in Pascal" o "non
devi pensare in C", ma per comprendere il perché dobbiamo ripartire
dalla frase: "Alla base della programmazione ci sono le variabili, che sono dei nomi scelti a piacere per riferirvi ai vostri dati".
L'INIZIO DI TUTTO: I NOMI... E LE
SCATOLE
In linguaggi come BASIC, C, Pascal, le variabili si possono immaginare
come delle scatole di capienza e tipo ben
definiti (intero, float, stringa ecc) che contengono i nostri dati da
elaborare. E i dati sono davvero dati
elementari a basso livello, cioè gruppi di byte che rappresentano
valori in
un range delimitato, o i caratteri di una stringa.
In Python invece si lavora ad un livello di astrazione maggiore, ogni
cosa elaborabile è un oggetto dotato di numerosi attributi, non
solo le stringhe e
le liste, ma anche il più semplice numero intero è un oggetto. Le
variabili sono invece
solo dei nomi per riferirsi agli oggetti con cui si lavora. Possono essere
immaginate come delle boe galleggianti su una superficie chiamata
spazio dei nomi (namespace), sotto la quale si trovano gli oggetti
appesi tramite catene (referenze).
Parlando più tecnicamente, tutte le variabili Python sono
l'equivalente delle variabili dinamiche dei linguaggi come C o Pascal, e
i nomi sono in effetti solo puntatori agli oggetti. La gestione dei
puntatori e della memoria sono implicite e invisibili al programmatore,
che può così permettersi di creare e usare qualsiasi tipo
di oggetto nel momento in cui ne ha necessità, senza dover
dichiarare nulla in anticipo e senza doversi occupare di dettagli di
basso livello.
Questa differenza, che potrebbe sembrare puramente descrittiva e
irrilevante (perché non comporta particolari problemi nell'uso elementare delle
istruzioni), produce invece effetti completamente diversi. La differenza
infatti è profonda già nelle cose più semplici come la seguente
espressione:
A =
15 + 33
In BASIC, C, Pascal:
-un'espressione crea dei dati
-un'assegnazione riempie una scatola
(posizione di memoria)
.-A------.
| |
| 48 |
| |
'--------'
Una variabile è una scatola di tipo e dimensioni ben precisi.
(A = 2^50000 impossibile)
In Python:
-un'espressione crea un oggetto
-un'assegnazione lega/aggancia un oggetto a
un nome (puntatore/riferimento)
___
( )
nome ( A )
(
) namespace
~~^~~^~~~~~'¿'~~~^~~~~~^~~
|
|referenza
|
.---.
| 48 | oggetto
'---'
Una variabile è solo un nome, e può referenziare (riferirsi/agganciarsi a)
qualsiasi oggetto.
(A = 2**50000 è possibile)
B = A
In BASIC, C, Pascal:
-un'assegnazione riempie una scatola
Questi due soli casi sono sufficienti per comprendere tutto il
resto. Visto che le variabili sono solo nomi legati/agganciati ad
oggetti, quello
che conta è sapere in ogni momento a quale oggetto si riferiscono, e
che uno stesso oggetto può avere più nomi (o, al limite, anche nessuno).
Assegnare qualcosa ad un nome significa sganciare quello che vi è
attaccato lasciandolo andare, e agganciarci qualcos'altro. Se quel nome
non esisteva già, viene creata la "boa-nome" corrispondente sullo
spazio dei nomi (lo spazio di nomi rappresenta l'unica parte visibile e
accessibile alle "cose sommerse" appese di sotto). Quando un oggetto
non ha più riferimenti/agganci diretti o indiretti con la superficie/namespace, "si
inabissa" scomparendo.
a = "stringa"
___
( )
( a )
(
)
~~^~~^~~~~~'¿'~~~^~~~~~^~~
|
|
|
.---------.
| "stringa" |
'---------'
b = a
___
___
( )
( )
( a ) ( b )
( )
( )
~~^~~~'¿'~~~^~~~'¿'~~~~^~~
\ /
\ /
\ / .---------.
| "stringa" |
'---------'
o
o
/ o
( )
( a )
( )
'¿' o
\
/ o
\ o
.---.
| 12 |
'---'
Un errore frequente, commesso da chi "non pensa in Python", è quello di
credere di aver copiato una variabile in un'altra, modificarne una e
scoprire con disappunto che le modifiche si sono riflesse anche
nell'altra, perché in realtà si è andati ad agire sullo stesso oggetto
comune
ai due nomi.
Ecco un esempio più complesso per vedere ancora meglio questo concetto,
il seguente codice...
a = 850 c = [3, 4, "z"] b = [2, "f", c] e = {"x":c,
5:13} d = e
...produce una serie di nomi ed oggetti che possono essere
rappresentati artisticamente così:
Il granchio rappresenta il garbage collector di Python, che distrugge (cancella
dalla memoria) gli oggetti che cadono sul fondo quando vengono
sganciati da tutti i punti a cui sono attaccati, o quando, dopo essere
stati creati, non vengono agganciati da nessuna parte e semplicemente
"cadono". Questo è il tipico caso del risultato di
un'espressione che non viene assegnato a nessun nome, infatti è
perfettamente lecito scrivere su una riga solamente:
"ca"
+ "sa"
Ma la stringa "casa", risultante dalla concatenazione, viene
persa. Stessa cosa quando si vuole volutamente
ignorare il valore di ritorno di una funzione, basta non assegnarlo a
nessun nome/riferimento.
Si può notare che la lista [3, 4, "z"]
ha tre agganci (riferimenti), e ci si può riferire
ad essa indifferentemente in uno qualsiasi di questi modi:
c b[2] e["x"]
d["x"]
L' oggetto stringa "z" della lista sarebbe raggiungibile in uno
qualsiasi di questi modi:
c[2] b[2][2] e["x"][2]
d["x"][2]
Quindi effettuare una modifica sulla lista scrivendo:
c[0] = 99
E' del tutto equivalente a scrivere:
b[2][0]
= 99 e["x"][0] = 99
d["x"][0] = 99
Inoltre, se adesso effettuassimo un nuovo assegnamento al nome c
(cioè
agganciassimo alla boa c un altro oggetto), la lista resterebbe senza
nomi "diretti", ma sarebbe comunque sempre raggiungibile attraverso i
due riferimenti rimanenti (tanto che la si potrebbe anche
successivamente
riagganciare a c scrivendo c=b[2] o c=e["x"] o c=d["x"]). Solo se
facessimo contemporaneamente delle assegnazioni a c b[2] ed e["x"] (o
d["x"]) la lista resterebbe senza alcun aggancio (diretto o indiretto)
con la superficie e "affonderebbe" assieme tutti gli altri oggetti
appesi solo ad essa.
Riassumendo: con linguaggi tipo C/Pascal lavoriamo con dati contenuti in
scatole (ed eventualmente con variabili dinamiche tramite puntatori),
mentre in Python lavoriamo sempre con oggetti dinamici a cui ci si
può riferire in diversi modi e anche con diversi nomi (senza
bisogno di usare esplicitamente i puntatori).
LE COSE... CON CUI LAVORARE
Nel prospetto seguente ci sono le principali cose/oggetti elaborabili,
a cui
si possono dare uno o piu' nomi, che
possono far parte di un' espressione (non necessariamente matematica), essere passate come
argomenti ad una funzione ed essere restituite come risultato.
Può sembrare strano che una funzione possa essere passata come
argomento ad un' altra funzione (in effetti ad esempio in Pascal è
considerata un' operazione avanzata e si parla di tipi procedurali), in Python è invece un'
operazione naturale, non c'è alcuna differenza tra passare
un intero, un dizionario o una funzione (anche se poi ovviamente c'è
nell'usarli), sono tutti comunque solo oggetti.
TIPI DI OGGETTI
.--- interi
dati elementari -|--- float
|--- stringhe
'--- booleani
.--- liste / tuple
strutture dati --|--- bytearray / bytes
|--- set
'--- dizionari
elementi del .--- funzioni
programma ------|--- classi
|--- moduli
|--- istanze
'--- None
Esempio:
import tkinter, math
a =
10
# intero
b = 3.141
#
float
c = "Sandokan" # stringa
d = False
#
booleano
e = [1, 2, 3] # lista
f = 4, 5,
6
# tupla
g = {7, 8,
9}
# set
h = {10: 200, "F": 600, c: "batbox"} # dizionario
i = bytearray([255, 0, 77]) # bytearray y = bytes([255, 0, 77]) # bytes k = math.sin
# funzione
p =
tkinter.Tk
# classe
m =
math
# modulo
n =
tkinter.Tk()
# istanza di TK
o =
None
# oggetto nullo
Gli oggetti hanno anche altre caratteristiche che li classificano:
.---
sequenza (liste / stringe /
bytearray)
|---
chiamabili (metodi / funzioni /
classi)
Ci sono |--- iterabili
(sequenze / generatori / file)
oggetti ---|--- indicizzabili (sequenze - accesso con
indice)
|---
mappabili (dizionari - accesso con
chiave)
|---
mutabili (liste / dizionari /
bytearray / set)
'---
immutabili (stringhe / numeri / tuple / bytes)
Gli oggetti sequenza sono indicizzabili [i], sezionabili [i:j],
iterabili (for x in).
Gli oggetti chiamabili contengono istruzioni eseguibili e si chiamano
posponendo le parentesi ().
Gli oggetti iterabili (iteratori) possono essere iterati tramite il
ciclo for.
Gli oggetti mutabili sono contenitori di altri oggetti, sono mutabili
perché se ne può mutare il contenuto.
Gli oggetti immutabili possono solo essere sostituiti con un nuovo
oggetto, ad esempio non si può modificare un carattere di una stringa,
ma si può costruire una nuova stringa modificata.
Gli oggetti hanno anche un valore booleano intrinseco di verità o
falsità, per cui ad esempio è possibile scrivere semplicemente if
n al posto di if n != 0:
Sono falsi (False):
i numeri 0 e
0.0
.--- liste /
tuple
le sequenze vuote -----|--- bytearray /
bytes
'---
stringhe
nulle
i dizionari
e set vuoti
l'oggetto None
il valore booleano False
ANCORA OGGETTI
.-> dati interni(variabili di istanza)
OGGETTI -> hanno ATTRIBUTI -|
'-> funzioni interne (METODI)
Un oggetto è un insieme di dati e funzioni per elaborarli uniti
assieme (incapsulamento).
Un oggetto si costruisce da una classe che ne definisce gli
attributi.
Un oggetto è un' istanza della classe, e dalla classe si possono
generare più istanze (istanze multiple).
Con dir(oggetto) si vedono gli attributi.
Con type(oggetto) si vede il tipo di oggetto.
Con help(oggetto) si vede una descrizione sintetica.
Con bool(oggetto) si vede il suo valore booleano.
Gli attributi si indicano con la notazione punto:
oggetto.attributo
Anche gli attributi sono oggetti, è normale trovare una
notazione:
oggetto.attributo.attributo.attributo
In un linguaggio come BASIC, C, Pascal fondamentalmente le
istruzioni servono per operare su dati elementari contenuti in
"scatole". Il tipo delle scatole è rigidamente predefinito e va
dichiarato in anticipo prima dell'esecuzione (tipizzazione statica).
Con le istruzioni si scrivono algoritmi per
manipolare i dati elementari. Si è vicini alla macchina e bisogna
farsi carico di diversi aspetti hardware, come i valori massimi
rappresentabili dalle variabili o l'allocazione/liberazione della
memoria.
In Python invece le istruzioni servono per creare espressioni con
oggetti di qualsiasi tipo senza bisogno di alcuna dichiarazione
anticipata (tipizzazione dinamica).
Con le istruzioni si scrivono/usano oggetti che rappresentano nel modo
più astratto e generale possibile le "cose" con cui vogliamo lavorare.
Normalmente non ci si deve occupare di alcun aspetto hardware, la
rappresentazione interna dei dati e la gestione della memoria sono
invisibili e automatiche. Se serve il numero 2 elevato alla 50mila
basta scrivere 2**50000.
CON LE PILE INCLUSE
Con questa frase si intende scherzosamente dire che Python ha
caratteristiche e funzioni predefinite per praticamente tutte
le necessità. Ad esempio le comode strutture dati lista e dizionario
sono incluse come
primitive, mentre con altri linguaggi ci si deve occupare anche della
loro creazione e gestione, allungando tempi, dimensione del codice e
rendendo facile commettere errori... si dice che i programmatori
Pascal passino una buona parte del tempo a scrivere e riscrivere liste,
quando invece quel
tempo potrebbe essere impiegato ad usarle. Un'altra interpretazione per
"pile incluse" è appunto la presenza di liste che possono funzionare da
pile dati lifo/fifo.
Python permette quindi di trattare subito le cose ad alto livello,
invece di
perdere tempo a descriverle a basso livello, e non richiede di
occuparsi di dettagli hardware come la rappresentazione dei dati o la
gestione della memoria. Tutto questo, assieme alla
sintassi poco "verbosa" e all'indentazione obbligatoria, rende la
scrittura dei programmi più veloce e ordinata.
PEP e Zen
La comunità di Python ci tiene ad associare la parola Python a
programmi di qualità altamente leggibili. Perciò tutti dovrebbero
cercare di aderire sia alle raccomandazioni sullo stile di scrittura
del codice (la famosa PEP8), sia
allo "zen di Python", che dà la massima priorità alla chiarezza e
semplicità.
In particolare ci si aspetta che i nomi abbiano questo stile:
nomi_di_funzioni_e_variabili (minuscolo con underscore)
NomiDiClassi
(Iniziali maiuscole no underscore)
NOMI_DI_COSTANTI (maiuscole con underscore)
_trattino_iniziale (nomi di attributi di classe privati)
Lo zen di Python è invece inserito come "uovo di pasqua" in Python stesso, per
visualizzarlo basta scrivere import this nella console.
LA VISIBILITA'
Ogni linguaggio moderno prevede che i nomi usati nel programma abbiano
degli ambiti di visibilità (scope) ben precisi ("ambito
di visibilità" è spesso tradotto in modo orribile con "scopo").
Tutto ciò che viene definito nel blocco principale del programma è
visibile anche all'interno di una funzione (si dice che ha visibilità
globale), mentre quello che viene definito all'interno di una funzione
(compresi i nomi dei parametri) è locale alla funzione stessa e cessa
di esistere quando la funzione termina.
Questo permette di usare nomi di variabili identici all'interno di
funzioni diverse senza che vi siano "interferenze" tra le funzioni o
effetti collaterali. L'idea è sempre quella di rendere ogni parte del
software contemporaneamente più espressiva e piu' indipendente
possibile da tutto il resto.
Sempre per
evitare effetti
collaterali, un buon programma dovrebbe ridurre al minimo possibile
l'uso di variabili globali, e non usarle per far passare i dati "sotto
il tappeto" (cioè aggirando i vincoli di visibilità) da una funzione
all'altra, per questa cosa si usano i parametri in ingresso e i valori
di ritorno in uscita.
Ecco un frequente errore da principiante, in Python NON si può
modificare una variabile con il
seguente codice:
def mia_funzione(a):
a = a + 1
a = 10
mia_funzione(a)
print(a) # risultato 10
Il nome "a" del parametro della funzione è un nome locale della
funzione stessa, che all'inizio referenzia lo stesso oggetto intero 10
passato come argomento nella chiamata (l'oggetto ha quindi due nomi:
"a" globale e "a" locale).
Nel momento in cui si calcola l'espressione a + 1, "a" locale
referenzia ancora
l'oggetto 10 e viene creato un oggetto 11. A questo punto però
l'assegnazione lega il nome "a" locale al nuovo oggetto 11, mentre il
nome "a" esterno con il relativo oggetto restano invariati. Quando la funzione termina, il nuovo
oggetto 11 e il relativo nome "a" locale vengono cancellati dalla
memoria e la funzione print stampa sempre 10.
Se si vuole invece modificare il valore della variabile "esterna", è
corretto ritornare il nuovo valore/oggetto:
def mia_funzione(a):
return a + 1
a = 10
a = mia_funzione(a)
print(a) # risultato 11
Anche in questo Python differisce da altri linguaggi. Infatti in
C o Pascal gli argomenti si passano o per copia (una copia locale dei
dati), o per riferimento (l'indirizzo fisico dei dati), nel secondo
caso
con un'assegnazione interna alla funzione è sempre possibile modificare
il valore di una
variabile esterna passata per riferimento.
In Python invece gli argomenti sono passati per assegnamento (copie
delle referenze), e i parametri di una funzione possono essere pensati
come variabili locali in ingresso alla funzione stessa. Ogni
assegnamento ad un nome locale lega quel nome ad un nuovo oggetto,
perdendo cosi' il riferimento con l'oggetto precedente.
Tornando a parlare di spazio dei nomi, una funzione ha
uno spazio dei nomi locale (formato dai parametri e dalle altre
variabili defnite internamente) che ha la priorità su quello
globale, cioè
se nella funzione sono definiti dei nomi identici a nomi globali,
quelli locali hanno la priorità e quelli globali non sono
visibili. La
ricerca dei nomi procede sempre dall'interno verso l'esterno.
MODULI
Un programma Python è strutturato in moduli. Ogni modulo è un file di
testo (con nome terminante con .py)
che contiene definizioni di classi, definizioni di funzioni, e
istruzioni isolate che vengono
eseguite immediatamente al
caricamento.
Il modulo principale, cioè il file .py che viene avviato per primo,
ha sempre il nome interno "__main__", mentre tutti gli altri moduli
eventualmente
importati con l'istruzione import hanno il nome uguale al file ma senza
il .py finale. Il nome di ogni modulo è identificato dall'attributo
__name__.
Il blocco principale del programma è rappresentato da istruzioni
scritte fuori da ogni classe o funzione che sono eseguite per prime.
Spesso come unica istruzione del blocco principale si usa
scrivere:
if __name__ == "__main__":
main()
In questo modo la funzione main viene eseguita solamente se il modulo è
avviato come file principale (può essere utile per testarlo), mentre
non viene eseguita se il modulo è
importato da altri moduli per usarne le funzioni/classi.
Dal punto di vista del proprio programma un modulo importato è un
oggetto, e tutte le funzioni / classi / costanti che vi sono contenute
sono attributi/oggetti a cui accedere con la notazione punto.
NON esiste invece l'equivalente di un "include" per includere
direttamente del codice in un punto del programma come se fosse un
testo unico.
BIGNAMI
Assegnamenti
a =
10 #
normale
a = b = c = 10 # destinazioni multiple
a, b = 10, 20 # spacchettamento di tuple
a, b = (10, 20) # spacchettamento di tuple
a, b = [10, 20] # spacchettamento di liste
a, b = b, a # swap tramite
spacchettamento
a = 10, 20, 30 # tupla
a = b, c = 10, 20 # tupla e spacchettamento a = b, c = (10, 20) # tupla e spacchettamento a = b, c = [10, 20] # lista e spacchettamento
(a, b) = 10, 20 # come a, b = 10, 20 [a, b] = 10, 20 # come a, b = 10, 20
Numeri
INTERI: 207
0
-300
0xC9 # in
esadecimale
0b01111110 # in binario
FLOAT: 299792.458
6.67E-11
0.0176
0.0
42. # il
punto indica float
0.
Assegnamento incrementale
Valide per qualsiasi operatore matematico o bitwise
Invece di scrivere a = a + 1
possiamo scrivere a += 1
Operazioni sui bit (operatori
bitwise per interi):
n << 3 scorrimento a sinistra di
3 bit
n >> 4 scorrimento a destra di 4 bit
x & y and tra x e y
x | y or tra x e y
x ^ y xor tra x e y
~x not dei bit di x
Attenzione: le operazioni sui bit lavorano con campi di bit di
lunghezza arbitraria (dai 32 bit in su). Gli shift a sinistra non
producono overflow ma interi sempre più grandi. Per riportare i
risultati in un range noto eseguire una maschera & finale, ad
esempio & 0xFF se vogliamo trattare i risultati come numeri a 8 bit:
a = ~1
print(a)
# -2 complemento a uno
a &= 0xFFFF
print(a)
# 65534
print("{:016b}".format(a)) # 1111111111111110
a &= 0xFF
print(a)
# 254
print("{:08b}".format(a)) # 11111110
a = -1
print(a)
# -1 complemento a due
a &= 0xFFFF
print(a)
# 65535
a &= 0xFF
print(a)
# 255
Anche gli operatori bitwise hanno una piorità di esecuzione, in
particolare il not ~ ha la stessa priorità degli elevamenti a
potenza, mentre gli shift vengono dopo tutte le operazioni aritmetiche,
e & ^ | vengono dopo gli shift.
Espressioni condizionali (ternarie)
Invece di scrivere:
if a == 10:
c = 30
else:
c = a - 5
Si può scrivere:
c = 30 if a == 10 else a - 5
Appartenenza ad intervallo
Invece di scrivere:
if a > 5 and a <=10:
Si può scrivere:
if 5 < a <= 10:
Strutture, elif, continue, else
Per quanto riguarda il controllo di flusso c'è poco da aggiungere, se
non la forma completa di if, while e for, e delle istruzioni break
continue.
La struttura if prevede l'uso di elif (else if) per evitare di dover
nidificare eccessivamente in caso di test su diverse possibilità:
I cicli while e for prevedono l'uso di else che indica le istruzioni da
eseguire se i cicli hanno terminato le iterazioni senza essere
interrotti con break.
while condizione:
istruzioni
else:
istruzioni
for x in z:
istruzioni
else:
istruzioni
L'istruzione continue, posta in un punto qualsiasi del blocco di
istruzioni di un ciclo, fa saltare immediatamente all'esecuzione del
ciclo successivo, mentre break causa l'immediata uscita dal ciclo.
pass
L'istruzione fittizia pass non esegue alcuna funzione, ma è utile in
alcuni casi come "segnaposto" dove è richiesta sintatticamente la
presenza di un'istruzione ma, per ragioni varie, non serve scrivere
alcuna istruzione. Ad esempio nella definizione di una classe o di una
funzione vuota:
class Nuova_classe:
pass
def nuova_funzione():
pass
STRINGHE
""
# stringa vuota
''
# stringa vuota
"stringa"
'stringa'
"stringa con un ' apice"
'stringa con un \' apice'
'stringa con "virgolette"'
"stringa con \"virgolette\""
"""triple virgolette
usate per blocchi di
commenti multiriga"""
'''tripli apici'''
Sequenze di escape
Principali sequenze di escape, servono per indicare caratteri
particolari, ogni sequenza identifica un solo carattere:
SEQUENZA
CARATTERE
\\
\
\'
'
\"
"
\r
ritorno a capo CR (come \x0D)
\n
nuova riga LF (come \x0A)
\t
tabulatore (come \x09)
\x41
A (in codice esadecimale)
\u20AC € (in
codice unicode)
Stringhe RAW
Con il prefisso r le sequenze di escape vengono ignorate, è una cosa
utile ad esempio per scrivere percorsi del filesystem:
Se si vogliono usare lettere accentate e altri caratteri non
ASCII all'interno del programma, il file va salvato come UTF-8 e la
prima riga del programma deve essere:
# -*- coding: utf8 -*-
Encoding e conversioni
Python3 lavora con stringhe di caratteri unicode (i codici dei primi
128
caratteri unicode corrispondono a quelli del codice ASCII).
Per codificare una stringa in un formato binario adatto ad
essere scritto su file, trasmesso su linea seriale o trasferito tramite
socket TCP/IP, si deve specificare la codifica desiderata (se nella
stringa ci sono caratteri non compatibili con l'encoding scelto viene
sollevata un'eccezione UnicodeEncodeError):
Per effettuare l'operazione opposta, e ottenere una stringa unicode
decodificando una sequenza binaria, si deve effettuare un decode (se
nei dati ci sono valori non compatibili con l'encoding scelto viene
sollevata un'eccezione UnicodeDecodeError):
Questa è una delle maggiori differenze rispetto a Python2, in cui
le stringhe sono invece utilizzate contemporaneamente per codificare
sia caratteri che dati binari (similmente ai linguaggi più vecchi e con
molti potenziali problemi).
Personalmente ritengo che l'impostazione di Python3 sia più logica e
ordinata, i caratteri e le stringhe sono oggetti astratti, mentre la
loro codifica in byte con un encoding esplicito è necessaria solo per
il trasferimento "fisico" da/verso dispositivi o da/verso altre
applicazioni che si attendono codifiche ben precise.
In particolare un encoding automatico (utf-8 di default) viene
applicato nella lettura e scrittura di file di testo (per cui si
possono scrivere/leggere direttamente stringhe su/da file aperti in
modalità "r" "a" o "w"), mentre nessun encoding viene applicato a file
aperti in modalità binaria "rb" "wb", che pertanto ritornano e
accettano esclusivamente sequenze binarie (oggetti bytes/bytearray).
Convertire stringhe in valori numerici:
n = int("714")
n = int("-15")
n = int("1100001110", 2) # conversione in base 2
n = int("0b1100001110", 2) # conversione in base 2
n = int("0FA9", 16) # conversione in base 16
n = int("0x0FA9", 16) # conversione in base 16
n = float("0.0176")
n = float("-1.")
Convertire valori numerici in stringhe:
s = str(714)
# "714"
s = str(-15)
# "-15"
s = bin(782)
# "0b1100001110"
s = bin(0b1100001110) # "0b1100001110"
s = "{:010b}".format(782) # "1100001110"
formattazione
s = "%010b" % (782) # "1100001110"
formattazione
s = hex(4009)
# "0xfa9"
s = hex(0x0FA9) #
"0xfa9"
s = "{:04X}".format(4009) # "0FA9" formattazione
s = "%04X" % (4009) # "0FA9"
formattazione
s = str(0.0176) #
"0.0176"
s = str(-1./55) #
"-0.01818181818181818"
Formattazione di stringa.
Le stringhe possono contenere delle parti sostituibili a cui associare
degli elementi tratti da una sequenza e rappresentarli nel formato
desiderato.
Opzioni: allineamento ^ centrato
< sinistra
> destra
segno
+ (abilita visualizzazione +)
zeri
0 (abilita visualizz.zeri non signific.)
lunghezza lunghezza campo compresa la virgola
decimali numero di cifre decimali
conversione d decimale
x esadecimale
b binario (solo per nuovo formato)
f float
e scientifico esponenziale
Tutte le opzioni sono facoltative a parte la conversione nel vecchio
formato.
Esempio:
stringa = "Il numero %d compare %d volt%s" % (n, c, "a" if n==1 else "e") stringa = "Il numero {} compare {} volt{}".format(n, c, "a" if n==1 else "e")
METODI DI STRINGA
Giustificare a sinistra/destra o
centrare una stringa in un campo di lunghezza lun
riempito con il carattere car:
.ljust(lun, car)
.center(lun, car)
.rjust(lun, car)
s = "TITOLO".center(80, "-")
Trasformare l'intera stringa in
minuscolo o maiuscolo:
.lower()
.upper()
s = s.lower()
Controllare se una stringa inizia o
finisce con la stringa s:
.startswith(s)
.endswith(s)
if s.startswith("ZX"):
Sostituzione di ric con sost
nell'intera stringa:
.replace(ric, sost)
s = "beta' dell'oro"
s = s.replace("beta", "eta") # "eta' dell'oro"
Esempio, leggere un file di testo linux e creare un nuovo file con i
fineriga Windows:
Il tutto condensabile nel seguente modo "oneline", che però è da
scoraggiare
perché troppo difficile da leggere, troppa
compattezza produce infatti offuscamento (illeggibilità) del codice:
diz = dict([e.split("=") for e in s.split("?",
1)[1].split("&")])
Esempio2, scomporre una stringa
in gruppi di 4 caratteri e ricomporla
unendo i gruppi con il carattere "-":
s = "stringa di prova per ricomposizione"
lista = [ s[i:i+4] for i in range(0, len(s), 4) ]
s = "-".join(lista)
print(s) # stri-nga -di p-rova- per-
ric-ompo-sizi-one
Esempio3, convertire un intero
in una stringa rappresentandolo in
binario con 32 bit e tenendo conto del segno:
n = -730000
n &= 0xFFFFFFFF
lista = [ chr( 48 + (n >> 31-i & 1)
) for i in range(32) ]
s = "".join(lista)
print(s) #
'11111111111101001101110001110000'
Questo è solo un esempio per il metodo .join, perché naturalmente
è possibile sfruttare direttamente le conversioni in stringa di Python:
s = "{:032b}".format(n & 0xFFFFFFFF)
s = bin(n & 0xFFFFFFFF)[2:].rjust(32,
"0")
Ricerca di una sottostringa:
.find(s)
.index(s)
Ritornano l'indice della sottostringa s (il primo carattere ha indice
0). Se la sottostringa non viene trovata .find ritorna -1 mentre .index
solleva un eccezione ValueError.
Oltre alla stringa da cercare è possibile specificare anche un range di
ricerca inizio, fine all'interno della stringa principale:
stringa = "stringa di prova"
stringa = stringa[::-1] # 'avorp id agnirts'
Appartenenza:
Come le altre sequenze anche le stringhe accettano il test di
appartenenza, di un singolo carattere o di un'intera stringa:
u_nomi = "usignolo upupa ululone urogallo
uistitti uricane"
print("fenicottero" in u_nomi) # False
print("upupa" in u_nomi) #
True
esa_cifre = "0123456789ABCDEF"
s = "0FC778BZA"
for c in s:
if c not in esa_cifre:
print("Errore carattere %s non valido" % (c))
break
else:
print("Tutti i caratteri
sono validi")
Nota: il ramo else non appartiene all' if, ma al ciclo for, e viene
eseguito se il for termina in modo "naturale" le sue iterazioni senza
essere interrotto da un break.
STRUTTURE DATI
Le strutture dati predefinite sono uno dei punti di forza di Python, e
permettono di manipolare gli oggetti in modo molto flessibile.
LISTE
La list comprehension, chiamata anche descrizione di lista, è una
potente forma sintattica che permette di costruire nuove liste
derivandole da qualsiasi oggetto iterabile (come una sequenza, un
generatore o un file di testo) elaborandone o filtrandone ogni elemento:
lista = [ espressione con x for
x in sequenza ]
è del tutto equivalente a:
lista = []
for x in sequenza:
lista.append(espressione con x)
E' possibile aggiungere una condizione:
lista = [ espressione for x in sequenza if
condizione ]
è del tutto equivalente a:
lista = []
for x in sequenza:
if condizione con x:
lista.append(espressione con x)
Esempio, Lista di righe lette da un file di testo e "ripulite" dal
carattere \n
di fine riga:
righe = [ riga.rstrip("\n") for riga in open("nome_file.txt", "r")
]
è del tutto equivalente a:
righe = []
for riga in open("nom_file.txt", "r"):
righe.append(riga.rstrip("\n"))
Le liste si possono costruire a
partire da oggetti iterabili:
for x in lista:
if a in lista:
indice = lista.index(elemento)
Ordinamento:
import random
lista = [random.randint(1, 100) for _ in range(1000)]
lista.sort() # modifica sul
posto
print(lista)
Reverse:
lista = [x for x in range(10)]
lista.reverse() # modifica sul posto
lista = lista[::-1] # crea nuova lista
print(lista) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
Cancellazione elementi:
del lista[indice]
del lista[inizio:fine]
lista.remove[elemento] # elimina la prima occorrenza di elemento
elemento = lista.pop() # toglie dalla fine
elemento = lista.pop(0) # toglie dall'inizio
Inserimento e modifica:
lista.append(x) # aggiunge elemento x alla fine
lista[indice] = elemento
lista[inizio:fine] = elemento
lista.insert(indice, elemento) # se indice negativo parte dal fondo
Copia:
lista2 = lista1[:]
lista2 = list(lista1)
lista2 = [x for x in lista1]
Scrivere lista2 = lista1 è un errore comune, in quanto lista2 sarebbe
solo un secondo nome per lo stesso oggetto referenziato dal nome
lista1. Invece effettuando un sezionamento di tutta la lista, o
creandola attraverso una nuova costruzione, si ottiene
una nuova lista a tutti gli effetti.
Il "problema" delle referenze condivise si può però ripresentare per
gli oggetti mutabili contenuti nella lista. Per fare copie "sicure"
senza aliasing è possibile usare il modulo copy (vale anche
per i dizionari):
import copy
lista2 = copy.deepcopy(lista1)
TUPLE
Le tuple sono oggetti sequenza immutabili, si possono considerare come delle
liste
non modificabili, sono usate fondamentalmente nel passaggio argomenti
alle funzioni e nei valori di ritorno. Hanno gli stessi metodi delle
altre sequenze immutabili, comprese concatenazione, ripetizione, appartenenza.
1,
# tupla di un elemento
"A", "B", "C" # tupla di tre elementi
Per evitare ambiguità nella sintassi è
comune, e in alcuni casi indispensabile, racchiudere le tuple tra
parentesi:
a = funzione(10, 20, 30, 40) # 4 argomenti, non è una tupla! a = funzione((10, 20, 30, 40)) # 1 argomento: tupla di 4 elementi a =
funzione((1,))
# 1 argomento: tupla di 1 elemento a = funzione(((1,),)) # 1 argomento: tupla di tuple
Come le liste, anche le tuple possono essere costruite a partire da
altri oggetti:
I dizionari sono un altro punto di forza. Sono strutture dati come le
liste e le tuple, ma gli elementi sono formati da una coppia di oggetti
chiave:valore.
E' possibile accedere agli elementi solo attraverso la chiave associata
(che deve essere un oggetto immutabile univoco, non sono cioè possibili
due chiavi identiche) e non attraverso indici (gli elementi all'interno
di un dizionario appaiono in ordine sparso).
dizionario = {}
for m, n in dati:
dizionario[m] = n
I dizionari si possono costruire a
partire da oggetti iterabili che forniscono coppie di oggetti:
Una tupla è un oggetto immutabile che può essere usato come chiave.
Dizionari come tabelle
Un dizionario può essere un'alternativa per scrivere una tabella
(attenzione: gli indici non sono indici reali, ma formano una tupla di
interi usata come chiave):
diz = {}
diz[0, 0] = 15
diz[(10, 8)] = 44
Dizionari come strutture
Poiché i valori possono essere qualsiasi oggetto, in un dizionario si
può annidare arbitrariamente qualsiasi struttura, compresi altri
dizionari o liste.
Un dizionario è infatti una
delle possibilità per scrivere strutture dati simili a record o struct
di altri linguaggi, e molti oggetti Python usano un dizionario interno
per contenere le proprie variabili o la propria configurazione.
chiave = "attori"
attore_preferito = film1[chiave][0]
print(attore_preferito) # Kate Beckinsale
Dizionari come switch
Un dizionario può anche contenere una serie di funzioni e
simulare/sostituire la struttura di controllo flusso switch o case di
altri linguaggi, o comunque evitare la scrittura di lunghe strutture if
elif elif...
diz = { 1: fa, 2: fb, 3: fc }
numFunz = 2
diz[numFunz]() # chiama la funzione fb
JSON
E' possibile convertire un dizionario in una stringa di testo e
viceversa usando il modulo json, in questo modo un dizionario può
essere trasmesso in rete o salvato su file sotto forma di semplice
stringa.
E' possibile creare una stringa formattata su più righe con
indentazione in modo che sia più facile da leggere:
stringa = json.dumps(dizionario, indent=4)
INSIEMI
Definizione:
a = set() # insieme vuoto a = set([1, 2, 3]) # costruzione da lista a = { 1, 2, 3 } # costruzione diretta
Operazioni:
a = { 1, 2, 3, 3 } # { 1, 2, 3 } i doppioni sono eliminati
b = { 2, 3, 5, 6 } # { 2, 3, 5, 6 }
a - b # { 1 }
b - a # { 5, 6 }
a.intersection(b) # { 2, 3 }
a.union(b) # { 1, 2, 3, 5, 6 }
len(a) # numero di elementi
for x in a: # iterazione sugli elementi
2 in a # appartenenza
Esempio: date due directory d1 e d2 trovare tutti i file comuni alle
due directory, quelli presenti solo in d1, quelli presenti solo in d2 (ignorare le sottodirectory).
def trova_directory(self, percorso, totale):
lista_directory = []
for nome in totale:
if os.path.isdir(percorso + os.sep + nome):
lista_directory.append(nome)
return set(lista_directory)
Esempio: data una lista x di numeri interi determinare se nella lista ci sono dei doppioni (non interessa quali o quanti):
if len(x) != len(set(x)):
BYTES e BYTEARRAY
Queste strutture dati servono a contenere semplici sequenze di
byte, servono per le operazioni di lettura/scrittura su porta seriale,
socket, file binari. bytearray è mutabile come un lista mentre bytes è immutabile come una
tupla.
Condividono metodi e caratteristiche di ogni sequenza.
enumerate crea un oggetto
iterabile che ad ogni iterazione ritorna una
tupla (indice, elemento). Si usa per fornire contemporaneamente a
indice ed elemento di una sequenza:
lista = [ "lunedi", "martedi", "mercoledi",
"giovedi" ]
for i, e in enumerate(lista):
print("Indice:{:d}Elemento:{:s}".format(i, e))
range crea un oggetto iterabile
che ad ogni iterazione ritorna un
valore:
range(10)
#
da 0 a 9
range(1, 11) # da 1 a 10
range(1, 101, 5) # da 1 a 100 passo 5 (1, 6, 11...)
range(9, -1, -1) # da 9 a 0
range(100, -101, -2) # da 100 a -100 passo -2 (100, 98...)
a = list(range(100)) # lista di interi da 0 a 99
zip crea un oggetto iterabile
che ad ogni iterazione restituisce una
tupla composta dagli elementi estratti in parallelo da due sequenze:
L'istruzione def crea un oggetto funzione e vi assegna un nome.
L'oggetto funzione viene chiamato con la doppia parentesi ().
L'oggetto funzione restituisce sempre un oggetto di ritorno.
Nel caso
non sia specificato nulla tramite return o yield, viene restituito
sempre oggetto nullo (None).
Se
l'oggetto restituito dalla funzione non viene legato ad un riferimento, viene
automaticamente cancellato.
Argomenti e Parametri
Durante una chiamata di una funzione le si possono passare
opzionalmente degli argomenti, che verranno assegnati ai parametri
della funzione esattamente come se fossero variabili (locali) in
ingresso alla funzione.
Passaggio normale per posizione:
Ogni parametro deve ricevere un argomento.
Passaggio per keyword:
Permette di specificare parametri di default, che assumono il valore
indicato nella definizione se nella chiamata non viene passato un
argomento corrispondente a quella posizione o a quel nome.
def funzione(a, b, c=300):
funzione(10, 20) #
a=10 b=20 c=300
funzione(10, 20, 30) # a=10
b=20 c=30
funzione(10, 20, c=30) # a=10
b=20 c=30
funzione(c=30, 10, 20) # ERRORE! Gli argomenti con
# keyword vanno dopo tutti
# gli altri, ma l'ordine non
# e' importante.
Argomenti arbitrari:
La funzione riceve un numero arbitrario di argomenti che verranno uniti
nella tupla a.
La forma lambda è un modo alternatvo per definire (costruire) una
funzione. Le due definizioni
seguenti, e l'uso della funzione risultante, sono del tutto equivalenti:
def quadrato(x):
return x * x
quadrato = lambda x: x * x
In entrambi i casi quadrato è il nome assegnato all'oggetto funzione.
Ma, mentre nel primo caso il nome è sintatticamente obbligatorio, nel
secondo caso un oggetto funzione in forma lambda può essere usato anche
senza assegnargli un nome (le lambda sono perciò anche dette funzioni
anonime).
L'uso della lambda è indispensabile in tutti quei casi in cui
sintatticamente è richiesto un oggetto funzione ma non si vuole
definire una apposita funzione esterna, oppure in tutti quei casi in
cui è necessario chiamare una funzione passandole degli argomenti ma
sintatticamente non si può scrivere la chiamata (pertanto si scrive una
lambda che a sua volta effettua la chiamata).
Vediamo il primo caso che usa map.
map crea un oggetto iterabile che ritorna il risultato di una funzione
a cui viene passato uno alla volta ciascun elemento di una sequenza
(sintatticamente richiede un
oggetto funzione e non un'espressione):
map(funzione, sequenza)
Ad esempio vogliamo una lista contenente tutti i quadrati dei numeri da
1 a 1000:
ma siccome la funzione quadrato può anche essere scritta in forma
lambda:
quadrato = lambda x: x * x
allora può essere scritta tale e quale in modo anonimo (senza nome)
all'interno di map:
lista_quadrati = list(map(lambda
x: x * x, range(1, 1001)))
Chiamate indirette
Visto che le funzioni sono oggetti (il cui nome viene stabilito con
l'istruzione def, o con un'eventuale assegnazione nel caso di forma
lambda), possono essere passate come argomenti per effettuare
successivamente delle "chiamate indirette":
def quadrato(x):
return x * x
def stampa(f, n):
print(f(n))
stampa(quadrato, 15)
Stessa cosa passando la funzione scritta direttamente in forma lambda
anonima come argomento:
def stampa(f, n):
print(f(n))
stampa(lambda x: x * x, 15)
Bisogna
sempre ricordare che la forma lambda non calcola direttamente
un'espressione, ma e' un'espressione che ritorna un oggetto
funzione
(che può a sua volta essere chiamato per calcolare un'espressione):
somma = lambda x, y: x + y
n = somma(10, 20) #--->30
altro = lambda : somma
n = altro()(10, 20) #--->30
n = (lambda : lambda x, y: x + y)()(10, 20) #--->30
Riepilogando, data questa funzione:
def fun():
print("44 gatti")
...tutte queste chiamate producono gatti:
...chiamata diretta o con alias:
fun()
a = fun
a()
...chiama una funzione che chiama una funzione:
a = lambda : fun()
a()
def a(): return fun()
a()
(lambda : fun())()
...chiama una funzione che restituisce una funzione:
a = lambda : fun
a()()
def a(): return fun
a()()
(lambda : fun)()()
GENERATORI
Le funzioni che contengono yield al posto di return servono per creare
oggetti generatori. Un generatore ad ogni chiamata restituisce un
valore/oggetto costruendolo a partire da una sequenza arbitrariamente
complessa di operazioni, senza il vincolo di dover calcolare tutti i
valori in una sola volta. Un oggetto generatore è naturalmente
iterabile con for.
def a(k):
for n in range(k):
yield(n)
generatore = a(19)
for x in generatore:
print(x) #---> stampa i numeri da 0 a 9
Nell'esempio seguente si vede come da una funzione si possono ricavare
più generatori indipendenti.
I singoli valori possono anche essere ottenuti uno dopo l'altro con next(generatore).
Se si tenta di acquisire un ulteriore valore quando sono già stati
"consumati" tutti, viene sollevata l'eccezione StopIteration.
Un confronto tramite operatori relazionali (come a > 10)
restituisce un valore (oggetto) booleano True o False.
Un test if su un oggetto considera il valore booleano
corrispondente a bool(oggetto).
L'operatore logico not inverte il valore booleano dell'oggetto
che lo segue.
Un'espressione logica formata da operatori logici and or
non calcola un valore booleano, ma ritorna un oggetto di cui
eventualmente si può testare il valore booleano tramite if.
and e or ritornano
un oggetto:
ogg1 and ogg2
equivale a:
ogg2 if bool(ogg1)
else ogg1
mentre
ogg1 or ogg2
equivale a:
ogg1 if bool(ogg1)
else ogg2
Il test
if ogg1 and ogg2:
equivale a:
if bool( ogg2 if
bool(ogg1) else ogg1 ):
Normalmente un'espressione logica può essere scritta e usata come in
qualsiasi linguaggio, in quanto questo meccanismo di scelta degli
oggetti e test sul loro valore booleano è implicito e automatico, i
risultati voluti sono cioè quelli che ci si aspetta.
Esecuzione condizionata di funzioni
abilitata and funzione()
il flusso equivale a:
if abilitata:
funzione()
Il seguente codice mostra come si possono eseguire funzioni a
catena condizionate, l'ultima viene eseguita solamente se tutte le
precedenti ritornano un oggetto vero, la prima che ritorna un oggetto
falso interrompe la sequenza:
fn1() and fn2() and fn3() and fn4()
il flusso equivale a:
if fn1():
if fn2():
if fn3():
fn4()
La torre di Hanoi
Il seguente programma simula con tre
liste (usate come stack lifo) gli
spostamenti dei dischi di una torre
di Hanoi. Le tre liste corrispondono ai pioli A B e C, e i
numeri, che vengono spostati da una lista all'altra, sono i dischi di
diversa dimensione. La funzione ricorsiva hanoi termina quando tutti i
dischi sono passati dal piolo A al piolo C. Ad ogni mossa vengono
stampati i pioli con i dischi in modo semigrafico tramite caratteri
ASCII. Il programma utilizza alcuni "trucchetti" permessi dai test di verità di Python:
#------------------------------------------- # Torre di Hanoi - by C.Fin
2012 #-------------------------------------------
def fs(x):
return ("="*(4*x-1) if x else "|").center(17, " ")
#-------------------------------------------
def stampa():
print("\n\n\n
|
|
|")
for i in range(3, -1,
-1): # i = indice liste pioli
na = (pA[i:i+1] or
[0])[0] # disco (0 se nessun disco)
nb = (pB[i:i+1] or [0])[0]
nc = (pC[i:i+1] or [0])[0]
print( " "
+ fs(na) + fs(nb) + fs(nc) )
print("
+-----------------------------------------------------+")
print("
|
pA
pB
pC |")
print("
+-----------------------------------------------------+")
#-------------------------------------------
def hanoi(n, sorg, app, dest):
n > 1 and hanoi(n-1, sorg, dest, app)
dest.append(sorg.pop())
stampa()
n > 1 and hanoi(n-1, app, sorg, dest)
#-------------------------------------------
pA = [4, 3, 2, 1]
pB = []
pC = []
stampa()
hanoi(len(pA), pA, pB, pC)
In particolare potrebbero essere da spiegare le righe seguenti:
na = (pA[i:i+1] or [0])[0] # disco (0
se nessun disco) nb = (pB[i:i+1] or [0])[0]
nc = (pC[i:i+1] or [0])[0]
pA[i:i+1]
Questa espressione ritorna una nuova lista con una "fetta" della lista
pA (un solo elemento dall'indice i), se quell'elemento non esiste viene
ritornata lista vuota.
pA[i:i+1] or [0]
Questa espressione ritorna il primo oggetto True tra i due. Nel caso in
cui il primo oggetto sia una lista vuota allora viene ritornato il
secondo, cioè una lista contenente 0. Tutto questo per avere comunque
sempre di ritorno una lista con almeno un elemento.
na = (pA[i:i+1] or [0])[0] Ad na viene assegnato l'elemento di indice 0 della lista
ritornata dall'espressione logica, quindi 0 nel caso fosse ritornata la
lista [0], oppure un altro valore (in questo caso da 1 a 4) se il
risultato di pA[i:i+1] è una lista contenente qualcosa.
Se non ci fosse stata questa possibilità avremmo dovuto scrivere
qualcosa del genere:
t = pA[i:i+1]
na = 0 if len(t) == 0 else t[0] t = pB[i:i+1]
nb = 0 if len(t) == 0 else t[0] t = pC[i:i+1]
nc = 0 if len(t) == 0 else t[0]
Gli altri "trucchetti" sono il test if x (che risulta vero se x diverso da 0) e le chiamate ricorsive ad hanoi condizionate con gli operatori and.
PROGRAMMAZIONE FUNZIONALE
La programmazione funzionale prevede di strutturare il programma sotto
forma di funzioni che calcolano espressioni piuttosto che di algoritmi
procedurali. Python non è un linguaggio funzionale, tuttavia le sue
caratteristiche permettono di scrivere le operazioni in stile
funzionale, soprattutto quelle che coinvolgono l'applicazione
(mappatura) di funzioni agli elementi di una sequenza e la raccolta dei
risultati in nuove sequenze.
List comprehension, assieme ad espressioni condizionali, forme lambda e
funzioni map, zip, filter, reduce, sono gli strumenti principali
che Python mette a disposizione per scrivere in stile funzionale.
Gli esempi della pagina indicata sono scritti in Python2, tuttavia i
concetti sono identici. Quello che cambia a livello
sintattico/semantico è che, mentre in Python3 map zip e filter
creano un oggetto iterabile (iteratore), in Python2 creano direttamente
una lista con i risultati.
In sostanza map(f, seq) in Python2 corrisponde a list(map(f, seq)) in Python3
filter
filter(funzione, sequenza)
Filtra gli elementi di una sequenza, passandoli ad una funzione che
restituisce True o False, "tenendo" solo quelli che hanno dato
risultato True.
Ad esempio data una sequenza di 5000 interi casuali tra 1 e 1000, estrarre tutti quelli compresi tra 100 e 200:
import random
lista = [random.randint(1, 1001) for _ in range(5000)]
lista_filtrata = list(filter(lambda x: 100 <= x <= 200, lista))
reduce (in Python3 è una funzione contenuta nel modulo functools)
reduce(funzione, seq)
Prende il primo elemento della sequenza e lo assegna ad una
variabile interna di "accumulo", chiamiamola a. Prende in sequenza gli
altri elementi di seq e li passa assieme ad a
alla funzione, il valore di ritorno della funzione diventa ogni volta
il nuovo
valore di a. Alla fine reduce restituisce il valore finale di a.
Esempio: vogliamo calcolare lo xor tra tutti gli interi contenuti in una lista:
from functools import reduce
lista = [100, 88, 92, 255, 4, 0, 137] risultato = reduce(lambda a, n: a ^ n, lista)
print(risultato) # -> 18
E' equivalente a:
lista = [100, 88, 92, 255, 4, 0, 137]
def f(a, n):
return a ^ n
a = lista[0]
for n in lista[1:]:
a = f(a, n) print(a) #-> 18
Le funzioni si possono nidificare, e come parametri e valori restituiti
è ovviamente possibile passare qualsiasi cosa, il seguente esempio
stampa due valori, il primo è la somma di tutti i numeri pari minori di
500 estratti da una lista di interi casuali da 1 a 1000, il secondo di
quelli dispari (sempre minori di 500):
import random
from functools import reduce
lista = [random.randint(1, 1000) for _ in range(5000)]
def f(a, n):
a[n & 1] += n
return a
r = reduce(f, filter(lambda n: n < 500, lista), [0, 0])
print(r[0], r[1])
In questo esempio reduce accetta un terzo argomento (una lista)
chiamato initializer, che viene assegnato all'accumulatore interno al
posto del primo elemento della sequenza. La sequenza su cui reduce
itera è il risultato di filter sulla lista di numeri casuali. La
funzione f restituisce sempre la lista che le viene passata dopo averne
modificato uno dei due valori.
E' equivalente a:
import random
lista = [random.randint(1, 1000) for _ in range(5000)]
def f(a, n):
a[n & 1] += n
return a
a = [0, 0]
for n in lista:
if n < 500:
a = f(a, n)
print(a[0], a[1])
Pagina creata 7/7/2012 - Ultimo
aggiornamento 10/11/2012