10  Introduzione a Python

Python è un linguaggio di programmazione multiparadigma, cioè abilita o supporta più paradigmi di programmazione, e multipiattaforma, potendo essere installato e utilizzato su gran parte dei sistemi operativi e hardware.

La storia di Python inizia colla pubblicazione del codice sorgente, da parte del suo creatore Guido van Rossum, nel 1991, nella versione 0.9.0. Circa tre anni dopo, nel 1994, raggiungerà la prima versione definitiva, quindi nove anni dopo il C++ e un anno prima di PHP e due rispetto a Java.

Python offre una combinazione unica di eleganza, semplicità, praticità e versatilità. Questa eleganza e semplicità derivano dal fatto che è stato progettato per essere molto simile al linguaggio naturale inglese, rendendo il codice leggibile e comprensibile. La sintassi di Python è pulita e minimalista, evitando simboli superflui come parentesi graffe e punti e virgola, e utilizzando indentazioni per definire blocchi di codice, il che forza una struttura coerente e essenziale. La semantica del linguaggio è intuitiva e coerente, il che riduce la curva di apprendimento e minimizza gli errori, anche per programmatori non professionali.

Python è gestito dalla Python Software Foundation (PSF), un’organizzazione no-profit che si occupa dello sviluppo e della promozione del linguaggio. Il sito ufficiale di Python, python.org, è la risorsa principale dove è possibile trovare la documentazione ufficiale, scaricare il linguaggio, e accedere a tutorial, guide e altre risorse utili.

Il processo di aggiornamento di Python è trasparente e comunitario. Le proposte di miglioramento del linguaggio vengono discusse attraverso le Python Enhancement Proposals (proposte di aggiornamento di Python, PEP), documenti di design che forniscono informazioni alle comunità di Python su nuove funzionalità proposte, miglioramenti al linguaggio e altre questioni correlate. Le PEP vengono valutate e accettate da un comitato di sviluppo centrale.

Python è distribuito sotto la Python Software Foundation License, una licenza open-source che consente l’uso, la modifica e la distribuzione del linguaggio senza costi. Questa licenza garantisce che Python rimanga libero e accessibile a tutti, permettendo l’uso commerciale e non commerciale. Per quanto riguarda i documenti e la documentazione, essi sono generalmente distribuiti sotto la licenza Creative Commons Attribution-NonCommercial-ShareAlike (CC BY-NC-SA), che consente di condividere e adattare il materiale, purché venga attribuito correttamente, non sia utilizzato per scopi commerciali e sia distribuito con la stessa licenza.

Tecnicamente, Python è un linguaggio interpretato e dinamico. Essere interpretato significa che il codice sorgente di Python viene eseguito direttamente dall’interprete, senza la necessità di una fase di compilazione separata. Questo offre vantaggi come la facilità di esecuzione del codice e il supporto per il debugging interattivo, ma può comportare una velocità di esecuzione inferiore rispetto ai linguaggi compilati.

Essere dinamico significa che molti aspetti del linguaggio, come i tipi delle variabili, vengono determinati a runtime piuttosto che a compile-time. Questo consente una maggiore flessibilità e facilita lo sviluppo rapido, poiché non è necessario dichiarare esplicitamente i tipi di variabili. Tuttavia, questo approccio può anche portare a errori di tipo che vengono rilevati solo durante l’esecuzione del programma.

Diventerai rapidamente produttivo con Python grazie alla sua coerenza e regolarità, alla sua ricca libreria standard e ai numerosi pacchetti e strumenti di terze parti prontamente disponibili. Python è facile da imparare, quindi è molto adatto se sei nuovo alla programmazione, ma è anche potente abbastanza per i più sofisticati esperti. Questa semplicità ha attratto una comunità ampia e attiva che ha contribuito sia alle librerie di programmi incluse nell’implementazione ufficiale, che a molte librerie scaricabili liberamente, ampliando ulteriormente l’ecosistema di Python.

10.1 Perché Python è un linguaggio di alto livello?

Python è considerato un linguaggio di programmazione di alto livello, cioè utilizza un livello di astrazione elevato rispetto alla complessità dell’ambiente in cui i suoi programmi sono eseguiti. Il programmatore ha a disposizione una sintassi che è più intuitiva rispetto ad altri linguaggi come Java, C++, PHP tradizionalmente anch’essi definiti di alto livello.

Infatti, consente ai programmatori di scrivere codice in modo più concettuale e indipendente dalle caratteristiche degli hardware, anche molto diversi, su cui è disponibile. Ad esempio, invece di preoccuparsi di allocare e deallocare memoria manualmente, Python gestisce queste operazioni automaticamente. Questo libera il programmatore dai dettagli del sistema operativo e dell’elettronica, permettendogli di concentrarsi sulla logica del problema da risolvere.

Ciò ha un effetto importante sulla versatilità perché spesso è utilizzato come interfaccia utente per linguaggi di livello più basso come C, C++ o Fortran. Questo permette a Python di sfruttare le prestazioni dei linguaggi compilati per le parti critiche e computazionalmente intensive del codice, mantenendo al contempo una sintassi semplice e leggibile per la maggior parte del programma. Buoni compilatori per i linguaggi compilati classici possono sì generare codice binario che gira più velocemente di Python, tuttavia, nella maggior parte dei casi, le prestazioni delle applicazioni codificate in Python sono sufficienti.

10.2 Python come linguaggio multiparadigma

Python è un linguaggio di programmazione multiparadigma, il che significa che supporta diversi paradigmi di programmazione, permettendo di mescolare e combinare gli stili a seconda delle necessità dell’applicazione. Ecco alcuni dei paradigmi supportati da Python:

  • Programmazione imperativa: Puoi scrivere ed eseguire script Python direttamente dalla linea di comando, permettendo un approccio interattivo e immediato alla programmazione, come se fosse una calcolatrice.

  • Programmazione procedurale: In Python, è possibile organizzare il codice in funzioni e moduli, rendendo più semplice la gestione e la riutilizzabilità del codice. Puoi raccogliere il codice in file separati e importarli come moduli, migliorando la struttura e la leggibilità del programma.

  • Programmazione orientata agli oggetti: Python supporta pienamente la programmazione orientata agli oggetti, consentendo la definizione di classi e oggetti. Questo paradigma è utile per modellare dati complessi e relazioni tra essi. Le caratteristiche orientate agli oggetti di Python sono concettualmente simili a quelle del C++, ma più semplici da usare.

  • Programmazione funzionale: Python include funzionalità di programmazione funzionale, come funzioni di prima classe e di ordine superiore, lambda e strumenti come map, filter e reduce.

Questa flessibilità rende Python adatto a una vasta gamma di applicazioni e consente ai programmatori di scegliere l’approccio più adatto al problema da risolvere.

10.3 Regole formali e esperienziali

Python non è solo un linguaggio con regole sintattiche precise e ben progettate, ma possiede anche una propria filosofia, un insieme di regole di buon senso esperienziali che sono complementari alla sintassi formale. Questa filosofia è spesso riassunta nel zen di Python, una raccolta di aforismi che catturano i principi fondamentali del design di Python. Tali principi aiutano i programmatori a comprendere e utilizzare al meglio le potenzialità del linguaggio e dell’ecosistema Python.

Ecco alcuni dei principi dello zen di Python1:

  • La leggibilità conta: Il codice dovrebbe essere scritto in modo che sia facile da leggere e comprendere.

  • Esplicito è meglio di implicito: È preferibile scrivere codice chiaro e diretto piuttosto che utilizzare scorciatoie criptiche.

  • Semplice è meglio di complesso: Il codice dovrebbe essere il più semplice possibile per risolvere il problema.

  • Complesso è meglio di complicato: Quando la semplicità non è sufficiente, la complessità è accettabile, ma il codice non dovrebbe mai essere complicato.

  • Pratico batte puro: Le soluzioni pragmatiche sono preferibili alle soluzioni eleganti ma poco pratiche.

Questi principi, insieme alle regole sintattiche, guidano il programmatore nell’adottare buone pratiche di sviluppo e nel creare codice che sia non solo funzionale ma anche mantenibile e comprensibile da altri.

10.4 L’ecosistema

Fino ad ora abbiamo visto Python come linguaggio, ma è molto di più: Python è anche una vasta collezione di strumenti e risorse a disposizione degli sviluppatori, strutturata in un ecosistema completo, di cui il linguaggio ne rappresenta la parte formale. Questo ecosistema è disponibile completamente, anche come sorgente, sul sito ufficiale python.org.

10.4.1 L’interprete

L’interprete Python è lo strumento di esecuzione dei programmi. È il software che legge ed esegue il codice Python. Python è un linguaggio interpretato, il che significa che il codice viene eseguito direttamente dall’interprete, senza bisogno di essere compilato in un linguaggio macchina. Esistono diverse implementazioni dell’interprete Python:

  • CPython: L’implementazione di riferimento dell’interprete Python, scritta in C. È la versione più utilizzata e quella ufficiale.

  • PyPy: Un interprete alternativo che utilizza tecniche di compilazione just-in-time (JIT) per migliorare le prestazioni.

  • Jython: Un’implementazione di Python che gira sulla JVM (Java Virtual Machine).

  • IronPython: Un’implementazione di Python integrata col .NET Framework della Microsoft.

10.4.2 L’ambiente di sviluppo

IDLE (integrated development and learning environment) è l’ambiente di sviluppo integrato ufficiale per Python. È incluso nell’installazione standard di Python ed è progettato per essere semplice e facile da usare, ideale per i principianti. Offre diverse funzionalità utili:

  • Editor di codice: Con evidenziazione della sintassi, indentazione automatica e controllo degli errori.

  • Shell interattiva: Permette di eseguire codice Python in modo interattivo.

  • Strumenti di debug: Include un debugger integrato con punti di interruzione e stepping.

10.4.3 Le librerie standard

Una delle caratteristiche più potenti di Python è il vasto insieme di librerie2 utilizzabili in CPython e IDLE, che fornisce moduli e pacchetti per quasi ogni necessità di programmazione. Alcuni esempi, tra le decine e al solo allo scopo di illustrarne la varietà, includono:

  • os: Fornisce funzioni per interagire con il sistema operativo.

  • sys: Offre accesso a funzioni e oggetti del runtime di Python.

  • datetime: Consente di lavorare con date e orari.

  • json: Permette di leggere e scrivere dati in formato JSON.

  • re: Supporta la manipolazione di stringhe tramite espressioni regolari.

  • http: Include moduli per l’implementazione di client e server HTTP.

  • unittest: Fornisce un framework per il testing del codice.

  • math e cmath: Contengono funzioni matematiche di base e complesse.

  • itertools, functools, operator: Offrono supporto per il paradigma di programmazione funzionale.

  • csv: Gestisce la lettura e scrittura di file CSV.

  • typing: Fornisce supporto per l’annotazione dei tipi di variabili, funzioni e classi.

  • email: Permette di creare, gestire e inviare email, facilitando la manipolazione di messaggi email MIME.

  • hashlib: Implementa algoritmi di hash sicuri come SHA-256 e MD5.

  • asyncio: Supporta la programmazione asincrona per la scrittura di codice concorrente e a bassa latenza.

  • wave: Fornisce strumenti per leggere e scrivere file audio WAV.

10.4.4 Moduli di estensione

Python supporta l’estensione del suo core tramite moduli scritti in C, C++ o altri linguaggi. Questi moduli permettono di ottimizzare parti critiche del codice o di interfacciarsi con librerie e API esterne:

  • Cython: Permette di scrivere moduli C estesi utilizzando una sintassi simile a Python. Cython è ampiamente utilizzato per migliorare le prestazioni di parti critiche del codice, specialmente in applicazioni scientifiche e di calcolo numerico. Ad esempio, molte librerie scientifiche popolari come SciPy e scikit-learn utilizzano Cython per accelerare le operazioni computazionalmente intensive.

  • ctypes: Permette di chiamare funzioni in librerie dinamiche C direttamente da Python. È utile per interfacciarsi con librerie esistenti scritte in C, rendendo Python estremamente versatile per l’integrazione con altre tecnologie. Ciò è utile in applicazioni che devono interfacciarsi con hardware specifico o utilizzare librerie legacy.

  • CFFI (C Foreign Function Interface): Un’altra interfaccia per chiamare librerie C da Python. È progettata per essere facile da usare e per supportare l’uso di librerie C complesse con Python. CFFI è utilizzato in progetti come PyPy e gevent, permettendo di scrivere codice ad alte prestazioni e di gestire le chiamate a funzioni C in modo efficiente.

10.4.5 Le utility e gli strumenti aggiuntivi

Python include anche una serie di strumenti e utility che facilitano lo sviluppo e la gestione dei progetti:

  • pip: Il gestore dei pacchetti di Python. Permette di installare e gestire moduli aggiuntivi, cioè non inclusi nello standard.

  • venv: Uno strumento per creare ambienti virtuali isolati, che permettono di gestire separatamente le dipendenze di diversi progetti.

  • Documentazione: Python include una documentazione dettagliata, accessibile tramite il comando pydoc o attraverso il sito ufficiale.

10.5 Un esempio di algorimo in Python: il bubble sort

Per chiudere il capitolo sul primo approccio a Python, possiamo confrontare un algoritmo, di bassa complessità ma non triviale, in diversi linguaggi di programmazione. Un buon esempio potrebbe essere l’implementazione dell’algoritmo di ordinamento bubble sort di una lista di valori. Vediamo come viene scritto in Python, C, C++, Java, Rust e Scala:

  • Python in versione procedurale:

    def bubble_sort(arr):
      n = len(arr)
    
      for i in range(n):
        for j in range(0, n-i-1):
          if arr[j] > arr[j+1]:
            arr[j], arr[j+1] = arr[j+1], arr[j]
    
    # Esempio di utilizzo
    arr = [64, 34, 25, 12, 22, 11, 90]
    
    bubble_sort(arr)
    
    print("Array ordinato con bubble sort: ", arr)
  • Python in versione sintatticamente orientata agli oggetti, ma praticamente procedurale:

    class BubbleSort:
      @staticmethod
      def bubble_sort(arr):
        n = len(arr)
    
        for i in range(n):
          for j in range(0, n-i-1):
            if arr[j] > arr[j+1]:
              arr[j], arr[j+1] = arr[j+1], arr[j]
    
    # Esempio di utilizzo
    arr = [64, 34, 25, 12, 22, 11, 90]
    
    BubbleSort.bubble_sort(arr)
    
    print("Array ordinato con bubble sort: ", arr)
  • Python in versione orientata agli oggetti, con una interfaccia di ordinamento implementata con due algoritmi (bubble e insertion sort):

    1from abc import ABC, abstractmethod
    
    # Classe astratta per algoritmi di ordinamento
    2class SortAlgorithm(ABC):
      def __init__(self, arr):
        self._arr = arr
    
      @abstractmethod
    3  def sort(self):
        # Metodo astratto che deve essere implementato dalle sottoclassi
        pass
    
      def get_array(self):
        # Metodo per ottenere l'array corrente
        return self._arr
    
      def set_array(self, arr):
        # Metodo per impostare un nuovo array
        self._arr = arr
    
    # Implementazione dell'algoritmo di bubble sort
    4class BubbleSort(SortAlgorithm):
      def sort(self):
        n = len(self._arr)
    
        for i in range(n):
          for j in range(0, n-i-1):
            if self._arr[j] > self._arr[j+1]:
              self._arr[j], self._arr[j+1] = self._arr[j+1], self._arr[j]
    
    # Implementazione dell'algoritmo di insertion sort
    class InsertionSort(SortAlgorithm):
      def sort(self):
        for i in range(1, len(self._arr)):
          key = self._arr[i]
    
          j = i - 1
    
          while j >= 0 and key < self._arr[j]:
            self._arr[j + 1] = self._arr[j]
    
            j -= 1
    
          self._arr[j + 1] = key
    
    # Esempio di utilizzo con bubble sort
    arr = [64, 34, 25, 12, 22, 11, 90]
    
    bubble_sorter = BubbleSort(arr)
    
    bubble_sorter.sort()
    
    print("Array ordinato con bubble sort: ", bubble_sorter.get_array())
    
    # Esempio di utilizzo con insertion sort
    arr = [64, 34, 25, 12, 22, 11, 90]
    
    insertion_sorter = InsertionSort(arr)
    
    insertion_sorter.sort()
    
    print("Array ordinato con insertion sort: ", insertion_sorter.get_array())
    1
    Importiamo ABC e abstractmethod dal modulo abc per definire la classe astratta.
    2
    SortAlgorithm è una classe astratta che rappresenta l’interfaccia di algoritmi di ordinamento.
    3
    sort è un metodo astratto che deve essere implementato nelle sottoclassi.
    4
    BubbleSort è una sottoclasse di SortAlgorithm che implementa l’algoritmo di ordinamento a bolle. Idem per InsertionSort.
  • Python in versione funzionale:

    def bubble_sort(arr):
    1  def sort_pass(arr, n):
        if n == 1:
          return arr
    
    2    new_arr = arr[:]
    
        for i in range(n - 1):
          if new_arr[i] > new_arr[i + 1]:
            new_arr[i], new_arr[i + 1] = new_arr[i + 1], new_arr[i]
    
    3    return sort_pass(new_arr, n - 1)
    
    4  return sort_pass(arr, len(arr))
    
    # Esempio di utilizzo
    arr = [64, 34, 25, 12, 22, 11, 90]
    
    sorted_arr = bubble_sort(arr)
    
    print("Sorted array is:", sorted_arr)
    1
    All’interno di bubble_sort, è definita una funzione interna sort_pass che esegue un singolo passaggio dell’algoritmo di ordinamento a bolle.
    2
    Viene creata una copia dell’array arr chiamata new_arr. Poi, per ogni coppia di elementi (new_arr[i], new_arr[i + 1]), se new_arr[i] è maggiore di new_arr[i + 1], vengono scambiati.
    3
    La funzione sort_pass viene chiamata ricorsivamente con new_arr e decrementando n di 1.
    4
    La funzione bubble_sort avvia il processo chiamando sort_pass con l’array completo e la sua lunghezza.
  • C:

    #include <stdio.h>
    
    void bubble_sort(int arr[], int n) {
      int i, j, temp;
    
      for (i = 0; i < n-1; i++) {
        for (j = 0; j < n-i-1; j++) {
          if (arr[j] > arr[j+1]) {
            temp = arr[j];
    
            arr[j] = arr[j+1];
    
            arr[j+1] = temp;
          }
        }
      }
    }
    
    int main() {
      int arr[] = {64, 34, 25, 12, 22, 11, 90};
      int n = sizeof(arr)/sizeof(arr[0]);
    
      bubble_sort(arr, n);
    
      printf("Array ordinato con bubble sort: ");
    
      for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
      }
    
      return 0;
    }
  • C++:

    #include <iostream>
    using namespace std;
    
    class BubbleSort {
    public:
      void sort(int arr[], int n) {
        for (int i = 0; i < n-1; i++) {
          for (int j = 0; j < n-i-1; j++) {
            if (arr[j] > arr[j+1]) {
              int temp = arr[j];
    
              arr[j] = arr[j+1];
    
              arr[j+1] = temp;
            }
          }
        }
      }
    
      void printArray(int arr[], int n) {
        for (int i = 0; i < n; i++) {
          cout << arr[i] << " ";
        }
    
        cout << endl;
      }
    };
    
    int main() {
      int arr[] = {64, 34, 25, 12, 22, 11, 90};
      int n = sizeof(arr)/sizeof(arr[0]);
    
      BubbleSort bs;
      bs.sort(arr, n);
    
      cout << "Array ordinato con bubble sort: ";
      bs.printArray(arr, n);
    
      return 0;
    }
  • Java:

    public class BubbleSort {
    
      public static void bubbleSort(int arr[]) {
        int n = arr.length;
    
        for (int i = 0; i < n-1; i++) {
          for (int j = 0; j < n-i-1; j++) {
            if (arr[j] > arr[j+1]) {
    
              int temp = arr[j];
    
              arr[j] = arr[j+1];
    
              arr[j+1] = temp;
            }
          }
        }
      }
    
      public static void main(String args[]) {
        int arr[] = {64, 34, 25, 12, 22, 11, 90};
    
        bubbleSort(arr);
    
        System.out.println("Array ordinato con bubble sort: ");
    
        for (int i = 0; i < arr.length; i++) {
          System.out.print(arr[i] + " ");
        }
      }
    }
  • Rust:

    fn bubble_sort(arr: &mut [i32]) {
      let n = arr.len();
    
      for i in 0..n {
        for j in 0..n-i-1 {
          if arr[j] > arr[j+1] {
            arr.swap(j, j+1);
          }
        }
      }
    }
    
    fn main() {
      let mut arr = [64, 34, 25, 12, 22, 11, 90];
    
      bubble_sort(&mut arr);
    
      println!("Array ordinato con bubble sort: {:?}", arr);
    }
  • Scala:

    object BubbleSort {
      def bubbleSort(arr: Array[Int]): Unit = {
        val n = arr.length
    
        for (i <- 0 until n) {
          for (j <- 0 until n - i - 1) {
            if (arr(j) > arr(j + 1)) {
              val temp = arr(j)
    
              arr(j) = arr(j + 1)
    
              arr(j + 1) = temp
            }
          }
        }
      }
    
      def main(args: Array[String]): Unit = {
        val arr = Array(64, 34, 25, 12, 22, 11, 90)
    
        bubbleSort(arr)
    
        println("Array ordinato con bubble sort: " + arr.mkString(", "))
      }
    }

Confrontando questi esempi, possiamo osservare le differenze sintattiche e di stile tra Python ed altri, importanti, linguaggi. Python si distingue per la sua sintassi concisa e espressiva soprattutto nella versione procedurale. L’implementazione colla gerarchia di oggetti ha un piccolo incremento di complessità che è ripagato dalla possibilità di creare gerarchie di algoritmi di ordinamento, con impatti nulli sul codice preesistente.

La versione procedurale in Python e l’implementazione C, già a primo acchito, presentano un evidente diverso grado di chiarezza del codice. Inoltre, la riga int n = sizeof(arr)/sizeof(arr[0]); in C si rende necessaria per calcolare il numero di valori a partire dalle dimensioni totale della lista e del singolo elemento, rispetto a n = len(arr) di Python, dove chiediamo direttamente il numero di valori.

Il C++ e Java aggiungono caratteristiche relative agli oggetti e funzionalità di alto livello rispetto a C, al prezzo di una sintassi più complessa e verbosa. Rust e Scala sono linguaggi più moderni e si pongono nel mezzo tra C, C++ e Java e Python.