¿Cómo extraño código oculta errores? Análisis TensorFlow.NET

TensorFlow.NET y PVS-Studio

El análisis estático es una herramienta extremadamente útil para cualquier desarrollador, ya que ayuda a encontrar a tiempo no solo errores, sino también piezas de código sospechosas y extrañas que pueden causar desconcierto a los programadores que tendrían que trabajar con él en el futuro. Esta idea se demostrará mediante un análisis del proyecto abierto C # de TensorFlow.NET, que se está desarrollando para trabajar con la popular biblioteca de aprendizaje automático TensorFlow.

Mi nombre es Nikita Lipilin. Hace algún tiempo me uní al departamento de programadores de C # de PVS-Studio. Tradicionalmente, todos los recién llegados al equipo escriben artículos que discuten los resultados de verificar varios proyectos abiertos utilizando el analizador estático PVS-Studio. Dichos artículos ayudan a los nuevos empleados a conocer mejor el producto y, al mismo tiempo, proporcionan beneficios adicionales en términos de popularización de la metodología de análisis estático. Le sugiero que se familiarice con mi primer trabajo sobre el tema de análisis de proyectos abiertos.

Introducción


La variedad de posibles errores en el código del programa es sorprendente. Algunos de ellos se revelan de inmediato, luego de un examen superficial de la aplicación creada, otros pueden ser difíciles de notar incluso cuando un equipo de desarrolladores experimentados realiza una revisión del código. Sin embargo, también sucede que, debido a la falta de atención o alguna otra razón, el programador a veces escribe código simplemente extraño e ilógico, que, sin embargo, (parece) cumplir con éxito su función. Pero solo más allá, al volver a lo que estaba escrito, o cuando otros estudian este código, comienzan a surgir preguntas que no se pueden responder.

Refactorizar el código antiguo puede convertirse en problemas, especialmente si otras partes del programa dependen de él, por lo que cuando encuentre incluso algunas construcciones curvadas francamente, el ″ ¿Funciona? ¡No lo toques! ″. Posteriormente, esto dificulta el estudio de la fuente y, por lo tanto, se hace difícil ampliar las capacidades disponibles. La base del código está obstruida, existe la posibilidad de que algunos problemas internos pequeños e invisibles, pero potencialmente muy desagradables, no se solucionen a tiempo.

En algún momento, se sentirán las consecuencias de este error, pero su búsqueda llevará mucho tiempo, porque las sospechas del desarrollador recaerán en una gran cantidad de fragmentos de código extraños que no se refactorizaron al mismo tiempo. De esto se deduce que varios problemas y rarezas en un fragmento particular deben corregirse inmediatamente después de su redacción. En el caso de que haya razones razonables para dejar todo tal como está (por ejemplo, si el código está escrito como algún tipo de espacio en blanco ″ para más adelante ″), dicho fragmento debe ir acompañado de un comentario explicativo.

También vale la pena señalar que, independientemente de las calificaciones del desarrollador, algunos momentos problemáticos y simplemente sin éxito pueden escaparse de su mirada, y a veces se puede aplicar una "solución temporal" en algún lugar, que pronto se olvidará y se convertirá en "permanente". Posteriormente, el análisis de dicho código (muy probablemente, otro desarrollador se involucrará en esto) tomará un esfuerzo inaceptablemente grande.

En tales casos, la revisión de código puede ayudar. Sin embargo, si la tarea es bastante seria, entonces esta opción requerirá mucho tiempo. Además, cuando hay muchos pequeños errores o deficiencias, entonces, detrás de ellos, el examinador puede no notar errores de alto nivel. Verificar el código se convierte en una rutina tediosa, y gradualmente disminuye la efectividad de la revisión.

Obviamente, las tareas de rutina se transferirán mucho mejor de persona a computadora. Este enfoque se utiliza en muchas áreas de la modernidad. La automatización de diversos procesos es la clave de la prosperidad. ¿Qué es la automatización en el contexto de este tema?

Un asistente confiable para resolver el problema de escribir un código de trabajo razonable y estable es el análisis estático. Cada vez antes de enviar los resultados de sus actividades a la revisión, el programador podrá realizar una verificación automática y no cargar a otros desarrolladores (y a sí mismo) con un trabajo innecesario. El código se enviará para su verificación solo después de que se hayan tenido en cuenta todas las advertencias del analizador: se han solucionado los errores y se han reescrito momentos extraños, o al menos explicados por un comentario.

Por supuesto, la necesidad de revisar el código no desaparece, pero el análisis estático complementa y simplifica enormemente su implementación. Una parte suficientemente grande de los errores se solucionará gracias al analizador, y los momentos extraños definitivamente no serán olvidados y marcados en consecuencia. En consecuencia, con una revisión de código, será posible centrarse en verificar la corrección de la implementación de interacciones lógicas complejas y encontrar problemas subyacentes que, desafortunadamente, no pueden ser detectados por el analizador (todavía).

TensorFlow.NET




Este artículo está inspirado en el proyecto TensorFlow.NET. Representa la implementación de la capacidad de trabajar con la funcionalidad de la popular biblioteca de aprendizaje automático TensorFlow a través del código C # (por cierto, también lo probamos ). Esta idea parecía bastante interesante, porque al momento de escribir, trabajar con la biblioteca solo está disponible en Python, Java y Go.

El código fuente disponible en GitHub se actualiza constantemente y ahora su volumen es un poco más de cien mil líneas. Después de un estudio superficial, hubo un deseo de realizar la verificación mediante análisis estático. PVS-Studio se utilizó como herramienta específica, lo que ha demostrado su eficacia en un número bastante grande de proyectos diferentes .


Para TensorFlow.NET, el analizador mostró una serie de alertas: 39 niveles altos, 227 niveles medios y 154 niveles bajos (puede leer acerca de los niveles de advertencia aquí en la subsección "Niveles de advertencia y conjuntos de reglas de diagnóstico"). Un análisis detallado de cada uno de ellos haría que este artículo sea gigantesco, por lo tanto, solo los que me parecieron más interesantes se describen a continuación. También vale la pena señalar que algunos de los problemas encontrados se encuentran varias veces en el proyecto, y el análisis de cada fragmento de código está más allá del alcance de este artículo.

El proyecto se plantea una tarea bastante seria y, desafortunadamente, la aparición de varios tipos de fragmentos de código extraños es inevitable. En este artículo intentaré mostrar que el uso del análisis estático puede simplificar enormemente el trabajo de los programadores, señalando áreas que pueden causar preguntas. No siempre una advertencia indica un error, pero a menudo indica un código que podría causar preguntas en una persona. En consecuencia, la probabilidad de que un código extraño se rediseñe o, en cualquier caso, se comente en consecuencia, aumenta significativamente.

Fragmentos que llamaron la atención al estudiar el informe del analizador.


De hecho, una cantidad bastante grande de advertencias del analizador para este proyecto puede llamarse no tanto errores, sino código extraño. Al ver las líneas para las cuales se emite una advertencia, al menos surge el desconcierto. Por supuesto, algunos de los ejemplos presentados a continuación son probablemente ″ soluciones temporales ″, pero no dan ningún comentario sobre este tema, lo que significa que alguien que trabaje con este código en el futuro tendrá preguntas que necesitan respuestas. Tomará tiempo extra.


Al mismo tiempo, algunas advertencias apuntan a un código que obviamente no solo es extraño, sino simplemente incorrecto. Este es el principal peligro de ″ código extraño ″: es extremadamente difícil notar un error real si aquí y allá ves soluciones incomprensibles y gradualmente te acostumbras al hecho de que el código parece ser incorrecto.

Tour sofisticado de colección


private static void _RemoveDefaultAttrs(....)
{
  var producer_op_dict = new Dictionary<string, OpDef>();
  producer_op_list.Op.Select(op =>
  {
    producer_op_dict[op.Name] = op;
    return op;
  }).ToArray();           
  ....
}

Advertencia del analizador : V3010 Se requiere utilizar el valor de retorno de la función 'ToArray'. importador.cs 218

El analizador considera que la llamada a Toray en este momento es sospechosa, ya que el valor devuelto por esta función no está asignado a una variable. Sin embargo, dicho código no es un error. Esta construcción se usa para llenar el diccionario productor_op_dict con valores correspondientes a los elementos de la lista productor_op_lista . Es necesaria una llamada a ToArray para que la función pasada como argumento al método Select se llame para todos los elementos de la colección.

En mi opinión, el código no se ve mejor. Completar el diccionario es algo obvio, y algunos desarrolladores pueden querer eliminar la llamada ″ innecesaria ″ de ToArray . Sería mucho más simple y comprensible usar el bucle foreach aquí :

var producer_op_dict = new Dictionary<string, OpDef>();

foreach (var op in producer_op_list.Op)
{
  producer_op_dict[op.Name] = op;
}

En este caso, el código parece lo más simple posible.

Otro momento similar se ve así:

public GraphDef convert_variables_to_constants(....)
{
  ....
  inference_graph.Node.Select(x => map_name_to_node[x.Name] = x).ToArray();
  ....
}

Advertencia del analizador : V3010 Se requiere utilizar el valor de retorno de la función 'ToArray'. graph_util_impl.cs 48

La única diferencia es que dicho registro parece más conciso, pero solo la tentación de eliminar la llamada ToArray aquí no desaparece, y todavía parece no obvio.

Solución temporal


public GraphDef convert_variables_to_constants(....)
{
  ....
  var source_op_name = get_input_name(node);
  while(map_name_to_node[source_op_name].Op == "Identity")
  {
    throw new NotImplementedException);
    ....
  }
  ....
}

Advertencia del analizador : V3020 Un 'lanzamiento' incondicional dentro de un bucle. graph_util_impl.cs 73

En el proyecto que se está considerando, a menudo se usa el siguiente enfoque: si algún comportamiento se implementa más adelante, se lanza una excepción NotImplementedException en el lugar apropiado . Está claro por qué el analizador advierte de un posible error en esta pieza: usar while en lugar de if realmente no parece demasiado razonable.

Esta no es la única advertencia que aparece debido al uso de soluciones temporales. Por ejemplo, hay un método:

public static Tensor[] _SoftmaxCrossEntropyWithLogitsGrad(
  Operation op, Tensor[] grads
)
{
  var grad_loss = grads[0];
  var grad_grad = grads[1];
  var softmax_grad = op.outputs[1];
  var grad = _BroadcastMul(grad_loss, softmax_grad);

  var logits = op.inputs[0];
  if(grad_grad != null && !IsZero(grad_grad)) // <=
  {
    throw new NotImplementedException("_SoftmaxCrossEntropyWithLogitsGrad");
  }

  return new Tensor[] 
  {
    grad,
    _BroadcastMul(grad_loss, -nn_ops.log_softmax(logits))
  };
}

Advertencia del analizador : La expresión V3022 'grad_grad! = Null &&! IsZero (grad_grad)' siempre es falsa. nn_grad.cs 93

De hecho, nunca se lanzará una NotImplementedException ("_ SoftmaxCrossEntropyWithLogitsGrad") , ya que este código es simplemente inalcanzable. Para desentrañar el motivo, debe ir al código de la función IsZero :

private static bool IsZero(Tensor g)
{
  if (new string[] { "ZerosLike", "Zeros" }.Contains(g.op.type))
    return true;

  throw new NotImplementedException("IsZero");
}

El método devuelve verdadero o produce una excepción. Este código no es un error, obviamente, la implementación aquí se deja para más adelante. Lo principal es que ″ entonces ″ realmente ha llegado. Bueno, resultó con mucho éxito que PVS-Studio no te permitirá olvidar que hay una imperfección aquí :)

¿Tensor es tensor?


private static Tensor[] _ExtractInputShapes(Tensor[] inputs)
{
  var sizes = new Tensor[inputs.Length];
  bool fully_known = true;
  for(int i = 0; i < inputs.Length; i++)
  {
    var x = inputs[i];

    var input_shape = array_ops.shape(x);
    if (!(input_shape is Tensor) || input_shape.op.type != "Const")
    {
      fully_known = false;
      break;
    }

    sizes[i] = input_shape;
  }
  ....
}

Advertencia del analizador : V3051 Una verificación de tipo excesiva. El objeto ya es del tipo 'Tensor'. array_grad.cs 154

El tipo de retorno del método de forma es Tensor . Entonces input_shape es Tensor check parece al menos extraño. Quizás, una vez que el método devuelve un valor de un tipo diferente y la verificación tiene sentido, pero también es posible que en lugar de Tensor la condición especifique algún tipo de heredero para esta clase. De una forma u otra, el desarrollador debe prestar atención a este fragmento.

Verificación exhaustiva


public static Tensor[] _BaseFusedBatchNormGrad(....)
{
  ....
  if (data_format == "NCHW") // <=
    throw new NotImplementedException("");

  var results = grad_fun(new FusedBatchNormParams
  {
    YBackprop = grad_y,
    X = x,
    Scale = scale,
    ReserveSpace1 = pop_mean,
    ReserveSpace2 = pop_var,
    ReserveSpace3 = version == 2 ? op.outputs[5] : null,
    Epsilon = epsilon,
    DataFormat = data_format,
    IsTraining = is_training
  });

  var (dx, dscale, doffset) = (results[0], results[1], results[2]);
  if (data_format == "NCHW") // <=
    throw new NotImplementedException("");

  ....
}

Advertencias del analizador :

  • V3021 Hay dos declaraciones 'if' con expresiones condicionales idénticas. La primera instrucción 'if' contiene el método return. Esto significa que la segunda declaración 'if' no tiene sentido nn_grad.cs 230
  • V3022 La expresión 'data_format == "NCHW"' siempre es falsa. nn_grad.cs 247

A diferencia de algunos de los ejemplos anteriores, claramente hay algo mal con el código. La segunda verificación no tiene ningún sentido, ya que si la condición es verdadera, el programa no la alcanzará en absoluto. Quizás se permite algún error tipográfico aquí, o uno de los controles es generalmente superfluo.

La ilusión de elección


public Tensor Activate(Tensor x, string name = null)
{
  ....
  Tensor negative_part;
  if (Math.Abs(_threshold) > 0.000001f)
  {
    negative_part = gen_ops.relu(-x + _threshold);
  } else
  {
    negative_part = gen_ops.relu(-x + _threshold);
  }
  ....
}

Advertencia del analizador : V3004 La declaración 'then' es equivalente a la declaración 'else'. gen_nn_ops.activations.cs 156

Una demostración bastante divertida de la efectividad del uso del análisis estático en el desarrollo. Es difícil encontrar una razón razonable por la que el desarrollador escribió este código en particular; lo más probable es que este sea un error típico de copiar y pegar (aunque esto, por supuesto, puede ser otro "para más adelante").

Hay otros fragmentos de un plan similar, por ejemplo:

private static Operation _GroupControlDeps(
  string dev, Operation[] deps, string name = null
)
{
  return tf_with(ops.control_dependencies(deps), ctl =>
  {
    if (dev == null)
    {
      return gen_control_flow_ops.no_op(name);
    }
    else
    {
      return gen_control_flow_ops.no_op(name);
    }
  });
}

Advertencia del analizador : V3004 La declaración 'then' es equivalente a la declaración 'else'. control_flow_ops.cs 135

Tal vez una vez que la verificación tuvo sentido, pero con el tiempo desapareció, o se planean más cambios en el futuro. Sin embargo, ni una ni la otra opción parecen ser una justificación suficiente para dejar algo similar en el código, sin explicar esta rareza de ninguna manera. Con un alto grado de probabilidad, se produjo un error de copiar y pegar exactamente de la misma manera.

Cheque tardío


public static Tensor[] Input(int[] batch_shape = null,
  TF_DataType dtype = TF_DataType.DtInvalid,
  string name = null,
  bool sparse = false,
  Tensor tensor = null)
{
  var batch_size = batch_shape[0];
  var shape = batch_shape.Skip(1).ToArray(); // <=

  InputLayer input_layer = null;
  if (batch_shape != null)                   // <=
    ....
  else
    ....

  ....
}

Advertencia del analizador : V3095 El objeto 'batch_shape' se usó antes de que se verificara como nulo. Verifique las líneas: 39, 42. keras.layers.cs 39

Un error clásico y bastante peligroso del uso potencial de una variable, que es un enlace a ninguna parte. En este caso, el código implica explícitamente la posibilidad de que batch_shape sea nulo ; esto queda claro tanto en la lista de argumentos como en la verificación posterior de la misma variable. Por lo tanto, el analizador aquí indica un claro error.

¿Otro ″ para después ″?


public MnistDataSet(
  NDArray images, NDArray labels, Type dataType, bool reshape // <=
) 
{
  EpochsCompleted = 0;
  IndexInEpoch = 0;

  NumOfExamples = images.shape[0];

  images = images.reshape(
    images.shape[0], images.shape[1] * images.shape[2]
  );
  images = images.astype(dataType);
  // for debug np.multiply performance
  var sw = new Stopwatch();
  sw.Start();
  images = np.multiply(images, 1.0f / 255.0f);
  sw.Stop();
  Console.WriteLine($"{sw.ElapsedMilliseconds}ms");
  Data = images;

  labels = labels.astype(dataType);
  Labels = labels;
}

Advertencia del analizador : no se utiliza el parámetro V3117 Constructor 'remodelar'. MnistDataSet.cs 15

Al igual que algunas otras rarezas, esto probablemente se deba al hecho de que la funcionalidad está lejos de estar completamente implementada y es muy posible que el parámetro de remodelación se use de alguna manera en este constructor en el futuro. Pero por el momento, parece que está siendo arrojado aquí así como así. Si esto realmente se hace ″ para más adelante ″, entonces sería prudente marcar esto con algún comentario. Si no es así, resulta que el código que construye el objeto tendrá que lanzar un parámetro adicional al constructor, y es muy posible que sea más conveniente no hacerlo.

Escurridizo posible nula desreferencia


public static Tensor[] _GatherV2Grad(Operation op, Tensor[] grads)
{
  ....
  if((int)axis_static == 0)
  {
    var params_tail_shape = params_shape.slice(new NumSharp.Slice(start:1));
    var values_shape = array_ops.concat(
      new[] { indices_size, params_tail_shape }, 0
    );
    var values = array_ops.reshape(grad, values_shape);
    indices = array_ops.reshape(indices, indices_size);
    return new Tensor[]
    {
      new IndexedSlices(values, indices, params_shape), // <=
      null,
      null
    };
  }
  ....
}

Advertencia del analizador : V3146 Posible desreferencia nula del primer argumento 'valores' dentro del método. '_Outputs.FirstOrDefault ()' puede devolver un valor nulo predeterminado. array_grad.cs 199

Para comprender cuál es el problema, primero debe recurrir al código del constructor indexedSlices :

public IndexedSlices(
  Tensor values, Tensor indices, Tensor dense_shape = null
)
{
  _values = values;
  _indices = indices;
  _dense_shape = dense_shape;

  _values.Tag = this; // <=
}

Obviamente, pasar nulo a este constructor arrojará una excepción. Sin embargo, ¿por qué el analizador considera que la variable de valores puede contener nulo ?

PVS-Studio utiliza la técnica de análisis de flujo de datos, que le permite encontrar los conjuntos de posibles valores de variables en diferentes partes del código. Una advertencia le indica que se puede devolver nulo en la variable especificada con la siguiente línea: _outputs.FirstOrDefault () . Sin embargo, al mirar el código anterior, puede encontrar que el valor de la variable de valores se obtiene llamando a array_ops.reshape (grad, values_shape) . Entonces, ¿dónde _outputs.FirstOrDefault () entonces ?

El hecho es que al analizar el flujo de datos, no solo se considera la función actual, sino también todas las llamadas; PVS-Studio obtiene información sobre el conjunto de valores posibles de cualquier variable en cualquier lugar. Por lo tanto, una advertencia significa que la implementación de array_ops.reshape (grad, values_shape) contiene una llamada a _outputs.FirstOrDefault () , cuyo resultado finalmente se devuelve.

Para verificar esto, vaya a la implementación de remodelación :

public static Tensor reshape<T1, T2>(T1 tensor, T2 shape, string name = null)
            => gen_array_ops.reshape(tensor, shape, null);

Luego vaya al método de remodelación llamado dentro:
public static Tensor reshape<T1, T2>(T1 tensor, T2 shape, string name = null)
{
  var _op = _op_def_lib._apply_op_helper(
    "Reshape", name, new { tensor, shape }
  );
  return _op.output;
}

La función _apply_op_helper devuelve un objeto de la clase Operation que contiene la propiedad de salida . Al recibir su valor, se llama al código descrito en la advertencia:

public Tensor output => _outputs.FirstOrDefault();

Tensor es, por supuesto, un tipo de referencia, por lo que el valor predeterminado para él será nulo . De todo esto se puede ver que PVS-Studio analiza meticulosamente la estructura lógica del código, penetrando profundamente en la estructura de las llamadas.

El analizador completó su trabajo, lo que indica un lugar potencialmente problemático. El programador tiene que verificar si puede surgir una situación cuando los elementos en _salidas están ausentes.

Por lo tanto, el análisis estático al menos obligará al desarrollador a prestar atención al fragmento sospechoso para evaluar si el error realmente puede ocurrir allí. Con este enfoque, la cantidad de errores que pasan desapercibidos se reducirá rápidamente.

¿Espera poco confiable?


private (LoopVar<TItem>, Tensor[]) _BuildLoop<TItem>(
  ....
) where ....
{
  ....
  // Finds the closest enclosing non-None control pivot.
  var outer_context = _outer_context;
  object control_pivot = null;
  while (outer_context != null && control_pivot == null) // <=
  {

  }

  if (control_pivot != null)
  {

  }
  ....
}

Advertencia del analizador : V3032 Esperar en esta expresión no es confiable, ya que el compilador puede optimizar algunas de las variables. Use variables volátiles o primitivas de sincronización para evitar esto. WhileContext.cs 212

El analizador indica que el compilador puede optimizar dicha implementación de espera, pero dudo que realmente hayan intentado implementar la espera aquí; lo más probable es que el código simplemente no se haya agregado y se desarrolle más en el futuro. Puede valer la pena lanzar una NotImplementedException aquí , dado que esta práctica se usa en otras partes del proyecto. De una forma u otra, creo que algún comentario explicativo obviamente no haría daño.

Rompiendo fronteras


public TensorShape(int[][] dims)
{
  if(dims.Length == 1)
  {
    switch (dims[0].Length)
    {
      case 0: shape = new Shape(new int[0]); break;
      case 1: shape = Shape.Vector((int)dims[0][0]); break;
      case 2: shape = Shape.Matrix(dims[0][0], dims[1][2]); break; // <=
      default: shape = new Shape(dims[0]); break;
    }
  }
  else
  {
    throw new NotImplementedException("TensorShape int[][] dims");
  }
}

Advertencia del analizador : V3106 Posiblemente el índice está fuera de límite. El índice '1' apunta más allá del límite 'dims'. TensorShape.cs 107

Entre los fragmentos extraños de código que vi, hubo un error real, que es muy difícil de notar. El siguiente fragmento es erróneo aquí: dims [1] [2] . Obviamente, obtener un elemento con el índice 1 de una matriz de un elemento es un error. Al mismo tiempo, si cambia el fragmento a dims [0] [2] , aparece otro error: obtener un elemento con índice 2 de la matriz dims [0] , cuya longitud en este caso es la rama 2. Por lo tanto, este problema resultó ser como con un "doble fondo".

En cualquier caso, este fragmento de código debe ser estudiado y corregido por el desarrollador. En mi opinión, este ejemplo es una excelente ilustración del rendimiento del Análisis de flujo de datos en PVS-Studio.

Olepatka?


private void _init_from_args(object initial_value = null, ....) // <=
{
  var init_from_fn = initial_value.GetType().Name == "Func`1"; // <=
  ....
  tf_with(...., scope =>
  {
    ....
    tf_with(...., delegate
    {
      initial_value = ops.convert_to_tensor(  // <=
        init_from_fn ? (initial_value as Func<Tensor>)():initial_value,
        name: "initial_value",
        dtype: dtype
      );
    });
    _shape = shape ?? (initial_value as Tensor).TensorShape;
    _initial_value = initial_value as Tensor; // <=
    ....
    _dtype = _initial_value.dtype.as_base_dtype(); // <=

    if (_in_graph_mode)
    {
      ....

      if (initial_value != null) // <=
      {
        ....
      }

      ....
    }

    ....
  });
}

Para comprender el código anterior, también vale la pena presentar la implementación de la función tf_with:

[DebuggerStepThrough] // with "Just My Code" enabled this lets the 
[DebuggerNonUserCode()]  //debugger break at the origin of the exception
public static void tf_with<T>(
  T py, Action<T> action
) where T : ITensorFlowObject
{
  try
  {
    py.__enter__();
    action(py);
  }
  finally
  {
    py.__exit__();
    py.Dispose();
  }
}

Advertencia del analizador : V3019 Posiblemente, una variable incorrecta se compara con nula después de la conversión de tipo usando la palabra clave 'as'. Verifique las variables 'initial_value', '_initial_value'. ResourceVariable.cs 137

_init_from_args es una función bastante voluminosa, por lo que se han omitido muchos fragmentos. Puedes verlo completamente haciendo clic en el enlace . Aunque la advertencia al principio no me pareció realmente seria, después de estudiar, me di cuenta de que algo definitivamente estaba mal con el código.

En primer lugar, debe tenerse en cuenta que el método se puede invocar sin pasar parámetros y, de forma predeterminada, será nulo en initial_value . En este caso, se lanzará una excepción en la primera línea.

En segundo lugar, la verificaciónvalor_inicial a la desigualdad nula parece extraño: si valor_inicial realmente se volvió nulo después de llamar a ops.convert_to_tensor , entonces _valor_inicial sería nulo , lo que significa que llamar a _initial_value.dtype.as_base_dtype () también arrojaría una excepción.

El analizador sugiere que es posible que deba verificar si el valor nulo es _inicial_valor , pero como se señaló anteriormente, la referencia a esta variable tiene lugar antes de esta prueba, por lo que esta opción también sería incorrecta.

¿Se notaría este pequeño error en una función tan gigante sin PVS-Studio? Lo dudo mucho.

Conclusión


En un proyecto con muchos ejemplos de código extraño, se pueden ocultar muchos problemas. El programador, acostumbrándose a ver lo incomprensible, deja de notar errores al mismo tiempo. Las consecuencias pueden ser muy tristes. De hecho, entre las advertencias del analizador también hay falsas, sin embargo, en la mayoría de los casos, las advertencias al menos indican fragmentos de código que pueden causar preguntas cuando las ve una persona. En el caso de que el código extraño se escriba intencionalmente, vale la pena dejar explicaciones para que el fragmento sea claro para el desarrollador que trabajará con este código en el futuro (incluso si eso significa dejar comentarios para sí mismo).

Al mismo tiempo, las herramientas de análisis estático, como PVS-Studio, pueden ser de gran ayuda para encontrar posibles errores y rarezas para que sean visibles y no se olviden, y todas las soluciones temporales se refinan posteriormente y se convierten en un trabajo limpio, estructurado y estable. el código.


Si desea compartir este artículo con una audiencia de habla inglesa, utilice el enlace a la traducción: Nikita Lipilin. ¿Cómo el código extraño oculta errores? Análisis TensorFlow.NET .

All Articles