python OOP classi oggetti programmazione-orientata-agli-oggetti

La Programmazione ad Oggetti in Python

Obiettivi della pillola

In questa guida approfondita imparerai a:

  • distinguere tra Classe (il progetto) e Oggetto (la realizzazione);
  • usare il costruttore __init__ e il parametro self;
  • definire metodi per incapsulare la logica;
  • utilizzare i “Magic Methods” come __str__;
  • applicare la OOP per creare una classe IPAddress per il networking.

Concetti Fondamentali

La Programmazione Orientata agli Oggetti (OOP) ci permette di strutturare il codice raggruppando dati e comportamenti in un unico contenitore.

  • Classe (Class): È lo “stampo” o il progetto. Definisce quali caratteristiche (attributi) e quali azioni (metodi) avrà l’oggetto. Esempio: il progetto di un indirizzo IP.
  • Oggetto (Instance): È l’oggetto concreto creato a partire dalla classe. Esempio: l’IP 192.168.1.10.
  • Attributi: Variabili che contengono i dati dell’oggetto (es. ottetti, maschera).
  • Metodi: Funzioni che operano sui dati dell’oggetto (es. convertire in binario).

Sintassi Base in Python

Il Costruttore __init__

È un metodo speciale che viene chiamato automaticamente quando crei un nuovo oggetto. Serve a inizializzare gli attributi.

Il parametro self

Rappresenta l’istanza corrente dell’oggetto. Quando chiami un metodo su un oggetto, Python passa automaticamente l’oggetto stesso come primo argomento. È per questo che va sempre dichiarato.

class Cane:
    # Costruttore
    def __init__(self, nome, razza):
        self.nome = nome    # Attributo istanza
        self.razza = razza  # Attributo istanza

    # Metodo
    def abbaia(self):
        return f"{self.nome} dice: Bau!"

# Creazione oggetti (Istanziazione)
fido = Cane("Fido", "Labrador")
rex = Cane("Rex", "Pastore Tedesco")

print(fido.abbaia()) # Fido dice: Bau!

Incapsulamento e Getters/Setters

In Python, l’incapsulamento non è rigido come in altri linguaggi (es. Java o C++), ma è buona norma non accedere direttamente agli attributi interni se si vuole mantenere il controllo su come vengono letti o modificati.

Perché usarli?

Immagina di avere un attributo eta. Se permetti a chiunque di fare persona.eta = -5, avrai un dato non valido. Usando un setter, puoi controllare il valore prima di assegnarlo.

La soluzione Pythonica: @property

Invece di scrivere metodi get_eta() e set_eta(), Python usa i decoratori per far sembrare i metodi degli attributi normali.

class Studente:
    def __init__(self, nome, voto):
        self.nome = nome
        self._voto = voto  # _ indica "uso interno" (convenzione)

    @property
    def voto(self):
        """Getter: viene chiamato quando scrivi studente.voto"""
        return self._voto

    @voto.setter
    def voto(self, nuovo_voto):
        """Setter: viene chiamato quando scrivi studente.voto = 10"""
        if 0 <= nuovo_voto <= 10:
            self._voto = nuovo_voto
        else:
            print("Voto non valido! Deve essere tra 0 e 10.")

s = Studente("Mario", 6)
print(s.voto)      # Chiama il getter -> 6
s.voto = 12        # Chiama il setter -> Voto non valido!
s.voto = 8         # Chiama il setter -> OK

Metodi Statici

A volte hai bisogno di una funzione che appartenga logicamente a una classe, ma che non abbia bisogno di accedere a self (cioè ai dati di una specifica istanza). Questi sono i metodi statici.

Si definiscono con il decoratore @staticmethod e non prendono self come primo argomento.

class Matematica:
    @staticmethod
    def somma(a, b):
        return a + b

# Non serve creare un oggetto!
print(Matematica.somma(5, 3)) # 8

Dataclasses

Spesso creiamo classi solo per contenere dati, scrivendo un lungo __init__ e un __str__ ripetitivo. Dalla versione 3.7, Python offre le dataclass per automatizzare tutto questo.

from dataclasses import dataclass

@dataclass
class Prodotto:
    nome: str
    prezzo: float
    quantita: int = 1  # Valore default

# __init__ e __str__ sono generati automaticamente!
p = Prodotto("Laptop", 999.99)
print(p)
# Output: Prodotto(nome='Laptop', prezzo=999.99, quantita=1)

Caso Studio: La Classe IPAddress

Invece di gestire indirizzi IP e netmask come semplici stringhe sparse nel codice, creiamo una classe che incapsuli la logica di rete.

Struttura della Classe

Vogliamo poter creare un IP, visualizzarlo, e (in futuro) fare calcoli di subnetting.

class IPAddress:
    def __init__(self, ip_string, cidr):
        """
        Inizializza l'oggetto.
        :param ip_string: Es. "192.168.1.10"
        :param cidr: Intero per la maschera. Es. 24 (per /24)
        """
        self.ip_string = ip_string
        self.cidr = cidr
        # Possiamo calcolare subito la netmask o farlo dopo
        self.netmask_string = self._cidr_to_netmask(cidr)

    def __str__(self):
        """
        Metodo magico per la rappresentazione a stringa.
        Viene chiamato quando fai print(oggetto).
        """
        return f"IP: {self.ip_string}/{self.cidr} (Mask: {self.netmask_string})"

    def _cidr_to_netmask(self, cidr):
        """
        Metodo 'privato' (convenzione _) per convertire /24 in 255.255.255.0
        """
        # Esempio semplificato (solo per dimostrazione):
        if cidr == 24: return "255.255.255.0"
        if cidr == 16: return "255.255.0.0"
        if cidr == 8:  return "255.0.0.0"
        return "Unknown"

    def to_binary(self):
        """Restituisce l'IP in formato binario (stringa di 32 bit)."""
        # ESERCIZIO: Implementare la conversione
        # Suggerimento: split('.'), int(), bin(), zfill(8)
        pass

    def get_network_address(self):
        """Restituisce l'indirizzo di rete."""
        # ESERCIZIO: Applicare l'AND bit a bit tra IP e Mask
        pass

    def is_same_network(self, other_ip_object):
        """
        Verifica se questo IP e un altro sono nella stessa sottorete.
        :param other_ip_object: Un'altra istanza di IPAddress
        """
        # ESERCIZIO: Confrontare i network address
        pass

Utilizzo della Classe

Ecco come useremo la nostra classe una volta completata:

# Creazione di due oggetti IP
ip1 = IPAddress("192.168.1.50", 24)
ip2 = IPAddress("192.168.1.200", 24)
ip3 = IPAddress("192.168.2.10", 24)

print(ip1)
# Output atteso: IP: 192.168.1.50/24 (Mask: 255.255.255.0)

# Controllo rete (quando avrai implementato i metodi)
# if ip1.is_same_network(ip2):
#     print("Sono nella stessa rete locale!")

Esercitazione Pratica

Obiettivo: Completare la classe IPAddress

Ora tocca a te implementare la logica mancante.

  1. Conversione Binaria: Completa to_binary(). L’indirizzo 192.168.1.1 deve diventare una stringa di 32 bit (o una lista di 4 byte).
  2. Gestione Maschera: Migliora _cidr_to_netmask per gestire qualsiasi CIDR (da /0 a /32), non solo quelli standard.
  3. Indirizzo di Rete: Implementa get_network_address(). Ricorda che devi fare l’operazione AND bit-a-bit tra l’IP e la Netmask.
  4. Verifica Rete: Completa is_same_network(self, other). Deve restituire True se i due IP hanno lo stesso indirizzo di rete, False altrimenti.
  5. Bonus - Broadcast: Aggiungi un metodo per calcolare l’indirizzo di Broadcast della rete.

Suggerimento: Per lavorare con i bit in Python, ricorda gli operatori bitwise: & (AND), | (OR), ~ (NOT). Spesso conviene convertire l’intero IP in un unico numero intero a 32 bit per fare i calcoli.