Python in realtà non ha buoni meccanismi di incapsulamento. Ti restano tre possibilità:
- Scoraggia l'utilizzo dei dettagli privati anteponendo il loro nome con caratteri di sottolineatura.
- Scoraggia le funzioni private dall'essere richiamate richiedendo un parametro "token".
- Utilizza il meccanismo di incapsulamento più potente disponibile in Python: chiusure.
La denominazione è chiara: chi crea un'istanza di _Category
? Il nome indica che questa classe è un dettaglio di implementazione e non deve essere utilizzata da codice esterno. Quando importi tutte le variabili da un modulo tramite from module import *
, i nomi con trattini bassi iniziali vengono saltati. Inoltre, alcuni strumenti di analisi statica (ad esempio pylint) ti avviseranno se accederai a tale nome da un altro ambito.
A volte vuoi che il nome della classe sia pubblico, ma vuoi solo evitare le chiamate al costruttore. Questo potrebbe essere il caso se hai bisogno del nome per i controlli isinstance()
, o se vuoi scrivere la documentazione per il tipo. Una tecnica che a volte uso è richiedere un token di accesso privato come parametro di funzione:
_PRIVATE_TOKEN = [] # any new reference will do
class OnlyPrivateInstantiation(object):
def __init__(self, _token, ...):
assert _token is _PRIVATE_TOKEN # check for identity
...
...
Tecnicamente il token privato potrebbe ancora essere visibile esternamente, ma vedi sopra sulla denominazione.
Se non vuoi fare affidamento su convenzioni di denominazione come un carattere di sottolineatura, puoi inserire tutti i dettagli privati in una chiusura che restituisce tutti gli oggetti pubblici. Questa tecnica è spesso usata in JavaScript, ma può anche essere usata in Python con una riga di codice aggiuntiva.
def _make_categories():
class Category:
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
# init any data outside of the objects you return
categories = (Category('A'), Category('B'), Category('C'))
category_dict = {c.name: c for c in categories}
class Categories:
# implement expected collection methods as required
def __iter__(self): return iter(categories)
def __contains__(self, item): return item.name in category_dict
def get(self, name): return category_dict[name]
return Categories()
CATEGORIES = _make_categories()
Ora questa o una combinazione delle funzionalità di cui sopra sarà la migliore che possiamo ottenere con Python, ma è ancora fondamentalmente futile. Una volta che abbiamo un'istanza, abbiamo la sua classe, e una volta che abbiamo una classe possiamo creare un'altra istanza: type(category)("name")
o category.__class__("name")
. E anche le variabili esterne possono essere lette da una funzione grazie alle ricche capacità di riflessione (almeno in CPython): la tupla function.__closure__
ti consente di accedere al valore di ogni variabile non locale utilizzata da quella funzione. Ma chiunque usi le funzioni di riflessione (non importa quanto convenienti possano essere) viola volontariamente il normale contratto intorno agli oggetti coinvolti, quindi provare a difendersi sarebbe inutilmente paranoico.