¿Cómo debo almacenar valores "desconocidos" y "faltantes" en una variable, sin dejar de mantener la diferencia entre "desconocido" y "perdido"?


57

Considere esta una pregunta "académica". Me he estado preguntando acerca de cómo evitar NULL de vez en cuando y este es un ejemplo en el que no puedo encontrar una solución satisfactoria.


Supongamos que almaceno medidas donde en ocasiones se sabe que la medida es imposible (o que falta). Me gustaría almacenar ese valor "vacío" en una variable evitando NULL. Otras veces el valor puede ser desconocido. Entonces, teniendo las medidas para un cierto marco de tiempo, una consulta sobre una medida dentro de ese período de tiempo podría devolver 3 tipos de respuestas:

  • La medición real en ese momento (por ejemplo, cualquier valor numérico incluido 0)
  • Un valor "faltante" / "vacío" (es decir, se realizó una medición y se sabe que el valor está vacío en ese punto).
  • Un valor desconocido (es decir, no se ha realizado ninguna medición en ese punto. Podría estar vacío, pero también podría ser cualquier otro valor).

Aclaración importante:

Suponiendo que tiene una función que get_measurement()devuelve una de "vacío", "desconocido" y un valor de tipo "entero". Tener un valor numérico implica que se pueden realizar ciertas operaciones en el valor de retorno (multiplicación, división, ...) pero el uso de tales operaciones en NULL bloqueará la aplicación si no se detecta.

Me gustaría poder escribir código, evitando verificaciones NULL, por ejemplo (pseudocódigo):

>>> 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()

Tenga en cuenta que ninguna de las printdeclaraciones causó excepciones (ya que no se utilizaron NULL). Por lo tanto, los valores vacíos y desconocidos se propagarían según sea necesario y la verificación de si un valor es realmente "desconocido" o "vacío" podría retrasarse hasta que sea realmente necesario (como almacenar / serializar el valor en algún lugar).


Nota al margen: La razón por la que me gustaría evitar los NULL es principalmente un desafío para la mente. Si quiero hacer cosas, no me opongo a usar NULL, pero descubrí que evitarlas puede hacer que el código sea mucho más robusto en algunos casos.


19
¿Por qué desea distinguir "medición realizada pero valor vacío" frente a "no medición"? De hecho, ¿qué significa "medición realizada pero valor vacío"? ¿El sensor no produjo un valor válido? En ese caso, ¿en qué se diferencia eso de "desconocido"? No podrá retroceder en el tiempo y obtener el valor correcto.
DaveG

3
@DaveG Suponga que busca el número de CPU en un servidor. Si el servidor está apagado o se ha descartado, ese valor simplemente no existe. Será una medida que no tiene ningún sentido (tal vez "falta" / "vacío" no son los mejores términos). Pero el valor "se sabe" que no tiene sentido. Si el servidor existe, pero el proceso de obtención del valor se bloquea, la medición es válida, pero falla, lo que da como resultado un valor "desconocido".
exhuma

2
@exhuma Lo describiría como "no aplicable", entonces.
Vincent

66
Por curiosidad, ¿qué tipo de medida está tomando donde "vacío" no es simplemente igual al cero de cualquier escala? "Desconocido" / "falta" Puedo ver que es útil, por ejemplo, si un sensor no está conectado o si la salida sin procesar del sensor es basura por una razón u otra, pero "vacío" en cada caso que se me ocurre puede ser más consistente representado por 0, []o {}(el escalar 0, la lista vacía y el mapa vacío, respectivamente). Además, ese valor "perdido" / "desconocido" es básicamente exactamente para lo que nullsirve: representa que podría haber un objeto allí, pero no lo hay.
Nic Hartley

77
Cualquiera que sea la solución que use para esto, asegúrese de preguntarse si tiene problemas similares a los que le hicieron querer eliminar NULL en primer lugar.
Ray

Respuestas:


85

La forma común de hacer esto, al menos con lenguajes funcionales es usar una unión discriminada. Este es entonces un valor que es uno de un int válido, un valor que denota "falta" o un valor que denota "desconocido". En F #, podría verse algo así como:

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

Un Measurementvalor será entonces a Reading, con un valor int, o a Missing, o an Unknowncon los datos sin procesar como value(si es necesario).

Sin embargo, si no está utilizando un lenguaje que admite uniones discriminadas, o su equivalente, es probable que este patrón no le sea de mucha utilidad. Entonces, podría usar, por ejemplo, una clase con un campo enum que denote cuál de los tres contiene los datos correctos.


77
puede hacer tipos de suma en idiomas OO, pero hay un poco de placa de caldera para que funcionen stackoverflow.com/questions/3151702/…
jk.

11
“[En lenguajes de idiomas no funcionales] este patrón no es de mucha utilidad para usted”: es un patrón bastante común en OOP. GOF tiene una variación de este patrón, y lenguajes como C ++ ofrecen construcciones nativas para codificarlo.
Konrad Rudolph

14
@jk. Sí, no cuentan (bueno, supongo que sí; son muy malos en este escenario debido a la falta de seguridad). Quise decir std::variant(y sus predecesores espirituales).
Konrad Rudolph

2
@Ewan No, dice "La medición es un tipo de datos que es ... o ...".
Konrad Rudolph

2
@DavidArno Bueno, incluso sin DUs hay una solución "canónica" para esto en OOP, que es tener una superclase de valores con subclases para valores válidos e inválidos. Pero eso probablemente va demasiado lejos (y en la práctica parece que la mayoría de las bases de código evitan el polimorfismo de la subclase a favor de una bandera para esto, como se muestra en otras respuestas).
Konrad Rudolph

58

Si aún no sabe qué es una mónada, hoy sería un gran día para aprender. Tengo una suave introducción para los programadores de OO aquí:

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

Su escenario es una pequeña extensión de "tal vez mónada", también conocida como Nullable<T>en C # y Optional<T>en otros lenguajes.

Supongamos que tiene un tipo abstracto para representar la mónada:

abstract class Measurement<T> { ... }

y luego tres subclases:

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 ...}

Necesitamos una implementación de 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 ...
  }

A partir de esto, puede escribir esta versión simplificada de Bind:

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

Y ahora que has terminado. Tienes una Measurement<int>en la mano. Quieres duplicarlo:

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

Y sigue la lógica; si mes Empty<int>entonces asStringes Empty<String>excelente.

Del mismo modo, si tenemos

Measurement<int> First()

y

Measurement<double> Second(int i);

entonces podemos combinar dos medidas:

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

y de nuevo, si First()es, Empty<int>entonces des Empty<double>y así sucesivamente.

El paso clave es obtener la operación de enlace correcta . Piénsalo bien.


44
Las mónadas (afortunadamente) son mucho más fáciles de usar que de entender. :)
Guran

11
@leftaroundabout: Precisamente porque no quería entrar en esa distinción desgarradora; Como señala el póster original, muchas personas carecen de confianza a la hora de tratar con las mónadas. La caracterización de la teoría de la categoría cargada de jerga de operaciones simples va en contra del desarrollo de un sentido de confianza y comprensión.
Eric Lippert

2
¿Entonces su consejo es reemplazar Nullcon Nullable+ algún código repetitivo? :)
Eric Duminil

3
@ Claude: Deberías leer mi tutorial. Una mónada es un tipo genérico que sigue ciertas reglas y proporciona la capacidad de unir una cadena de operaciones, por lo que, en este caso, Measurement<T>es el tipo monádico.
Eric Lippert

55
@daboross: aunque estoy de acuerdo en que las mónadas con estado son una buena forma de introducir mónadas, no creo que llevar el estado sea lo que caracteriza a una mónada. Creo que el hecho de que puedes unir una secuencia de funciones es lo más convincente; la capacidad de estado es solo un detalle de implementación.
Eric Lippert

18

Creo que en este caso sería útil una variación en un patrón de objeto nulo:

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
    };
}

Puede convertirlo en una estructura, anular Equals / GetHashCode / ToString, agregar conversiones implícitas desde o hacia int, y si desea un comportamiento similar a NaN, también puede implementar sus propios operadores aritméticos, por ejemplo. Measurement.Unknown * 2 == Measurement.Unknown.

Dicho esto, C # 's Nullable<int>implementa todo eso, con la única advertencia de que no se puede diferenciar entre diferentes tipos de nulls. No soy una persona Java, pero entiendo que el Java OptionalIntes similar, y es probable que otros lenguajes tengan sus propias instalaciones para representar un Optionaltipo.


66
La implementación más común que he visto de este patrón implica la herencia. Podría haber un caso para dos subclases: MissingMeasurement y UnknownMeasurement. Podrían implementar o anular métodos en la clase de medición principal. +1
Greg Burghardt

2
¿No es el punto del patrón de objeto nulo que no fallas en valores no válidos, sino que no haces nada?
Chris Wohlert

2
@ChrisWohlert en este caso, el objeto realmente no tiene ningún método, excepto el Valuecaptador, que absolutamente debe fallar, ya que no puede convertir una Unknowncopia de seguridad en un int. Si la medición tuviera, por ejemplo, un SaveToDatabase()método, entonces una buena implementación probablemente no realizaría una transacción si el objeto actual es un objeto nulo (ya sea mediante la comparación con un singleton o una anulación del método).
Maciej Stachowski

3
@MaciejStachowski Sí, no estoy diciendo que no debería hacer nada, estoy diciendo que el Patrón de objetos nulos no encaja bien. Su solución podría estar bien, pero no lo llamaría el patrón de objeto nulo .
Chris Wohlert

14

Si literalmente DEBE usar un número entero, entonces solo hay una solución posible. Use algunos de los valores posibles como 'números mágicos' que significan 'falta' y 'desconocido'

por ejemplo, 2,147,483,647 y 2,147,483,646

Si solo necesita el int para mediciones 'reales', cree una estructura de datos más complicada

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

Aclaración importante:

Puede lograr el requisito matemático sobrecargando los operadores para la clase

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

10
@KakturusOption<Option<Int>>
Bergi

55
@ Bergi No puedes pensar que eso es remotamente aceptable ...
BlueRaja - Danny Pflughoeft

8
@ BlueRaja-DannyPflughoeft En realidad, se ajusta bastante bien a la descripción de los OP, que también tiene una estructura anidada. Para ser aceptable, introduciríamos un alias de tipo apropiado (o "nuevo tipo"), por supuesto, pero un type Measurement = Option<Int>resultado que era un entero o una lectura vacía está bien, y también lo es Option<Measurement>para una medición que podría haberse tomado o no. .
Bergi

77
@arp "Enteros cerca de NaN"? ¿Podría explicar qué quiere decir con eso? Parece algo contradictorio decir que un número está "cerca" del concepto mismo de que algo no es un número.
Nic Hartley

3
@Nic Hartley En nuestro sistema, un grupo de lo que "naturalmente" habrían sido los enteros negativos más bajos posibles se reservó como NaN. Utilizamos ese espacio para codificar varias razones por las cuales esos bytes representaban algo diferente a los datos legítimos. (fue hace décadas y es posible que haya confundido algunos de los detalles, pero definitivamente había un conjunto de bits que podría poner en un valor entero para que arrojara NaN si intentaba hacer cálculos matemáticos con él.
arp

11

Si sus variables son números de punto flotante, IEEE754 (el estándar de número de punto flotante que es compatible con la mayoría de los procesadores e idiomas modernos) le respalda: es una característica poco conocida, pero el estándar no define uno, sino una familia completa de Valores de NaN (no un número), que pueden usarse para significados arbitrarios definidos por la aplicación. En flotantes de precisión simple, por ejemplo, tiene 22 bits libres que puede usar para distinguir entre 2 ^ {22} tipos de valores no válidos.

Normalmente, las interfaces de programación exponen solo una de ellas (por ejemplo, Numpy's nan); No sé si hay una forma integrada de generar los otros que no sea la manipulación explícita de bits, pero es solo una cuestión de escribir un par de rutinas de bajo nivel. (También necesitará uno para distinguirlos, porque, por diseño, a == bsiempre devuelve falso cuando uno de ellos es un NaN).

Usarlos es mejor que reinventar su propio "número mágico" para indicar datos no válidos, porque se propagan correctamente y señalan la invalidez: por ejemplo, no corre el riesgo de dispararse en el pie si usa una average()función y se olvida de verificar Sus valores especiales.

El único riesgo es que las bibliotecas no los admitan correctamente, ya que son una característica bastante oscura: por ejemplo, una biblioteca de serialización puede 'aplanarlos' a todos de la misma manera nan(lo que parece equivalente para la mayoría de los propósitos).


6

Siguiendo la respuesta de David Arno , puede hacer algo como una unión discriminada en OOP, y en un estilo funcional de objeto como el que ofrece Scala, los tipos funcionales de Java 8, o una biblioteca Java FP como Vavr o Fugue, se siente bastante natural escribir algo como:

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));

impresión

Value(4)
Empty()
Unknown()

( Implementación completa como una esencia ).

Un lenguaje o biblioteca FP proporciona otras herramientas como Try(también conocido como Maybe) (un objeto que contiene un valor o un error) y Either(un objeto que contiene un valor de éxito o un valor de falla) que también podrían usarse aquí.


2

La solución ideal para su problema dependerá de por qué le importa la diferencia entre una falla conocida y una medición no confiable conocida, y qué procesos posteriores desea respaldar. Tenga en cuenta que los 'procesos posteriores' para este caso no excluyen a los operadores humanos ni a otros desarrolladores.

El simple hecho de obtener un "segundo sabor" de nulo no le da al conjunto de procesos aguas abajo suficiente información para derivar un conjunto razonable de comportamientos.

Si, en cambio, confía en suposiciones contextuales sobre la fuente de los malos comportamientos que está haciendo el código descendente, llamaría a esa mala arquitectura.

Si sabe lo suficiente como para distinguir entre una razón de falla y una falla sin una razón conocida, y esa información informará comportamientos futuros, debe comunicar ese conocimiento corriente abajo o manejarlo en línea.

Algunos patrones para manejar esto:

  • Tipos de suma
  • Sindicatos discriminados
  • Objetos o estructuras que contienen una enumeración que representa el resultado de la operación y un campo para el resultado
  • Cuerdas mágicas o números mágicos que son imposibles de lograr a través del funcionamiento normal
  • Excepciones, en idiomas en los que este uso es idiomático.
  • Al darse cuenta de que en realidad no hay ningún valor en diferenciar entre estos dos escenarios y simplemente usar null

2

Si me preocupara "hacer algo" en lugar de una solución elegante, el truco rápido y sucio sería simplemente usar las cadenas "desconocido", "faltante" y "representación de cadena de mi valor numérico", que luego sería convertido de una cadena y utilizado según sea necesario. Implementado más rápido que escribir esto, y en al menos algunas circunstancias, totalmente adecuado. (Ahora estoy formando un grupo de apuestas sobre el número de votos negativos ...)


Votado por mencionar "hacer algo".
barbacoa

44
Algunas personas pueden notar que esto sufre la mayoría de los mismos problemas que el uso de NULL, es decir, que simplemente pasa de necesitar verificaciones NULL a necesitar verificaciones "desconocidas" y "faltantes", pero mantiene el bloqueo del tiempo de ejecución para la corrupción de datos silenciosa y afortunada para la mala suerte como los únicos indicadores de que olvidó un cheque. Incluso los cheques NULL que faltan tienen la ventaja de que los linters pueden atraparlos, pero esto pierde eso. Sin embargo, agrega una distinción entre "desconocido" y "perdido", por lo que supera a NULL allí ...
8bittree

2

Lo esencial si la pregunta parece ser "¿Cómo devuelvo dos datos no relacionados de un método que devuelve un solo int? Nunca quiero verificar mis valores de retorno, y los valores nulos son malos, no los use".

Veamos lo que quieres pasar. Está pasando una razón int o una razón no int de por qué no puede dar int. La pregunta afirma que solo habrá dos razones, pero cualquiera que haya hecho una enumeración sabe que cualquier lista crecerá. El alcance para especificar otras razones simplemente tiene sentido.

Inicialmente, entonces, parece que podría ser un buen caso para lanzar una excepción.

Cuando desea decirle a la persona que llama algo especial que no está en el tipo de retorno, las excepciones son a menudo el sistema apropiado: las excepciones no son solo para estados de error, y le permiten devolver una gran cantidad de contexto y justificación para explicar por qué simplemente puede No estoy hoy.

Y este es el ÚNICO sistema que le permite devolver entradas válidas garantizadas y garantizar que todos los operadores y métodos int que toman entradas pueden aceptar el valor de retorno de este método sin necesidad de verificar valores no válidos como valores nulos o mágicos.

Pero las excepciones son realmente solo una solución válida si, como su nombre lo indica, este es un caso excepcional , no el curso normal de los negocios.

Y un try / catch and handler es tan repetitivo como un cheque nulo, que fue lo que se objetó en primer lugar.

Y si la persona que llama no contiene el try / catch, entonces la persona que llama tiene que hacerlo, y así sucesivamente.


Un segundo paso ingenuo es decir "Es una medida. Las mediciones de distancia negativas son poco probables". Entonces, para algunas mediciones Y, solo puedes tener consts para

  • -1 = desconocido
  • -2 = imposible de medir,
  • -3 = se negó a responder,
  • -4 = conocido pero confidencial,
  • -5 = varía según la fase lunar, ver tabla 5a,
  • -6 = cuatro dimensiones, medidas dadas en el título,
  • -7 = error de lectura del sistema de archivos,
  • -8 = reservado para uso futuro,
  • -9 = cuadrado / cúbico, entonces Y es igual a X,
  • -10 = es una pantalla de monitor, por lo que no utiliza mediciones X, Y: utilice X como diagonal de la pantalla,
  • -11 = escribió las medidas en el reverso de un recibo y se lavó en ilegibilidad, pero creo que eran 5 o 17,
  • -12 = ... entiendes la idea.

Esta es la forma en que se hace en muchos sistemas C antiguos, e incluso en sistemas modernos donde hay una restricción genuina para int, y no se puede ajustar a una estructura o mónada de algún tipo.

Si las mediciones pueden ser negativas, simplemente aumenta el tipo de datos (por ejemplo, int largo) y hace que los valores mágicos sean más altos que el rango de int, e idealmente comience con algún valor que se muestre claramente en un depurador.

Sin embargo, hay buenas razones para tenerlos como una variable separada, en lugar de solo tener números mágicos. Por ejemplo, mecanografía estricta, mantenibilidad y conforme a las expectativas.


En nuestro tercer intento, entonces, observamos casos en los que es normal que los negocios tengan valores no int. Por ejemplo, si una colección de estos valores puede contener múltiples entradas no enteras. Esto significa que un controlador de excepciones puede ser el enfoque incorrecto.

En ese caso, parece un buen caso para una estructura que pasa el int y la justificación. Nuevamente, esta justificación puede ser una constante como la anterior, pero en lugar de mantener ambas en el mismo int, las almacena como partes distintas de una estructura. Inicialmente, tenemos la regla de que si se establece la justificación, no se establecerá el int. Pero ya no estamos atados a esta regla; También podemos proporcionar fundamentos para números válidos, si es necesario.

De cualquier manera, cada vez que lo llame, todavía necesita repetitivo, para probar la justificación para ver si el int es válido, luego retire y use la parte int si la justificación nos lo permite.

Aquí es donde debe investigar su razonamiento detrás de "no usar nulo".

Al igual que las excepciones, nulo significa un estado excepcional.

Si una persona que llama está llamando a este método e ignorando completamente la parte "racional" de la estructura, esperando un número sin ningún manejo de errores, y obtiene un cero, entonces manejará el cero como un número, y estará equivocado. Si obtiene un número mágico, lo tratará como un número y se equivocará. Pero si se anula, se caerá , como debería hacerlo.

Por lo tanto, cada vez que llame a este método, debe realizar comprobaciones de su valor de retorno; sin embargo, maneja los valores no válidos, ya sea dentro o fuera de banda, try / catch, verificando la estructura para un componente "racional", verificando el int para un número mágico, o buscando un int para un nulo ...

La alternativa, para manejar la multiplicación de una salida que puede contener un int inválido y una justificación como "Mi perro se comió esta medida", es sobrecargar el operador de multiplicación para esa estructura.

... Y luego sobrecargue a cualquier otro operador en su aplicación que pueda aplicarse a estos datos.

... Y luego sobrecargue todos los métodos que puedan tomar ints.

... Y todas esas sobrecargas aún deberán contener comprobaciones de entradas inválidas, solo para que pueda tratar el tipo de retorno de este método como si siempre fuera un int válido en el momento en que lo llama.

Entonces, la premisa original es falsa de varias maneras:

  1. Si tiene valores no válidos, no puede evitar verificar esos valores no válidos en cualquier punto del código donde maneja los valores.
  2. Si está devolviendo algo que no sea un int, no está devolviendo un int, por lo que no puede tratarlo como un int. La sobrecarga del operador le permite simular , pero eso es solo simular.
  3. Un int con números mágicos (incluidos NULL, NAN, Inf ...) ya no es realmente un int, es una estructura de pobre.
  4. Evitar los valores nulos no hará que el código sea más robusto, solo ocultará los problemas con ints o los moverá a una estructura compleja de manejo de excepciones.

1

No entiendo la premisa de su pregunta, pero aquí está la respuesta nominal. Para Missing or Empty, puede hacer math.nan(Not a Number). Puede realizar cualquier operación matemática math.nany permanecerá math.nan.

Puede usar None(nulo de Python) para un valor desconocido. De todos modos, no debe manipular un valor desconocido, y algunos lenguajes (Python no es uno de ellos) tienen operadores nulos especiales para que la operación solo se realice si el valor no es nulo; de lo contrario, el valor permanece nulo.

Otros idiomas tienen cláusulas de guardia (como Swift o Ruby), y Ruby tiene un retorno anticipado condicional.

He visto esto resuelto en Python de diferentes maneras:

  • con una estructura de datos envolventes, ya que la información numérica generalmente está a punto de una entidad y tiene un tiempo de medición. El reiniciador puede anular los métodos mágicos, de __mult__modo que no se generen excepciones cuando aparezcan sus valores Desconocido o Falta. Numpy y los pandas podrían tener tal capacidad en ellos.
  • con un valor centinela (como su Unknowno -1 / -2) y una declaración if
  • con una bandera booleana separada
  • con una estructura de datos diferida: su función realiza alguna operación en la estructura, luego regresa, la función más externa que necesita el resultado real evalúa la estructura de datos diferida
  • con una cartera de operaciones perezosa, similar a la anterior, pero esta se puede usar en un conjunto de datos o una base de datos

1

La forma en que se almacena el valor en la memoria depende del idioma y los detalles de implementación. Creo que lo que quieres decir es cómo debe comportarse el objeto para el programador. (Así es como leo la pregunta, dime si me equivoco).

Ya ha propuesto una respuesta a eso en su pregunta: use su propia clase que acepte cualquier operación matemática y se devuelva sin generar una excepción. Dices que quieres esto porque quieres evitar cheques nulos.

Solución 1: no evite las verificaciones nulas

Missingse puede representar como math.nan
Unknownse puede representar comoNone

Si tiene más de un valor, filter()solo puede aplicar la operación en valores que no son Unknowno Missing, o cualquier valor que desee ignorar para la función.

No puedo imaginar un escenario en el que necesite una comprobación nula de una función que actúa en un solo escalar. En ese caso, es bueno forzar comprobaciones nulas.


Solución 2: use un decorador que capture excepciones

En este caso, Missingpodría aumentar MissingExceptiony Unknownpodría aumentar UnknownExceptioncuando se realizan operaciones en él.

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

La ventaja de este enfoque es que las propiedades de Missingy Unknownsolo se suprimen cuando se solicita explícitamente que se supriman. Otra ventaja es que este enfoque es autodocumentado: cada función muestra si espera o no un desconocido o falta y cómo funciona la función.

Cuando llama a una función que no espera que un Missing obtenga un Missing, la función se elevará inmediatamente, mostrándole exactamente dónde ocurrió el error en lugar de fallar silenciosamente y propagar un Missing en la cadena de llamadas. Lo mismo vale para Desconocido.

sigmoidaún puede llamar sin, aunque no espera un Missingo Unknown, ya que sigmoidel decorador detectará la excepción.


1
maravilla cuál es el punto de publicar dos respuestas a la misma pregunta (esta es su respuesta previa , mal nada con ella?)
mosquito

@gnat Esta respuesta proporciona un razonamiento de por qué no debe hacerse de la manera que muestra el autor, y no quería pasar por la molestia de integrar dos respuestas con ideas diferentes; es más fácil escribir dos respuestas que se puedan leer de forma independiente . No entiendo por qué te importa tanto el razonamiento inofensivo de otra persona.
noɥʇʎԀʎzɐɹƆ

0

Suponga que busca el número de CPU en un servidor. Si el servidor está apagado o se ha descartado, ese valor simplemente no existe. Será una medida que no tiene ningún sentido (tal vez "falta" / "vacío" no son los mejores términos). Pero el valor "se sabe" que no tiene sentido. Si el servidor existe, pero el proceso de obtención del valor se bloquea, la medición es válida, pero falla, lo que da como resultado un valor "desconocido".

Ambas suenan como condiciones de error, por lo que juzgaría que la mejor opción aquí es simplemente get_measurement()lanzar ambas como excepciones de inmediato (como DataSourceUnavailableExceptiono SpectacularFailureToGetDataException, respectivamente). Luego, si se produce alguno de estos problemas, el código de recopilación de datos puede reaccionar ante él de inmediato (por ejemplo, al intentarlo de nuevo en el último caso), y get_measurement()solo tiene que devolver uno inten el caso de que pueda obtener con éxito los datos de los datos fuente - y sabes que intes válido.

Si su situación no admite excepciones o no puede hacer mucho uso de ellas, entonces una buena alternativa es usar códigos de error, tal vez devueltos a través de una salida separada a get_measurement(). Este es el patrón idiomático en C, donde la salida real se almacena en un puntero de entrada y se devuelve un código de error como valor de retorno.


0

Las respuestas dadas están bien, pero aún no reflejan la relación jerárquica entre valor, vacío y desconocido.

  • Lo más alto viene desconocido .
  • Luego, antes de usar un valor, primero debe vaciarse .
  • Por último viene el valor para calcular.

Feo (por su abstracción que falla), pero completamente operativo sería (en Java):

Optional<Optional<Integer>> unknowableValue;

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

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

Aquí los lenguajes funcionales con un buen sistema de tipos son mejores.

De hecho: Los vacíos / faltantes y * desconocidos que no son valores parecen más bien parte de algún estado del proceso, alguna línea de producción. Al igual que las celdas de hoja de cálculo Excel con fórmulas que hacen referencia a otras celdas. Allí se podría pensar en almacenar lambdas contextuales. Cambiar una celda volvería a evaluar todas las celdas recursivamente dependientes.

En ese caso, un proveedor int obtendría un valor int. Un valor vacío daría a un proveedor int lanzando una excepción vacía, o evaluando para vaciar (recursivamente hacia arriba). Su fórmula principal conectaría todos los valores y posiblemente también devolvería un valor vacío (valor / excepción). Un valor desconocido deshabilitaría la evaluación lanzando una excepción.

Los valores probablemente serían observables, como una propiedad vinculada a Java, notificando a los oyentes sobre el cambio.

En resumen: el patrón recurrente de la necesidad de valores con estados adicionales vacíos y desconocidos parece indicar que una hoja de cálculo más como el modelo de datos de propiedades vinculadas podría ser mejor.


0

Sí, el concepto de múltiples tipos de NA diferentes existe en algunos idiomas; más aún en los estadísticos, donde es más significativo (a saber, la gran distinción entre Missing-At-Random, Missing-Complely-At-Random, Missing-At-Random ).

  • si solo estamos midiendo las longitudes de los widgets, entonces no es crucial distinguir entre 'falla del sensor' o 'corte de energía' o 'falla de la red' (aunque el 'desbordamiento numérico' transmite información)

  • pero, por ejemplo, en la minería de datos o en una encuesta, preguntando a los encuestados, por ejemplo, sus ingresos o su estado de VIH, un resultado de 'Desconocido' es distinto de 'Rechazar respuesta', y puede ver que nuestras suposiciones anteriores sobre cómo imputar a este último tenderán ser diferente al primero. Entonces, los lenguajes como SAS admiten múltiples tipos de NA diferentes; el lenguaje R no lo hace, pero los usuarios a menudo tienen que hackear eso; Los NA en diferentes puntos de una tubería pueden usarse para denotar cosas muy diferentes.

  • también está el caso en el que tenemos múltiples variables de NA para una sola entrada ("imputación múltiple"). Ejemplo: si no conozco la edad, el código postal, el nivel de educación o los ingresos de una persona, es más difícil imputar sus ingresos.

En cuanto a cómo representa diferentes tipos de NA en lenguajes de uso general que no los admiten, en general las personas piratean cosas como NaN de punto flotante (requiere la conversión de enteros), enumeraciones o centinelas (por ejemplo, 999 o -1000) para enteros o valores categóricos Por lo general, no hay una respuesta muy clara, lo siento.


0

R tiene soporte de valor perdido incorporado. https://medium.com/coinmonks/dealing-with-missing-data-using-r-3ae428da2d17

Editar: porque fui rechazado voy a explicar un poco.

Si va a tratar con estadísticas, le recomiendo que use un lenguaje de estadísticas como R porque R está escrito por estadísticos para estadísticos. La falta de valores es un tema tan grande que te enseñan todo un semestre. Y hay grandes libros solo sobre valores perdidos.

Sin embargo, puede marcar sus datos faltantes, como un punto o "falta" o lo que sea. En R puedes definir lo que quieres decir con faltar. No necesitas convertirlos.

La forma normal de definir el valor perdido es marcarlos como NA.

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

Entonces puede ver qué valores faltan;

is.na(x)

Y entonces el resultado será;

FALSE FALSE  TRUE FALSE FALSE

Como puedes ver ""no falta. Puedes amenazar ""como desconocido. Y NAfalta.


@Hulk, ¿qué otros lenguajes funcionales admiten valores perdidos? Incluso si admiten valores faltantes, estoy seguro de que no puede completarlos con métodos estadísticos en una sola línea de código.
ilhan

-1

¿Hay alguna razón por la que la funcionalidad del *operador no se pueda alterar en su lugar?

La mayoría de las respuestas implican un valor de búsqueda de algún tipo, pero podría ser más fácil modificar el operador matemático en este caso.

A continuación, sería capaz de tener similares empty()/ unknown()funcionalidad a través de todo el proyecto.


44
Esto significa que tendrían que sobrecargar todos los operadores
tubería
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.