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 parametroself; - definire metodi per incapsulare la logica;
- utilizzare i “Magic Methods” come
__str__; - applicare la OOP per creare una classe
IPAddressper 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.
- Conversione Binaria: Completa
to_binary(). L’indirizzo192.168.1.1deve diventare una stringa di 32 bit (o una lista di 4 byte). - Gestione Maschera: Migliora
_cidr_to_netmaskper gestire qualsiasi CIDR (da /0 a /32), non solo quelli standard. - Indirizzo di Rete: Implementa
get_network_address(). Ricorda che devi fare l’operazione AND bit-a-bit tra l’IP e la Netmask. - Verifica Rete: Completa
is_same_network(self, other). Deve restituireTruese i due IP hanno lo stesso indirizzo di rete,Falsealtrimenti. - 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.
EC