Corso
L’incapsulamento è un principio fondamentale dell’orientamento agli oggetti in Python. Protegge le tue classi da modifiche o eliminazioni accidentali e favorisce il riuso e la manutenibilità del codice. Considera questa semplice definizione di classe:
class Smartphone:
def __init__(self, brand, os):
self.brand = brand
self.os = os
iphone = Smartphone("Apple", "iOS 17")
Molti programmatori Python definiscono classi in questo modo. Tuttavia, siamo lontani dalle best practice seguite dai Pythonisti più esperti. Il problema con questa classe è evidente quando provi a modificarne i dati:
iphone.os = "Android"
print(iphone.os)
Android
Immagina un iPhone che gira su Android — che oltraggio! È chiaro che dobbiamo fissare dei limiti all’interno della nostra classe, in modo che gli utenti non possano cambiare i suoi attributi come vogliono.
Se decidono di cambiarli, le modifiche devono avvenire alle nostre condizioni, seguendo le nostre regole. Ma vogliamo comunque che gli attributi .os o .brand restino gli stessi in superficie.
Tutto questo si può ottenere con l’incapsulamento e, in questo tutorial, lo esploreremo a fondo. Iniziamo!
Come si realizza l’incapsulamento in Python?
Python supporta l’incapsulamento tramite convenzioni e pratiche di programmazione piuttosto che con modificatori di accesso imposti. Vedi, il principio di base alla radice di molta progettazione del codice Python è “Siamo tutti adulti qui”. Possiamo implementare l’incapsulamento come semplice convenzione e aspettarci che altri sviluppatori Python si fidino e rispettino il nostro codice.
In altri linguaggi OOP come Java e C++, l’incapsulamento è rigorosamente applicato con modificatori di accesso come public, private o protected, ma Python non li ha:
| Funzionalità | Python | Java | C++ |
|---|---|---|---|
| Modificatori di accesso | Nessun modificatore imposto; usa convenzioni di naming (_protected, __private) |
Imposti con le keyword public, protected, private |
Imposti con le keyword public, protected, private |
| Metodi getter/setter | Opzionali, spesso usati con il decoratore @property per un accesso controllato |
Pratica comune, tipicamente implementati come metodi | Pratica comune, tipicamente implementati come metodi |
| Accesso ai dati | Accessibile via convenzioni di naming; si basa sulla disciplina degli sviluppatori | Controllato dai modificatori di accesso; imposto dal compilatore | Controllato dai modificatori di accesso; imposto dal compilatore |
| Filosofia | "Siamo tutti adulti qui" - si basa su convenzioni e fiducia | Applicazione rigorosa del controllo di accesso | Applicazione rigorosa del controllo di accesso |
Quindi, la maggior parte, se non tutte, delle tecniche di incapsulamento che sto per mostrarti sono convenzioni Python. Possono essere facilmente infrante, se lo decidi. Ma confido che le rispetterai e le seguirai nei tuoi progetti di sviluppo.
Modificatori di accesso in Python
Supponiamo di avere questa semplice classe:
class Tree:
def __init__(self, height):
self.height = height
pine = Tree(20)
print(pine.height)
20
Ha un singolo attributo height che possiamo stampare. Il problema è che possiamo anche cambiarlo come vogliamo:
pine.height = 50
pine.height
50
pine.height = "Grandma"
pine.height
'Grandma'
Quindi, come facciamo a dire agli utenti che modificare height è off-limits? Be’, potremmo trasformarlo in un membro protetto aggiungendo un singolo underscore davanti:
class Tree:
def __init__(self, height):
self._height = height
pine = Tree(20)
pine._height
20
Ora, chi conosce questa convenzione saprà che può solo accedere all’attributo e che scoraggiamo fortemente l’uso e la modifica. Ma, se vuole, può modificarlo, oh sì.
Come evitiamo anche questo? Usando un’altra convenzione — trasformiamo l’attributo in un membro privato aggiungendo due underscore davanti:
class Tree:
def __init__(self, height):
self.__height = height
pine = Tree(20)
pine.__height
AttributeError: 'Tree' object has no attribute '__height'
Ora, Python solleverà un errore se qualcuno prova ad accedere all’attributo, per non parlare di modificarlo.
Ma noti cosa abbiamo appena fatto? Abbiamo nascosto agli utenti l’unica informazione relativa ai nostri oggetti. La nostra classe è appena diventata inutile perché non ha attributi pubblici.
Quindi, come esponiamo l’altezza dell’albero agli utenti ma controlliamo comunque come viene letta e modificata? Per esempio, vogliamo che le altezze degli alberi siano in un intervallo specifico e abbiano solo valori interi. Come lo imponiamo?
A questo punto, il tuo amico che usa Java potrebbe intervenire e suggerire di usare i metodi getter e setter. Proviamo prima così:
class Tree:
def __init__(self, height):
self.__height = height
def get_height(self):
return self.__height
def set_height(self, new_height):
if not isinstance(new_height, int):
raise TypeError("Tree height must be an integer")
if 0 < new_height <= 40:
self.__height = new_height
else:
raise ValueError("Invalid height for a pine tree")
pine = Tree(20)
pine.get_height()
20
In questo modo, crei un attributo privato __height ma consenti agli utenti di accedervi e modificarlo in modo controllato usando i metodi get_height e set_height.
pine.set_height(25)
pine.get_height()
25
Prima di impostare un nuovo valore, set_height si assicura che la nuova altezza sia entro un certo intervallo e numerica.
pine.set_height("Password")
TypeError: Tree height must be an integer
Ma questi metodi sembrano eccessivi per un’operazione semplice. Inoltre, è brutto scrivere codice così:
# Increase height by 5
pine.set_height(pine.get_height() + 5)
Non sarebbe più bello e leggibile poter scrivere questo codice:
pine.height += 5
e imporre comunque il tipo di dato e l’intervallo corretti per height? La risposta è sì, e vedremo come farlo nella prossima sezione.
Usare il decoratore @property nelle classi Python
Introduciamo una nuova tecnica — creare delle property per gli attributi:
class Tree:
def __init__(self, height):
# First, create a private or protected attribute
self.__height = height
@property
def height(self):
return self.__height
pine = Tree(17)
pine.height
17
Vogliamo che gli utenti accedano a un attributo nascosto chiamato __height come se fosse un normale attributo chiamato height. Per riuscirci, definiamo un metodo chiamato height che restituisce self.__height e lo decoriamo con @property.
Ora possiamo chiamare height e accedere all’attributo privato:
pine.height
17
Ma la parte migliore è che gli utenti non possono modificarlo:
pine.height = 15
AttributeError: can't set attribute 'height'
Quindi aggiungiamo un altro metodo chiamato height(self, new_height) che è avvolto da un decoratore height.setter. All’interno di questo metodo, implementiamo la logica che impone il tipo di dato e l’intervallo desiderati per height:
class Tree:
def __init__(self, height):
self.__height = height
@property
def height(self):
return self.__height
@height.setter
def height(self, new_height):
if not isinstance(new_height, int):
raise TypeError("Tree height must be an integer")
if 0 < new_height <= 40:
self.__height = new_height
else:
raise ValueError("Invalid height for a pine tree")
Ora, quando un utente prova a modificare l’attributo height, viene chiamato @height.setter, garantendo così che venga passato il valore corretto:
pine = Tree(10)
pine.height = 33 # Calling @height.setter
pine.height = 45 # An error is raised
ValueError: Invalid height for a pine tree
Possiamo anche personalizzare come l’attributo height viene letto tramite dot-notation con @height.getter:
class Tree:
def __init__(self, height):
self.__height = height
@property
def height(self):
return self.__height
@height.getter
def height(self):
# You can return a custom version of height
return f"This tree is {self.__height} meters"
pine = Tree(33)
pine.height
'This tree is 33 meters'
Anche se abbiamo creato pine con un’altezza intera, potremmo modificarne il valore con @height.getter.
Questi erano esempi di come possiamo favorire l’incapsulamento in una classe Python. Ricorda, l’incapsulamento resta una convenzione perché possiamo ancora forzare il membro privato interno __height:
pine._Tree__height = "Gotcha!"
pine.height
'This tree is Gotcha! meters'
Tutto nelle classi Python è pubblico, e lo sono anche i metodi privati. Non è un difetto di progettazione, ma un esempio dell’approccio “Siamo tutti adulti qui”.
Ecco una panoramica dei modificatori di accesso Python che abbiamo appena rivisto:
| Modificatore/Convenzione | Descrizione | Esempio |
|---|---|---|
| Pubblico | Livello di accesso predefinito; attributi e metodi sono accessibili dall’esterno della classe | self.attribute |
| Protetto | Indicato da un singolo underscore; convenzione per denotare accesso parzialmente ristretto | self._attribute |
| Privato | Indicato da doppio underscore; il name mangling fornisce una limitata restrizione d’accesso | self.__attribute |
| Property | Forniscono accesso controllato agli attributi privati usando il @property decorator |
@property def attribute(self): |
| Property di sola lettura | Nessun @attribute.setter; permette l’accesso in sola lettura a un attributo |
@property def attribute(self): |
Best practice per implementare l’incapsulamento
Ci sono diverse best practice che puoi seguire per assicurarti che il tuo codice sia in linea con quello scritto da altri OOPisti esperti:
- Crea attributi o metodi protetti o privati se vengono usati solo da te. I membri protetti o privati vengono esclusi dalla documentazione e segnalano agli altri che possono essere cambiati da te, sviluppatore, senza preavviso, scoraggiandone così l’uso.
- Non devi sempre creare property per ogni singolo attributo della classe. Per classi grandi con molti attributi, scrivere metodi
attr.gettereattr.setterpuò diventare un mal di testa. - Valuta di generare un avviso ogni volta che un utente accede a un membro protetto (
_). - Usa i membri privati con parsimonia, perché possono rendere il codice illeggibile per chi non conosce la convenzione.
- Dai priorità alla chiarezza rispetto all’oscurità. Poiché l’incapsulamento mira a migliorare la manutenibilità del codice e la protezione dei dati, non nascondere completamente dettagli importanti dell’implementazione della tua classe.
- Se vuoi creare property di sola lettura, non implementare il metodo
@attr.setter. Gli utenti potranno accedere alla property ma non modificarla. - Ricorda sempre che l’incapsulamento è una convenzione, non un aspetto imposto dalla sintassi di Python.
- Per classi semplici, valuta l’uso delle dataclass, che ti consentono di abilitare l’incapsulamento con una singola riga di codice. Tuttavia, le dataclass sono pensate per classi più semplici con attributi e metodi prevedibili. Dai un’occhiata a questo tutorial sulle dataclass per saperne di più.
Conclusione e risorse aggiuntive
In questo tutorial, abbiamo appreso uno dei pilastri della programmazione orientata agli oggetti in Python: l’incapsulamento.
L’incapsulamento ti consente di definire un accesso controllato ai dati memorizzati all’interno degli oggetti della tua classe. Ciò ti permette di scrivere codice pulito, leggibile ed efficiente e di prevenire modifiche o eliminazioni accidentali dei dati della tua classe.
Ecco alcune risorse correlate per migliorare le tue conoscenze OOP:
FAQ
In che cosa l’incapsulamento differisce dall’astrazione in Python?
L’incapsulamento consiste nel raggruppare dati e metodi che operano su quei dati all’interno di un’unica unità, come una classe, e nel limitare l’accesso ad alcune componenti. L’astrazione, invece, consiste nel nascondere la complessità esponendo solo le parti essenziali. Mentre l’incapsulamento si concentra sul limitare l’accesso, l’astrazione punta a semplificare le interazioni.
L’incapsulamento può essere usato nella programmazione funzionale o è esclusivo dell’OOP?
L’incapsulamento è un concetto strettamente legato alla programmazione orientata agli oggetti (OOP) e non è tipicamente usato nella programmazione funzionale. La programmazione funzionale enfatizza l’immutabilità e le funzioni senza stato, a differenza dell’OOP, che si concentra sugli oggetti e sull’incapsulamento dei dati all’interno di essi.
Quali sono alcuni errori comuni da evitare quando si implementa l’incapsulamento in Python?
Gli errori comuni includono l’uso eccessivo di variabili private, che può rendere il codice difficile da mantenere, e il non fornire adeguati metodi di accesso (getter/setter), il che può portare a codice fragile. Inoltre, gli sviluppatori potrebbero dimenticare che l’incapsulamento in Python è basato su convenzioni e, dunque, può essere aggirato.
In che modo l’incapsulamento può migliorare la sicurezza del codice?
L’incapsulamento può migliorare la sicurezza del codice limitando l’accesso allo stato interno di un oggetto e prevenendo modifiche non autorizzate. Controllando come i dati vengono letti e modificati, gli sviluppatori possono garantire che restino validi e coerenti.
Come incide sull’esecuzione in Python l’uso del decoratore @property?
L’uso del @property può introdurre un leggero overhead a causa delle chiamate ai metodi aggiuntive, ma nella maggior parte dei casi questo impatto è trascurabile. I benefici in termini di leggibilità e manutenibilità del codice superano spesso eventuali piccoli costi prestazionali.
È possibile applicare l’incapsulamento in modo rigoroso in Python come in Java o C++?
Python non applica l’incapsulamento in modo rigoroso come Java o C++. La sua filosofia si basa su convenzioni e sulla consapevolezza che gli sviluppatori rispetteranno l’uso previsto dei membri privati e protetti. Tuttavia, usare convenzioni come gli underscore può scoraggiare gli abusi.
Quali sono alcune applicazioni reali dell’incapsulamento in progetti Python?
L’incapsulamento viene usato per proteggere dati sensibili in applicazioni come il software bancario, dove i dettagli dei conti sono nascosti. È usato anche in librerie e framework per celare dettagli di implementazione complessi, permettendo agli sviluppatori di interagire con interfacce semplici.
Come posso testare in modo efficace il codice incapsulato in Python?
Testare codice incapsulato implica scrivere unit test per l’interfaccia pubblica di una classe. Testando i metodi e le property esposte all’utente, ti assicuri che i cambiamenti allo stato interno siano gestiti correttamente e che la logica di incapsulamento tenga.
L’incapsulamento può essere applicato ai moduli Python o è limitato alle classi?
Sebbene l’incapsulamento sia associato principalmente alle classi, il concetto può essere esteso ai moduli usando funzioni o variabili con prefisso underscore per indicarne l’uso privato all’interno di un modulo.
In che modo l’incapsulamento è correlato al polimorfismo in Python?
Incapsulamento e polimorfismo sono entrambi principi dell’OOP ma hanno scopi diversi. L’incapsulamento limita l’accesso a determinate parti di un oggetto, mentre il polimorfismo permette di trattare gli oggetti come istanze della loro classe genitore, facilitando l’espansione e la manutenzione del codice. Possono essere usati insieme per creare strutture di codice flessibili e sicure.
Sono un creator di contenuti sulla data science con oltre 2 anni di esperienza e uno dei più seguiti su Medium. Mi piace scrivere articoli dettagliati su AI e ML con un pizzico di sarcasmo, perché bisogna pur fare qualcosa per renderli un po’ meno noiosi. Ho pubblicato oltre 130 articoli e un corso su DataCamp, con un altro in arrivo. I miei contenuti sono stati visti da oltre 5 milioni di persone, 20.000 delle quali sono diventate follower sia su Medium che su LinkedIn.


