Home

Pymus¶

Indice¶

  • Introduzione
  • Prerequisiti
  • Installazione
  • Ambienti operativi
  • Risorse in rete
  • Utilità
  • Variabili
    • Nomi legali
    • Nomi illegali
    • Sintassi
    • Assegnazione valori multipli
    • Assegnazione un valore a più variabili
    • Utilizzo
    • Riassegnazione
    • Cancellazione
    • Costanti
  • Tipi di dato
    • Dati singoli
    • Collezioni
    • Casting
  • Librerie e moduli
  • Numeri
  • Stringhe
    • Creare
    • Concatenare
    • Eliminare
    • Escape character
    • Formattare
  • Collezioni, operazioni e indici
    • Liste
    • Tuple
    • Dictionary
    • Set
    • Array (NumPy)
    • Operazioni matematiche
    • Indexing e slicing
  • Boolean, condizioni e cicli (strutture di controllo)
    • Espressioni condizionali
    • Iterazione (ciclo for)
    • Iterazione (ciclo while)
    • List comprehension
    • Generator expression
  • Modelli informatici
  • Funzioni
  • Moduli custom
  • Programmazione orientata ad oggetti
    • Classi e istanze
    • Metodi
    • Variabili di istanza e variabili di classe
    • Ereditarietà
    • Decoratori
      • @propriety
      • @classmethod
      • @staticmethod
    • Metodi speciali (dunder methods)
  • Esportare/importare file di testo
  • Ambienti virtuali
  • Synth

Introduzione Indice

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

Prerequisiti Indice

  • Familiarità con concetti comuni ai diversi linguaggi di programmazione (variabili, tipi di dato, funzioni, operazioni aritmetiche, etc.).
  • Familiarità con concetti comuni alle problematiche legate all'informatica musicale.

Installazione Indice

Scarichiamo ed installiamo:

  • Python (Anaconda distribution)
  • VScode (Incluso con l'Anaconda distribution)

Ambienti operativiIndice

Possiamo eseguire il codice di python in diversi modi.

Per i nostri scopi osserviamo due approcci differenti.

Notebook¶

Dalle celle di questo Notebook - modalità comoda per presentazioni e lezioni.

  1. Selezioniamo la cella da eseguire e poi ctrl + enter. I dati a schermo (output) sono visualizzati sotto la cella.
In [1]:
out = "La bella gigogin" # Codice
print(out)               # Scrive il codice in ouput 
La bella gigogin

Editor esterno (VSCode)¶

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.

  1. Lanciamo VSCode (per evitare possibili problematiche di incompatibilità in questa fase di studio lanciamo la versione rilasciata con Anaconda-navigator).
In [2]:
from IPython.display import HTML
HTML('<center><img src="media/vsc_1.png" width="80%"></center>')
Out[2]:
  1. Installiamo l'estensione per python cercandola tra quelle disponibili nel marketplace.
In [3]:
from IPython.display import HTML
HTML('<center><img src="media/vsc_2.png" width="40%"></center>')
Out[3]:
  1. Apriamo in VSCode un nuovo file di python
  2. Scriviamo del codice
  3. Salviamolo in una nuova cartella dedicata.
In [4]:
from IPython.display import HTML
HTML('<center><img src="media/vsc_3.png" width="45%"></center>')
Out[4]:
  1. Clicchiamo su esplora risorse (l'icona con le due pagine in alto a sinistra).
  2. Selezioniamo la cartella (Apri cartella) che contiene il file.
In [5]:
from IPython.display import HTML
HTML('<center><img src="media/vsc_4.png" width="26%"></center>')
Out[5]:
  1. Selezioniamo il file su cui vogliamo lavorare.
  2. Apriamo un Nuovo terminale (Menù Terminale).
  3. Eseguiamo il codice python cliccando sulla freccia in alto a destra.

Se tutto è andato a buon fine l'output del nostro programma viene visualizzato nel terminale.

In [6]:
from IPython.display import HTML
HTML('<center><img src="media/vsc_5.png" width="85%"></center>')
Out[6]:

Terminale¶

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

In [7]:
from IPython.display import HTML
HTML('<center><img src="media/vsc_6.png" width="70%"></center>')
Out[7]:

Ora possiamo scrivere una linea di codice ed eseguirla con enter.

Per uscire dalla modalità interattiva scrivere exit() oppure quit() + enter

Risorse in rete Indice

  • Sito della comunità italiana di Python.
  • Sito di Python.
  • Think Python (in italiano).
  • SoftPython (in italiano).
  • Python su YouTube (in italiano).
  • Py4e.

UtilitàIndice

Terminale

  • Pulire il Terminale: cmd + k.
  • Uscire dal Terminale: scrivere q + enter.

Python

  • I commenti sono preceduti da #.
  • Help di un oggetto in python: scrivere help(nome_oggetto)
  • Best pratics.

Notebook

  • Se vogliamo eseguire comandi da Terminale (Shell) all'interno di una cella scriviamo ! all'inizio della linea.

VScode

  • Commentare e decommentare: cmd + shift + /
  • Eseguire python: tasto destro: Run Python File in Terminal oppure schiacciare freccina in alto a dx.
  • Uscire da una cartella di lavoro: tasto destro: Rimuovi cartella dall'area di lavoro.

Variabili Indice

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

In [8]:
ciao = 12    # etichetta = contenuto
print(ciao)  # print() comando che genera l'output
12

In python a differenza di altri linguaggi di programmazione:

  • Non possiamo dichiarare variabili vuote per poi assegnare data successivamete.
  • Non dobbiamo dichiarare il tipo di data assegnato.

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.

In [9]:
DoM = (60, 64, 67, 72)

Li potremo richiamare a nostro piacimento scrivendone solo l'etichetta.

In [10]:
print(DoM)
print(DoM)
print(DoM)
(60, 64, 67, 72)
(60, 64, 67, 72)
(60, 64, 67, 72)

Osserviamo alcune regole proprie di Python.

Nomi legali Indice (utilizzabili)

In [11]:
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 

Nomi illegali Indice (vietati)

In [12]:
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

Sintassi Indice diverse

In [13]:
ciaoCiao  = 2 # Camel case
CiaoCiao  = 2 # Pascal case
ciao_ciao = 2 # Snake case (suggerito come best pratice di Python)

Assegnazione valori multipli Indice

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.

In [14]:
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.

Assegnazione un valore a più variabili Indice

Il codice seguente potrebbe rappresentare musicalmente la dichiarazione di una condizione enarmonica.

In [15]:
c = bs = dff = 60  # do, si diesis, re doppio bemolle...

print(c)
print(bs)
print(dff)
60
60
60

Utilizzo Indice

Il codice seguente potrebbe rappresentare musicalmente un algoritmo di trasposizione ma in realtà è una semplice somma tra due interi.

In [16]:
c     = 60      # valore MIDI
trasp = 12      # trasposizione in semitoni

out = c + trasp # algoritmo
print(out)      # risultato
72

Riassegnazione Indice

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.

In [17]:
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.

Cancellazione Indice

In [18]:
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

Costanti Indice

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.

In [19]:
SEMITONI_PER_OTTAVA = 12

print(SEMITONI_PER_OTTAVA)
12

Tipi di dato Indice

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:

  • il valore.
  • il tipo di operazione che possiamo effettuare su di esso.
  • l'ammontare di memoria da assegnare alla variabile.

Per ottenere informazioni sul tipo di data di un oggetto:

In [20]:
x = 12

type(x)     # riporta il tipo di data
Out[20]:
int

In Python così come nella maggior parte dei linguaggi informatici esistono due tipologie di dati.

Dati singoli Indice

In [21]:
# --------------------- 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

Collezioni Indice

In [22]:
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]

Casting Indice

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.

In [23]:
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

Librerie e moduli Indice

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.

Pip install¶

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:

In [ ]:
pip --version

Se non è installato scriviamo ed eseguiamo nel Terminale (non da questo Notebook):

In [ ]:
python get-pip.py

A questo punto possiamo installare le librerie che vogliamo direttamente dal Terminale (non da questo Notebook):

In [ ]:
pip install camelcase

Oppure disintallarle:

In [ ]:
pip uninstall camelcase

Se vogliamo conoscere quali librerie sono installate sul computer:

In [ ]:
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).

Importare moduli¶

Per poter utilizzare i moduli di una libreria dobbiamo:

  1. importarli all'inizio dello script di python.
  2. richamare le funzioni desiderate attraverso la sintassi illustrata.
In [24]:
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:

In [25]:
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:

In [26]:
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.

In [27]:
from random import randint

x = randint(10,15)
print(x)
14

Numeri Indice

Abbiamo visto come in Python abbiamo a disposizione quattro tipi di numeri:

  • int - interi.
  • float - decimali.
  • boolean - booleiani.
  • complex complessi.

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.

In [28]:
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.

Stringhe Indice

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.

Creare Indice

Possiamo crearle in tre modi:

  1. Su una linea - singolo o doppio apice.
In [29]:
#    0123
x = "ciao" # doppio apice
y = 'miao' # singolo apice (da preferire)

print(x, y)
print(y)
ciao miao
miao
  1. Su più linee - tre singoli o doppi apici.
In [30]:
x = """ciao 
amici
miei"""

print(x)
ciao 
amici
miei
  1. Con la funzione str('')
In [31]:
x = str("ciao sono Andrea")

print(x)
ciao sono Andrea

Concatenare Indice

In [32]:
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 }

Eliminare Indice

In [33]:
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

Escape character Indice

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:

  • \' - apice. Definisce un apice come semplice carattere.
  • \" - doppie virgolette. Definisce le doppie virgolette come semplice carattere.
  • \\ - doppio backslash. Definisce il backslash come semplice carattere.
  • \n - newline. Va a capo in una nuova linea.
  • \r - carriage return. Va a capo.
  • \t - tab. Aggiunge una tabulazione (4 spazi).
  • \b - backspace.

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.

In [34]:
# 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
In [35]:
# 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'' }

Formattare Indice

Se vogliamo inserire all'interno di una stringa altri tipi di dato come int in precise posizioni possiamo utilizzare quattro diverse modalità:

  1. Parametri ordinati

    • Definiamo le variabili con i valori che vogliamo inserire nella stringa (possono essere qualsiasi tipo di dato).
    • Inseriamo nella stringa alle posizioni desiderate la formula { }.
    • Invochiamo sulla variabile il metodo .format( ) specificando l'ordine in cui vogliamo inserire i dati collegati alle variabili.
In [36]:
# --------------------- 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 | 
  1. Parametri indicizzati

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.

In [37]:
# 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 
  1. Indici nominali

Come nei casi precedenti ma al posto degli indici specifichiamo i nomi direttamente nelle parentesi graffe

In [38]:
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 |
  1. Operatore di formato

    Se utilizziamo il simbolo % all'interno di una stringa viene interpretato come operatore di formato.

    • %d = decimale
    • %q = floating-point
    • %s = stringa

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

In [39]:
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

Collezioni Indice

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:

  • Ordinata - gli items sono in un ordine ben definito. L'aggiunta di nuovi elementi o la rimozione di quelli presenti non incide sull'ordine (possono cambiare gli indici ma rimane ordinata).
  • Indicizzata - ad ogni item corrisponde un indice che comincia da 0:
    indici      0    1   2   3
    elementi  ('a', 'b', 12, 34.34)
    Questo ci permette di richiamare i singoli elementi attraverso gli indici.
  • Immutabile - non possiamo aggiungere o rimuovere elementi dopo aver creato la collezione.
  • Modificabile - possiamo aggiungere o rimuovere elementi dopo aver creato la collezione.
  • Permettere duplicati - può contenere elementi con lo stesso valore.
  • Non permettere duplicati - non può contenere elementi con lo stesso valore.

Tuple Indice

Tra parentesi tonde.

  • Ordinate
  • Indicizzate
  • Non modificabili
  • Permettono duplicati

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

In [40]:
#         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

Liste Indice

Tra parentesi quadre.

  • Ordinate
  • Indicizzate
  • Modificabili
  • Permettono duplicati

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.

In [41]:
#         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']

Dictionary Indice

Tra parentesi graffe.

  • Ordinate
  • Non indicizzate
  • Modificabili
  • Non permettono duplicati

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.

In [42]:
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'

Set Indice

Tra parentesi graffe.

  • Non ordinate
  • Non indicizzate
  • Non modificabili
  • Non permettono duplicati

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.

In [43]:
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

Array (NumPy) Indice

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.

  • Ordinati
  • Indicizzati
  • Modificabili
  • Permettono duplicati
In [44]:
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]

Operazioni matematiche Indice

Sugli Array di NumPy a differenza delle altre collezioni possiamo effettuare operazioni matematiche su tutti gli items della collezione.

In [45]:
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.

In [46]:
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.

  • Un'operazione tra int e float restituisce un float.
  • L'ordine delle operazioni è:
    *  /  +  - 
In [47]:
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. ]
  • Operazioni di assegnamento.
In [48]:
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).

In [49]:
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.  ]

Indexing e slicing Indice

Possiamo richiamare i singoli elementi di quasi tutte le collezioni di python attraverso l'indice o la chiave:

In [50]:
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.

In [51]:
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).

In [52]:
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]

Boolean, condizioni e cicli Indice

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.

In [53]:
x = 5 < 10 # True
y = 5 > 10 # False

print(x)
print(y)
True
False

Le seguenti assegnazioni di variabili restituiscono sempre False.

In [54]:
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:

  1. Operatori di comparazione (o relazionali).
In [55]:
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
  1. Operatori logici
In [56]:
# --------------------- 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.

  1. costrutto if True::

    • se la condizione è vera: esegui l'operazione (o le operazioni). Notiamo che print(x) è spostato rispetto al margine sinistro di una tabulazione (quattro spazi). Questo perchè in python non si usano parentesi per definire blocchi di codice ma l'indentazione. Nel caso di if tutte le operazioni all'interno del costrutto saranno indentate con una tabulazione. Le indentazioni corrette sono automatiche se andiamo a capo con enter.
In [57]:
x = 4

if x < 10:      # se x è minore di 10
    print(x)    # stampa il valore in output
4
  1. costrutto if con else:

    se la condizione è vera: esegui questa operazione altrimenti esegui quest'altra operazione

In [58]:
x = 12

if x < 10:
    print(True)
else:
    print(False)
False
  1. 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

    ...

In [59]:
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
  1. costrutto con if annidati:
In [60]:
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

Espressioni condizionali Indice

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.

In [61]:
x = 65

if x > 60:
    y = x + 12
else:
    y = x
    
print(y)
77

Può essere riscritta in questo modo:

In [62]:
x = 57
y = x + 12 if x > 60 else x
print(y)
57

Iterare (ciclo for) Indice

Possiamo iterare gli elementi di una collezione richiamandoli uno a uno con il ciclo for. Abbiamo a disposizione due possibilità:

  1. Iterazione completa - richiamiamo direttamante gli elementi della collezione.
In [63]:
#       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
  1. Iterazione parziale (slicing) - richiamiamo gli indici.
In [64]:
#       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.

  • else - quando finisce l'iterazione esegue l'operazione.
In [65]:
#       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 
  • break - se vogliamo interrompere l'iterazione al verificarsi di una condizione.
In [66]:
#       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
  • continue - se vogliamo saltare un elemento al verificarsi di una condizione.
In [67]:
#       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 indentati - possiamo inserire dei cicli dentro altri cicli.
In [68]:
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]

Iterare (ciclo while) Indice

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.

In [69]:
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.

In [70]:
# --------------------- 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

List comprehension Indice

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.

In [71]:
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:

In [72]:
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]

Generator expression Indice

Sono simili alle list comprehension ma con parenesi tonde invece che quadre.

In [73]:
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.

In [74]:
next(o)
Out[74]:
0

Alla fine dell'iterazione solleva un'eccezione StopIteration.

Modelli informatici Indice

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:

  • Funzioni
  • Classi

Funzioni Indice

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:

  • un nome
  • uno (o più) dati in ingresso (parametri della funzione).
  • una serie di operazioni da effettuare sui dati inviati ai parametri (argomenti).
  • uno (o più) dati in uscita (il risultato delle operazioni effettuate sui dati in ingresso).

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

  1. Definiamo l'algoritmo secondo le nostre necessità musicali.
In [75]:
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]
  1. Definiamo una funzione che contiene l'algoritmo.
In [76]:
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)
  1. Richiamiamo la funzione passando gli argomenti.
In [77]:
a = r_minmax(60,72,5)             # Funzione con argomenti (Dati in ingresso)

print(a)
[68 60 71 60 71]

Operazioni principali delle funzioni¶

  • Argomenti arbitrari - quando non conosciamo il numero esatto di argomenti.
In [78]:
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]
  • Keywords.
In [79]:
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]
  • Return. Le funzioni possono essere produttive se effettuano le operazioni e restituiscono dati in uscita o non produttive se effettuano le operazioni ma non restituiscono valori in uscita (per inizializzazioni o simili).
  • Return booleiani.
In [80]:
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
  • Funzioni ricursive. Possiamo richiamare una funzione all'interno di se stessa.
In [81]:
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!

Modularità Indice

Possiamo salvare una o più funzioni in un file con l'estensione .py.

In [82]:
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.

In [83]:
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.

In [84]:
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

In [85]:
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.

Programmazione orientata agli oggetti Indice

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.

Classi e istanze Indice

Se dovessimo definire attraverso il codice una sequenza musicale avremmo tre possibilità:

  • Memorizzare le note che lo formano in più variabili.
  • Memorizzare le note che lo formano in una collezione.
  • Creare una nuova Classe che rappresenta qualsiasi tipo di sequenza di altezze come oggetto.

Pensiamo una classe come uno stampino che ci permette di fabbricare diversi oggetti del medesimo tipo (istanze).

In [86]:
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.

In [87]:
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.

In [88]:
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
In [89]:
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

Metodi Indice

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.

In [90]:
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.

In [91]:
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).

In [92]:
class Sequenza:                                   
    '''Rappresenta le altezze di una sequenza'''   
    
    def note(*args):      # metodo con attributi dinamici
        return args       # senza l'asterisco...        
In [93]:
Sequenza.note(60, 64, 67, 72, 80,34)
Out[93]:
(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.

In [94]:
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:

  • il primo attributo di init è 'self' e serve per differenziare le singole istanze.
  • le proprietà sono riferite alla singola istanza e di prassi coincidono con gli attributi della funzione init.
  • per ottenere i valori (compiere un'azione) abbiamo definito un metodo separato che accetta come attributi tutte le propietà della singola istanza.
  • per trasportare abbiamo definito un ulteriore metodo.

Generiamo tre istanze

In [95]:
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()

In [96]:
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)
In [97]:
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.

In [98]:
a.nota1 = 45       # modifica l'attributo

print(a.ottieni())
(45, 64, 67)

Se vogliamo informazioni riguardo le caratteristiche di una Classe.

In [99]:
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.

In [100]:
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'

Variabili di classe Indice

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.

In [101]:
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 
In [102]:
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.

In [103]:
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:

  • le Classi sono come stampini per la creazione di oggetti e corrispondono a cose o concetti del mondo reale. Musicalmente ad esempio potremmo definire la classe 'Scala'.
  • le istanze sono oggetti costruiti a partire dallo stampino, ognuna delle quali ha caratteristiche diverse. Musicalmente potremmo costruire 'DO_maggiore', 'RE_minore', etc.
  • i metodi sono azioni che possiamo richiedere di compiere invocandoli sulle singole istanze. Musicalmente potremmo chiedere di restituirci solo alcune altezza di una scala.
  • le variabili sono gli attributi (o proprietà) che caratterizzano le singole istanze (variabili d'istanza) oppure che sono comuni a tutte le istanze (variabili di classe). Musicalmente gli intervalli che caratterizzano la scala specifica (maggiore, minore,etc.).
In [104]:
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

Ereditarietà Indice

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.

  • scrivedo gli intervalli della sequenza come attributo della classe sotto forma di lista.
  • inserendo ogni singolo intervallo digitandolo sulla tastiera del computer.

In questa ultima modalità vogliamo poter effettuare anche le seguenti operazioni.

  • cancellare l'ultima immissione.
  • pulire il contenuto della lista.
  • ottenere una lista di midinote in output calcolata su una nota perno definita come variabile di classe (in quanto comune a tutti gli accordi).
In [105]:
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.

In [106]:
seq = Sequenza([0,4,12])  
[0, 4, 12]

Se vogliamo inserirle una alla volta.

In [107]:
seq = Sequenza()  
[]
In [116]:
seq.inserisci() # inserisce
scrivi nota
84
[65, 72, 84]
In [111]:
seq.cancella()  # cancella l'ultima nota
[65, 72]
In [112]:
seq.pulisci()   # cancella tutta la lista
[]
In [117]:
seq.interv()       # lista in intervalli
Out[117]:
[65, 72, 84]
In [118]:
seq.note()        # lista in midinote
Out[118]:
[125, 132, 144]

Supponiamo ora di voler definire due nuove classi.

  • Scala
  • Serie

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.

In [119]:
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
In [120]:
seq = Scala([0,2,4,5,5,7,9,11], 'r') # c'è un duplicato...
[0, 2, 4, 5, 5, 7, 9, 11]
In [121]:
seq.no_dup()
5 eliminato dalla scala
[0, 2, 4, 5, 7, 9, 11]
In [122]:
a = seq.ordina()
print(a)
[11, 5, 0, 7, 2, 4, 9]
In [123]:
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]
In [124]:
seq = Scala() 
[]
In [128]:
seq.inserisci() 
scrivi nota
7
[0, 4, 4, 7]
In [129]:
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.

In [130]:
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
In [131]:
seq = Scala([0,2,4,6])
[0, 2, 4, 6]
In [132]:
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.

In [133]:
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.

In [134]:
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]
In [135]:
seq = Serie([0,3,5,4,1])
[0, 3, 5, 4, 1]
In [136]:
seq.recto()    # originale

print(seq.interv())
print(seq.note())
[0, 3, 5, 4, 1]
[60, 63, 65, 64, 61]
In [137]:
seq.verso()     # retrogrado

print(seq.interv())
print(seq.note())
[1, 4, 5, 3, 0]
[61, 64, 65, 63, 60]
In [138]:
seq.inv()     # inversione

print(seq.interv())
print(seq.note())
[0, -3, -5, -4, -1]
[60, 57, 55, 56, 59]
In [139]:
seq.rinv()     # retrogrado dell'inversione

print(seq.interv())
print(seq.note())
[-1, -4, -5, -3, 0]
[59, 56, 55, 57, 60]

Decoratori Indice

In Python un decoratore è una funzione che:

  • prende come argomento un'altra funzione,
  • aggiunge delle funzionalità o operazioni.
  • restituisce una nuova funzione senza alterare il codice di quella passata come argomento.

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.

In [140]:
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:

  • settare uno o più valori accedendo agli attributi (setter).
  • ottenere i valori memorizzati in una o più proprietà (getter).

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.

In [141]:
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.

In [142]:
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.

  • costruttore alternativo - come esempio nel codice seguente abbiamo aggiunto la possibilità alla classe Sequenza di specificare le altezze sotto forma di stringa e popolare automaticamente la lista.
  • metodo dinamico ovvero che cambia comportamento a seconda delle sottoclassi da cui viene ereditato.
In [143]:
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
In [144]:
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.

In [145]:
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)
Out[145]:
0.995

Metodi speciali (dunder methods) Indice

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.

In [146]:
seq = [0,4,6,7,8] # lista

seq[2]            # richiamiamo l'item
Out[146]:
6

Attraveso la sintassi precedente in realtà Python richiama implicitamente il metodo __getitem__ che possiamo anche utilizzare esplicitamente.

In [147]:
seq = [0,4,6,7,8] # lista

seq.__getitem__(2)            # richiamiamo l'item
Out[147]:
6

Se richiamiamo l'help del nostro oggetto possiamo leggere tutti i dunder method che possiamo invocare su di esso sia implicitamente che esplicitamente.

In [148]:
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__

In [149]:
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

Esportare/importare file di testo Indice

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.

Dal Notebook¶

Direttamente dalla cella di un Notebook attraverso un comando magico (in questo caso non dobbiamo formattare il codice sotto forma di stringa:

In [150]:
%%writefile esempi/02_Salva_1.py 

c = 60
print(c)
Overwriting esempi/02_Salva_1.py

Dal codice¶

  1. Apriamo o generariamo il file di testo:
    • primo argomento: " nome.estensione ".
    • secondo argomento:
      • r - Read - apre il file, dà errore se non esiste.
      • a - Append - apre il file per aggiungere alla fine, crea il file se non esiste.
      • w - Write - apre il file per scrivere, crea il file se non esiste.
      • x - Create - crea il file per scrivere, da errore se esiste
In [151]:
f = open("esempi/03_Salva_2.ly", "w") 
  1. Scriviamo il contenuto al suo interno.
In [152]:
out = "\\relative c\'{ a b c d }" # Stringa
f.write(out)                      # Scrive il contenuto nel file                     
Out[152]:
23
  1. Chiudiamo il file in python.
In [153]:
f.close()                             

Possiamo anche leggere un file di testo esterno.

In [154]:
f = open("esempi/03_Salva_2.ly", "r") # read 
x = f.read()

print(x)
\relative c'{ a b c d }

Ambienti virtuali Indice

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.

  • Creiamo un nuovo ambiente virtuale
In [ ]:
python -m venv mioProgetto

E' stata creata una nuova cartella con all'interno:

  • una cartella chiamata bin (Scripts in window) che contiene a sua volta diversi scripts.
  • una cartella vuota chiamata include.
  • una cartella chiamata lib che contiene le librerie già instalate.
  • uno script di configurazione.

Ora dobbiamo attivare l'ambiente virtuale.

In Mac e Linux:

In [ ]:
source ./mioProgetto/bin/activate

In Windows:

In [ ]:
# su Windows, da PowerShell
.\nome_cartella_ambiente\Scripts\Activate.ps1

# su Windows, da cmd
nome_cartella_ambiente\Scripts\activate

Su Windows potrebbe capitare un errore relativo all'esecuzione di Script sul sistema. Ci sono diverse soluzioni al riguardo.

Per verificare potete verificare quale versione di python è in uso nell'ambiente virtuale.

In [ ]:
which python

Oppure verificare quali pacchetti sono installati.

In [ ]:
pip list

Ora possiamo installare le librerie che vogliamo utilizzare nel nostro ambiente virtuale.

In [ ]:
pip install numpy

Per verificare quali pacchetti abbiamo installato.

In [ ]:
pip freeze

Per disinstallare un pacchetto.

In [ ]:
pip uninstall numpy

Possiamo creare automaticamente un file requirements.txt che include tutti i pacchetti installati.

In [ ]:
pip freeze > requirements.txt

Ora se vogliamo eseguire il progetto su un altro computer o condividerlo con qualcun altro basterà digitare il comando seguente per installare tutti i pacchetti necessari per eseguire il progetto.

In [ ]:
pip install -r requirements.txt

Per disattivare l'ambiente virtuale.

In [ ]:
deactivate