Appunti

Linguaggio

Esistono due tipi di linguaggio, e sono:

Linguaggi formali Composto da frasi che possono essere classificate come “corrette” da un sistema logico/matematico/algoritmico.

Linguaggi naturali Evoluto nel tempo naturalmente, e quindi senza una struttura sintattica imposta.

Lessico

Vocabolario. Stabilisce quali sono le parole della lingua.

Alfabeto

Pezzo su cui si costruisce tutto il linguaggio. È un insieme di simboli che, appropriatamente concatenati, formano le parole del linguaggio, e quindi il lessico.

Esempio: binario

Codice ASCII (da aggiungere il set UNICODE)

Sintassi

Frasi. Fornisce le regole in base alle quali sequenze si parole formano espressioni legali (frasi).

Le regole sintattiche caratterizzano entrambi i tipi di linguaggi.

Una fondamentale differenza è la presenza di costruzioni ambigue nei linguaggi naturali.

Semantica

Significato. Insieme di regole che permettono di dare un significato una frase legale del linguaggio. Nel linguaggio formale, situazioni imbarazzanti nel linguaggio naturale si trasformano in situazioni di errore.

Nei linguaggi formali, a differenza di quelli naturali, è sempre possibile dire se una parola fa o meno parte del lessico usando regole formali.

Metalinguaggi

Sistema linguistico usato per descrivere un altro linguaggio, chiamato linguaggio oggetto.

while (<espressione>)
	<istruzione>

<espressione> e <istruzione> sono metasimboli.

Compilatori e interpreti

Permettono di eseguire il codice di alto livello.

Implementazione di un linguaggio Realizzare compilatore e interprete per questo nuovo linguaggio.

Compilatore

Programma che converte il codice da un linguaggio ad un altro. Può essere visto come un traduttore di codice. Il linguaggio di partenza è un linguaggio di alto livello, che traduce in un linguaggio intermedio, che poi viene a sua volta tradotto in linguaggio macchina.

Linguaggio di alto livello → Programma sorgente

Linguaggio intermedio

Linguaggio macchina Codice oggetto

Il compilatore è implementato da due componenti, ovvero:

Front end Specializzato nell’analisi del linguaggio sorgente

Back end Specializzato nel linguaggio e nell’hardware della macchina

Le due componenti lavorano in modo indipendente. Il front end genera un codice intermedio (vicino alla macchina ma non hardware dependant) che verrà processato dal back end.

Questa architettura a due livelli è vantaggiosa, dal momento che rende modularizzabile il processo, ed evita la creazione di molteplici compilatori monolitici (uno per ogni macchina e linguaggio, ovvero macchine * linguaggi)

Interprete

Combinazione del front end del compilatore e di un esecutore di codice intermedio.

Compilatore e interprete condividono la parte di analisi del programma nel linguaggio di alto livello.

Per il codice intermedio serve del software apposito, che solitamente viene considerato l’interprete vero e proprio.

Front end

Analisi lessicale e sintattica

Per l’analisi lessicale si utilizzano espressioni regolari. Gli analizzatori lessicali sono detti anche scanner.

La correttezza sintattica è tipicamente stabilita usando grammatiche libere da contesto. Analizzatori sintattici conosciuti anche come parser.

Analisi semantica

Durante la compilazione, l’analisi semantica è effettuata come un’analisi sintattica con tecniche differenti, rispetto a quelle dell’analisi sintattica di cui sopra.

Esempio:

  • controllo su tipi di dato
  • controllo su dichiarazione di variabili
  • controllo su numero di argomenti

Il codice intermedio viene interpretato in questa maniera

  1. Codice sorgente (Python)  → AST
  2. AST → Control Flow Graph (serializzazione di AST)
  3. Control Flow Graph → bytecode
  4. bytecode → bytecode ottimizzato

Viene spesso utilizzato bytecode, che viene fatto girare in un ambiente virtuale che riesce a interpretarlo.

Esiste anche la compilazione JIT (Just In Time), dove la rappresentazione intermedia (AST o bytecode) viene compilata a tempo di escuzione in codice macchina, che naturalmente poi viene eseguito dalla CPU. Questo incrementa il tempo di esecuzione, ma generalmente è più performante in sede di esecuzione.

Linguaggi compilati e interpretati

In teoria tutti i linguaggi possono essere entrambi, ma solitamente tutte le implementazioni di un linguaggio sono dello stesso tipo.

Linguaggio interpretato

Vantaggi

  • portabilità
  • flessibilità (capacità di adattarsi a condizioni di utilizzo differenti)
  • supporto per debugging
  • dynamic type-check

Svantaggi

  • peggiore performance
  • maggiore uso di memoria
  • necessità dell’interprete dove viene eseguito il programma

Linguaggi dinamici

Linguaggio interpretato in cui le operazioni eseguite a tempo di esecuzione non sono legate esclusivamente all’esecuzione di codice.

Gran parte delle operazioni eseguite non corrispondono al codice scritto dal programmatore, ad esempio:

  • controllo del tipo di dato in un’operazione
  • gestione della memoria
  • individuazione e gestione di situazioni di errore

Un linguaggio è quindi dinamico se possiede:

  1. tipizzazione dei dati a tempo di esecuzione
  2. capacità di metaprogramming
  3. gestione dinamica della memoria
  4. meccanismi di rilevamento di errori
  5. modello di generazione del codice che prevede una fase di compilazione in un codice intermedio

Linguaggio dinamico risulta particolarmente adatto alla prototipazione veloce, unita alla portabilità.

Python

In Python, essendo tutto un oggetto, i termini classe e tipo sono sinonimi. Sono oggetti anche funzioni e le classi stesse.

Tipizzazione dinamica

In un assegnamento, una variabile acquisisce il tipo dell’oggetto mentre l’oggetto stesso ne rappresenta il valore. Se quindi si assegna un altro tipo di oggetto ad una variabile, anche il tipo della variabile cambia.

La somma è rappresentata da questo metodo di int:

int.__add__(x,y)
 
# Può anche essere eseguita in questo modo:
x.__add__(y)

Operazioni disponibili

F-string

s = "per me si va"
F=f"{s} nella città dolente\n{s} nell'eterno dolore,\n{s} tra la perduta gente."
print(F)

R-string (Raw string)

R = r"Un'espressione regolare contiene sequenze speciali:\n come \b e \d"
print(R)

Le stringhe sono implementate come un array di stringhe di lunghezza 1 (carattere), ma sono immutabili, quindi non si può cambiare una sola lettera ad esempio usando s[0] = "c"

Le stringhe, essendo immutabili, se si prova a concatenare una variabile con un’altra stringa come sotto, non si concatena veramente, ma la variabile A viene legata con una stringa differente in una cella di memoria differente

A = A+' 3'
A

Identificativo oggetti

Ogni oggetto ha un id univoco, che in Python è il numero della cella di memoria in cui è salvato. Ad esempio,

y = 1
x = 1
print(id(y))
print(id(x))

Il codice sopra stamperà lo stesso numero. Esistono però oggetti identici che possono esistere in doppia copia, e sono i float e gli int maggiori di 256.

L’interprete cerca di riutilizzare oggetti stringa già creati, per risparmiare memoria, e lo fa con stringhe che possono assomigliare ad identificativi, ovvero solo con lettere, cifre e il trattino basso.

Esempio:

z = 'Hello world'
w = 'Hello world'
id(z) == id(w)  # false
 
z = 'Hello_world'
w = 'Hello_world'
id(z) == id(w)  # true
n = (3)   # type -> int
n = (3,)  # type -> tuple

Le tuple sono immutable, mentre le liste sono mutable.

dict comprende un metodo dict.get(d, 'key', default) dove se non esiste un valore associato alla chiave key viene restituito quello di default.

Set

Per assegnare un set vuoto si utilizza set() e non {} che rappresenta un dizionario vuoto.

Non ha accesso sequenziale, quindi non si possono accedere gli elementi tramite la posizione.

set.intersection(S, T)
set.union(S, T)
set.difference(S, T)

Funzioni

In Python le funzioni sono first class object, e possono essere assegnate ad una variabile, possono essere passate come parametro ad un’altra funzione e possono essere restituite come risultato di una funzione.

def FUNZIONE({<parametro posizionale>}, {<parametro keyword>}):
	print("ciao")

I parametri posizionali devono sempre essere passati, mentre i parametri keyword hanno un valore di default. I parametri keyword devono sempre essere posizionati in fondo alla lista.

Se si mette un * prima del nome di un parametro, si potranno avere 0 o più parametri posizionali, mentre se se ne mettono due ** si avranno da 0 a più parametri keyword.

Lambda functions

Funzioni anonime definite usando la cosiddetta lambda notation. One-liner functions

scalar = lambda x: x*2
scalar(5)  # 10

DocString

Namespace

Lexical scoping

Quali siano i nomi visibili in un preciso punto del programma è cioè determinabile dalla struttura statica del programma stesso.

LEGB

globals e locals

def repeat(times):
    n = max(1,times)
    def inner(s):
        i = n
        while i>0:
            print(s)
            i -= 1
    return inner

La funzione definita è parametrica rispetto (cioè dipende da) un valore passato come argomento alla funzione definente (in questo caso repeat).

Questo funziona perché inner che viene restituito non è una semplice funzione, bensì una closure, che quindi mantiene l’ambiente non locale di quando è stata definita con sé.

Decoratori