<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Elementi di Programmazione
Un linguaggio di programmazione serve sia per istruire una macchina ad eseguire dei conti, che per organizzare le nostre idee su come quei conti devono essere eseguiti. Per questo, nella scelta di un linguaggio di programmazione, dobbiamo tener presente quali sono gli strumenti che vengono offerti dal linguaggio per formare idee più complesse partendo da poche idee semplici.
Ogni linguaggio di programmazione dovrebbe avere almeno tre caratteristiche per ottenere questo obiettivo
Step1:  Semplici espressioni numeriche possono essere combinate usando delle procedure primitive che rappresentano l'applicazione di procedure a quei numeri. Per esempio
Step2:  Si noti come in questo caso, per queste semplici procedure numeriche che corrispondono agli operatori aritmetici, viene implicitamente usata una notazione chiamata postfix. Importando la libreria operator è possibile esprimere le stesse espressioni in notazione prefix
Step3:  Si consiglia di leggere la documentazione della libreria operator sul sito di python. Le funzioni principali che useremo in questo notebook sono
Step4:  Uno dei vantaggi della notazione prefix è che rende sempre chiaro qual è l'operatore/procedura che deve essere svolta, applicandola a quali dati
Step5:  Si noti come l'espressione precedente sarebbe più chiara se scritta come
Step6:  In questo caso abbiamo una variabile, che abbiamo chiamato a, e il cui valore è il numero 13. A questo punto possiamo usare la variabile a come un oggetto di tipo numerico
Step7:  In questo caso, l'interprete del linguaggio, ha prima valutato l'espressione 2*pi*raggio, e dopo ha assegnato il valore ottenuto dalla valutazione dell'espressione alla variabile di nome circonferenza.
In questo caso, l'operatore di assegnamento = rappresenta il più semplice meccanismo di astrazione, perché permette di dare un nome al risultato di operazioni più complesse. 
In pratica, qualsiasi programma viene costruito partendo dalla costruzione, passo passo, di oggetti computazionali via via più complessi.
L'uso di un interprete, che in modo incrementale valute le espressioni che li vengono passate, favorisce la definizione di tante piccole procedure, innestate l'uno nell'altra.
Dovrebbe essere chiaro a questo punto, che l'interprete deve mantenere una sorta di MEMORIA che tiene traccia di tutti gli assegnamenti di nomi a oggetti, chiamato il global environment. Per vedere quali sono i nomi memorizzati in memoria si usa il comando who
Step8:  La valutazione di espressioni composte
Uno degli obiettivo di questo corso è di insegnare a pensare in maniera "algoritmica". Proviamo ad analizzare come l'interprete del linguaggio valuta operazioni composte come quelle viste prima. In pratica, la valutazione di operazioni composte avviene attraverso la procedura seguente
Step9:  Per ottenere un livello di astrazione più alto abbiamo bisogno di un meccanismo (una sintassi del linguaggio) per definire nuove procedure (funzioni). La sintassi è la seguente
Step10:  A questo punto possiamo anche definire nuove procedure in termini della procedura appena definita, definendo per esempio una nuova procedura chiamata SommaDiQuadrati
Step11:  ESEMPIO
<ASSISTANT_TASK:>
Python Code:
345
Explanation: Elementi di Programmazione
Un linguaggio di programmazione serve sia per istruire una macchina ad eseguire dei conti, che per organizzare le nostre idee su come quei conti devono essere eseguiti. Per questo, nella scelta di un linguaggio di programmazione, dobbiamo tener presente quali sono gli strumenti che vengono offerti dal linguaggio per formare idee più complesse partendo da poche idee semplici.
Ogni linguaggio di programmazione dovrebbe avere almeno tre caratteristiche per ottenere questo obiettivo:
Delle espressioni primitive che rappresentano le entità più semplici del linguaggio
Dei metodi per combinare gli elementi primitivi in elementi composti
Dei metodi per astrarre concetti primitivi in modo che gli elementi composti possano essere utilizzati a loro volta come elementi primitivi di entità ancora più complesse
In programmazione abbiamo due tipi di elementi: le PROCEDURE e i DATI.
In modo informale possiamo definire i dati come gli oggetti che vorremmo manipolare, e le procedure come la descrizione delle regole per manipolare i dati. Quindi, per quanto spiegato sopra, un linguaggio dovrebbe avere dei dati primitivi e delle procedure primitive, e dovrebbe avere dei metodi per combinare e astrarre i dati e le procedure.
Dati e procedure numeriche
Iniziamo a vedere qualche semplice iterazione con l'interprete di Python: se digitiamo sulla tastiera una espressione, l'interprete risponde valutando tale espressione. Per esempio se digitiamo il numero 345 e poi premiamo la combinazione di tasti shift + enter, l'interprete valuta l'espressione che abbiamo appena scritto:
End of explanation
339 + 6
345 - 6
2.7 / 12.1
345 - 12/6
Explanation: Semplici espressioni numeriche possono essere combinate usando delle procedure primitive che rappresentano l'applicazione di procedure a quei numeri. Per esempio:
End of explanation
# Importa tutte le procedure (funzioni) definite nel modulo "operator"
from operator import *
Explanation: Si noti come in questo caso, per queste semplici procedure numeriche che corrispondono agli operatori aritmetici, viene implicitamente usata una notazione chiamata postfix. Importando la libreria operator è possibile esprimere le stesse espressioni in notazione prefix:
End of explanation
add(339, 6)
Explanation: Si consiglia di leggere la documentazione della libreria operator sul sito di python. Le funzioni principali che useremo in questo notebook sono:
add(a,b) corrisponde ad a+b
sub(a,b) corrisponde ad a-b
mul(a,b) corrisponde ad a*b
truediv(a,b) corrisponde ad a/b
Per esempio:
End of explanation
sub(345, truediv(12, 6))
mul(add(2,3), (sub(add(2,2), add(3,2))))
Explanation: Uno dei vantaggi della notazione prefix è che rende sempre chiaro qual è l'operatore/procedura che deve essere svolta, applicandola a quali dati: add è il nome dell'operatore, mentre tra parentesi sono definiti i due dati numerici a cui deve essere applicata l'operazione.
End of explanation
a = 13
Explanation: Si noti come l'espressione precedente sarebbe più chiara se scritta come:
mul(
    add(2, 3),
    sub(
        add(2, 2),
        add(3, 2)
    )
)
In questo caso l'interprete lavora in un ciclo chiamato "leggi-valuta-stampa": legge le espressioni composte e le espressioni primitive, le valuta nell'ordine in cui le trova, e stampa alla fine il risultato finale.
Assegnazione di nomi ad oggetti
Un aspetto critico della programmazione è il modo in cui vengono assegnati i nomi agli oggetti computazionali.
Si dice che un nome identifica una variabile il cui valore è l'oggetto a cui viene associato. Per esempio:
End of explanation
3*a
add(a, add(a,a))
pi = 3.14159
raggio = 5
circonferenza = 2*pi*raggio
circonferenza
raggio = 10
circonferenza
Explanation: In questo caso abbiamo una variabile, che abbiamo chiamato a, e il cui valore è il numero 13. A questo punto possiamo usare la variabile a come un oggetto di tipo numerico:
End of explanation
who
Explanation: In questo caso, l'interprete del linguaggio, ha prima valutato l'espressione 2*pi*raggio, e dopo ha assegnato il valore ottenuto dalla valutazione dell'espressione alla variabile di nome circonferenza.
In questo caso, l'operatore di assegnamento = rappresenta il più semplice meccanismo di astrazione, perché permette di dare un nome al risultato di operazioni più complesse. 
In pratica, qualsiasi programma viene costruito partendo dalla costruzione, passo passo, di oggetti computazionali via via più complessi.
L'uso di un interprete, che in modo incrementale valute le espressioni che li vengono passate, favorisce la definizione di tante piccole procedure, innestate l'uno nell'altra.
Dovrebbe essere chiaro a questo punto, che l'interprete deve mantenere una sorta di MEMORIA che tiene traccia di tutti gli assegnamenti di nomi a oggetti, chiamato il global environment. Per vedere quali sono i nomi memorizzati in memoria si usa il comando who:
End of explanation
quadrato = mul(3, 3)
quadrato
power = mul(x,x)
print(power)
x = 7
Explanation: La valutazione di espressioni composte
Uno degli obiettivo di questo corso è di insegnare a pensare in maniera "algoritmica". Proviamo ad analizzare come l'interprete del linguaggio valuta operazioni composte come quelle viste prima. In pratica, la valutazione di operazioni composte avviene attraverso la procedura seguente:
Per valutare un'espressione composta:
1.1 Prima, valuta le sottoespressioni della espressioni composta
1.2 Applica la procedura indicata dalla sottoespressioni più a sinistra (l'operatore), agli argomenti che sono i valori della sottoespressione (gli operandi).
Si noti come questa procedura per valutare un'operazione composta, per prima cosa deve eseguire il processo di valutazione a ogni elemento dell'espressione composta. Quindi la regola di valutazione di un'espressione è intrinsecamente RICORSIVA, ovvero include come uno dei suoi passi la chiamata a se stessa.
NOTA: mostrare alla lavagna un recursion tree con l'espressione precedente.
Definizione di procedure composte
Abbiamo identificato alcuni elementi che devono appartenere ad un linguaggio di programmazione:
I numeri e le operazioni aritmetiche sono dati e procedure primitive (in gergo, vengono chiamate builtin)
L'annidamento di espressioni composte offrono un meccanismo per comporre delle operazioni
Gli assegnamenti di nomi di variabili a valori offrono un livello di astrazione piuttosto limitato
Abbiamo quindi bisogno di un modo per poter definire nuove procedure, in modo che una nuova operazione possa essere definita in termini di composizione di operazioni più semplici.
Consideriamo per esempio una procedura di elevamento al quadrato.
End of explanation
def Quadrato(numero):
    return mul(numero, numero)
Quadrato
who
Quadrato(532)
quadrato(mul(3,2))
Explanation: Per ottenere un livello di astrazione più alto abbiamo bisogno di un meccanismo (una sintassi del linguaggio) per definire nuove procedure (funzioni). La sintassi è la seguente:
def &lt;Nome&gt;(&lt;parametri formali&gt;):
    &lt;corpo della procedura&gt;
Si noti come sia apparsa la prima parole chiave riservata del linguaggio: def. Inoltre, &lt;Nome&gt; è il nome che noi vogliamo associare alla procedura (funzione) che stiamo definendo, e i &lt;parametri formali&gt; (chiamati argomenti della procedura) sono le variabili che non appartengono direttamente al working eniviroment (ovvero alla MEMORIA dell'interprete), ma sono "visibili" solo internamente alla procedura in cui sono definite.
Se torniamo all'esempio delle definizione di una procedura per l'elevamento al quadrato, possiamo scrivere:
End of explanation
def SommaQuadrati(x, y):
    return add(Quadrato(x), Quadrato(y))
SommaQuadrati(4,3)
x
del x
Explanation: A questo punto possiamo anche definire nuove procedure in termini della procedura appena definita, definendo per esempio una nuova procedura chiamata SommaDiQuadrati:
End of explanation
def F(a):
    return SommaQuadrati(add(a, 1), mul(a, 2))
F(5)
Explanation: ESEMPIO: considera la seguente espressione composta:
End of explanation
<END_TASK>