Repository: Qiskit/qiskit

Issue:
Provide `@property` style class attributes `atol` and `rtol` for Operators and States
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?

When using BaseOperator or QuantumState derived classes, you can read `atol` and `rtol` values using those names in an instance of a class, but not in the class:
```
> from qiskit.quantum_info.operators.scalar_op import ScalarOp
> so = ScalarOp(1)
> so.atol
1e-8
> ScalarOp.atol  # Doesn't return the value
<property at 0x127fc55f0>
```
To set these values you need to call `set_atol` or `set_rtol` class methods. It can be misleading and it is easy to mistakenly do `ScalarOp.atol = 3e-4` or `so.atol = 3e-4`, which will create a new var `atol` or `rtol` instead of changing the private class attributes, overriding the restrictions imposed on `set_atol` and `set_rtol`.

Besides, due to the way python class attributes are handled, you can manipulate a derived class attributes through base class, until you modify the value through the derived class:
```
> sc = ScalarOp(1)
> sc.atol
 1e-08

> BaseOperator.set_atol(2e-5)
> sc.atol    # ScalarOp.atol changed through BaseOperator
 2e-05

> ScalarOp.set_atol(4e-5)
> sc.atol
 4e-05

> BaseOperator.set_atol(2.7e-5)                                                                                                                                                                                                     
> sc.atol   # No longer changed through BaseOperator                                                                                                                                                                                                                           
 4e-05
```

I propose to set and get `atol` and `rtol` class members through property style attributes using a metaclass, to avoid the misleading behaviours shown above, and get the following behaviour:

```
> ScalarOp.atol 
 1e-08

> BaseOperator.atol                                                                                                                                                                                                                  
 1e-08

> BaseOperator.atol = 3e-7                                                                                                                                                                                                           
> BaseOperator.atol
3e-07

> ScalarOp.atol
1e-08

> sc = ScalarOp(1)                                                                                                                                                                                                                   
> sc.atol                                                                                                                                                                                                                            
1e-08

> ScalarOp.atol = 3e-8                                                                                                                                                                                                              
> ScalarOp.atol
3e-08

> sc.atol
3e-08

> BaseOperator.atol
3e-07
```


Discussion:

Resolving patch:
diff --git a/qiskit/quantum_info/operators/base_operator.py b/qiskit/quantum_info/operators/base_operator.py
--- a/qiskit/quantum_info/operators/base_operator.py
+++ b/qiskit/quantum_info/operators/base_operator.py
@@ -18,7 +18,7 @@
 
 import copy
 import warnings
-from abc import ABC, abstractmethod
+from abc import ABCMeta, abstractmethod
 
 import numpy as np
 
@@ -26,12 +26,53 @@
 from qiskit.quantum_info.operators.predicates import ATOL_DEFAULT, RTOL_DEFAULT
 
 
-class BaseOperator(ABC):
-    """Abstract linear operator base class."""
+class TolerancesMeta(type):
+    """Metaclass to handle tolerances"""
+    def __init__(cls, *args, **kwargs):
+        cls._ATOL_DEFAULT = ATOL_DEFAULT
+        cls._RTOL_DEFAULT = RTOL_DEFAULT
+        cls._MAX_TOL = 1e-4
+        super().__init__(cls, args, kwargs)
+
+    @property
+    def atol(cls):
+        """The default absolute tolerance parameter for float comparisons."""
+        return cls._ATOL_DEFAULT
+
+    def _check_value(cls, value, value_name):
+        """Check if value is within valid ranges"""
+        if value < 0:
+            raise QiskitError(
+                "Invalid {} ({}) must be non-negative.".format(value_name, value))
+        if value > cls._MAX_TOL:
+            raise QiskitError(
+                "Invalid {} ({}) must be less than {}.".format(value_name, value, cls._MAX_TOL))
+
+    @atol.setter
+    def atol(cls, value):
+        """Set the class default absolute tolerance parameter for float comparisons."""
+        cls._check_value(value, "atol")  # pylint: disable=no-value-for-parameter
+        cls._ATOL_DEFAULT = value
+
+    @property
+    def rtol(cls):
+        """The relative tolerance parameter for float comparisons."""
+        return cls._RTOL_DEFAULT
+
+    @rtol.setter
+    def rtol(cls, value):
+        """Set the class default relative tolerance parameter for float comparisons."""
+        cls._check_value(value, "rtol")  # pylint: disable=no-value-for-parameter
+        cls._RTOL_DEFAULT = value
+
 
-    _ATOL_DEFAULT = ATOL_DEFAULT
-    _RTOL_DEFAULT = RTOL_DEFAULT
-    _MAX_TOL = 1e-4
+class AbstractTolerancesMeta(TolerancesMeta, ABCMeta):
+    """Abstract Metaclass to handle tolerances"""
+    pass
+
+
+class BaseOperator(metaclass=AbstractTolerancesMeta):
+    """Abstract linear operator base class."""
 
     def __init__(self, input_dims, output_dims):
         """Initialize an operator object."""
@@ -87,36 +128,34 @@ def num_qubits(self):
     @property
     def atol(self):
         """The default absolute tolerance parameter for float comparisons."""
-        return self.__class__._ATOL_DEFAULT
+        return self.__class__.atol
 
     @property
     def rtol(self):
         """The relative tolerance parameter for float comparisons."""
-        return self.__class__._RTOL_DEFAULT
+        return self.__class__.rtol
 
     @classmethod
     def set_atol(cls, value):
-        """Set the class default absolute tolerance parameter for float comparisons."""
-        if value < 0:
-            raise QiskitError(
-                "Invalid atol ({}) must be non-negative.".format(value))
-        if value > cls._MAX_TOL:
-            raise QiskitError(
-                "Invalid atol ({}) must be less than {}.".format(
-                    value, cls._MAX_TOL))
-        cls._ATOL_DEFAULT = value
+        """Set the class default absolute tolerance parameter for float comparisons.
+
+        DEPRECATED: use operator.atol = value instead
+        """
+        warnings.warn("`{}.set_atol` method is deprecated, use `{}.atol = "
+                      "value` instead.".format(cls.__name__, cls.__name__),
+                      DeprecationWarning)
+        cls.atol = value
 
     @classmethod
     def set_rtol(cls, value):
-        """Set the class default relative tolerance parameter for float comparisons."""
-        if value < 0:
-            raise QiskitError(
-                "Invalid atol ({}) must be non-negative.".format(value))
-        if value > cls._MAX_TOL:
-            raise QiskitError(
-                "Invalid atol ({}) must be less than {}.".format(
-                    value, cls._MAX_TOL))
-        cls._RTOL_DEFAULT = value
+        """Set the class default relative tolerance parameter for float comparisons.
+
+        DEPRECATED: use operator.rtol = value instead
+        """
+        warnings.warn("`{}.set_rtol` method is deprecated, use `{}.rtol = "
+                      "value` instead.".format(cls.__name__, cls.__name__),
+                      DeprecationWarning)
+        cls.rtol = value
 
     def reshape(self, input_dims=None, output_dims=None):
         """Return a shallow copy with reshaped input and output subsystem dimensions.
diff --git a/qiskit/quantum_info/states/quantum_state.py b/qiskit/quantum_info/states/quantum_state.py
--- a/qiskit/quantum_info/states/quantum_state.py
+++ b/qiskit/quantum_info/states/quantum_state.py
@@ -18,24 +18,19 @@
 
 import copy
 import warnings
-from abc import ABC, abstractmethod
+from abc import abstractmethod
 
 import numpy as np
 
 from qiskit.exceptions import QiskitError
-from qiskit.quantum_info.operators.base_operator import BaseOperator
+from qiskit.quantum_info.operators.base_operator import BaseOperator, AbstractTolerancesMeta
 from qiskit.quantum_info.operators.operator import Operator
-from qiskit.quantum_info.operators.predicates import ATOL_DEFAULT, RTOL_DEFAULT
 from qiskit.result.counts import Counts
 
 
-class QuantumState(ABC):
+class QuantumState(metaclass=AbstractTolerancesMeta):
     """Abstract quantum state base class"""
 
-    _ATOL_DEFAULT = ATOL_DEFAULT
-    _RTOL_DEFAULT = RTOL_DEFAULT
-    _MAX_TOL = 1e-4
-
     def __init__(self, dims):
         """Initialize a state object."""
         # Dimension attributes
@@ -64,36 +59,34 @@ def num_qubits(self):
     @property
     def atol(self):
         """The absolute tolerance parameter for float comparisons."""
-        return self.__class__._ATOL_DEFAULT
+        return self.__class__.atol
 
     @property
     def rtol(self):
         """The relative tolerance parameter for float comparisons."""
-        return self.__class__._RTOL_DEFAULT
+        return self.__class__.rtol
 
     @classmethod
     def set_atol(cls, value):
-        """Set the class default absolute tolerance parameter for float comparisons."""
-        if value < 0:
-            raise QiskitError(
-                "Invalid atol ({}) must be non-negative.".format(value))
-        if value > cls._MAX_TOL:
-            raise QiskitError(
-                "Invalid atol ({}) must be less than {}.".format(
-                    value, cls._MAX_TOL))
-        cls._ATOL_DEFAULT = value
+        """Set the class default absolute tolerance parameter for float comparisons.
+
+        DEPRECATED: use operator.atol = value instead
+        """
+        warnings.warn("`{}.set_atol` method is deprecated, use `{}.atol = "
+                      "value` instead.".format(cls.__name__, cls.__name__),
+                      DeprecationWarning)
+        cls.atol = value
 
     @classmethod
     def set_rtol(cls, value):
-        """Set the class default relative tolerance parameter for float comparisons."""
-        if value < 0:
-            raise QiskitError(
-                "Invalid atol ({}) must be non-negative.".format(value))
-        if value > cls._MAX_TOL:
-            raise QiskitError(
-                "Invalid atol ({}) must be less than {}.".format(
-                    value, cls._MAX_TOL))
-        cls._RTOL_DEFAULT = value
+        """Set the class default relative tolerance parameter for float comparisons.
+
+        DEPRECATED: use operator.rtol = value instead
+        """
+        warnings.warn("`{}.set_rtol` method is deprecated, use `{}.rtol = "
+                      "value` instead.".format(cls.__name__, cls.__name__),
+                      DeprecationWarning)
+        cls.rtol = value
 
     @property
     def _rng(self):


Test patch: