73 lines
3.5 KiB
Plaintext
73 lines
3.5 KiB
Plaintext
This rule raises an issue when direct and indirect equality/inequality checks are made on floats.
|
|
|
|
== Why is this an issue?
|
|
|
|
Floating point math is imprecise because of the challenges of storing such values in a binary representation.
|
|
|
|
In base 10, the fraction `1/3` is represented as `0.333...` which, for a given number of significant digit, will never exactly be `1/3`. The same problem happens when trying to represent `1/10` in base 2, with leads to the infinitely repeating fraction `0.0001100110011...`. This makes floating point representations inherently imprecise.
|
|
|
|
Even worse, floating point math is not associative; push a ``++float++`` through a series of simple mathematical operations and the answer will be different based on the order of those operation because of the rounding that takes place at each step.
|
|
|
|
Even simple floating point assignments are not simple, as can be vizualized using the `format` function to check for significant digits:
|
|
|
|
[source,python]
|
|
----
|
|
>>> format(0.1, ".17g")
|
|
'0.10000000000000001'
|
|
----
|
|
|
|
This can also be vizualized as a fraction using the `as_integer_ratio` method:
|
|
|
|
[source,python]
|
|
----
|
|
>>> my_float = 0.1
|
|
>>> numerator, denominator = my_float.as_integer_ratio()
|
|
>>> f"{numerator} / {denominator}"
|
|
'3602879701896397 / 36028797018963968'
|
|
----
|
|
|
|
Therefore, the use of the equality (``++==++``) and inequality (``++!=++``) operators on ``++float++`` values is almost always erroneous.
|
|
|
|
== How to fix it
|
|
|
|
Whenever attempting to compare float values, it is important to consider the inherent imprecision of floating-point arithmetic.
|
|
|
|
One common solution to this problem is to use a tolerance value (also called epsilon) to define an acceptable range of difference between two floats. A tolerance value may be relative (based on the magnitude of the numbers being compared) or absolute. Note that comparing a value to 0 is a special case: as it has no magnitude, it is important to use an absolute tolerance value.
|
|
|
|
The `math.isclose` function allows to compare floats with a relative and absolute tolerance. One should however be careful when comparing values to 0, as by default, the absolute tolerance of `math.isclose` is `0.0` (this case is covered by rule S6727)
|
|
. Depending on the library you're using, equivalent functions exist, with possibly different default tolerances (e.g `numpy.isclose` or `torch.isclose` which are respectively designed to work with `numpy` arrays and `pytorch` tensors).
|
|
|
|
If precise decimal arithmetic is needed, another option is to use the `Decimal` class of the `decimal` module, which allows for exact decimal arithmetic.
|
|
|
|
=== Code examples
|
|
|
|
==== Noncompliant code example
|
|
|
|
[source,python,diff-id=1,diff-type=noncompliant]
|
|
----
|
|
def foo(a, b):
|
|
return a == b - 0.1
|
|
----
|
|
|
|
==== Compliant solution
|
|
|
|
[source,python,diff-id=1,diff-type=compliant]
|
|
----
|
|
import math
|
|
def foo(a, b):
|
|
return math.isclose(a, b - 0.1, rel_tol=1e-09, abs_tol=1e-09)
|
|
----
|
|
|
|
== Resources
|
|
|
|
=== Documentation
|
|
|
|
* Python Documentation - https://docs.python.org/3/tutorial/floatingpoint.html#floating-point-arithmetic-issues-and-limitations[Floating Point Arithmetic: Issues and Limitations]
|
|
* Python Documentation - https://docs.python.org/3/library/decimal.html#module-decimal[Decimal fixed point and floating point arithmetic]
|
|
* NumPy Documentation - https://numpy.org/doc/stable/reference/generated/numpy.isclose.html[numpy.isclose]
|
|
* PyTorch Documentation - https://pytorch.org/docs/stable/generated/torch.isclose.html[torch.isclose]
|
|
|
|
=== Related rules
|
|
|
|
* S6727 - The `abs_tol` parameter should be provided when using `math.isclose` to compare values to `0`
|