Incapsulamento in Python
Impara l'incapsulamento in Python: membri pubblici, protetti e privati, name mangling e getter/setter con @property.
L'incapsulamento è uno dei quattro pilastri della programmazione orientata agli oggetti. Consiste nel raggruppare i dati di un oggetto (attributi) e i metodi che operano su quei dati in un'unica unità, controllando quali parti dell'oggetto il mondo esterno può accedere o modificare.
Fatto bene, l'incapsulamento mantiene consistente lo stato interno di un oggetto, nasconde i dettagli di implementazione in modo da poterli modificare in seguito senza rompere il codice chiamante, e rende le classi più sicure da usare.
Questo capitolo tratta:
- Cosa è l'incapsulamento e perché è importante
- Membri pubblici, protetti e privati — e le convenzioni di denominazione che Python utilizza
- Name mangling — come funzionano realmente gli attributi con
__doppio_underscore - Getter e setter con
@property - Un esempio reale che mette tutto insieme
Prima di leggere, assicurati di essere a tuo agio con le classi e oggetti Python. Per il controllo degli accessi tramite attributi calcolati, consulta il capitolo strettamente correlato su @property.
Perché l'Incapsulamento è Importante
Considera un conto bancario. Internamente tiene traccia di un saldo. Se quel saldo fosse un attributo semplice che chiunque potrebbe impostare, nulla impedirebbe a un bug (o a un malintenzionato) di fare:
account.balance = -9999999L'incapsulamento risolve questo problema nascondendo il saldo dietro un'interfaccia controllata. Il codice esterno alla classe può solo depositare o prelevare tramite metodi che applicano le regole aziendali. L'archiviazione interna è un dettaglio di implementazione — i chiamanti non la toccano mai direttamente.
I tre vantaggi che questo offre sono:
- Integrità dei dati — logica di validazione in un unico posto, applicata ogni volta.
- Flessibilità — puoi cambiare la rappresentazione interna (ad es., memorizzare i saldi in centesimi invece che in euro) senza modificare il codice chiamante.
- Riduzione dell'accoppiamento — i chiamanti dipendono solo dall'interfaccia pubblica, non da come la classe funziona internamente.
Livelli di Accesso: Pubblico, Protetto e Privato
Python non ha modificatori di accesso come le parole chiave private o public. Usa invece una convenzione di denominazione per segnalare l'intento:
| Prefisso | Esempio | Livello di accesso | Significato |
|---|---|---|---|
| Nessun prefisso | balance | Pubblico | Destinato all'uso da parte di chiunque |
Underscore singolo _ | _balance | Protetto | Per uso interno e sottoclassi; utilizzabile esternamente ma sconsigliato |
Doppio underscore __ | __pin | Privato | Solo per questa classe; Python lo rinomina attivamente per impedire un accesso facile |
Queste sono convenzioni e meccanismi, non regole rigide applicate da un compilatore. Python si fida degli sviluppatori nel rispettare il segnale.
Membri pubblici
Gli attributi e i metodi pubblici formano l'interfaccia ufficiale della classe — la parte che si intende che i chiamanti usino:
class BankAccount:
account_type = 'savings' # public class attribute
def __init__(self, owner, balance):
self.owner = owner # public instance attribute
def deposit(self, amount):
pass # public methodNon è necessaria alcuna denominazione speciale. Qualsiasi codice può leggere o scrivere liberamente un membro pubblico.
Membri protetti (underscore singolo _)
Un underscore iniziale singolo è un segnale che dice "questo è un dettaglio interno — non farci affidamento dall'esterno della classe." Python non lo applica; è puramente una convenzione:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance # protected — internal, but subclasses may need it
def _validate_amount(self, amount): # protected helper
return isinstance(amount, (int, float)) and amount > 0_balance è ancora accessibile come account._balance dall'esterno, ma l'underscore avverte altri sviluppatori (e i linter) che stanno violando il contratto previsto.
Un uso comune: una classe base memorizza i dati in un attributo _ in modo che le sottoclassi possano leggerlo, mantenendolo nascosto dal codice non correlato.
Membri privati (doppio underscore __)
Un doppio underscore iniziale attiva il name mangling — Python rinomina l'attributo internamente in _NomeClasse__attributo. Questo rende molto più difficile l'accesso accidentale dall'esterno:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance
self.__pin = 1234 # private — not meant to be touched at all
def verify_pin(self, pin):
return pin == self.__pinDall'esterno della classe:
acc = BankAccount('Alice', 1000)
print(acc.owner) # Alice — public, fine
print(acc._balance) # 1000 — protected, works but frowned upon
print(acc.__pin) # AttributeError: 'BankAccount' object has no attribute '__pin'L'attributo esiste ancora, ma sotto un nome diverso. Vedi la sezione successiva per scoprire come trovarlo.
Name Mangling
Quando Python vede self.__nome all'interno di una definizione di classe, lo riscrive internamente in self._NomeClasse__nome. Questo è il name mangling. Lo scopo è evitare collisioni accidentali nelle sottoclassi — non fornire vera sicurezza.
class Counter:
def __init__(self):
self.__count = 0
def increment(self):
self.__count += 1
def value(self):
return self.__count
c = Counter()
c.increment()
c.increment()
print(c.value()) # 2
# Direct access fails:
# print(c.__count) # AttributeError
# But mangled name still works if you know it:
print(c._Counter__count) # 2Puoi ispezionare tutti gli attributi con vars() o dir() per scoprire il nome mangled:
print(list(vars(c)))
# ['_Counter__count']Name mangling ed ereditarietà
Il name mangling è particolarmente utile nell'ereditarietà. Senza di esso, una sottoclasse potrebbe sovrascrivere accidentalmente un attributo privato del suo genitore usando lo stesso nome. Con il mangling, ogni classe ottiene il proprio spazio dei nomi:
class Base:
def __init__(self):
self.__secret = 'base'
def reveal(self):
return self.__secret # accesses _Base__secret
class Child(Base):
def __init__(self):
super().__init__()
self.__secret = 'child' # stored as _Child__secret, not the same thing
def reveal_child(self):
return self.__secret # accesses _Child__secret
c = Child()
print(c.reveal()) # base — Base.reveal() reads _Base__secret
print(c.reveal_child()) # child — Child.reveal_child() reads _Child__secretEntrambi gli attributi coesistono senza collisioni, cosa che non sarebbe possibile senza il name mangling.
Getter e Setter con @property
In molti linguaggi si scrivono metodi espliciti get_x() e set_x(). Python offre un approccio più pulito: il decoratore @property ti permette di esporre un metodo come se fosse un attributo semplice, in modo che il codice chiamante rimanga leggibile mentre mantieni il pieno controllo sulla lettura e scrittura.
Getter di base
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsiusI chiamanti leggono t.celsius, non t.celsius(). Il @property rende invisibile la chiamata al metodo:
t = Temperature(25)
print(t.celsius) # 25 — no parentheses neededAggiungere un setter con validazione
Abbina @property a un .setter per validare i valori prima di memorizzarli:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero')
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32t = Temperature(25)
print(t.celsius) # 25
print(t.fahrenheit) # 77.0
t.celsius = 100
print(t.fahrenheit) # 212.0
t.celsius = -300 # ValueError: Temperature below absolute zerofahrenheit è una proprietà calcolata in sola lettura — nessun setter è definito, quindi Python solleva un AttributeError se si prova ad assegnarle un valore.
Perché preferire @property a getter/setter semplici?
Puoi iniziare con un attributo pubblico semplice e aggiornarlo a una property in seguito senza modificare alcun codice chiamante:
# v1 — plain attribute
class Circle:
def __init__(self, radius):
self.radius = radius
# v2 — property with validation, same public interface
class Circle:
def __init__(self, radius):
self.radius = radius # still works from the caller's point of view
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError('Radius cannot be negative')
self._radius = valueI chiamanti che hanno scritto c.radius = 5 continuano a funzionare senza modifiche. Cambia solo il comportamento — ora si valida il valore.
Per un riferimento completo su @property inclusi i deleter, consulta Python @property.
Un Esempio Completo: Account Utente
Il seguente esempio mostra tutti e tre i livelli di accesso che lavorano insieme in una classe realistica:
class UserAccount:
def __init__(self, username, password):
self.username = username # public
self._login_attempts = 0 # protected — subclasses may need this
self.__password_hash = self.__hash(password) # private
def __hash(self, password):
"""Private helper — implementation detail, may change."""
return hash(password)
def check_password(self, password):
"""Public method — part of the official interface."""
return self.__hash(password) == self.__password_hash
def login(self, password):
if self._login_attempts >= 3:
return 'Account locked'
if self.check_password(password):
self._login_attempts = 0
return 'Login successful'
self._login_attempts += 1
return f'Wrong password ({self._login_attempts}/3)'
user = UserAccount('alice', 'secret123')
print(user.login('bad')) # Wrong password (1/3)
print(user.login('bad')) # Wrong password (2/3)
print(user.login('bad')) # Wrong password (3/3)
print(user.login('secret123')) # Account lockedNota:
usernameè pubblico — va bene che chiunque lo legga._login_attemptsè protetto — una sottoclasseThrottledAccountpotrebbe leggerlo per implementare una logica più intelligente.__password_hashe__hash()sono privati — la strategia di archiviazione della password è puramente interna. I chiamanti non hanno motivo di vederla, e se in seguito passi a bcrypt devi cambiare solo queste due cose.
Incapsulamento e gli Altri Pilastri OOP
L'incapsulamento è uno dei quattro principi OOP:
| Principio | Definizione in una riga |
|---|---|
| Incapsulamento | Raggruppa dati + metodi; nasconde i dettagli interni |
| Ereditarietà | Permette a una classe di riutilizzare ed estendere un'altra classe |
| Polimorfismo | Permette a tipi diversi di rispondere alla stessa chiamata di metodo |
| Astrazione | Espone un'interfaccia semplificata; nasconde la complessità |
Vedi ereditarietà Python, polimorfismo Python e classi astratte Python per gli altri pilastri.
Riferimento Rapido
| Convenzione | Cosa segnala | Applicata da Python? |
|---|---|---|
name | Pubblico — usa liberamente | No (sempre accessibile) |
_name | Protetto — uso interno | No (accessibile ma convenzionalmente da evitare) |
__name | Privato — solo questa classe | Parzialmente — il nome viene modificato in _NomeClasse__nome |
@property | Accesso in lettura controllato | Sì — hook getter/setter/deleter |
@name.setter | Accesso in scrittura controllato con validazione | Sì |