Appunti bignami Python3




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 




 
.-A------.      .-B------.
|        |      |        |
|   48   | ---> |   48   |
|        |      |        |
'--------'      '--------'




      (copia di variabili)
In Python:
-un'assegnazione lega un oggetto a un nome

      ___       ___
     (   )     (   )
     ( A )     ( B )
     (   )     (   )
~~^~~~'¿'~~~^~~~'¿'~~~~^~~
        \       /
         \     /
          \   /
           \ /
          .---. 
         | 48  |
          '---' 

 (alias di nomi, riferimenti/agganci multipli)


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" |
       '---------'


a = 12

      ___        ___
     (   )      (   )
     ( a )      ( b )
     (   )      (   )
~~^~~~'¿'~~~^
~~~~'¿'~~~~^~~
       |          |
       |          |
       |          |
     .---.   .---------.
    | 12  | | "stringa" |
     '---'   '---------'


b = 207

      ___       ___
     (   )     (   )
     ( a )     ( b )
     (   )     (   )
~~^~~~'¿'~~~^~~~'¿'~~~~^~~
       |         |
       |         |
       |         |
     .---.     .---.
    | 12  |   | 207 |
     '---'     '---'

                        o

                    o
          o   o       
             o        o
           o         o
           .---------.
          | "stringa" |
           '---------'



del(a)

            ___
           (   )
           ( b )
           (   )
~~^~~~^~~~~~'¿'~~~~^~~~^~~
             |
             |
             |
           .---.
          | 207 |
           '---'



       o
           o
 
         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 = [34"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ì:

     ___       ___            ___       ___       ___
    (   )     (   )          (   )     (   )     (   )
    ( a )     ( b )          ( c )     ( e )     ( d )
    (   )     (   )          (   )     (   )     (   )
~^~~~'¿'~~~^~~~'¿'~~~~^~~^~~~~'¿'~~~^~~~'¿'~~~^~~~'¿'~~~~^~~~^~~~~^~~~~
      |         |              |          \       /
      |         |              |           \     /
      |         |              |        .-----------.
      |   .-----------.        |       |             |
      |  |             |       |       | {¿:¿,  ¿:¿} |
      |  | [¿,  ¿, ¿]  |       |       |  |/    | |  |
      |  |  |   |   \  |       |        '-/-----|-|-'
      |   '-|---|----\'        |         /|     | |
      |     |   |     \        |        / |     | |
      |     | .---.    \       |       /.---.   .---.
      |     || "f" |    \      |      /| "x" | | 13  |
    .---.   | '---'      \     |     /  '---'   '---'
   | 850 |  |             \    |    /           |
    '---'   |              \   |   /            |
            |           .-------------.       .---.
            |          |               |     |  5  |
          .---.        |  [¿,  ¿,  ¿]  |      '---'
         |  2  |       |   |   |   |   |
          '---'         '--|---|---|--'
                           |   |   |
                         .---. | .---.                         _
                        |  3  ||| "z" |                  _   ><_>
                         '---' | '---'                 ><_>        _
                             .---.                               ><_>
                            |  4  |
                             '---'


         _        _                                               o
        <_><     <_><                                            
            _                                                    o
           <_><                                                   o
                               /(_      _)\                     o
                               \ _/    \_ /                      o
       )                       //        \\
      (                        \\ (@)(@) //                
       )                        \'="=="='/                         (
      (                     ,===/        \===,                      )
    )  )                   ",===\        /===,"                    (
   (  (   )                " ,==='------'===, "                  )  )
    )  ) (               ___"__crab_by_jgs___"___               (  (
   (  (   )  None   "g" /                        \ 365    {1:9}  )  )
-Fin__)__(__(0,)__43___'                          \  "string"  -------_[]_
                                                   '----------'

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 [34"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)


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à:

if condizione1:
    istruzioni
elif condizione2:
    istruzioni
elif condizione3:
    istruzioni
else:
    istruzioni


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:

nome_file = r"C:\nuova\rimandati\totali\utenti.txt"


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):

stringa = "la stringa"
dati_binari = stringa.encode("ascii")
dati_binari = bytes(stringa, "ascii")

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):

stringa = dati_binari.decode("ascii")
stringa = str(dati_binari, "ascii")

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.

Vecchio formato (deprecato):
"Nome: %s  Codice: %d" % ("Pinco", 877905142)

risultato:
Nome: Pinco  Codice: 877905142


Nuovo formato:
"Grandezza: {:s}  Valore: {:6.4e}".format("G", 6.67E-11)

risultato:
Grandezza: G  Valore: 6.6700e-11


Principali opzioni di formattazione:

Per stringhe:
"% lunghezza s"
"{: allineamento lunghezza s}"

Per interi:
"% segno zeri lunghezza conversione"
"{: allineamento segno zeri lunghezza conversione}"

Per float:
"% segno zeri lunghezza . decimali f/e"
"{: allineamento segno zeri lunghezza . decimali f/e}"

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:
testo = open("nome_file.txt", "r").read()
open("nuovo_file.txt", "w").write(
    testo.replace("\n", "\r\n")
)



Scomposizione e composizione stringa:
.split(sep, max)
"sep".join(seq)


Esempio1, Scomporre una stringa URL estraendo le variabili e inserirle in un dizionario:
s = "http://un.sito.com/pagina.php?p=cost&t=elem&v=45"

# splittiamo una volta la stringa sul carattere "?"
a = s.split("?", 1)

print(a)
# otteniamo la lista
# ['http://un.sito.com/pagina.php', 'p=cost&t=elem&v=45']

# splittiamo la sottostringa destra sui caratteri "&"
b = a[1].split("&")

print(b)
# otteniamo la lista
# ['p=cost', 't=elem', 'v=45']

# creiamo una lista di liste con le varie coppie nome, valore
c = [ e.split("=") for e in b ]

print(c)
# otteniamo la lista
# [['p', 'cost'], ['t', 'elem'], ['v', '45']]

# creiamo un dizionario con la dict comprehension di Python3
diz = { m: n  for m, n  in  c }

print(diz)
# otteniamo il dizionario
# {'p': 'cost', 't': 'elem', 'v': '45'}


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:

 s = "jjdoi 78634 dd  732747324"
print(s.find("78", 4, 20))   #  6
print(s.find("78", 9))       #  -1



Inversione ordine caratteri con il sezionamento:
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:

lista_caratteri = list("string")      # ['s', 't', 'r', 'i', 'n', 'g']
dati = bytes((10, 11, 12, 13, 14))
lista_interi = list(dati)             # [10, 11, 12, 13, 14]
lista_range = list(range(10, -1, -2)) # [10, 8, 6, 4, 2, 0]



Liste come tabelle

tabella = [ [0] * colonne  for _ in range(righe) ] ]
tabella[riga][colonna] = 52

è del tutto equivalente a:

tabella = []
for _ in range(righe):
    tabella.append([0] * colonne)


Le list comprehension si possono nidificare:

ESTRAZIONI = 5000
RUOTE = 10
ESTRATTI = 5
storico = [[[0]*ESTRATTI for _ in range(RUOTE)] for _ in range(ESTRAZIONI)]
storico[4000][5][3] = 12

è del tutto equivalente a:

storico = []
for e in range(ESTRAZIONI):
    lista_ruote = []
    for r in range(RUOTE):
        lista_ruote.append([0]*ESTRATTI)
    storico.append(lista_ruote)


I for nelle list comprehension si possono nidificare:

Nel seguente esempio creiamo una lista contenente le sole vocali di una lista di parole
parole = [ "abaco", "Abbiategrasso", "Alberobello" ]
vocali = [ c for parola in parole for c in parola if c.upper() in "AEIOU" ]


è del tutto equivalente a:

parole = [ "abaco", "Abbiategrasso", "Alberobello" ]
vocali = []
for parola in parole:
    for c in parola:
        if c.upper in "AEIOU":
            vocali.append(c)




Liste come strutture fifo (code)

lista = []
lista.append("A")
lista.append("B")
print(lista.pop(0))  # "A"
lista.append("C")
print(lista.pop(0))  # "B"
print(lista.pop(0))  # "C"



Liste come strutture lifo (stack)

lista = []
lista.append("stringa")
lista.append(42)
coda = lista.pop()
print(coda)    # stampa 42
coda = lista.pop()
print(coda)    # stampa stringa



Principali metodi di lista

Concatenazione e ripetizione:
lista1 = [1, 2, 3]
lista2 = [4, 5, 6]

lista1 += lista2        # lista1=[1, 2, 3, 4, 5, 6]
lista1.extend(lista2)   # lista1=[1, 2, 3, 4, 5, 6]

[1, 2] * 3              # [1, 2, 1, 2, 1, 2]



Iterazione e appartenenza:
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:

tupla_caratteri = tuple("string")      # ('s', 't', 'r', 'i', 'n', 'g')
dati = bytes((10, 11, 12, 13, 14))
tupla_interi = tuple(dati)             # (10, 11, 12, 13, 14)
tupla_range = tuple(range(10, -1, -2)) # (10, 8, 6, 4, 2, 0)



DIZIONARI


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).

giorni = {  "lunedi":    "Monday",
            "martedi":   "Tuesday",
            "mercoledi": "Wednesday",
            "giovedi":   "Thursday",
            "venerdi":   "Friday",
            "sabato":    "Saturday",
            "domenica":  "Sunday"     }



Principali operazioni con i dizionari:

Accesso tramite chiave:

print(giorni["martedi"])   # Tuesday


Verifica esistenza chiave (appartenenza):
if "giovedi" in giorni:


Lista delle chiavi:
lista_chiavi = list(giorni.keys())


Lista dei valori:
lista_valori = list(giorni.values())


Lista elementi come coppie (chiave, valore):
lista_elementi = list(giorni.items())


Cerca un elemento (se non c'è ritorna un oggetto default):
elemento = giorni.get("miercoledi", "errore")


Aggiunge e cancella un elemento:
giorni["festivaldi"] = "Hurrahday"
del giorni["festivaldi"]



Lunghezza (numero di elementi):
len(giorni)


Unisce (concatena) due dizionari (modifica sul posto, le chiavi duplicate vengono sovrascritte):
giorni.update(altro_dizionario)


Dict comprehension (nuova in Python3), nel seguente esempio si crea un dizionario iterando su una tupla di tuple:
dati = (  ("lunedi", "Monday"),
          ("martedi", "Tuesday"),
          ("mercoledi", "Wednesday"),
          ("giovedi", "Thursday"),
          ("venerdi", "Friday"),
          ("sabato", "Saturday"),
          ("domenica", "Sunday")   )

dizionario = { m: n  for m, n  in dati }


è del tutto equivalente a:

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.
dati = ((7, "nani"), ["oro", 24], ((437, 850), "CP"))
dizionario = dict(dati)
print(dizionario)  # {'oro': 24, (437, 850): 'CP', 7: 'nani'}


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.

film1 = { "titolo": "Underworld",  
          "regia":  "Len Wiseman",  
          "attori": [  "Kate Beckinsale",
                       "Scott Speedman",
                       "Michael Sheen",
                       "Bill Nighy"    ],
          "codice": 79420  }

for attore in film1["attori"]:
    print(attore)

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

#--------------------------------------
def fa():
    pass
#--------------------------------------
def fb():
    pass
#--------------------------------------
def fc():
    pass
#--------------------------------------

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.

import json
stringa = json.dumps(dizionario)
dizionario = json.loads(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)


tot_d1 = set(os.listdir(d1))
tot_d2 = set(os.listdir(d2))
file_d1 = tot_d1 -
trova_directory(d1, tot_d1)
file_d2 = tot_d2 - trova_directory(d2, tot_d2)
comuni = file_d1.intersection(file_d2)
solo_d1 = file_d1 - file_d2
solo_d2 = file_d2 - file_d1

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.

Alcune conversioni:
codici_stringa = bytearray("stringa", "utf-8") # stringa->bytearray
lista_di_interi = list(codici_stringa)         # bytearray->lista
dati_binari = bytes(lista_di_interi)           # lista->bytes
nuova_stringa = dati_binari.decode("utf-8")    # bytes->stringa
dati_binari = bytes().fromhex("FACC104CAFFE")  # stringa esa->bytes





enumerate   range  zip



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))

Risultato:
Indice:0 Elemento:lunedi
Indice:1 Elemento:martedi
Indice:2 Elemento:mercoledi
Indice:3 Elemento:giovedi


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:

lista1 = [ "lunedi", "martedi" ]
lista2 = [ "Monday", "Tuesday"]
lista_dati = list(zip(lista1, lista2))
print(lista_dati)  #  [('lunedi', 'Monday'), ('martedi', 'Tuesday')]
dict_dati = dict(zip(lista1, lista2))
print(dict_dati)   #  {'lunedi': 'Monday', 'martedi': 'Tuesday'}



FUNZIONI


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.

def funzione(a, b, c):
funzione(10, 20, 30)    # a=10 b=20 c=30


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.
def funzione(*a):
funzione()                # a=()
funzione(10, 20)          # a=(10, 20)
funzione(10, 20, 30)      # a=(10, 20, 30)

La funzione riceve degli argomenti provenienti da uno spacchettamento.
def funzione(a, b, c):
lista = [10, 20, 30]
funzione(*lista)          # a=10 b=20 c=30
funzione(*(10, 20, 30))   # a=10 b=20 c=30

La funzione riceve alcuni argomenti posizionali e i rimanenti arbitrari.
def funzione(a, b, *c):
funzione(50, 60)          # a=50 b=60 c=()
funzione(44, 10, 20, 30)  # a=44 b=10 c=(20, 30)
lista = [10, 20, 30]
funzione(44, *lista)      # a=44 b=10 c=(20, 30)
funzione(*(10, 20, 30))   # a=10 b=20 c=(30,)

La funzione riceve argomenti con keyword arbitrari:
def funzione(a, b, z=999, **c):
funzione(10, 20, s=9)            # a=10 b=20 z=999 c={'s':9}
funzione(50, 60)                 # a=50 b=60 z=999 c={}
funzione(50, 60, s=9, y=4, z=8)  # a=50 b=60 z=8 c={'s':9,'y':4}

La funzione riceve argomenti con e senza keyword totalmente arbitrari:
def funzione(*args, **kwargs):
funzione(10, 20, s=9)                # args=(10, 20) kwargs={'s':9}
funzione(50, 60)                     # args=
(50, 60) kwargs={}
funzione(50, 60, s=9, y=4, z="mQx")  # args=
(50, 60)
                                     # kwargs={'s':9, 'y':4, 'z':'mQx'}



FORMA LAMBDA

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:

def quadrato(x):
    return x * x

lista_quadrati = list(map(quadrato, range(1, 1001)))

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.

def dati(*a):
    for x in a:
        yield(x)   

g1 = dati(12, 45, 89, 0, 33, 127)
g2 = dati("a", "b", "c", "d", "e", "f")

v1 = next(g1)
v2 = next(g1)
v3 = next(g1)
v4 = next(g2)
v5 = next(g2)
v6 = next(g2)

print(v1, v2, v3)  #---> 12 45 89
print(v4, v5, v6)  #---> a b c

v7 = list(zip(g1, g2))
print(v7) #--> [(0, 'd'), (33, 'e'), (127, 'f')]



TEST DI VERITA'


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.


Un'introduzione alla programmazione funzionale:
http://www.python.it/doc/articoli/funct.html


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