Come devo memorizzare i valori "sconosciuto" e "mancante" in una variabile, pur mantenendo la differenza tra "sconosciuto" e "mancante"?


57

Considera questa una domanda "accademica". Mi sono chiesto di evitare di tanto in tanto i NULL e questo è un esempio in cui non riesco a trovare una soluzione soddisfacente.


Supponiamo di memorizzare le misurazioni in cui in alcune occasioni è noto che la misurazione è impossibile (o mancante). Vorrei memorizzare quel valore "vuoto" in una variabile evitando NULL. Altre volte il valore potrebbe essere sconosciuto. Pertanto, avendo le misurazioni per un determinato intervallo di tempo, una query su una misurazione in quel periodo di tempo potrebbe restituire 3 tipi di risposte:

  • La misurazione effettiva in quel momento (ad esempio, qualsiasi valore numerico compreso 0)
  • Un valore "mancante" / "vuoto" (ovvero, è stata eseguita una misurazione e il valore è noto per essere vuoto in quel punto).
  • Un valore sconosciuto (ovvero, a quel punto non è stata effettuata alcuna misurazione. Potrebbe essere vuoto, ma potrebbe anche essere qualsiasi altro valore).

Chiarimento importante:

Supponendo di avere una funzione che get_measurement()restituisce una "vuota", "sconosciuta" e un valore di tipo "intero". La presenza di un valore numerico implica che è possibile eseguire determinate operazioni sul valore restituito (moltiplicazione, divisione, ...) ma l'utilizzo di tali operazioni su NULL provoca l'arresto anomalo dell'applicazione se non viene rilevato.

Vorrei poter scrivere codice, evitando i controlli NULL, ad esempio (pseudocodice):

>>> value = get_measurement()  # returns `2`
>>> print(value * 2)
4

>>> value = get_measurement()  # returns `Empty()`
>>> print(value * 2)
Empty()

>>> value = get_measurement()  # returns `Unknown()`
>>> print(value * 2)
Unknown()

Si noti che nessuna delle printistruzioni ha causato eccezioni (poiché non sono stati utilizzati NULL). Quindi i valori vuoti e sconosciuti si propagerebbero se necessario e il controllo se un valore è in realtà "sconosciuto" o "vuoto" potrebbe essere ritardato fino a quando non è realmente necessario (come memorizzare / serializzare il valore da qualche parte).


Nota a margine: il motivo per cui mi piacerebbe evitare i NULL è principalmente un rompicapo. Se voglio fare cose non sono contrario all'utilizzo dei NULL, ma ho scoperto che evitarli può rendere il codice molto più robusto in alcuni casi.


19
Perché desideri distinguere "misurazione eseguita ma valore vuoto" rispetto a "nessuna misurazione"? In effetti, cosa significa "misurazione effettuata ma valore vuoto"? Il sensore non ha prodotto un valore valido? In tal caso, in che modo differisce da "sconosciuto"? Non sarai in grado di tornare indietro nel tempo e ottenere il valore corretto.
DaveG,

3
@DaveG Supponiamo di recuperare il numero di CPU in un server. Se il server è spento o è stato eliminato, quel valore semplicemente non esiste. Sarà una misura che non ha alcun senso (forse "mancante" / "vuoto" non sono i termini migliori). Ma il valore è "noto" per essere privo di senso. Se il server esiste, ma il processo di recupero del valore si arresta in modo anomalo, la sua misurazione è valida, ma non riesce a generare un valore "sconosciuto".
exhuma,

2
@exhuma Lo descriverei come "non applicabile", quindi.
Vincent,

6
Per curiosità, che tipo di misurazione stai prendendo dove "vuoto" non è semplicemente uguale allo zero di qualunque scala? "Sconosciuto" / "mancante" Riesco a vedere utile, ad esempio se un sensore non è collegato o se l'output non elaborato del sensore è spazzatura per un motivo o per l'altro, ma "vuoto" in ogni caso a cui riesco a pensare può essere più coerente rappresentato da 0, []o {}(rispettivamente lo 0 scalare, l'elenco vuoto e la mappa vuota). Inoltre, quel valore "mancante" / "sconosciuto" è fondamentalmente esattamente ciò che nullserve - rappresenta che potrebbe esserci un oggetto lì, ma non lo è.
Nic Hartley,

7
Qualunque sia la soluzione che usi per questo, assicurati di chiederti se soffre di problemi simili a quelli che ti hanno fatto desiderare di eliminare NULL in primo luogo.
Ray

Risposte:


85

Il modo comune per farlo, almeno con i linguaggi funzionali, è usare un'unione discriminata. Questo è quindi un valore che è uno di un int valido, un valore che indica "mancante" o un valore che indica "sconosciuto". In F #, potrebbe assomigliare a:

type Measurement =
    | Reading of value : int
    | Missing
    | Unknown of value : RawData

Un Measurementvalore sarà quindi a Reading, con un valore int, o a Missing, o Unknowncon i dati non valueelaborati come (se richiesto).

Tuttavia, se non stai usando un linguaggio che supporti i sindacati discriminati o il loro equivalente, questo modello non è molto utile per te. Quindi, ad esempio, potresti usare una classe con un campo enum che indica quale dei tre contiene i dati corretti.


7
puoi fare i tipi di somma nelle lingue OO ma c'è un bel po 'di piastra della caldaia per farli funzionare stackoverflow.com/questions/3151702/…
jk.

11
"[In linguaggi linguistici non funzionali] questo modello non è molto utile per te" - È un modello abbastanza comune in OOP. GOF ha una variazione di questo modello e linguaggi come C ++ offrono costrutti nativi per codificarlo.
Konrad Rudolph,

14
@jk. Sì, non contano (beh, immagino che lo facciano; sono solo molto cattivi in ​​questo scenario a causa della mancanza di sicurezza). Intendevo std::variant(e i suoi predecessori spirituali).
Konrad Rudolph,

2
@Ewan No, sta dicendo "La misurazione è un tipo di dati che è ... o ...".
Konrad Rudolph,

2
@DavidArno Beh, anche senza i DU esiste una soluzione “canonica” per questo in OOP, che deve avere una superclasse di valori con sottoclassi per valori validi e non validi. Ma probabilmente sta andando troppo lontano (e in pratica sembra che la maggior parte delle basi di codice eviti il ​​polimorfismo della sottoclasse in favore di una bandiera per questo, come mostrato in altre risposte).
Konrad Rudolph,

58

Se non sai già cos'è una monade, oggi sarebbe un grande giorno per imparare. Ho una delicata introduzione per i programmatori OO qui:

https://ericlippert.com/2013/02/21/monads-part-one/

Il tuo scenario è una piccola estensione della "forse monade", nota anche come Nullable<T>in C # e Optional<T>in altre lingue.

Supponiamo che tu abbia un tipo astratto per rappresentare la monade:

abstract class Measurement<T> { ... }

e poi tre sottoclassi:

final class Unknown<T> : Measurement<T> { ... a singleton ...}
final class Empty<T> : Measurement<T> { ... a singleton ... }
final class Actual<T> : Measurement<T> { ... a wrapper around a T ...}

Abbiamo bisogno di un'implementazione di Bind:

abstract class Measurement<T>
{ 
    public Measurement<R> Bind(Func<T, Measurement<R>> f)
  {
    if (this is Unknown<T>) return Unknown<R>.Singleton;
    if (this is Empty<T>) return Empty<R>.Singleton;
    if (this is Actual<T>) return f(((Actual<T>)this).Value);
    throw ...
  }

Da questo puoi scrivere questa versione semplificata di Bind:

public Measurement<R> Bind(Func<A, R> f) 
{
  return this.Bind(a => new Actual<R>(f(a));
}

E ora hai finito. Hai un Measurement<int>in mano. Vuoi raddoppiarlo:

Measurement<int> m = whatever;
Measurement<int> doubled = m.Bind(a => a * 2);
Measurement<string> asString = m.Bind(a => a.ToString());

E segui la logica; se mè Empty<int>allora asStringè Empty<String>eccellente.

Allo stesso modo, se abbiamo

Measurement<int> First()

e

Measurement<double> Second(int i);

allora possiamo combinare due misure:

Measurement<double> d = First().Bind(Second);

e ancora, se First()è Empty<int>allora dè Empty<double>e così via.

Il passaggio chiave consiste nel rendere corretta l'operazione di associazione . Pensaci bene.


4
Le monadi (per fortuna) sono molto più facili da usare che da capire. :)
Guran,

11
@leftaroundabout: Proprio perché non volevo entrare in quella distinzione da spaccare i capelli; come osserva il poster originale, molte persone mancano di fiducia quando si tratta di affrontare le monadi. Le caratterizzazioni della teoria delle categorie cariche di gergo di semplici operazioni si oppongono allo sviluppo di un senso di fiducia e comprensione.
Eric Lippert,

2
Quindi il tuo consiglio è di sostituire Nullcon Nullable+ un po 'di codice boilerplate? :)
Eric Duminil,

3
@Claude: dovresti leggere il mio tutorial. Una monade è un tipo generico che segue determinate regole e offre la possibilità di legare insieme una catena di operazioni, quindi in questo caso Measurement<T>è il tipo monadico.
Eric Lippert,

5
@daboross: Anche se concordo sul fatto che le monadi con stato siano un buon modo per introdurre le monadi, non penso che portare lo stato sia la cosa che caratterizza una monade. Penso che il fatto che tu possa unire una sequenza di funzioni sia la cosa convincente; lo stato è solo un dettaglio di implementazione.
Eric Lippert,

18

Penso che in questo caso sarebbe utile una variazione su un modello di oggetto null:

public class Measurement
{
    private int value;
    private bool isUnknown = false;
    private bool isMissing = false;

    private Measurement() { }
    public Measurement(int value) { this.value = value; }

    public int Value {
        get {
            if (!isUnknown && !isMissing)
            {
                return this.value;
            }
            throw new SomeException("...");
        }                   
    }

    public static readonly Measurement Unknown = new Measurement
    {
        isUnknown = true
    };

    public static readonly Measurement Missing = new Measurement
    {
        isMissing = true
    };
}

Puoi trasformarlo in una struttura, sovrascrivere Equals / GetHashCode / ToString, aggiungere conversioni implicite da o verso inte, se vuoi un comportamento simile a NaN, puoi anche implementare i tuoi operatori aritmetici in modo che, ad es. Measurement.Unknown * 2 == Measurement.Unknown.

Detto questo, C # Nullable<int>implementa tutto ciò, con l'unico avvertimento che non è possibile distinguere tra diversi tipi di nulls. Non sono una persona Java, ma la mia comprensione è che Java OptionalIntè simile e altre lingue probabilmente hanno le proprie strutture per rappresentare un Optionaltipo.


6
L'implementazione più comune che ho visto di questo modello riguarda l'ereditarietà. Potrebbe esserci un caso per due sottoclassi: MissingMeasurement e UnknownMeasurement. Potrebbero implementare o sovrascrivere i metodi nella classe di misurazione principale. +1
Greg Burghardt

2
Non è il punto del modello a oggetti nulli che non fallisci su valori non validi, ma piuttosto non fai nulla?
Chris Wohlert,

2
@ChrisWohlert in questo caso l'oggetto non ha davvero alcun metodo tranne il Valuegetter, che dovrebbe assolutamente fallire in quanto non è possibile riconvertire Unknownun int. Se la misurazione avesse un SaveToDatabase()metodo , diciamo, allora una buona implementazione probabilmente non eseguirà una transazione se l'oggetto corrente è un oggetto nullo (tramite il confronto con un singleton o una sostituzione del metodo).
Maciej Stachowski il

3
@MaciejStachowski Sì, non sto dicendo che non dovrebbe fare nulla, sto dicendo che il Null Object Pattern non è adatto. La tua soluzione potrebbe andare bene, ma non la definirei Null Object Pattern .
Chris Wohlert,

14

Se DEVI letteralmente usare un numero intero, allora c'è solo una possibile soluzione. Usa alcuni dei possibili valori come "numeri magici" che significano "mancante" e "sconosciuto"

ad es. 2.147.483.647 e 2.147.483.646

Se hai solo bisogno dell'int per misurazioni "reali", crea una struttura dati più complicata

class Measurement {
    public bool IsEmpty;
    public bool IsKnown;
    public int Value {
        get {
            if(!IsEmpty && IsKnown) return _value;
            throw new Exception("NaN");
            }
        }
}

Chiarimento importante:

È possibile raggiungere il requisito matematico sovraccaricando gli operatori per la classe

public static Measurement operator+ (Measurement a, Measurement b) {
    if(a.IsEmpty) { return b; }
    ...etc
}

10
@KakturusOption<Option<Int>>
Bergi

5
@Bergi Non puoi nemmeno pensare che sia anche lontanamente accettabile ..
BlueRaja - Danny Pflughoeft

8
@ BlueRaja-DannyPflughoeft In realtà si adatta abbastanza bene alla descrizione dei PO, che ha anche una struttura nidificata. Per diventare accettabili, introdurremmo ovviamente un alias di tipo corretto (o "newtype"), ma a type Measurement = Option<Int>per un risultato che era un numero intero o una lettura vuota va bene, e quindi è un Option<Measurement>per una misurazione che avrebbe potuto essere presa o no .
Bergi,

7
@arp "Integers near NaN"? Potresti spiegare cosa intendi con questo? Sembra in qualche modo controintuitivo dire che un numero è "vicino" al concetto stesso di qualcosa che non è un numero.
Nic Hartley,

3
@Nic Hartley Nel nostro sistema un gruppo di quelli che sarebbero stati "naturalmente" i numeri interi negativi più bassi possibili è stato riservato come NaN. Abbiamo usato quello spazio per codificare vari motivi per cui quei byte rappresentavano qualcosa di diverso dai dati legittimi. (È stato decenni fa e potrei aver confuso alcuni dettagli, ma c'era sicuramente una serie di bit che potresti mettere in un valore intero per fargli lanciare NaN se provassi a fare matematica con esso.
Arp

11

Se le variabili sono numeri in virgola mobile, IEEE754 (lo standard dei numeri in virgola mobile supportato dalla maggior parte dei processori e dei linguaggi moderni) ha le spalle: è una caratteristica poco conosciuta, ma lo standard definisce non uno, ma un'intera famiglia di Valori NaN (non un numero), che possono essere utilizzati per significati arbitrari definiti dall'applicazione. Nei float a precisione singola, ad esempio, sono disponibili 22 bit liberi che è possibile utilizzare per distinguere tra 2 ^ {22} tipi di valori non validi.

Normalmente, le interfacce di programmazione ne espongono solo una (ad es. Numpy nan); Non so se esiste un modo integrato per generare gli altri oltre alla manipolazione esplicita dei bit, ma si tratta solo di scrivere un paio di routine di basso livello. (Ne avrai anche bisogno per distinguerli, perché, in base alla progettazione, a == brestituisce sempre false quando uno di essi è un NaN.)

Utilizzarli è meglio che reinventare il proprio "numero magico" per segnalare dati non validi, perché si propagano correttamente e segnalano invalidità: ad esempio, non si rischia di spararsi nel piede se si utilizza una average()funzione e si dimentica di controllare per i tuoi valori speciali.

L'unico rischio è che le librerie non le supportino correttamente, dal momento che sono una caratteristica piuttosto oscura: ad esempio, una libreria di serializzazione può "appiattirle" tutte allo stesso nan(che sembra equivalente ad essa per la maggior parte degli scopi).


6

Seguendo la risposta di David Arno , puoi fare qualcosa di simile a un'unione discriminata in OOP, e in uno stile oggetto-funzionale come quello offerto da Scala, dai tipi funzionali Java 8 o da una libreria FP di Java come Vavr o Fugue sembra abbastanza naturale scrivere qualcosa del tipo:

var value = Measurement.of(2);
out.println(value.map(x -> x * 2));

var empty = Measurement.empty();
out.println(empty.map(x -> x * 2));

var unknown = Measurement.unknown();
out.println(unknown.map(x -> x * 2));

stampa

Value(4)
Empty()
Unknown()

( Implementazione completa come sintesi .)

Un linguaggio o una libreria FP fornisce altri strumenti come Try(aka Maybe) (un oggetto che contiene un valore o un errore) e Either(un oggetto che contiene un valore di successo o un valore di errore) che possono essere utilizzati anche qui.


2

La soluzione ideale al tuo problema dipenderà dal motivo per cui ti preoccupi della differenza tra un errore noto e una misurazione nota inaffidabile e quali processi a valle vuoi supportare. Si noti che i "processi a valle" per questo caso non escludono operatori umani o colleghi sviluppatori.

La semplice presentazione di un "secondo sapore" di null non fornisce alla serie di processi a valle informazioni sufficienti per derivare una serie ragionevole di comportamenti.

Se invece fai affidamento su ipotesi contestuali sulla fonte di comportamenti scorretti fatti dal codice a valle, chiamerei quella cattiva architettura.

Se conosci abbastanza per distinguere tra un motivo di fallimento e un fallimento senza un motivo noto e che le informazioni informeranno i comportamenti futuri, dovresti comunicare tale conoscenza a valle o gestirla in linea.

Alcuni schemi per gestirlo:

  • Tipi di somma
  • Sindacati discriminati
  • Oggetti o strutture contenenti un enum che rappresenta il risultato dell'operazione e un campo per il risultato
  • Stringhe magiche o numeri magici impossibili da ottenere tramite il normale funzionamento
  • Eccezioni, nelle lingue in cui questo uso è idiomatico
  • Rendersi conto che in realtà non vi è alcun valore nel differenziare tra questi due scenari e il solo utilizzo null

2

Se fossi preoccupato di "fare qualcosa" piuttosto che una soluzione elegante, il trucco rapido e sporco sarebbe semplicemente usare le stringhe "sconosciuto", "mancante" e "rappresentazione in forma di stringa del mio valore numerico", che sarebbe quindi convertito da una stringa e utilizzato secondo necessità. Implementato più rapidamente della scrittura di questo e, almeno in alcune circostanze, del tutto adeguato. (Ora sto formando un pool di scommesse sul numero di downgrade ...)


È stato votato per aver menzionato "fare qualcosa".
barbecue,

4
Alcune persone potrebbero notare che questo soffre della maggior parte degli stessi problemi dell'utilizzo di NULL, vale a dire che passa dalla necessità di controlli NULL alla necessità di controlli "sconosciuti" e "mancanti", ma mantiene l'arresto anomalo del tempo di esecuzione per il danneggiamento fortunato e silenzioso dei dati per gli sfortunati come unici indicatori che hai dimenticato un assegno. Anche i controlli NULL mancanti hanno il vantaggio che i linter potrebbero catturarli, ma questo lo perde. Aggiunge una distinzione tra "sconosciuto" e "mancante", tuttavia, quindi batte NULL lì ...
8bittree

2

L'essenziale se la domanda sembra essere "Come posso restituire due informazioni non correlate da un metodo che restituisce un singolo int? Non voglio mai controllare i miei valori di ritorno, e i valori null sono cattivi, non usarli".

Diamo un'occhiata a ciò che vuoi passare. Stai passando un int o una logica non int per il motivo per cui non puoi dare l'int. La domanda afferma che ci saranno solo due ragioni, ma chiunque abbia mai fatto un enum sa che ogni elenco crescerà. La portata di specificare altri razionali ha semplicemente senso.

Inizialmente, quindi, sembra che potrebbe essere un buon caso per lanciare un'eccezione.

Quando vuoi dire al chiamante qualcosa di speciale che non è nel tipo restituito, le eccezioni sono spesso il sistema appropriato: le eccezioni non sono solo per gli stati di errore e ti consentono di restituire un sacco di contesto e logica per spiegare perché puoi semplicemente oggi int.

E questo è l'UNICO sistema che consente di restituire ints validi garantiti e garantire che ogni operatore e metodo int che accetta ints possa accettare il valore di ritorno di questo metodo senza mai dover verificare valori non validi come null o valori magici.

Ma le eccezioni sono davvero solo una soluzione valida se, come suggerisce il nome, questo è un caso eccezionale , non il normale corso degli affari.

E un try / catch and handler è lo stesso boilerplate di un controllo null, che era ciò a cui si era opposto in primo luogo.

E se il chiamante non contiene il tentativo / cattura, allora il chiamante deve e così via.


Un secondo passaggio ingenuo è dire "È una misurazione. È improbabile che si verifichino misurazioni di distanza negative". Quindi per alcune misurazioni Y, puoi avere solo costi per

  • -1 = sconosciuto,
  • -2 = impossibile da misurare,
  • -3 = rifiutato di rispondere,
  • -4 = noto ma riservato,
  • -5 = varia a seconda della fase lunare, vedi tabella 5a,
  • -6 = quadridimensionale, misure fornite nel titolo,
  • -7 = errore di lettura del file system,
  • -8 = riservato per uso futuro,
  • -9 = quadrato / cubico, quindi Y è uguale a X,
  • -10 = è uno schermo monitor quindi non usa le misurazioni X, Y: usa X come diagonale dello schermo,
  • -11 = ha scritto le misure sul retro di una ricevuta ed è stata riciclata nell'illegabilità ma penso che fosse 5 o 17,
  • -12 = ... hai avuto l'idea.

Questo è il modo in cui viene fatto in molti vecchi sistemi C, e persino nei sistemi moderni in cui esiste un vero vincolo per int, e non è possibile racchiuderlo in una struttura o monade di qualche tipo.

Se le misurazioni possono essere negative, allora devi solo ingrandire il tuo tipo di dati (ad esempio long int) e avere i valori magici più alti dell'intervallo dell'int, e idealmente iniziare con un valore che verrà mostrato chiaramente in un debugger.

Ci sono buoni motivi per averli come variabili separate, invece di avere solo numeri magici. Ad esempio, tipizzazione rigorosa, manutenibilità e conformità alle aspettative.


Nel nostro terzo tentativo, quindi, esaminiamo i casi in cui è normale che il business abbia valori non int. Ad esempio, se una raccolta di questi valori può contenere più voci non intere. Ciò significa che un gestore di eccezioni potrebbe essere l'approccio sbagliato.

In tal caso, sembra un buon caso per una struttura che passa l'int e la logica. Ancora una volta, questa logica può essere solo una const come la precedente, ma invece di tenerle entrambe nello stesso int, le memorizzi come parti distinte di una struttura. Inizialmente, abbiamo la regola che se viene impostata la logica, l'int non verrà impostato. Ma non siamo più legati a questa regola; possiamo fornire razionali anche per numeri validi, se necessario.

Ad ogni modo, ogni volta che lo chiami, hai ancora bisogno di una piastra di cottura, per testare la logica per vedere se l'int è valido, quindi estrai e usa la parte int se la logica ci consente.

È qui che devi indagare sul tuo ragionamento alla base di "non usare null".

Come per le eccezioni, null significa uno stato eccezionale.

Se un chiamante sta chiamando questo metodo e ignora completamente la parte "logica" della struttura, aspettandosi un numero senza alcuna gestione degli errori e ottiene uno zero, allora gestirà lo zero come un numero e si sbaglierà. Se ottiene un numero magico, lo tratterà come un numero e si sbaglia. Ma se diventa nullo, cadrà , come dannatamente bene dovrebbe fare.

Quindi ogni volta che chiami questo metodo devi mettere un segno di spunta per il suo valore di ritorno, tuttavia gestisci i valori non validi, sia nella banda che fuori banda, try / catch, controllando la struttura per un componente "razionale", controllando l'int per un numero magico, o controllando un int per un null ...

L'alternativa, per gestire la moltiplicazione di un output che potrebbe contenere un int non valido e una logica come "Il mio cane ha mangiato questa misura", è quella di sovraccaricare l'operatore di moltiplicazione per quella struttura.

... E quindi sovraccaricare tutti gli altri operatori dell'applicazione che potrebbero essere applicati a questi dati.

... E quindi sovraccaricare tutti i metodi che potrebbero richiedere ints.

... E tutti questi sovraccarichi dovranno contenere ancora controlli per gli inte non validi, solo per poter trattare il tipo di ritorno di questo metodo come se fosse sempre un int valido nel momento in cui lo chiami.

Quindi la premessa originale è falsa in vari modi:

  1. Se hai valori non validi, non puoi evitare di cercare quei valori non validi in qualsiasi punto del codice in cui gestisci i valori.
  2. Se stai restituendo qualcosa di diverso da un int, non stai restituendo un int, quindi non puoi trattarlo come un int. Il sovraccarico dell'operatore ti consente di fingere , ma è solo finta.
  3. Un int con numeri magici (inclusi NULL, NAN, Inf ...) non è più un vero int, è una struttura da povero.
  4. Evitare i null non renderà il codice più robusto, nasconderà solo i problemi con gli ints o li sposterà in una complessa struttura di gestione delle eccezioni.

1

Non capisco la premessa della tua domanda, ma ecco la risposta del valore nominale. Per Missing o Empty, potresti farlo math.nan(Not a Number). Puoi eseguire qualsiasi operazione matematica math.nane rimarrà math.nan.

È possibile utilizzare None(null di Python) per un valore sconosciuto. Non dovresti comunque manipolare un valore sconosciuto e alcune lingue (Python non è una di queste) hanno operatori null speciali in modo che l'operazione venga eseguita solo se il valore non è null, altrimenti il ​​valore rimane nullo.

Altre lingue hanno clausole di guardia (come Swift o Ruby) e Ruby ha un ritorno anticipato condizionato.

Ho visto questo risolto in Python in diversi modi:

  • con una struttura di dati wrapper, poiché le informazioni numeriche di solito si trovano su un'entità e hanno un tempo di misurazione. Il wrapper può ignorare i metodi magici in __mult__modo tale che non vengano sollevate eccezioni quando emergono i tuoi valori Unknown o Missing. Numpy e Panda potrebbero avere tale capacità in loro.
  • con un valore sentinella (come il tuo Unknowno -1 / -2) e un'istruzione if
  • con una bandiera booleana separata
  • con una struttura di dati pigri: la tua funzione esegue alcune operazioni sulla struttura, quindi ritorna, la funzione più esterna che richiede il risultato effettivo valuta la struttura di dati pigri
  • con una pipeline pigra di operazioni, simile alla precedente, ma questa può essere utilizzata su un set di dati o su un database

1

La modalità di memorizzazione del valore dipende dalla lingua e dai dettagli di implementazione. Penso che tu voglia dire come l'oggetto dovrebbe comportarsi con il programmatore. (Ecco come ho letto la domanda, dimmi se sbaglio.)

Hai già proposto una risposta a questa domanda: usa la tua classe che accetta qualsiasi operazione matematica e restituisce se stessa senza sollevare un'eccezione. Dici di volerlo perché vuoi evitare controlli nulli.

Soluzione 1: non evitare controlli nulli

Missingpuò essere rappresentato come math.nan
Unknownpuò essere rappresentato comeNone

Se si dispone di più di un valore, è possibile filter()applicare l'operazione solo su valori che non lo sono Unknowno Missingo su qualsiasi valore si desideri ignorare per la funzione.

Non riesco a immaginare uno scenario in cui è necessario un controllo null su una funzione che agisce su un singolo scalare. In tal caso, è bene forzare i controlli null.


Soluzione 2: utilizzare un decoratore che rileva le eccezioni

In questo caso, Missingpotrebbe aumentare MissingExceptione Unknownpotrebbe aumentare UnknownExceptionquando vengono eseguite operazioni su di esso.

@suppressUnknown(value=Unknown) # if an UnknownException is raised, return this value instead
@suppressMissing(value=Missing)
def sigmoid(value):
    ...

Il vantaggio di questo approccio è che le proprietà di Missinge Unknownvengono soppresse solo quando si chiede esplicitamente che vengano soppresse. Un altro vantaggio è che questo approccio è auto-documentante: ogni funzione mostra se si aspetta o meno uno sconosciuto o una mancanza e come la funzione.

Quando si chiama una funzione, non ci si aspetta che un Missing diventi Missing, la funzione si solleverà immediatamente, mostrandoti esattamente dove si è verificato l'errore invece di fallire silenziosamente e propagando un Missing up nella catena di chiamate. Lo stesso vale per Unknown.

sigmoidpuò ancora chiamare sin, anche se non si aspetta un Missingo Unknown, poiché sigmoidil decoratore catturerà l'eccezione.


1
mi chiedo qual è il punto di pubblicare due risposte alla stessa domanda (questa è la tua risposta precedente , qualcosa di sbagliato in essa?)
moscerino

@gnat Questa risposta fornisce un ragionamento sul perché non dovrebbe essere fatto come mostra l'autore, e non volevo passare attraverso la seccatura di integrare due risposte con idee diverse - è solo più facile scrivere due risposte che possono essere lette in modo indipendente . Non capisco perché ti importi così tanto del ragionamento innocuo di qualcun altro.
noɥʇʎԀʎzɐɹƆ

0

Supponiamo di recuperare il numero di CPU in un server. Se il server è spento o è stato eliminato, quel valore semplicemente non esiste. Sarà una misura che non ha alcun senso (forse "mancante" / "vuoto" non sono i termini migliori). Ma il valore è "noto" per essere privo di senso. Se il server esiste, ma il processo di recupero del valore si arresta in modo anomalo, la sua misurazione è valida, ma non riesce a generare un valore "sconosciuto".

Entrambi questi sembrano condizioni di errore, quindi giudicherei che l'opzione migliore qui è semplicemente get_measurement()lanciare entrambi immediatamente come eccezioni (come rispettivamente, DataSourceUnavailableExceptiono SpectacularFailureToGetDataException). Quindi, se si verifica uno di questi problemi, il codice di raccolta dati può reagire immediatamente (come riprovando in quest'ultimo caso) e get_measurement()deve solo restituire un intnel caso in cui possa ottenere correttamente i dati dai dati fonte - e sai che intè valido.

Se la tua situazione non supporta le eccezioni o non è in grado di utilizzarle molto, una buona alternativa è quella di utilizzare i codici di errore, magari restituiti tramite un output separato a get_measurement(). Questo è il modello idiomatico in C, in cui l'output effettivo è memorizzato in un puntatore di input e un codice di errore viene restituito come valore di ritorno.


0

Le risposte fornite vanno bene, ma non riflettono ancora la relazione gerarchica tra valore, vuoto e sconosciuto.

  • Il più alto viene sconosciuto .
  • Quindi, prima di utilizzare un valore, è necessario chiarire innanzitutto il valore vuoto .
  • Ultimo arriva il valore con cui calcolare.

Brutto (per la sua astrazione fallita), ma pienamente operativo sarebbe (in Java):

Optional<Optional<Integer>> unknowableValue;

unknowableValue.ifPresent(emptiableValue -> ...);
Optional<Integer> emptiableValue = unknowableValue.orElse(Optional.empty());

emptiableValue.ifPresent(value -> ...);
int value = emptiableValue.orElse(0);

Qui i linguaggi funzionali con un bel sistema di tipi sono migliori.

Infatti: I vuoti / mancanti e sconosciuti * disvalori sembrano piuttosto parte di un qualche stato del processo, alcuni pipeline di produzione. Come le celle di foglio di calcolo Excel con formule che fanno riferimento ad altre celle. Lì si potrebbe pensare di memorizzare forse lambda contestuali. Cambiare una cellula rivaluterebbe tutte le cellule dipendenti ricorsivamente.

In tal caso un valore int verrebbe ottenuto da un fornitore int. Un valore vuoto darebbe a un fornitore int che lancia un'eccezione vuota o che valuta di svuotare (ricorsivamente verso l'alto). La tua formula principale collegherà tutti i valori e probabilmente restituirà anche un valore vuoto (valore / eccezione). Un valore sconosciuto disabiliterebbe la valutazione generando un'eccezione.

I valori probabilmente sarebbero osservabili, come una proprietà associata a Java, che notifica agli ascoltatori il cambiamento.

In breve: il modello ricorrente che richiede valori con stati aggiuntivi vuoti e sconosciuti sembra indicare che un modello di dati più diffuso come le proprietà associate potrebbe essere migliore.


0

Sì, il concetto di più diversi tipi di NA esiste in alcune lingue; ancora di più in quelli statistici, dove è più significativo (vale a dire l'enorme distinzione tra Missing-At-Random, Missing-Completely-At-Random, Missing-Not-At-Random ).

  • se stiamo solo misurando le lunghezze dei widget, non è fondamentale distinguere tra "guasto del sensore" o "interruzione di corrente" o "guasto di rete" (sebbene "overflow numerico" trasmetta informazioni)

  • ma ad esempio nel data mining o in un sondaggio, chiedendo agli intervistati, ad esempio, il loro reddito o lo stato dell'HIV, il risultato di "Sconosciuto" è distinto da "Rifiuta di rispondere" e si può vedere che le nostre assunzioni precedenti su come imputare quest'ultima essere diverso dal primo. Quindi linguaggi come SAS supportano diversi tipi di NA; il linguaggio R non lo fa, ma gli utenti devono spesso hackerarlo; Le NA in diversi punti di una pipeline possono essere utilizzate per indicare cose molto diverse.

  • c'è anche il caso in cui abbiamo più variabili NA per una singola voce ("imputazione multipla"). Esempio: se non conosco l'età, il codice postale, il livello di istruzione o il reddito di una persona, è più difficile imputare il proprio reddito.

Per quanto riguarda il modo in cui rappresenti diversi tipi di NA in linguaggi di uso generale che non li supportano, generalmente le persone hackerano cose come NaN a virgola mobile (richiede la conversione di numeri interi), enumerazioni o sentinelle (ad esempio 999 o -1000) per intero o valori categorici. Di solito non c'è una risposta molto chiara, scusa.


0

R ha il supporto del valore mancante incorporato. https://medium.com/coinmonks/dealing-with-missing-data-using-r-3ae428da2d17

Modifica: perché sono stato sottovalutato ho intenzione di spiegare un po '.

Se hai a che fare con le statistiche ti consiglio di usare un linguaggio statistico come R perché R è scritto da statistici per statistici. La mancanza di valori è un argomento così importante che ti insegnano un intero semestre. E ci sono grandi libri solo sui valori mancanti.

Puoi comunque contrassegnare i dati mancanti, come un punto o "mancante" o altro. In R puoi definire cosa intendi per mancante. Non è necessario convertirli.

Il modo normale di definire il valore mancante è contrassegnarli come NA.

x <- c(1, 2, NA, 4, "")

Quindi puoi vedere quali valori mancano;

is.na(x)

E poi il risultato sarà;

FALSE FALSE  TRUE FALSE FALSE

Come vedi ""non manca. Puoi minacciare ""come sconosciuto. E NAmanca.


@Hulk, quali altri linguaggi funzionali supportano i valori mancanti? Anche se supportano valori mancanti, sono sicuro che non puoi riempirli con metodi statistici in una sola riga di codice.
ilhan,

-1

C'è un motivo per cui la funzionalità *dell'operatore non può essere modificata invece?

La maggior parte delle risposte comporta un valore di ricerca di qualche tipo, ma in questo caso potrebbe essere più semplice modificare l'operatore matematico.

Saresti quindi in grado di avere funzionalità empty()/ simili in unknown()tutto il tuo progetto.


4
Ciò significa che dovresti sovraccaricare tutti gli operatori
pipe
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.