Il linguaggio Python: Introduzione

1.0 - Computer: Hardware e software.

Il computer è una macchina costituita da due elementi fondamentali: hardware e software. L’hardware comprende l’insieme degli elementi fisici ed il software l’insieme delle istruzioni capaci di governare tali elementi. Lo analizzeremo in maniera molto schematica: ci proponiamo semplicemente di introdurre un livello minimo di terminologia necessaria ai nostri scopi.

Hardware: componenti fisici di un computer.

Hardware system: un insieme di elementi interconnessi in modo tale da comporre un computer funzionante in grado di eseguire delle istruzioni.

Dal punto di vista funzionale un computer ha la seguente architettura.

funzioni
Figura 0.1

Input: dispositivo hardware in grado di accettare dati che vengono forniti al computer per essere elaborati ( tastiera, mouse, ..).

Output: dispositivo hardware che presenta i dati elaborati dal computer (video, stampante, ..).

CPU (Unità Centrale di Processo): parte del computer che esegue le istruzioni lette dalla memoria primaria.

Dati: informazione che può essere gestita da un computer.

I dati gestibili da un computer sono rappresentati tramite successioni di bit, dove un bit vale 0 o 1; inoltre, 8 bit formano 1 byte; con 1 byte si codificano tutti i caratteri maiuscoli e minuscoli, le cifre, i segni di interpunzione e alcuni comandi (andare a capo, cancellare lo schermo, ..) secondo la codifica ASCII ( per esempio 01000001 corrisponde alla lettera maiuscola A ). Le ultime versioni dei sistemi operativi supportano la codifica Unicode che estende e completa quella ASCII. Lo standard Unicode utilizza 2 byte, 16 bit, per comporre un singolo carattere, per cui è in grado di rappresentare 65536 simboli distinti; in questo modo è possibile utilizzare anche lingue complesse come l'arabo ed il cinese.

La CPU ( Unità Centrale di Processo ) corrisponde in linea di massima ad un microprocessore; esso è costituito dai registri dove si conservano i dati o le istruzioni, dall’Unità Aritmetico-Logica e dall’Unità di Controllo. Il compito della CPU consiste nell’eseguire calcoli e confronti sui dati, controllare il flusso dei dati in ingresso ed in uscita, gestire le varie memorie, etc.

Possiamo schematizzare i compiti della CPU attribuendogli delle sottounità nel modo seguente

Memoria primaria: la parte del computer che contiene i dati che vengono utilizzati durante una sessione di lavoro.

Tale memoria, detta RAM "Random Access Memory", è una memoria volatile, nel senso che conserva i dati soltanto quando il computer è acceso. D’altra parte è qui che si conservano le istruzioni e i dati. Ogni informazione è codificabile in un numero apposito di byte; l’indirizzo del primo di questi byte consente di accedere direttamente all’area di memoria che contiene l’informazione.

Memoria secondaria: la parte del computer che contiene i dati in maniera permanente.

La memoria secondaria non è volatile, nel senso che conserva i dati anche quando il computer è spento: questo tipo di memoria è quella presente nei floppy-disk, negli hard-disk, nei CD-ROM, e così via.

Tutte le operazioni della CPU, siano esse eseguite sulla RAM o sulle porte di I/O, hanno la necessità di "muovere dei dati" da una parte all’altra del computer. Il BUS costituisce il canale di comunicazione attraverso cui "viaggiano i dati". Tutte le attività interne del computer sono scandite dal clock; esso rappresenta, grosso modo, il tempo che intercorre tra una prestazione e l’altra della CPU. Esiste un componente elettronico che scandisce il tempo mandando un segnale a tutti gli altri componenti elettronici che ne hanno bisogno: in questo modo tutti gli eventi della CPU sono scanditi e gli altri componenti si sincronizzano alla CPU.

Un computer può servire più utenti contemporaneamente, sistema multiutente, oppure può eseguire più compiti, sistema multitasking. Il termine contemporaneamente può indurre l’utente a ritenere sia a sua completa disposizione ( nel senso che tutte le sue risorse sono completamente disponibili).
In realtà, in un sistema multiutente, ogni utente gestisce il sistema soltanto per una piccola porzione di tempo; data l’elevata capacità di elaborazione, tale tempo gli sembrerà sufficiente per la completa gestione delle risorse del computer. Il termine time-sharing è una modalità con cui un sistema distribuisce le risorse tra i vari utenti.

Il sistema multitasking consente ad un singolo utente di eseguire più compiti nello stesso momento. Anche in questo caso le parole "nello stesso momento" hanno un significato diverso: un computer manda in stampa un certo documento, la stampante lo conserva nella sua memoria e lo stampa un po’ alla volta mentre attende un input dalla tastiera o dal mouse ed esegue un certo calcolo.

Il computer è in grado di elaborare dei dati eseguendo una sequenza ordinata di comandi ( i programmi ). Esso interpreta ogni singolo comando e lo esegue.

Indichiamo con il termine software l’insieme di tutti i programmi che consentono la gestione del sistema computer.
Con il termine programmazione intendiamo la realizzazione del software necessario ai nostri scopi: possiamo pensare di realizzare un sistema operativo(!), una sua componente, un programma di utilità, un programma che

1.1 - Processi, Algoritmi, Programmi

Il termine "applicazione", app per i tablet, viene riservato a qualcosa che ci consente di risolvere problemi. Così, il software di sistema si occupa dei problemi di comunicazione uomo-computer mentre il software applicativo si occupa di problemi relativi all’utente. Cominciamo col definire cosa intendiamo per problema.

Problema: una questione che richiede una soluzione.

Se abbiamo un problema mettiamo in atto delle azioni per risolverlo. Discutendo in generale di azioni possiamo dare la seguente definizione.

Azione: evento che si compie in un intervallo di tempo finito e che produce un effetto previsto e ben determinato.

Ogni azione modifica lo stato di qualche oggetto e l’effetto dell’azione è riconoscibile appunto dal cambiamento di stato dell’oggetto.
In generale, un’attività complessa non si risolve con una sola azione ma con una sequenza o successione di azioni che si svolgono una dietro l’altra. Definiamo allora

Processo: azione composta in esecuzione.

Istruzione: descrizione di una azione in un linguaggio.

Programma: descrizione di un Processo in un linguaggio.

Blocco: un qualsiasi raggruppamento di istruzioni interne ad un Programma.

Esecutore: entità, umana o non, che esegue le azioni secondo le istruzioni-base e gli algoritmi secondo i programmi.

Nel senso che suggeriamo qui, il concetto di Programma abbraccia un campo di situazioni molto più vasto di dieci righe scritte in C++. Una ricetta di un libro di cucina è un Programma, così come è un Programma il foglietto delle istruzioni per montare un mobile "fai da te".
L’aver spostato su un diverso soggetto il compito di eseguire le azioni fa nascere due ordini distinti di problemi:

  1. chi compie le azioni non comprende le frasi espresse nel nostro linguaggio; oppure
  2. non è in grado di eseguire, almeno così come sono proposte, le azioni che compongono il Processo, indipendentemente dal linguaggio in cui sono espresse.

Prendiamo come esempio il disegno di un quadrato: quello che per noi è scontato non lo è altrettanto per un bambino di tre anni, che non conosce i termini "quadrato", "lato", "angolo retto", e così via e che, molto probabilmente, non ha la manualità necessaria a maneggiare correttamente una squadra ed una matita. Analogamente, una nostra corretta descrizione del processo di disegnare un quadrato può risultare del tutto incomprensibile per un quindicenne australiano che, pur sapendo usare perfettamente matita e squadra (disponendo cioé delle risorse necessarie), non conosca una parola di italiano.
Quando l’esecutore diventa un computer questi due ordini di problemi diventano centrali: dobbiamo sempre tenere presenti le risorse a disposizione del nostro esecutore, altrimenti ci troveremo nella frustrante condizione di non riuscire a fare eseguire il più semplice dei processi. Parallelamente, dobbiamo farci carico del problema di comunicare con un esecutore che non comprende il nostro linguaggio naturale.
Dopo queste premesse, possiamo dire che

Risolvere un Problema vuol dire "identificare un processo alla portata di un esecutore reale che consenta di ottenere almeno una soluzione del problema dato".

Scrivere il programma relativo al problema in esame vorrà dire esprimere il Processo nel linguaggio comprensibile dall’esecutore con cui si lavora.
Questa definizione ci apparirà meno astratta se pensiamo non tanto alla maniera in cui ognuno di noi risolve il problema, quanto al modo in cui lo spieghiamo ad un’altra persona. Nel primo caso, infatti, sottintendiamo a noi stessi molte istruzioni ed operiamo dei salti logici consentiti dal controllo che abbiamo sul processo, essendo noi stessi al contempo programmatori ed esecutori.
Nel secondo caso, invece, ci dobbiamo premurare di fornire all’esecutore un programma che sia alla sua portata sia in termini di linguaggio che in termini di risorse.

Le risorse disponibili dall’esecutore determinano la strategia risolutiva di un problema. L’esecutore conosce le quattro operazioni? Sa confrontare numeri e caratteri? è capace di stampare dei dati?
Riprendendo l’esempio del compito di tracciare un quadrato da parte di un esecutore umano, il Programma dovrà tener conto della sua capacità di comprendere o meno termini come perpendicolare, equidistanza, e così via.

Dopo queste brevi considerazioni, la parola-chiave da analizzare è "linguaggio": l’esecutore è in grado di compiere un’azione se comprende il linguaggio con cui comunicare le istruzioni all’esecutore.
Il problema è più complesso nel caso dell’esecutore automatico (computer o, più genericamente, automa).
Sappiamo, infatti, che il linguaggio naturale impiegato nella comunicazione tra esseri umani trae la propria potenza espressiva da elementi accessori, quali l’intonazione della voce, la mimica del viso e delle mani, il contesto in cui viene effettuata la comunicazione. In questo modo, anche se il messaggio non è completo, oppure non rispetta le regole della sintassi, un ricevente esperto può comunque trarne il significato completo e coerente.
Non bisogna avere una grande esperienza di computer per sapere che ogni elaboratore ha una "sua lingua naturale", basata su un alfabeto composto da due soli simboli, 0 ed 1, da un lessico contenente circa un centinaio di parole e da una sintassi estremamente semplice. Come fare allora per coniugare la ricchezza del linguaggio naturale con l’incomprensibilità del linguaggio della macchina? Dal punto di vista formale il problema non è molto diverso dalla comunicazione tra emittenti umani che parlino solo le rispettive lingue (per esempio italiano ed inglese): è possibile superare l’incomunicabilità servendosi di un terzo soggetto, in grado di comunicare in entrambe le lingue, ossia di quello che comunemente viene chiamato un interprete.

1.2 - Algoritmi

Il concetto di Algoritmo occupa una posizione centrale nell’informatica teorica: l’analisi della sua struttura e delle tecniche di rappresentazione e verifica occuperanno il resto del capitolo.

Un Algoritmo è un programma che viene formulato senza fare riferimento ad un particolare esecutore.

In un certo senso, l’algoritmo è una procedura computazionale comprensibile dall’uomo, mentre un programma è una procedura computazionale comprensibile dalla macchina.

Per comprendere la natura del concetto di algoritmo, calcoliamo il Massimo Comun Divisore di 154 e 84:

L’algoritmo ( intuito da Euclide duemila anni fa ) può essere scritto in questo modo (tenete presente l’esempio appena esposto):

INPUT a,b (cioé, dammi due numeri interi a e b)
Ripeti questo gruppo di istruzioni finché il resto non diventa ZERO
Calcola il resto della divisione tra a e b
Poni b in a ( poni il divisore nel dividendo: a=b )
Poni il resto della divisione in b: b=resto
Quando il resto è ZERO, il MCD è l’ultimo divisore nonnullo, cioé il numero contenuto in a
OUTPUT a (restituiscimi il valore di a che è il MCD)

Notiamo come il linguaggio sia ricco di istruzioni, al di là delle operazioni sui numeri. Per esempio, l’algoritmo ripete più volte lo stesso gruppo di due istruzioni, per cui esistono comandi del tipo
Ripeti questo gruppo di istruzioni finché il resto non diventa ZERO.
Ancora: come possiamo ordinare all’esecutore di eseguire le operazioni soltanto per numeri positivi? Ebbene, possiamo supporre di poter dare la seguente istruzione
Se a e b sono numeri positivi allora esegui le operazioni successive...
Dalle osservazioni precedenti si evince che l’algoritmo è determinato dal modo in cui si esprimono le istruzioni-base e quindi da un opportuno linguaggio.

Secondo Donald E. Knuth, autore di diversi volumi intitolati "The art of computer programming", nel suo primo volume dal titolo "Fundamental Algorithms", il termine algoritmo deriva dal nome del matematico persiano "Al-Khowarizmi", vissuto nel IX secolo d.C.

Nello stesso libro, Knuth associa al termine algoritmo le seguenti proprietà:

  1. Finitezza: un algoritmo deve avere un numero finito di comandi
  2. NON ambiguità: ogni passo di un algoritmo deve essere definito in modo preciso: le azioni devono essere rigorose, chiaramente definite e specificate in tutti i casi possibili
  3. Eseguibilità: un algoritmo deve essere effettivamente eseguibile, cioé tutte le azioni possono essere svolte, senza problemi ed in un tempo finito, da un esecutore opportuno, uomo o macchina
  4. Input: un algoritmo può avere zero o più dati in ingresso
  5. Output: un algoritmo può avere zero o più dati in uscita.

Dalle considerazioni svolte fino a questo momento, possiamo immaginare che un esecutore deve saper gestire queste tre strutture:

  1. Sequenza: un insieme di azioni da eseguire l’una dietro l’altra;
  2. Selezione: un’azione che spezza il flusso sequenziale in più flussi;
  3. Iterazione: un insieme di azioni che vanno ripetute più volte.

Innanzitutto supporremo che il nostro esecutore sappia eseguire senza errori tutte le operazioni e confronti con i numeri. Successivamente introduciamo due linguaggi di rappresentazione degli algoritmi che ci consentiranno di descrivere un semplice algoritmo:
assegnato un numero intero positivo N, stampare tutti i numeri pari multipli di 5 e minori o uguali ad N.

1.3 - Rappresentazione degli algoritmi (flow chart)

I diagrammi di flusso descrivono un algoritmo utilizzando delle figure geometriche convenzionali collegate da frecce.
Le frecce uniscono i vari blocchi dei diagrammi ed indicano l’ordine in cui saranno eseguite le varie istruzioni. Il flusso dei comandi procede dall’alto verso il basso e da sinistra a destra, a meno che le frecce indichino una direzione diversa.

funzioni
Figura 1.1

La figura successiva mostra, in particolare, la struttura di selezione. Essa traduce in maniera schematica la seguente espressione del linguaggio naturale:

SE il numero N è maggiore di zero allora scrivi sul monitor "è positivo" ALTRIMENTI scrivi sul monitor "è negativo o nullo".

funzioni
Figura 1.2

La figura 1.3 illustra, invece, il caso in cui la selezione è unica:
SE si avvera una certa situazione allora si agisce in un certo modo, ALTRIMENTI non si fa nulla. L’esempio in figura afferma:

SE il numero N è uguale a zero allora scrivi sul monitor "è nullo".

funzioni
Figura 1.3

La figura 1.4 illustra, invece, l’iterazione, cioè la ripetizione di un "Blocco" di istruzioni.
L’algoritmo chiede un numero intero N minore di 100 e, successivamente, stampa tutti gli interi compresi tra N e 100.

funzioni
Figura 1.4

L’ultima figura propone l’algoritmo che risolve il problema seguente:
assegnato un numero intero positivo N, stampare tutti i numeri pari multipli di 5 e minori o uguali ad N.

funzioni
Figura 1.5

I diagrammi di flusso diventano rapidamente illeggibili se superano certe dimensioni e sono facilmente esposti ad errori logici. Inoltre, come potete vedere anche dall’esempio proposto, è difficile evidenziare a dovere le strutture ed i blocchi presenti nell’algoritmo.
Dove sono annidate le strutture di selezione ed iterative nell’esercizio proposto?