Impostazione di classe / metodi in modo condizionale

3

Voglio che l'utente finale (spesso me stesso) non venga presentato con metodi inapplicabili durante la codifica direttamente in un interprete, ma io non voglio che l'utente debba ispezionare i dati in arrivo e quindi determinare quale classe istanziare . Questo è probabilmente il problema XY, ma ecco un esempio funzionante, in cui imposto dinamicamente la classe:

class _BaseNumber(object):
    def __init__(self, x):
        self.x = x

class _Even(_BaseNumber):
    def cool_method_just_for_evens(self):
        pass

class Number(object):
    def __new__(self, x):
        if x % 2 == 0:
            return _Even(x)
        else:
            return _BaseNumber(x)

print(type(Number(3)))
print(type(Number(4)))

Funziona, con un risultato di

<class '__main__._BaseNumber'>
<class '__main__._Even'>

Esiste un modo più pulito / più efficiente per abilitare condizionalmente determinati metodi? O è scoraggiato questo tipo di funzionalità condizionale per qualche motivo che tornerà a mordermi più tardi?

Sono in Python 2.7, ma a quanto pare non è un tag qui.

    
posta Tom 27.08.2016 - 00:57
fonte

2 risposte

2

Quello che vedi qui, Tom, è il modello di progettazione Strategia , che può o meno essere una variante di Facciata o Decoratore e utilizza il Adattatore modello di progettazione.

Il metodo di strategia stesso è un metodo di fabbrica . Un metodo factory semplice produce un oggetto con i parametri forniti, ma hai fornito un ritorno condizionale a uno specifico sottotipo (o il supertipo stesso), hai fornito una strategia.

Quando si utilizza il modello di strategia di solito si fornisce un'interfaccia comune (adattatore) per tutte le implementazioni sottostanti e si usa quella. In questo modo stai astrarre le parti interiori di cui non ti interessa.

Considera il tuo esempio in una lingua compilata C++ :

#include <exception>

class Number
{
protected:
    int _number;

public:
    explicit Number(const int number)
        : _number(number)
    {       
    }

    int getNumber() const
    {
        return _number;
    }
};

class EvenNumber : public Number
{
public:
    explicit EvenNumber(const int number)
        : Number(number)
    {
    }

    bool isNumberReallyEven() const
    {
        return _number % 2 == 0;
    }
};

namespace NumberStrategy
{
    Number createNumber(const int number)
    {
        switch (number % 2)
        {
        case 0:
            return EvenNumber(number);
        case 1:
            return Number(number);
        }
        throw std::exception("Should never happen.");
    }
}

int main(int agrc, char* argv[])
{
    auto number = NumberStrategy::createNumber(2);
    auto isItEven = number.isNumberReallyEven();

    return EXIT_SUCCESS;
}

Compilando questo breve snippert, si otterrà un errore di compilazione sulla riga 52:

'isNumberReallyEven': is not a member of 'Number'

Ma perché, potresti chiedere? Passando il numero 2 alla funzione NumberStrategy::createNumber , dovrei avere l'istanza EvenNumber che ha il metodo isNumberReallyEven .

In realtà hai l'istanza, ma il compilatore non lo sa, perché il tipo di ritorno della funzione NumberStrategy::createNumber è Number . Se vuoi accedere ai metodi sottostanti, devi dire al compilatore quale tipo hai veramente colando.

C ++ 11 ha un concetto molto carino per questo chiamato reinterpret_cast . È completamente pericoloso, quindi quando lo usi devi essere assolutamente sicuro di sapere cosa stai facendo. Ovviamente, in questo esempio sai che passare un numero pari alla funzione NumberStrategy::createNumber restituirà il tipo EvenNumber .

Cambia il programma in questo modo:

int main(int agrc, char* argv[])
{
    auto number = NumberStrategy::createNumber(2);

    // I know I recieved a number but I am 100% sure it has to be even,
    // thus I am casting it manually without any worry.
    auto evenNumber = reinterpret_cast<EvenNumber&>(number);
    auto isItEven = evenNumber.isNumberReallyEven();

    return EXIT_SUCCESS;
}

E si compila.

Potresti chiederti perché ho deciso di mostrarti un esempio in C ++ invece di uno in Python? Non sono un programmatore Python ma lavoro con PHP (anche un linguaggio dinamico). Nei linguaggi dinamici il programma probabilmente funzionerebbe anche senza il cast, a causa della natura del linguaggio - controllerà semplicemente il tipo durante il runtime e vedrà se il metodo è disponibile o meno e nel caso di passaggio di un 2 al metodo sarebbe.

Usando per esempio C ++, il compilatore non ti permetterà di superare certe restrizioni che non dovresti affrontare in un linguaggio interpretato e che è buono. Quando un altro programmatore legge il codice, saprà immediatamente che lo stai facendo apposta.

Lo stesso vale per Python. Se chiami un metodo che sai essere disponibile in un sottotipo, ma il contratto del metodo afferma che viene restituito un supertipo, probabilmente dovresti commentare che sei esattamente sicuro che il metodo sarà disponibile quando lo chiami.

Poi c'è l'altra cosa: astrazione errata . Se incontri una situazione in cui sei costretto a digitare una variabile per accedere ai suoi metodi, dovrebbe esistere anche la relazione supertype<->subtype ?

Nello snippet C ++ questo problema viene facilmente risolto inserendo il metodo isNumberReallyEven nella classe Number . Se non puoi fare qualcosa del genere, il tuo design potrebbe essere sbagliato.

    
risposta data 27.08.2016 - 10:44
fonte
0

Questo è polimorfismo. Lo stai facendo solo delegando e decidendo cosa delegare a più tardi del necessario. Quando x è impostato, la delega dovrebbe essere impostata. Davvero non c'è bisogno di andare a chiedere più tardi. Oltre a essere clunky, invita anche i problemi di previsione delle filiali .

Ora sono sicuro che esistono modi orientati agli oggetti per eseguire dinamicamente il polimorfismo, il schema di stato o schema di strategia per citarne alcuni. Ma questo è python. Non devi farlo in questo modo OO qui.

In Python c'è questo trucco elegante chiamato patch per scimmia . Poiché le funzioni sono di prima classe in Python, puoi trattarle come dati. Quindi piuttosto che passare il valore di X attorno a te puoi passare attorno alla funzione che vuoi chiamare.

Qualsiasi approccio evita i problemi di previsione dei rami. Scegliere quale fare è più di leggibilità e manutenibilità. OO sembra ancora buono se vuoi un insieme di funzioni che cambiano insieme. Ma il patch delle scimmie è meglio se vuoi scambiarlo in modo indipendente.

    
risposta data 27.08.2016 - 21:35
fonte

Leggi altre domande sui tag