Questo Notebook vuole fornire le conoscenze di base del linguaggio di programmazione python necessarie al suo utilizzo nella programmazione di applicazioni musicali di diverso tipo e scopo come la generazione e manipolazione di dati simbolici orientati alla composizione algoritmica (Computer Aided Composition) oppure l'analisi, sintesi ed elaborazione del suono per diversi ambiti (information data retrival, etnomusicologia, applicativi musicali per il web, etc.).
Scarichiamo ed installiamo:
Possiamo eseguire il codice di python in diversi modi.
Per i nostri scopi osserviamo due approcci differenti.
Dalle celle di questo Notebook - modalità comoda per presentazioni e lezioni.
out = "La bella gigogin" # Codice
print(out) # Scrive il codice in ouput
La bella gigogin
Con un editor esterno come VSCode che ci facilita l'organizzazione dell'ambiente di lavoro in cartelle - modalità da utilizzare per la creazione di programmi.
from IPython.display import HTML
HTML('<center><img src="media/vsc_1.png" width="80%"></center>')
from IPython.display import HTML
HTML('<center><img src="media/vsc_2.png" width="40%"></center>')
from IPython.display import HTML
HTML('<center><img src="media/vsc_3.png" width="45%"></center>')
from IPython.display import HTML
HTML('<center><img src="media/vsc_4.png" width="26%"></center>')
Se tutto è andato a buon fine l'output del nostro programma viene visualizzato nel terminale.
from IPython.display import HTML
HTML('<center><img src="media/vsc_5.png" width="85%"></center>')
Possiamo testare velocemente codice di python utilizzando la modalità interattiva (da riga di comando).
In un Terminale (Shell) qualsiasi (va bene anche quello di VSCode scriviamo python + enter.
Comparirà il prompt dei comandi (>>>).
from IPython.display import HTML
HTML('<center><img src="media/vsc_6.png" width="70%"></center>')
Ora possiamo scrivere una linea di codice ed eseguirla con enter.
Per uscire dalla modalità interattiva scrivere exit() oppure quit() + enter
Terminale
Python
Notebook
VScode
Le variabili sono come dei contenitori contrassegnati da un nome che possiamo riempire con degli oggetti.
Quando vogliamo recuperare un oggetto specifico da un contenitore lo possiamo fare scrivendone il nome che lo contrassegna.
In informatica i contenitori si chiamano allocazioni di memoria, i nomi etichette (key) e il contenuto tipi di data (type).
ciao = 12 # etichetta = contenuto
print(ciao) # print() comando che genera l'output
12
In python a differenza di altri linguaggi di programmazione:
Le varibili sono utili nella formalizzazione del pensiero in un programma in quanto ci permettono di richiamare più volte all'interno del codice insiemi di dati o blocchi di codice senza doverli riscrivere per intero ogni volta.
Pensiamo ad esempio a una sequenza melodica o alle note di un accordo codificate attraverso valori di midinote.
Se assegnamo i valori numerici a un'etichetta.
DoM = (60, 64, 67, 72)
Li potremo richiamare a nostro piacimento scrivendone solo l'etichetta.
print(DoM)
print(DoM)
print(DoM)
(60, 64, 67, 72) (60, 64, 67, 72) (60, 64, 67, 72)
Osserviamo alcune regole proprie di Python.
ciaociao = 1 # tutto minuscolo
ciao_ciao = 1 # underscore
_ciao_ciao = 1 # undescore inizio
ciaoCiao = 1 # con maiuscole (case sensitive) Camel Case
CIAOCIAO = 1 # tutto maiuscolo
ciaociao23 = 1 # numeri alla fine o in mezzo
2ciaociao = 1 # NO numero all'inizio
ciao-ciao = 1 # NO trattini
ciao ciao = 1 # NO spazi
File "/var/folders/sk/v1k0nf8n0vl19k7h2gyp9ykr0000gn/T/ipykernel_4542/396536858.py", line 1 2ciaociao = 1 # NO numero all'inizio ^ SyntaxError: invalid syntax
ciaoCiao = 2 # Camel case
CiaoCiao = 2 # Pascal case
ciao_ciao = 2 # Snake case (suggerito come best pratice di Python)
Se pensiamo musicalmente l'esempio seguente possiamo pensare all'assegnazione di valori MIDI ai nomi delle note in lingua inglese ma in realtà è un'assegnazione astratta di valori numerici interi a character che non rappresentano nulla se non diversi tipi di data.
c, d, e = 60, 61, 62 # no punto e virgola a fine riga...
print(c)
print(d)
print(e)
60 61 62
Attenzione a non confondere l'assegnazione precedente con collezioni di dati.
Il codice seguente potrebbe rappresentare musicalmente la dichiarazione di una condizione enarmonica.
c = bs = dff = 60 # do, si diesis, re doppio bemolle...
print(c)
print(bs)
print(dff)
60 60 60
Il codice seguente potrebbe rappresentare musicalmente un algoritmo di trasposizione ma in realtà è una semplice somma tra due interi.
c = 60 # valore MIDI
trasp = 12 # trasposizione in semitoni
out = c + trasp # algoritmo
print(out) # risultato
72
In python il codice viene eseguito dall'alto verso il basso e da sinistra verso destra.
Nel corso dell'esecuzione possiamo riassegnare le variabili, ovvero cambiare il contenuto dei contenitori mantenendo le etichette invariate.
c = 60
print(c)
c = c + 12 # un ottava sopra...
print(c)
c = c - 24 # due ottave sotto rispetto l'ultimo valore..
print(c)
c = c + 12 # un ottava sopra rispetto l'ultimo valore..
print(c)
60 72 48 60
Questo è possibile perchè quando assegnamo qualcosa ad una variabilie tutto ciò che è scritto a destra del simbolo uguale viene eseguito prima di quanto è scritto a sinistra (etichetta) diventando un'eccezione alla regola illustrata ad inizio paragrafo.
c = 60
print(c)
del c # del = delete
print(c) # Dà errore...
60
--------------------------------------------------------------------------- NameError Traceback (most recent call last) /var/folders/sk/v1k0nf8n0vl19k7h2gyp9ykr0000gn/T/ipykernel_4542/2901041819.py in <module> 3 4 del c # del = delete ----> 5 print(c) # Dà errore... NameError: name 'c' is not defined
Possiamo utilizzare le variabili anche come magazzino di dati da richiamare al bisogno senza variarli nel corso dell'esecuzione del codice.
Per convenzione le etichette sono scritte in maiuscolo con underscores.
SEMITONI_PER_OTTAVA = 12
print(SEMITONI_PER_OTTAVA)
12
Un tipo di dato indica l'insieme di valori che una variabile o il risultato di un'espressione possono assumere.
Il tipo di dato determina:
Per ottenere informazioni sul tipo di data di un oggetto:
x = 12
type(x) # riporta il tipo di data
int
In Python così come nella maggior parte dei linguaggi informatici esistono due tipologie di dati.
# --------------------- int
x = 5
print(type(x), x)
# --------------------- float
x = 5.12
print(type(x), x)
# --------------------- bool
x = True # Lettera Maiuscola
print(type(x), x)
# --------------------- numeri complessi
x = 4 + 5j # parte reale + parte immaginaria
print(type(x), x)
# --------------------- stringhe
x = 'ciao'
print(type(x), x)
<class 'int'> 5 <class 'float'> 5.12 <class 'bool'> True <class 'complex'> (4+5j) <class 'str'> ciao
import numpy as np # Importa la libreria numpy (info più avanti...)
# --------------------- liste (non Array...)
x = ["ciao", 12, 34.5, 'miao']
print(type(x), x)
# --------------------- tuple
x = ("ciao", 12,34.5, 'miao')
print(type(x), x)
# --------------------- range
x = range(6)
print(type(x), x)
# --------------------- dict
x = {"nome":"Andrea", "eta":52} # key:value
print(type(x), x)
# --------------------- set
x = {"nome", "Andrea", "eta", "peso"}
print(type(x), x)
# --------------------- numpy array (array numerici)
x = np.array([60,6,62,83])
print(type(x), x)
<class 'list'> ['ciao', 12, 34.5, 'miao'] <class 'tuple'> ('ciao', 12, 34.5, 'miao') <class 'range'> range(0, 6) <class 'dict'> {'nome': 'Andrea', 'eta': 52} <class 'set'> {'nome', 'Andrea', 'peso', 'eta'} <class 'numpy.ndarray'> [60 6 62 83]
Con il termine casting definiamo la procedura di conversione dei tipi di dato necessaria quando dobbiamo effettuare determinati tipi di operazioni.
A seconda del tipo di data implicato il risultato (output) di una stessa operazione può restituire risultati molto differenti.
x = "32 " # stringa
y = 400 # integer
z = 3.2 # float
print(y + z) # Stesso tipo (number)
print(int(x) + y) # Calcola somma
print(x + str(y)) # Concatena stringa
print(float(y)) # int --> float
print(int(z)) # float --> int (tronca i decimali)
403.2 432 32 400 400.0 3
Le librerie aggiungono funzionalità a python.
Sono collezioni di moduli, ovvero files esterni contenenti funzioni dedicate a compiere specifiche operazioni.
Con la distribuzione ufficiale viene fornita una vastissima libreria chiamata Standard Library ma ce ne sono di ogni tipo e per ogni esigenza.
Alcune librerie non incluse nella Standard Library sono incluse in altre distribuzioni di python come Anaconda.
Altre librerie le dobbiamo installare sul nostro computer dal Terminale (Shell) attraverso il modulo pip che è un package manager.
Per verificare se è già installato sul nostro computer apriamo un Terminale (se abbiamo aperto questo notebook dobbiamo aprirne uno nuovo con profilo Basic) e scriviamo:
pip --version
Se non è installato scriviamo ed eseguiamo nel Terminale (non da questo Notebook):
python get-pip.py
A questo punto possiamo installare le librerie che vogliamo direttamente dal Terminale (non da questo Notebook):
pip install camelcase
Oppure disintallarle:
pip uninstall camelcase
Se vogliamo conoscere quali librerie sono installate sul computer:
pip list
Facciamo attenzione che sullo stesso computer possono essere presenti diverse versioni di python.
Ognuna di queste potrà avere installate delle librerie non presenti nelle altre (ogni versione è un ambiente indipendente).
Per poter utilizzare i moduli di una libreria dobbiamo:
import math # Importa il modulo math della Standard library
x = math.floor(2.45) # modulo.funzione(argomento)
print(x)
2
Se vogliamo visualizzare tutte le funzioni presenti in un modulo:
import math
x = dir(math)
print(x)
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'comb', 'copysign', 'cos', 'cosh', 'degrees', 'dist', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'isqrt', 'lcm', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'nextafter', 'perm', 'pi', 'pow', 'prod', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc', 'ulp']
Alcuni moduli hanno nomi articolati. In questi casi possiamo creare un alias:
import numpy as np
x = np.arange(12,15) # nome_alias.funzione()
print(x)
[12 13 14]
Possiamo anche importare singole funzioni di un modulo. In questo caso non dobbiamo specificare il nome del modulo.
from random import randint
x = randint(10,15)
print(x)
14
Abbiamo visto come in Python abbiamo a disposizione quattro tipi di numeri:
Su di essi possiamo effettuare tutte le principali operazioni matematiche mentre per operazioni più complesse o non deterministiche dobbiamo necessariamente utilizzare una o più librerie.
somma = 12 + 23.4 # Cating automatico a float
print(somma)
bool = 12 == 13
print(bool)
img = 4 + 5j * 6 + 4j
print(img)
35.4 False (4+34j)
I numeri sono impiegati in tutti gli ambiti legati all'nformatica musicale: dalla sintesi del suono alla composizione assistita dall'analisi dei dati legata al mondo dell'industria musicale alla progettazione di data sonification e auditory display.
Le stringhe sono sequenze ordinate di characters (singole lettere o char).
Sono un tipo di data molto versatile in Python e in ambito informatico-musicale ricoprono una grande importanza nel processo di "traduzione" di dati numerici in codice lilypond per la generazione automatica di partiture musicali.
# 0123
x = "ciao" # doppio apice
y = 'miao' # singolo apice (da preferire)
print(x, y)
print(y)
ciao miao miao
x = """ciao
amici
miei"""
print(x)
ciao amici miei
x = str("ciao sono Andrea")
print(x)
ciao sono Andrea
seqa = "c d e f " # anche gli spazi sono character...
seqb = 'g a b'
seqc = '{ ' + seqa + seqb + ' }' # concatenazione
print(seqc)
{ c d e f g a b }
seqa = "c d e f "
print(seqa)
del seqa # elimina
print(seqa) # errore...
c d e f
--------------------------------------------------------------------------- NameError Traceback (most recent call last) /var/folders/sk/v1k0nf8n0vl19k7h2gyp9ykr0000gn/T/ipykernel_4542/4057073701.py in <module> 3 4 del seqa # elimina ----> 5 print(seqa) # errore... NameError: name 'seqa' is not defined
Se vogliamo compiere determinate operazioni all'interno di una stringa oppure inserire caratteri particolari come virgolette e apici dobbiamo farli precedere da un backslash.
Questo costrutto sintattico si chiama escape character ed è necessario a differenziare particolari tipi di carattere dalla loro funzione sintattica all'interno del linguaggio di programmazione.
Ci sono alcuni escape characters che ci possono tornare utili nella formattazione delle stringhe:
In taluni casi una sintassi simile riguarda le parentesi graffe.
Se le vogliamo definire come semplice carattere all'interno di una stringa in situazioni particolari che incontreremo le dobbiamo raddoppiare - {{ }}.
Come esempio confrontiamo il seguente file scritto nella sintassi di lilypond con la sua traduzione in python.
# Sintassi lilypond (NON eseguire)
\version "2.24.1"
\language "english"
\header {
title = "Bella musica"
composer = "L'ho scritta io"
}
{ c'' a' b' c'' }
File "/var/folders/sk/v1k0nf8n0vl19k7h2gyp9ykr0000gn/T/ipykernel_4542/3941078239.py", line 3 \version "2.24.1" ^ SyntaxError: unexpected character after line continuation character
# Sintassi python
versione = '\\version \"2.24.0\"\n' # assegna stringhe a variabili
lingua = '\\language \"english\"\n\n'
titolo = '''\\header {
title = \"Bella musica\"
composer = \"L'ho scritta io\"
}\n\n'''
expr = "{ c\'\' a\' b\' c\'\' }"
out = versione + lingua + titolo + expr # concatena più stringhe
print(out) # stampa il risultato
\version "2.24.0" \language "english" \header { title = "Bella musica" composer = "L'ho scritta io" } { c'' a' b' c'' }
Se vogliamo inserire all'interno di una stringa altri tipi di dato come int in precise posizioni possiamo utilizzare quattro diverse modalità:
Parametri ordinati
# --------------------- Variabili che andramo a generare o elaborare algoritmicamente
m1 = 'c e d c |'
m2 = 'f g d e |'
# --------------------- Costanti che costruiscono la partitura
SCORE = '''La mia bella melodia: {} {} '''
# --------------------- Concatenazione per l'output
out = SCORE.format(m1,m2) # .format(ordine_inserimento)
print(out)
La mia bella melodia: c e d c | f g d e |
Come nel caso precedente ma specifichiamo gli indici direttamente nelle parentesi graffe.
Se come nel caso seguente all'interno della stringa da formattare vogliamo utilizzare le parentesi graffe come carattere non possiamo utilizzare l'escape character ma dobbiamo duplicarle per distinguerle dalle variabili di formattazione.
# Sintassi python
# --------------------- Variabili che andremo a generare o elaborare algoritmicamente
I = 'c e g c'
IV = 'f a c f'
V = 'g b d g'
# --------------------- Costanti che costruiscono la partitura
SCORE = '''La mia progressione: {0} \n \t
{1} \n
{2} \n
{0} '''
# --------------------- Concatenazione per l'output
out = SCORE.format(I,IV,V)
print(out)
La mia progressione: c e g c f a c f g b d g c e g c
Come nei casi precedenti ma al posto degli indici specifichiamo i nomi direttamente nelle parentesi graffe
seqA = '| a b c d |'
seqB = '| c f g d |'
seqC = '| b g d e |'
SCORE = '''{seqA} {seqB} {seqC} {seqB} {seqA}'''
# --------------------- Concatenazione per l'output
out = SCORE.format(seqA=seqA, # modifichiamo i parametri da qua
seqB=seqB,
seqC=seqC)
print(out)
| a b c d | | c f g d | | b g d e | | c f g d | | a b c d |
Operatore di formato
Se utilizziamo il simbolo % all'interno di una stringa viene interpretato come operatore di formato.
La sintassi è quella illustrata nell'esempio.
In questo caso le parentesi graffe non devono essere raddoppiate in quanto presenti solo come carattere all'interno della stringa.
Non si usa .format()
var = ('Violino', 'Andante', 'quarto', 100, '2/4', 'Mi maggiore') # Tuple
SCORE = "Brano per %s, %s, %s = %d, Tempo: %s, Tonalità: %s" % var
# include i parametri specificati nella tuple nello stesso ordine
out = SCORE
print(out) # non si usa .format()
Brano per Violino, Andante, quarto = 100, Tempo: 2/4, Tonalità: Mi maggiore
Una collezione è un insieme di dati inclusi tra un qualche tipo di partentesi che fanno capo ad un'unica variabile e possono essere richiamati uno alla volta.
Musicalmente questi tipi di data sono molto utili in quanto i parametri musicali o sonori possono essere rappresentati in python (e nella maggior parte dei linguaggi informatici) sotto forma di collezioni di lettere (stringhe) o numeri (int e float).
Una collezione può essere:
indici 0 1 2 3 elementi ('a', 'b', 12, 34.34)Questo ci permette di richiamare i singoli elementi attraverso gli indici.
Tra parentesi tonde.
Possiamo utilizzare questo tipo di data quando ad esempio vogliamo definire i gradi di una scala o gli intervalli di una serie dodecafonica o una cellula ritmica dalla quali vogliamo ricavare variazioni ritmico - melodiche (le collezioni originali non sono modificate dalle trasformazioni successive).
# 0 1 2 3 4
seq_a = ('c','d','e','f','d')
print(seq_a)
seq_b = (60, 62, 63, 64, 62)
print(seq_b)
print(seq_b[0]) # Richiama un elemento
seq_a[2] = 'a' # Modifica un elemento...errore
print(seq_a)
('c', 'd', 'e', 'f', 'd') (60, 62, 63, 64, 62) 60
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) /var/folders/sk/v1k0nf8n0vl19k7h2gyp9ykr0000gn/T/ipykernel_4542/779584940.py in <module> 7 print(seq_b[0]) # Richiama un elemento 8 ----> 9 seq_a[2] = 'a' # Modifica un elemento...errore 10 print(seq_a) TypeError: 'tuple' object does not support item assignment
Tra parentesi quadre.
Possiamo utilizzare questo tipo di data quando ad esempio vogliamo definire una sequenza ritmico-melodica che si trasforma dinamicamente nel tempo sia come quantità di elementi presenti che come valori senza mantenere memoria della sequenza originale.
# 0 1 2 3 4
seq_a = ['c','d','e','f','d']
print(seq_a)
seq_b = (60, 62, 63, 64, 62)
print(seq_b)
print(seq_b[0]) # Richiama un elemento
seq_a[2] = 54 # Modifica un elemento...non da errore...
print(seq_a)
['c', 'd', 'e', 'f', 'd'] (60, 62, 63, 64, 62) 60 ['c', 'd', 54, 'f', 'd']
Tra parentesi graffe.
Sono formate da una coppia chiave:elemento dove per richiamare l'elemento non utilizziamo l'indice ma la chiave corrispondente, questo ci permette di avere collezioni non indicizzate. Un loro utilizzo tipico è nel mappare di elementi come il richiamare un accordo ogni qualvolta compare il grado di una scala.
seq_a = {'I':(3,5,7), 'II':[2,4,6], 'III':[5,8,10]}
print(seq_a)
print(seq_a['II']) # Richiama un elemento
seq_a['III'] = 12 # Modifica un elemento...non da errore...
print(seq_a)
seq_b = {60:'c\'', 61:'cs\'', 62:'d\''} # Valore MIDI --> nome nota
print(seq_b)
print(seq_b[61])
{'I': (3, 5, 7), 'II': [2, 4, 6], 'III': [5, 8, 10]} [2, 4, 6] {'I': (3, 5, 7), 'II': [2, 4, 6], 'III': 12} {60: "c'", 61: "cs'", 62: "d'"} cs'
Tra parentesi graffe.
Anche se è la meno duttile tra le collezioni ci permette di compiere alcune operazioni impossibili con altri tipi di dato sopratutto nei confronti tra collezioni diverse.
seq_a = {'c','d','e','f','d'} # Il duplicato non viene preso in considerazione...
print(seq_a)
seq_b = {60, 62, 63, 64, 62}
print(seq_b)
print(seq_b[0]) # non permette di richiamare un item..e dunque di modificarlo
{'e', 'c', 'f', 'd'} {64, 60, 62, 63}
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) /var/folders/sk/v1k0nf8n0vl19k7h2gyp9ykr0000gn/T/ipykernel_4542/2417563689.py in <module> 4 seq_b = {60, 62, 63, 64, 62} 5 print(seq_b) ----> 6 print(seq_b[0]) # non permette di richiamare un item..e dunque di modificarlo TypeError: 'set' object is not subscriptable
Se vogliamo lavorare con collezioni di numeri è molto più pratico utilizzare i moduli di una libreria esterna: NumPy.
NumPy non fa parte della standard library, ma se abbiamo scaricato Anaconda dovrebbe essere già installata. In caso contrario possiamo installarla seguendo queste istruzioni.
import numpy as np # Importa la libreria numpy e le assegna un nome abbreviato per comodità (np)
seq_a = [60, 62, 64] # Lista (sia lettere che numeri)
seq_b = np.array([60, 62, 64]) # Array di NumPy (solo numeri)
print(seq_a)
print(seq_b) # Array senza virgolette...
seq_a = seq_a * 3 # Lista # Notare il risultato differente per la stessa operazione...
seq_b = seq_b * 3 # Array
print(seq_a)
print(seq_b)
seq_c = seq_b.tolist() # Casting da Array a List
print(seq_c)
[60, 62, 64] [60 62 64] [60, 62, 64, 60, 62, 64, 60, 62, 64] [180 186 192] [180, 186, 192]
Sugli Array di NumPy a differenza delle altre collezioni possiamo effettuare operazioni matematiche su tutti gli items della collezione.
import numpy as np
a = np.array([0, 4, 6, 8, 12]) # Sequenza di intervalli
b = 60 + a # Trasposizione...
print(a)
print(b)
[ 0 4 6 8 12] [60 64 66 68 72]
Osserviamo ora le principali operazioni.
x = 9 # int
y = 5.3 # float
z = np.array([1, 4.3, 6, 8, 12]) # Array
print(x + y)
print(x + z)
print("")
print(x * y)
print(x * z)
print("")
print(x - y)
print(x - z)
print("")
print(x / y)
print(x / z)
print("")
print(y % x) # modulo (resto)
print(z % x)
print("")
print(y ** x) # esponente
print(z ** x)
print("")
print(y // 1) # floor (approssima all'intero più basso) trunc
print(z // 1)
14.3 [10. 13.3 15. 17. 21. ] 47.699999999999996 [ 9. 38.7 54. 72. 108. ] 3.7 [ 8. 4.7 3. 1. -3. ] 1.6981132075471699 [9. 2.09302326 1.5 1.125 0.75 ] 5.3 [1. 4.3 6. 8. 3. ] 3299763.591802132 [1.00000000e+00 5.02592612e+05 1.00776960e+07 1.34217728e+08 5.15978035e+09] 5.0 [ 1. 4. 6. 8. 12.]
Alcune regole generali.
* / + -
x = 9
z = np.array([1, 4.3, 6, 8, 12])
print( x + z * 2)
print((x + z) * 2)
[11. 17.6 21. 25. 33. ] [20. 26.6 30. 34. 42. ]
x = 9
z = np.array([1, 4.3, 6, 8, 12])
print(z)
z = z + 2 # riassegnazione variabile
print(z)
z += 10 # riassegnazione variabile
print(z)
# -= # vale per tutte le operazioni viste
# *=
# /=
# **=
# //=
[ 1. 4.3 6. 8. 12. ] [ 3. 6.3 8. 10. 14. ] [13. 16.3 18. 20. 24. ]
Accettano i principali metodi aritmetici (oltre a molti altri richiamabili dalle librerie).
z = np.array([1, 4.3, -6, 8, 12])
print(min(z)) # Valore minimo
print(max(z)) # Valore massimo
print(abs(z)) # Valore assoluto
print(pow(z, 2)) # Esponente
-6.0 12.0 [ 1. 4.3 6. 8. 12. ] [ 1. 18.49 36. 64. 144. ]
Possiamo richiamare i singoli elementi di quasi tutte le collezioni di python attraverso l'indice o la chiave:
import numpy as np # Importa la libreria numpy
# --------------------- stringhe
# 0123456
x = 'a b c d' # anche gli spazi hanno un indice...
y = x[2]
print(y)
# --------------------- tuple
x = (60,62,64,65)
y = x[0]
print(y)
# --------------------- liste
x = ['a','b','c']
y = x[0] # indice
print(y)
# --------------------- numpy array (array numerici)
x = np.array([60,73,62,83])
y = x[1] # indice
print(y)
# --------------------- dict
x = {60:'c', 61:'cs', 62:'d'}
y = x[61] # chiave
print(y)
b 60 a 73 cs
Per quanto riguarda le collezioni indicizzabili possiamo anche utilizzare indici negativi.
In questo caso l'indice -1 corrisponde al primo elemento da destra.
import numpy as np # Importa la libreria numpy
# --------------------- stringhe
x = 'a b c d'
y = x[-3]
print(y)
# --------------------- tuple
x = (60,62,64,65)
y = x[-1]
print(y)
# --------------------- liste
x = ['a','b','c']
y = x[-2] # indice
print(y)
# --------------------- numpy array (array numerici)
x = np.array([60,73,62,83])
y = x[-3] # indice
print(y)
c 65 b 73
Un'altra tecnica che possiamo adottare con le collezioni indicizzabili è lo slicing che consiste nello specificare un range compreso tra un indice iniziale e uno finale (funziona solo da destra a sinistra).
import numpy as np # Importa la libreria numpy
# 012345678
x = 'a b c d e' # Stringa
y = x[-5:] # da -5 alla fine (gli spazi hanno un indice)
print(y)
# 0 1 2 3 4 5
x = (60,62,64,65,69,72) # tuple
y = x[:3 ] # da 0 a 2 (compreso)
print(y)
# 0 1 2 3 4 5
x = ['a','b','c','d','e','f'] # lista
y = x[3:] # da 3 alla fine
print(y)
# 0 1 2 3 4 5
x = np.array([60,73,62,83,95, 97]) # Array
y = x[1:4] # da 1 a 4 (escluso)
print(y)
c d e (60, 62, 64) ['d', 'e', 'f'] [73 62 83]
I valori booleiani derivano da un ramo dell'algebra sviluppata da matematico George Boole in cui le variabili possono assumere solamente i valori vero e falso (valori di verità) e sono impiegati in operazioni condizionali.
Attraverso il loro impiego possiamo costruire strutture di controllo.
In python a differenza di altri linguaggi di programmazione cominciano con la lettera maiuscola.
x = 5 < 10 # True
y = 5 > 10 # False
print(x)
print(y)
True False
Le seguenti assegnazioni di variabili restituiscono sempre False.
x = 0
x = False
x = None
x = "" # String
x = () # Tuple
x = [] # List
x = {} # Set
print(x == True) # bool()
False
Nell'algebra di boole esistono due tipi di operatori:
x = 10 == 12
y = 10 < 12
z = 10 > 12
h = 10 <= 12
w = 10 >= 12
l = 10 != 12
print(x,y,z,h,w,l)
False True False True False True
# --------------------- AND
x = 16
y = x > 5 and x < 10
print(y)
x = 6
y = 5 < x < 10 # Solo in Python (no JavaScript o altro...)
print(y)
# --------------------- OR
x = 16
y = x > 5 or x < 10
print(y)
# --------------------- NOT
x = 16
y = not(x > 10)
print(y)
False True True False
I valori booleiani sono impiegati nelle operazioni condizionali.
costrutto if True::
x = 4
if x < 10: # se x è minore di 10
print(x) # stampa il valore in output
4
costrutto if con else:
se la condizione è vera: esegui questa operazione altrimenti esegui quest'altra operazione
x = 12
if x < 10:
print(True)
else:
print(False)
False
costrutto if con elif (else if):
se questa condizione è vera: esegui questa operazione
altrimenti se quest'altra condizione è vera: esegui quest'altra operazione
altrimenti se quest'altra condizione è vera: esegui quest'altra operazione
altrimenti se quest'altra condizione è vera: esegui quest'altra operazione
...
x = 13
if x < 10: # solo uno
print("minore di 10")
elif x == 10: # quanti ne vogliamo
print("uguale a 10")
elif x == 11:
print("uguale a 11")
else: # solo uno
print("maggiore di 10")
maggiore di 10
x = 11
if x % 2 == 0: # se x modulo 2 è uguale a 0
print("numero pari") # esegui questo e...
if(x < 10): # se x (senza modulo 4) è minore di 10
print("numero pari e minore di 10") # esegui questo...
else: # altrimenti
print("numero dispari") # esegui questo...
numero dispari
Possiamo sostituire un'istruzione condizionale con un espressione condizionale (abbreviazione sintattica).
La seguente struttura di controllo che traspone di un'ottava solo le altezze maggiori del DO centrale.
x = 65
if x > 60:
y = x + 12
else:
y = x
print(y)
77
Può essere riscritta in questo modo:
x = 57
y = x + 12 if x > 60 else x
print(y)
57
Possiamo iterare gli elementi di una collezione richiamandoli uno a uno con il ciclo for. Abbiamo a disposizione due possibilità:
# 0 1 2 3 4 5 6 7
smt = ('c','cs','d','ds','e','f','fs','g') # tuple o altra collezione indicizzabile
for i in smt:
print(i)
c cs d ds e f fs g
# 0 1 2 3 4 5 6 7
smt = ('c','cs','d','ds','e','f','fs','g') # tuple o altra collezione indicizzabile
for i in range(3): # da 0 a 2
print(smt[i])
print('-------')
for i in range(3, 6): # da 3 a 5
print(smt[i])
print('-------')
for i in range(1, 6, 2): # da 1 a 5 con passo di 2
print(smt[i])
c cs d ------- ds e f ------- cs ds f
Vediamo altri elementi sintattici che possono tornare utili.
# 0 1 2 3 4 5 6 7
smt = ('c','cs','d','ds','e','f','fs','g') # tuple o altra collezione indicizzabile
for i in range(3): # da 0 a 2
print(smt[i])
else:
print('\\key \\treble ')
c cs d \key \treble
# 0 1 2 3 4 5 6 7
smt = ('c','cs','d','ds','e','f','fs','g') # tuple o altra collezione indicizzabile
for i in range(5): # da 0 a 4
if i == 3: # se i è uguale a 3 si ferma
break
print(smt[i])
c cs d
# 0 1 2 3 4 5 6 7
smt = ('c','cs','d','ds','e','f','fs','g') # tuple o altra collezione indicizzabile
for i in smt: # da 0 a 4
if i == 'ds': # se i è uguale a 'ds' salta l'elemento
continue
print(i)
print('----------')
for i in range(5): # da 0 a 4
if i == 3: # se i è uguale a 3 salta l'elemento
continue
print(smt[i])
c cs d e f fs g ---------- c cs d e
for riga in range(3):
for colonna in range(4):
print("[riga " + str(riga) + " : colonna " + str(colonna) + "]")
[riga 0 : colonna 0] [riga 0 : colonna 1] [riga 0 : colonna 2] [riga 0 : colonna 3] [riga 1 : colonna 0] [riga 1 : colonna 1] [riga 1 : colonna 2] [riga 1 : colonna 3] [riga 2 : colonna 0] [riga 2 : colonna 1] [riga 2 : colonna 2] [riga 2 : colonna 3]
In alcuni casi dobbiamo legare il numero di iterazioni non alla lunghezza di una collezione o a un numero arbitrario ma al verificarsi o meno di una condizione. Per farlo utilizziamo i costrutto sintattico while che esegue il ciclo fino a quando la condizione restituisce true.
i = 0 # init contatore
while i < 6: # fino a quando la condizione è vera
print(i) # esegui
i += 1 # aggiorna il contatore
print("Fine") #è fuori dal ciclo (non indentato...)
0 1 2 3 4 5 Fine
Valgono i costrutti sintattici illustrati per for.
# --------------------- else
i = 0 # init contatore
while i < 6:
print(i)
i += 1
else:
print("ho finito") # dentro al ciclo
# --------------------- break
i = 0 # init contatore
while i < 6:
print(i)
if i == 3: # se 3...stoppa ed esce dal ciclo
break
i += 1
print("ho finito") # fuori dal ciclo
# --------------------- continue
i = 0 # init contatore
while i < 6:
i += 1
if i == 3: # se 3...salta
continue
print(i)
print("ho finito") # fuori dal ciclo
0 1 2 3 4 5 ho finito 0 1 2 3 ho finito 1 2 4 5 6 ho finito
La list comprehension è una sintassi alternativa (abbreviazione sintattica) usata spesso quando vogliamo generare, analizzare o modificare liste attraverso iterazioni.
Ad esempio la seguente operazione che genera una lista comprendente le frequenze dei primi cinque armonici di uno spettro.
fond = 100 # frequenza fondamentale
freqs = [] # lista vuota
for i in range(5):
freqs.append(fond * (i+1))
print(freqs)
[100, 200, 300, 400, 500]
Può essere scritta in modo più conciso in questo modo:
fond = 100 # frequenza fondamentale
freqs = [] # lista vuota
# operazione iterazione
[freqs.append(fond * (i+1)) for i in range(5)]
print(freqs)
[100, 200, 300, 400, 500]
Sono simili alle list comprehension ma con parenesi tonde invece che quadre.
o = (i**2 for i in range(5))
La differenza consiste nel fatto che non calcola immediatamente tutti i valori ma solo quando lo chiediamo attraverso il comando next.
next(o)
0
Alla fine dell'iterazione solleva un'eccezione StopIteration.
Ora che conosciamo le principali caratteristiche sintattiche di python possiamo cominciare ad organizzare il nostro pensiero computazionale in modo più organico.
Per farlo abbiamo a disposizione due modelli differenti ognuno dei quali ha proprie caratteristiche e fa riferimento a due oggetti specifici:
Attraverso le funzioni possiamo formalizzare alcune operazioni ricorrenti in modelli riproducibili e facilmente modificabili.
E' possibile anche salvare una o più funzioni di uso comune in files esterni (moduli) per poi importarle successivamente in un qualsiasi script.
Le funzioni consistono in un blocco di codice riutilizzabile, con alcune caratteristiche specifiche:
In questo processo i dati in uscita variano in funzione di quelli in ingresso.
N.B. I dati in input specificati con il nome si chiamano parametri mentre i dati che saranno inviati ad essi richiamando la funzione argomenti.
Esempio
Possiamo formalizzare ad esempio la necessità musicale di ottenere (dati in uscita) un determinato numero di valori pseudocasuali compresi tra un minimo e un massimo (dati in ingresso).
import numpy as np # Modulo programmato da terzi
n = 5 # Dati in ingresso
min = 60
max = 72
rng = np.random.default_rng() # Sequenza finita di operazioni
seq = rng.integers(min,max,size=n)
print(seq) # Dati in uscita
[68 64 71 60 61]
import numpy as np
# --------------------- definiamo la funzione
def r_minmax(min=0,max=127,n=3): # Nome e parametri con valori di default
# Stringa di documentazione (opzionale ma consigliata)
'''Genera un numero di interi n
compreso tra min e max
'''
rng = np.random.default_rng() # Algoritmo con parametri
seq = rng.integers(min,max,size=n)
return seq # Restituisce il risultato in output (funzione produttiva)
a = r_minmax(60,72,5) # Funzione con argomenti (Dati in ingresso)
print(a)
[68 60 71 60 71]
def arbitra(*args): # Asterisco prima del nome
rng = np.random.default_rng()
seq = rng.integers(args[0],args[1],size=args[2])
return(seq)
a = arbitra(56,79,3)
print(a)
[61 76 59]
def arghi(min=0,max=127,n=3): # Nomi dei parametri
rng = np.random.default_rng()
seq = rng.integers(min,max,size=n)
return(seq)
a = arghi(n=6, max=83,min=45) # Con nomi va bene qualsiasi ordine
print(a)
[77 58 59 73 67 50]
def valore_assoluto(x):
if x < 0:
return -x # return 1
else: # return 2
return x
a = valore_assoluto(-12)
print(a)
# -----------------------------------
def divisibile(x, y):
if x % y == 0:
return True
else:
return False
b = divisibile(5,2)
print(b)
12 False
def contoallarovescia(n):
if n <= 0:
print('Via!')
else:
print(n)
contoallarovescia(n-1) # richiama se stessa...
contoallarovescia(5)
5 4 3 2 1 Via!
Possiamo salvare una o più funzioni in un file con l'estensione .py.
import numpy as np # modulo necessario alla funzione salvata
# ------------------------------- r_minmax
def r_minmax(min=0,max=127,n=3):
'''Genera un numero di interi n
compreso tra min e max
'''
rng = np.random.default_rng()
seq = rng.integers(min,max,size=n)
return seq
Per poi importarle in qualsiasi altro file con le stesse modalità impiegate nell'importare moduli e librerie già realizzate da altri.
Eseguendo la cella precedente abbiamo salvato il file in una cartella dedicata ai moduli. Questa può essere una best pratics ma, se vogliamo importare un modulo in uno script deve essere nella stessa cartella. Per ovviare aggiungiamo al path di sistema la cartella in cui si trovano i moduli che vogliamo importare attraverso le librerie os e sys.
import os
import sys
path = os.path.abspath('moduli') # Restituisce il path assoluto della cartella
sys.path.insert(0, path) # Aggiunge la cartella moduli alla directory di lavoro
import funzione as fn # Importa il modulo custom
a = fn.r_minmax(60,72,5) # Richiama la funzione con argomenti (Dati in ingresso)
print(a)
[63 61 63 60 65]
A questo punto possiamo strutturare i nostri moduli in librerie dedicate a determinate operazioni più o meno astratte a seconda delle necessità e in base a criteri dettati da un ordine personale.
Un solo file di testo esterno può contenere più funzioni
Possiamo scegliere di definire una funzione per ogni singola necessità come appena illustrato aumentendo la modularità oppure accorpare diverse necessità rendendo meno aperti e più finalizzati gli algoritmi.
Come esempio programmiamo nello stesso file una seconda funzione che realizza una trasposizione in semitoni.
import numpy as np # modulo necessario alla funzione salvata
# ------------------------------- r_minmax
def r_minmax(min=0,max=127,n=3):
'''Genera un numero di interi n
compreso tra min e max
'''
rng = np.random.default_rng()
seq = rng.integers(min,max,size=n)
return seq
# ------------------------------- trasp
def trasp(seq=np.array([60,61,62]), semit=0):
'''Traspone una sequenza in semitoni
'''
seq = seq + semit
return seq
Richiamiamo le funzioni
import os
import sys
path = os.path.abspath('moduli')
sys.path.insert(0, path)
import funzione as fn
# -------------------------------
seq = fn.r_minmax(60,72,8)
print("Originale:", seq)
seq = fn.trasp(seq, 10)
print("Trasposto:", seq)
Originale: [63 68 62 63 67 70 65 67] Trasposto: [73 78 72 73 77 80 75 77]
La scelta di come organizzare le nostre funzioni in moduli e librerie dipende dalle esigenze musicali e personali di ognuno.
La programmazione orientata agli oggetti (OOP) è un paradigma di programmazione in cui i dati e operazioni sui dati vengono organizzati in classi, istanze e metodi.
Se dovessimo definire attraverso il codice una sequenza musicale avremmo tre possibilità:
Pensiamo una classe come uno stampino che ci permette di fabbricare diversi oggetti del medesimo tipo (istanze).
c, e, g = 60, 64, 67 # variabili
print(c)
print(e)
print(g)
seq = (60, 64, 67) # collezione
print(seq)
class Sequenza: # intestazione (nome della Classe)
'''Rappresenta le altezze di un accordo qualsiasi''' # Stringa di documentazione
print(Sequenza)
60 64 67 (60, 64, 67) <class '__main__.Sequenza'>
In un albero gerarchico questa classe è stata creata al livello principale e il suo nome è: '\__main___.Sequenza'_.
Notiamo che comincia con una lettera maiuscola.
Definita la classe possiamo creare tanti accordi (per ora vuoti - istanze) utilizzandola come stampino.
a = Sequenza()
b = Sequenza()
print(a)
print(b)
<__main__.Sequenza object at 0x7fe71806e550> <__main__.Sequenza object at 0x7fe71806e340>
Osserviamo come Python ci riporta in quale allocazione di memoria ha collocato la singola istanza.
Se vogliamo Eliminare un'istanza o una classe.
del a # elimina l'istanza
print(a) # dà errore...
--------------------------------------------------------------------------- NameError Traceback (most recent call last) /var/folders/sk/v1k0nf8n0vl19k7h2gyp9ykr0000gn/T/ipykernel_4542/4104448987.py in <module> 1 del a # elimina l'istanza ----> 2 print(a) # dà errore... NameError: name 'a' is not defined
print(Sequenza)
del Sequenza # Elimina la Classe
print(Sequenza) # dà errore...
<class '__main__.Sequenza'>
--------------------------------------------------------------------------- NameError Traceback (most recent call last) /var/folders/sk/v1k0nf8n0vl19k7h2gyp9ykr0000gn/T/ipykernel_4542/1631169874.py in <module> 1 print(Sequenza) 2 del Sequenza # Elimina la Classe ----> 3 print(Sequenza) # dà errore... NameError: name 'Sequenza' is not defined
Nel passaggio precedente abbiamo definito la classe Sequenza (stampino) grazie alla quale abbiamo fabbricato due oggetti (istanze).
Qeste copie però sono identiche (in questo caso oltretutto sono dei contenitori vuoti).
Se vogliamo differenziarli tra loro (costruire sequenze diverse) dobbiamo necessariamente specificare all'interno della definizione della classe uno o più attributi come ad esempio i valori delle altezze.
Si chiamano attributi perchè sono valori ai quali attribuiamo un nome.
In questo frangente possiamo specificarli come valori midi o attraverso un'altra rappresentazione numerica a nostro piacimento delle altezze che formano una sequenza specifica.
Per farlo dobbiamo dichiarare dei metodi ovvero delle funzioni definite all'interno della Classe.
class Sequenza: # intestazione (nome della Classe)
'''Rappresenta le altezze di una sequenza''' # Stringa di documentazione
def note(nota1,nota2,nota3): # metodo con attributi
return (nota1, nota2, nota3) # azione da compiere
Possiamo poi passare attributi alle Classi invocando i metodi attraverso la sintassi seguente.
a = Sequenza.note(60,64,67) # Classe.metodo(arg1,arg2,arg3)
b = Sequenza.note(90,34,56) # Classe.metodo(arg1,arg2,arg3)
print(a)
print(b)
(60, 64, 67) (90, 34, 56)
Nell'esempio precedente abbiamo definito tre attributi e tutte le sequenze dovranno essere formate sempre da tre note.
Se invece volessimo rendere dinamico il numero di attributi (in questo caso il numero di note della sequenza) dovremmo utilizzare la parola chiave *args come abbiamo visto per le funzioni (i metodo sono a tutti gli effetti delle funzioni dichiarate all'interno di una classe).
class Sequenza:
'''Rappresenta le altezze di una sequenza'''
def note(*args): # metodo con attributi dinamici
return args # senza l'asterisco...
Sequenza.note(60, 64, 67, 72, 80,34)
(60, 64, 67, 72, 80, 34)
Esiste un metodo speciale di inizializzazione che permette di differenziare gli attributi delle diverse istanze chiamato anche costruttore che è presente in quasi tutte le classi.
class Sequenza:
'''Rappresenta le altezze di una sequenza'''
# Costruttore
def __init__(self, nota1=60, nota2=64, nota3=67, trasp=0): # Attributi con valori di default
self.nota1 = nota1 # Proprietà
self.nota2 = nota2
self.nota3 = nota3
self.trasp = trasp
def ottieni(self): # metodo 2 - azione da compiere
return (self.nota1, self.nota2, self.nota3)
def trasponi(self): # metodo 3 - azione da compiere
self.nota1 = self.nota1 + self.trasp
self.nota2 = self.nota2 + self.trasp
self.nota3 = self.nota3 + self.trasp
Osserviamo alcune cose:
Generiamo tre istanze
a = Sequenza() # Istanza con attributi di default
b = Sequenza(56,78,92,10) # Passiamo tutti gli attributi
c = Sequenza(60,89) # Parte sostituiti e parte di default
Invochiamo il metodo .ottieni()
print(a.ottieni()) # Invoca il metodo ottieni
print(b.ottieni()) # non fa alcuna trasposizione in quanto non abbiamo invocato il metodo
# anche se abbiamo passato l'argomento nell'istanzializzazione
print(c.ottieni())
(60, 64, 67) (56, 78, 92) (60, 89, 67)
b.trasponi() # invoca la trasposizione
print(b.ottieni()) # stampa il risultato della trasposizione
(66, 88, 102)
Se eseguiamo nuovamente la cella precedente ci sarà una nuova trasposizione in quanto le proprietà sono state sostituite da quelli generati nell'ultima computazione.
Modificare gli attributi di un'istanza.
Se vogliamo modificare uno o più attributi la sintassi è la seguente.
a.nota1 = 45 # modifica l'attributo
print(a.ottieni())
(45, 64, 67)
Se vogliamo informazioni riguardo le caratteristiche di una Classe.
help(Sequenza)
Help on class Sequenza in module __main__: class Sequenza(builtins.object) | Sequenza(nota1=60, nota2=64, nota3=67, trasp=0) | | Rappresenta le altezze di una sequenza | | Methods defined here: | | __init__(self, nota1=60, nota2=64, nota3=67, trasp=0) | Initialize self. See help(type(self)) for accurate signature. | | ottieni(self) | | trasponi(self) | | ---------------------------------------------------------------------- | Data descriptors defined here: | | __dict__ | dictionary for instance variables (if defined) | | __weakref__ | list of weak references to the object (if defined)
Se vogliamo eliminare uno o più attributi di un'istanza.
del a.nota1 # rimuove solo dall'istanza non dalla Classe...
print(a.nota1) # dà errore...
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) /var/folders/sk/v1k0nf8n0vl19k7h2gyp9ykr0000gn/T/ipykernel_4542/1448131272.py in <module> 1 del a.nota1 # rimuove solo dall'istanza non dalla Classe... 2 ----> 3 print(a.nota1) # dà errore... AttributeError: 'Sequenza' object has no attribute 'nota1'
Le proprietà di una singola istanza si chiamano variabili d'istanza in quanto ne definiscono le caratteristiche che la differenziano da altre istanze.
Si riconoscono per la presenza di self.
Ci sono però casi in cui abbiamo la necessità di definire la stessa proprietà a tutte le istanze attrverso un unico comando.
Ad esempio se volessimo specificare le altezze della sequenza non in valori assoluti (note midi) ma in intervalli dovremmo specificare per tutti gli accordi la stessa nota perno da sommare agli intervalli.
In questo caso utilizzeremo una variabile di classe condivisa da tutte le istanze.
class Sequenza:
'''Rappresenta le altezze di una sequenza'''
nota_perno = 60 # Variabile di classe
def __init__(self, nota1=0, nota2=4, nota3=7, trasp=0): # Attributi
self.nota1 = nota1 # Proprietà (Variabili d'istanza)
self.nota2 = nota2
self.nota3 = nota3
self.trasp = trasp
# self.nota_perno N.B. all'interno della classe possiamo richiamare
# il valore anche come variabile d'istanza..
def ottieni(self):
return (Sequenza.nota_perno + self.nota1,
Sequenza.nota_perno + self.nota2,
Sequenza.nota_perno + self.nota3)
def trasponi(self):
self.nota1 = Sequenza.nota_perno + self.nota1 + self.trasp # Triade.nota_perno --> accesso come classe
self.nota2 = self.nota_perno + self.nota2 + self.trasp # self.nota_perno --> accesso come istanza
self.nota3 = Sequenza.nota_perno + self.nota3 + self.trasp
I = Sequenza(0, 4, 7)
IV = Sequenza(0, 5, 9)
V = Sequenza(-1,2, 7)
print(I.ottieni())
print(IV.ottieni())
print(V.ottieni())
Sequenza.nota_perno = 80 # Modifica la variabile di classe per tutte le istanze
print('--------')
print(I.ottieni())
print(IV.ottieni())
print(V.ottieni())
(60, 64, 67) (60, 65, 69) (59, 62, 67) -------- (80, 84, 87) (80, 85, 89) (79, 82, 87)
Notiamo che come esempio all'interno della classe abbiamo richiamato la variabile sia come variabile di classe che come variabile d'istanza.
Questo perchè quando richiamiamo un attributo all'interno di una classe Python prima cerca tra le variabili d'istanza e poi tra quelle di classe.
A seconda dei casi possiamo utilizzare una sintassi piuttosto che l'altra.
Per ottenere informazioni sullo stato delle variabili.
print(Sequenza.__dict__) # Classe
print(I.__dict__) # Istanza
IV.nota1 = -3 # modifica...
print(IV.__dict__)
print(V.__dict__)
{'__module__': '__main__', '__doc__': 'Rappresenta le altezze di una sequenza', 'nota_perno': 80, '__init__': <function Sequenza.__init__ at 0x7fe7080211f0>, 'ottieni': <function Sequenza.ottieni at 0x7fe708021670>, 'trasponi': <function Sequenza.trasponi at 0x7fe708021af0>, '__dict__': <attribute '__dict__' of 'Sequenza' objects>, '__weakref__': <attribute '__weakref__' of 'Sequenza' objects>} {'nota1': 0, 'nota2': 4, 'nota3': 7, 'trasp': 0} {'nota1': -3, 'nota2': 5, 'nota3': 9, 'trasp': 0} {'nota1': -1, 'nota2': 2, 'nota3': 7, 'trasp': 0}
Come schema generale:
class Class_name: # Nome della Classe
'Stringa di documentazione'
class_data1 = val # Variabili di Classe
class_data2 = val # (Class Static Data Definitions)
def __init__(self, arg1, arg2, ...): # Costruttore(attributi)
# (Class Instance Inizializer)
self.instance_data1 = val # Proprietà
self.instance_data1 = val # (Instance Data Definitions)
def method(self, arg1, arg2, ...) # Metodi di istanza
return arg1 + arg2
def method(self, arg1, arg2, ...)
return arg1 + arg2
File "/var/folders/sk/v1k0nf8n0vl19k7h2gyp9ykr0000gn/T/ipykernel_4542/1891918914.py", line 8 def __init__(self, arg1, arg2, ...): # Costruttore(attributi) ^ SyntaxError: invalid syntax
Una delle potenzialità che caratterizza la programmazione orientata agli oggetti è l'ereditarietà, ovvero la possibilità di definire una nuova classe come versione modificata di una classe già esistente.
Poniamo di voler definire una sequenza di altezze generica in due modi.
In questa ultima modalità vogliamo poter effettuare anche le seguenti operazioni.
class Sequenza:
'''
crea una sequenza di altezze
memorizzate in una lista
'''
perno = 60 # Variabile di Classe
def __init__(self, altezze=None):
self.altezze = altezze # Proprietà
if self.altezze is None: # se non specifichiamo la lista di intervalli
self.altezze = [] # ne crea una vuota
else: self.altezze = altezze
print(self.altezze)
def inserisci(self): # permette di inserire le note
print("scrivi nota")
nota = input() # input da tastiera (stringa)
nota = int(nota) # casting in int in quanto l'input da tastiera è una stringa
self.altezze.append(nota) # aggiunge alla lista
print(self.altezze)
def cancella(self): # cancella l'ultimo valore'
self.altezze.pop()
print(self.altezze)
def pulisci(self): # cancella tutta la lista
self.altezze = []
print(self.altezze)
def interv(self): # genera l'output in intervalli
return self.altezze
def note(self): # genera l'output in valori midi
self.seq = []
for i in self.altezze: # itera la lista e somma la nota perno a tutti gli intervalli
self.seq.append(i + Sequenza.perno)
return self.seq
Se specifichiamo la lista come attributo.
seq = Sequenza([0,4,12])
[0, 4, 12]
Se vogliamo inserirle una alla volta.
seq = Sequenza()
[]
seq.inserisci() # inserisce
scrivi nota 84 [65, 72, 84]
seq.cancella() # cancella l'ultima nota
[65, 72]
seq.pulisci() # cancella tutta la lista
[]
seq.interv() # lista in intervalli
[65, 72, 84]
seq.note() # lista in midinote
[125, 132, 144]
Supponiamo ora di voler definire due nuove classi.
Entrmbe sono sequenze di altezze ma ognuna con caratteristiche leggermente diverse.
Una scala ha altezze ordinate in modo crescente e può non contenere tutti i numeri compresi tra 0 e 12.
Una serie dodecafonica ha le altezze non ordinate in modo crescente e deve contenere tutti i numeri compresi tra 0 a 11.
Hanno dunque alcune caratteristiche in comune ed altre che le differenziano.
Aggiungiamo anche altre funzionalutà specifiche.
Per la classe Scala la possibilità di ordinare le altezze in modo crescente, decrescente o randomico.
Per la classe Serie le principali operazioni legate alla tecnica dodecafonica: retrogrado, inversione e retrogrado dell'inversione.
IN entrambe i casi cobbiamo verificare che non ci siano duplicati nella lista di intervalli.
Per non riscrivere tutto il codice precedente possiamo creare una classe figlia (child - sottoclasse) che eredita tutti i metodi e gli attributi dalla classe madre (parent - superclasse).
Questa operazione si realizza specificando come attributo del nome la classe dalla quale vogliamo ereditare uno o più attributo o metodi.
import random # modulo per shuffle
class Scala(Sequenza): # Attributo --> nome della superclasse
'''genera una scala musicale memorizzata in una lista e la:
< = riordina in modo crescente
> = decrescente
r = randomico
None = sequenza originale
'''
def __init__(self, altezze=None, ordine='<'): # Costruttore Classe Figlia con proprietà
super().__init__(altezze) # Quali proprietà sono ereditate dalla Classe Madre (SuperClass)
self.ordine = ordine # Nuova proprietà della classe figlia
def no_dup(self): # verifica la presenza di duplicati (non produttiva)
verifica = []
for i in self.altezze: # verifica la presenza
if i not in verifica:
verifica.append(i)
else: print(str(i) + ' eliminato dalla scala')
self.altezze.clear() # pulisce la lista originale
self.altezze = verifica # sostituisce la lista con i duplicati con quella senza
print(self.altezze)
def ordina(self): # Nuovo metodo per ordinare che si aggiunge a quelli della classe madre
if self.ordine == '<':
self.altezze.sort() # dalla proprietà ereditata...
elif self.ordine == '>':
self.altezze.sort(reverse=True)
elif self.ordine == 'r':
random.shuffle(self.altezze)
else: self.altezze = self.altezze
return self.altezze
seq = Scala([0,2,4,5,5,7,9,11], 'r') # c'è un duplicato...
[0, 2, 4, 5, 5, 7, 9, 11]
seq.no_dup()
5 eliminato dalla scala [0, 2, 4, 5, 7, 9, 11]
a = seq.ordina()
print(a)
[11, 5, 0, 7, 2, 4, 9]
a = seq.interv() # metodi ereditati...
b = seq.note()
print(a)
print(b)
[11, 5, 0, 7, 2, 4, 9] [71, 65, 60, 67, 62, 64, 69]
seq = Scala()
[]
seq.inserisci()
scrivi nota 7 [0, 4, 4, 7]
seq.no_dup()
4 eliminato dalla scala [0, 4, 7]
Possiamo anche specificare attributi dei singoli metodi.
Modifichiamo la definizione della classe spostando la scelta delle variazioni come attributo del metodo.
import random # modulo per shuffle
class Scala(Sequenza): # Attributo --> nome della superclasse
'''genera una scala musicale memorizzata in una lista e la:
< = riordina in modo crescente
> = decrescente
r = randomico
None = sequenza originale
'''
def __init__(self, altezze=None, ordine='<'): # Costruttore Classe Figlia con proprietà
super().__init__(altezze) # Quali proprietà sono ereditate dalla Classe Madre (SuperClass)
self.ordine = ordine # Nuova proprietà della classe figlia
def no_dup(self): # verifica la presenza di duplicati
verifica = []
for i in self.altezze: # verifica la presenza
if i not in verifica:
verifica.append(i)
else: print(str(i) + ' eliminato dalla scala')
self.altezze.clear() # pulisce la lista originale
self.altezze = verifica # sostituisce la lista con i duplicati con quella senza
print(self.altezze)
def ordina(self, ordine='<'): # Aggiunto attributo al metodo
self.ordine = ordine # proprietà del metodo
if self.ordine == '<':
self.altezze.sort()
elif self.ordine == '>':
self.altezze.sort(reverse=True)
elif self.ordine == 'r':
random.shuffle(self.altezze)
else: self.altezze = self.altezze
seq = Scala([0,2,4,6])
[0, 2, 4, 6]
seq.ordina('r')
print(seq.interv())
print(seq.note())
[6, 4, 0, 2] [66, 64, 60, 62]
Notiamo che per come abbiamo programmato la classe la scala originale è stata sostituita da quella riordinata e se vogliamo ottenerla nuovamente in output dobbiamo passarla nuovamente come attributo.
seq.altezze = [0,2,4,6]
print(seq.interv())
print(seq.note())
[0, 2, 4, 6] [60, 62, 64, 66]
Questo ci fa comprendere come sia importante nella prototipazione del codice organizzare attributi, proprietà e metodi a seconda delle nostre esigenze musicali.
Dichiariamo ora la classe Serie che eredita tutte le proprietà dalla classe Scala (e di conseguenza da Sequenza).
Abbiamo aggiunto una variabile d'istanza dove viene memorizzata la serie originale e un metodo (recto) che ci permette di ripristinare lo stato iniziale da eventuali sovrascritture interne.
class Serie(Scala): # Attributo --> nome della superclasse
'''crea una serie dodecafonica in una lista
ed effettua le seguenti operazioni:
ret = calcola il retrogrado
inv = calcola l'inversion
rinv = calcola il retrogrado dell'inversione
None = sequenza originale
'''
def __init__(self, altezze=None, ordine=None):
super().__init__(altezze, ordine)
self.originale = altezze # memorizza la serie originale
def recto(self):
self.altezze = self.originale
def verso(self):
self.altezze = self.originale # ripristina la serie originale
self.altezze = self.altezze[::-1] # calcola il retrogrado
def inv(self):
self.altezze = self.originale # ripristina la serie originale
self.inver = []
for i in self.altezze:
u = i * -1
self.inver.append(u)
self.altezze = self.inver
def rinv(self):
self.altezze = self.originale # ripristina la serie originale
self.rinver = []
for i in self.altezze:
u = i * -1
self.rinver.append(u)
self.altezze = self.rinver[::-1]
seq = Serie([0,3,5,4,1])
[0, 3, 5, 4, 1]
seq.recto() # originale
print(seq.interv())
print(seq.note())
[0, 3, 5, 4, 1] [60, 63, 65, 64, 61]
seq.verso() # retrogrado
print(seq.interv())
print(seq.note())
[1, 4, 5, 3, 0] [61, 64, 65, 63, 60]
seq.inv() # inversione
print(seq.interv())
print(seq.note())
[0, -3, -5, -4, -1] [60, 57, 55, 56, 59]
seq.rinv() # retrogrado dell'inversione
print(seq.interv())
print(seq.note())
[-1, -4, -5, -3, 0] [59, 56, 55, 57, 60]
In Python un decoratore è una funzione che:
Sono uno strumento molto utile nella programmazione orientata agli oggetti.
Python ci fornisce alcuni decoratori built-in che ci possono tornare utili.
@property. Indice
Come esempio scriviamo una classe che accetta valori di frequenza in Hertz e attraverso un metodo li converte in midinote.
from math import log2 # Modulo per calocloare log2
class Ftom:
def __init__(self, hertz=440):
self.hertz = hertz
def to_midi(self):
return 12*log2(self.hertz/440) + 69 # Formula per la convertire da hz a midi
#-----------------------
freq = Ftom() # Crea un'istanza
freq.hertz = 880 # Setta un valore (setter)
print(freq.hertz) # Richiama il valore memorizzato(getter)
print(freq.to_midi()) # Accede al metodo che converte il valore
880 81.0
Nella definizione di una classe per ogni attributo abbiamo generalmente bisogno di almeno due operazioni:
Supponiamo ora di voler estendere le caratteristiche di questa classe limitando i valori midi tra 0 e 127.
Implementiamo il codice per ottenere questa restrizione nascondendo l'attributo hertz (rendendolo privato) e definendo due nuovi metodi per manipolarlo.
from math import log2
class Ftom:
def __init__(self, hertz=440):
self.set_hertz(hertz) # Modificata la proprietà
def to_midi(self):
return int(12*log2(self.get_hertz()/440) + 69) # Modificato con metodo
def get_hertz(self): # metodo getter
return self._hertz # undescore = variabile privata (non è riscritta dall'attributo hertz)
def set_hertz(self, value): # metodo setter
if value < 10 or value > 13000:
raise ValueError("Fuori dal range MIDI")
self._hertz = value
freq = Ftom() # Crea un'istanza
print(freq.get_hertz()) # Richiama il valore memorizzato(getter)
print(freq.to_midi()) # Effettua la conversione
freq.set_hertz(12040) # Setta una nuova freqenza se < di 10 o > di 1300 produce un errore
print(freq.to_midi()) # Effettua la conversione
440 69 126
Abbiamo dovuto fare numerose modifiche al codice originale e, nel caso di programmi complessi con classi che ereditano proprietà da altre classi il tutto si pù facilmente trasformare in un dedalo sintattico di complessa comprensione.
In Python possiamo però implementare il codice attraverso il decoratore @property che ovvia a queste problematiche mantenendo la sintassi originale.
from math import log2
class Ftom:
def __init__(self, hertz=440):
self.hertz = hertz # Sintassi originale
def to_midi(self):
return int(12*log2(self.hertz/440) + 69) # Sintassi originale
@property # Decoratore getter
def hertz(self): # Nome dell'attributo
print('Getter...')
return self._hertz # undescore = variabile privata (non è riscritta dall'attributo hertz)
@hertz.setter # Decoratore setter
def hertz(self, value): # metodo setter
if value < 10 or value > 13000:
raise ValueError("Fuori dal range MIDI")
print('Setter...')
self._hertz = value
freq = Ftom() # Crea un'istanza
print(freq.hertz) # Richiama il valore memorizzato(getter)
print(freq.to_midi()) # Effettua la conversione
freq.hertz = 11245 # Sintassi comune per settare i valori
print(freq.hertz)
print(freq.to_midi()) # Effettua la conversione
Setter... Getter... 440 Getter... 69 Setter... Getter... 11245 Getter... 125
Le variabili private (che valgono solo all'interno della classe) non esistono in Python, la notazione con l'undescore è solo una convenzione.
@classmethod. Indice
Queto decoratore è utilizzato per definire i metodi di classe.
Principalmente si utilizza in due situazioni.
from math import log2 # Modulo per calocloare log2
class Sequenza: # SuperClasse
'''
crea una sequenza di altezze
memorizzate in una lista
'''
perno = 60
def __init__(self, altezze=None): # Costruttore principale
self.altezze = altezze
if self.altezze is None:
self.altezze = []
else: self.altezze = altezze
print(self.altezze)
# Costruttore alternativo
@classmethod # decoratore
def da_stringa(cls, stringa, *args): # Metodo di classe (non c'è self ma cls...)
altezze = []
li = list(stringa.split(' ')) # casting da stringa a lista
for i in li:
altezze.append(int(i)) # casting da stringa a int
return cls(altezze) # restituisce la classe...
@classmethod
def tipo(cls): # Metodo dinamico (non c'è self ma cls...)
if cls.__name__ == 'Frase':
return 'Output in note MIDI'
else:
return 'Output in Herts'
def inserisci(self): # Metodi di istanza
print("scrivi nota")
nota = input()
nota = int(nota)
self.altezze.append(nota)
print(self.altezze)
def cancella(self):
self.altezze.pop()
print(self.altezze)
def pulisci(self):
self.altezze = []
print(self.altezze)
def interv(self):
return self.altezze
def note(self):
self.seq = []
for i in self.altezze:
self.seq.append(i + Sequenza.perno)
return self.seq
class Frase(Sequenza): # SottoClasse
def __init__(self, altezze=None):
super().__init__(altezze)
def m_seq(self, elem=2):
self.elem = elem
self.out = self.altezze[:elem]
return self.out
miaSeq = '0 2 5 7 12 6 8 3' # Sequenza (stringa)
seq = Sequenza.da_stringa(miaSeq) # Costruttore alternativo (@classmethod)
print(seq.note())
dina = Frase([0,4,5,7,6,8]) # Costruttore principale (ereditato)
print(dina.m_seq(3)) # Metodo d'istanza
print(dina.tipo()) # Metodo dinamico (@classmethod)
[0, 2, 5, 7, 12, 6, 8, 3] [60, 62, 65, 67, 72, 66, 68, 63] [0, 4, 5, 7, 6, 8] [0, 4, 5] Output in note MIDI
@staticmethod. Indice
Sono metodi di classe che possono essere invocati anche senza creare un'istanza e che hanno qualcosa in comune con la classe che li contiene.
In genere sono utilizzati per effettuare calcoli che possono tornare utili nella classe e/o restituire informazioni utili.
class Test():
'''
Solo una dimostrazione dei metodi statici
Riscala i valori compresi tra +/- 1 in valori tra 0 e 1
'''
def __init__(self, altezze): # Costruttore principale
self.altezze = altezze
@staticmethod
def normalizza(val, min_val=0, max_val=1):
return (((val + 1) / 2) * (max_val - min_val)) + min_val
Test.normalizza(0.99)
0.995
I dunder methods sono un insieme di metodi predefiniti che ci permette di espandere facilmente le potenzialità delle classi.
Sono caratterizzati dalla presenza di __ (due underscores) sia prima che dopo il nome.
__init__ è uno di questi.
Vediamo come funzionano.
seq = [0,4,6,7,8] # lista
seq[2] # richiamiamo l'item
6
Attraveso la sintassi precedente in realtà Python richiama implicitamente il metodo __getitem__ che possiamo anche utilizzare esplicitamente.
seq = [0,4,6,7,8] # lista
seq.__getitem__(2) # richiamiamo l'item
6
Se richiamiamo l'help del nostro oggetto possiamo leggere tutti i dunder method che possiamo invocare su di esso sia implicitamente che esplicitamente.
help(seq)
Help on list object: class list(object) | list(iterable=(), /) | | Built-in mutable sequence. | | If no argument is given, the constructor creates a new empty list. | The argument must be an iterable if specified. | | Methods defined here: | | __add__(self, value, /) | Return self+value. | | __contains__(self, key, /) | Return key in self. | | __delitem__(self, key, /) | Delete self[key]. | | __eq__(self, value, /) | Return self==value. | | __ge__(self, value, /) | Return self>=value. | | __getattribute__(self, name, /) | Return getattr(self, name). | | __getitem__(...) | x.__getitem__(y) <==> x[y] | | __gt__(self, value, /) | Return self>value. | | __iadd__(self, value, /) | Implement self+=value. | | __imul__(self, value, /) | Implement self*=value. | | __init__(self, /, *args, **kwargs) | Initialize self. See help(type(self)) for accurate signature. | | __iter__(self, /) | Implement iter(self). | | __le__(self, value, /) | Return self<=value. | | __len__(self, /) | Return len(self). | | __lt__(self, value, /) | Return self<value. | | __mul__(self, value, /) | Return self*value. | | __ne__(self, value, /) | Return self!=value. | | __repr__(self, /) | Return repr(self). | | __reversed__(self, /) | Return a reverse iterator over the list. | | __rmul__(self, value, /) | Return value*self. | | __setitem__(self, key, value, /) | Set self[key] to value. | | __sizeof__(self, /) | Return the size of the list in memory, in bytes. | | append(self, object, /) | Append object to the end of the list. | | clear(self, /) | Remove all items from list. | | copy(self, /) | Return a shallow copy of the list. | | count(self, value, /) | Return number of occurrences of value. | | extend(self, iterable, /) | Extend list by appending elements from the iterable. | | index(self, value, start=0, stop=9223372036854775807, /) | Return first index of value. | | Raises ValueError if the value is not present. | | insert(self, index, object, /) | Insert object before index. | | pop(self, index=-1, /) | Remove and return item at index (default last). | | Raises IndexError if list is empty or index is out of range. | | remove(self, value, /) | Remove first occurrence of value. | | Raises ValueError if the value is not present. | | reverse(self, /) | Reverse *IN PLACE*. | | sort(self, /, *, key=None, reverse=False) | Sort the list in ascending order and return None. | | The sort is in-place (i.e. the list itself is modified) and stable (i.e. the | order of two equal elements is maintained). | | If a key function is given, apply it once to each list item and sort them, | ascending or descending, according to their function values. | | The reverse flag can be set to sort in descending order. | | ---------------------------------------------------------------------- | Class methods defined here: | | __class_getitem__(...) from builtins.type | See PEP 585 | | ---------------------------------------------------------------------- | Static methods defined here: | | __new__(*args, **kwargs) from builtins.type | Create and return a new object. See help(type) for accurate signature. | | ---------------------------------------------------------------------- | Data and other attributes defined here: | | __hash__ = None
Se li definiamo all'interno di una classe possiamo invocarli direttamente sille istanze.
__iter__ __next__
class Accordo:
'''
Definisce un accordo
'''
def __init__(self, altezze=None,mul=1):
self.altezze = altezze
self.mul = mul
def __add__(self, other):
return self.altezze + other.altezze
def __mul__(self, other):
return self.altezze * other.mul
def __len__(self):
return len(self.altezze)
a1 = Accordo([60,64,67,89],1)
a2 = Accordo([70,75,87],3)
print(a1 + a2) # Concatena le due liste...
print(a1 * a2) # Espande le due liste...
print(len(a1)) # Restituisce la capacità della lista
[60, 64, 67, 89, 70, 75, 87] [60, 64, 67, 89, 60, 64, 67, 89, 60, 64, 67, 89] 4
Possiamo esportare il contenuto di uno script come file di testo in due modi specificandone eventualmente l'estensione (.py, .ly, .scd, .txt, etc.).
Nelle pratiche legate all'informatica musicale questa operazione ci può essere utile in tre situazioni:
scrivere uno o più moduli personali nel quale specificare classi, variabili globali e funzioni di uso comune e da riutilizzare in altri files.
scrivere una partitura simbolica da importare e/o modificare in altri file.
generare automaticamente files eseguibili da terminale che permettono la comunicazione e lo scambio di codice tra diversi software.
Direttamente dalla cella di un Notebook attraverso un comando magico (in questo caso non dobbiamo formattare il codice sotto forma di stringa:
%%writefile esempi/02_Salva_1.py
c = 60
print(c)
Overwriting esempi/02_Salva_1.py
f = open("esempi/03_Salva_2.ly", "w")
out = "\\relative c\'{ a b c d }" # Stringa
f.write(out) # Scrive il contenuto nel file
23
f.close()
Possiamo anche leggere un file di testo esterno.
f = open("esempi/03_Salva_2.ly", "r") # read
x = f.read()
print(x)
\relative c'{ a b c d }
Un ambiente virtuale è uno strumento che aiuta a mantenere separate le dipendenze richieste da diversi progetti creando per loro ambienti virtuali isolati.
Un ambiente virtuale è una cartella che contiene una copia privata di Python e di tutti i pacchetti installati.
Se ad esempio per un progetto stiamo lavorando con una specifica versione di python che accetta specifiche versioni di librerie possiamo creare un ambiente virtuale all'interno del quale possiamo installare python, le librerie necessarie e tutti i files che saranno utilizzati nel progetto (audio, video, immagini, etc.).
Se poi dobbiamo condividere il progetto con altre persone queste dovranno solamente ricreare l'ambiente virtuale nel loro computer senza preoccuparsi delle dipendenze oppure del fatto che sul loro computer c'è installata una versione più recente o più vecchia di python.
Un nuovo ambiente virtuale dovrebbe essere usato ogni volta che lavoriamo su di un progetto basato su Python.
Per creare e gestire gli ambienti virtuali utilizziamo un modulo chiamato venv e dei comandi da terminale.
python -m venv mioProgetto
E' stata creata una nuova cartella con all'interno:
Ora dobbiamo attivare l'ambiente virtuale.
In Mac e Linux:
source ./mioProgetto/bin/activate
In Windows: