Categorie con design a scelte fisse

0

Ho bisogno di creare una classe categoria / tipo per identificare un oggetto. Il solito modo per farlo è passare una stringa per inizializzare una nuova categoria:

category = Category('A')
obj = Object(category)

Tuttavia, l'insieme di categorie da utilizzare dovrebbe essere corretto. Nessuno dovrebbe essere in grado di creare una nuova categoria e usarla in un oggetto.

Quale design si adatta meglio a questo scenario?

Un modo ingenuo per fare ciò può essere il seguente:

class Category:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

class CategoryFactory:
    categories = (Category('A'), Category('B'), Category('C'))
    category_dict = {c.name: c for c in categories}

    @staticmethod
    def get(name):
        return CategoryFactory.category_dict[name]

class Object:
    def __init__(self, category):
        if category not in CategoryFactory.categories:
            raise ValueError
        self.category = category

    def __str__(self):
        return "Object of category '%s'" % self.category

    __repr__ = __str__

In questo modo posso creare un'istanza di Object con una categoria valida:

valid_category = CategoryFactory.get('B')
# output: Object of category 'B'
Object(valid_category)

E agisci su categorie non valide:

invalid_category = Category('D')
# ValueError is produced
Object(invalid_category)

Ma non sono molto contento di questo design. Come viene gestito normalmente questo scenario?

    
posta dabadaba 14.06.2017 - 16:14
fonte

1 risposta

1

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.

    
risposta data 14.06.2017 - 18:16
fonte

Leggi altre domande sui tag