Repository: PennyLaneAI/pennylane
Pull request #3588 (2023-01-03 11:51:05)

Make Pauli Word pickleable
By default, unpickling a dictionary calls `__setitem__`.

```python
import pennylane as qml
import pickle

op = qml.PauliX(0)
pic = pickle.dumps(op)
pickle.loads(pic)
```
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In [4], line 1
----> 1 pickle.loads(pic)

File pennylane/pennylane/pauli/pauli_arithmetic.py:130, in PauliWord.__setitem__(self, key, item)
    128 def __setitem__(self, key, item):
    129     """Restrict setting items after instantiation."""
--> 130     raise TypeError("PauliWord object does not support assignment")

TypeError: PauliWord object does not support assignment
```

To avoid this, `PauliWord` will now define `__reduce__()`.  See https://docs.python.org/3/library/pickle.html#object.__reduce__ for more information.

While the documentation does warn "Although powerful, implementing `__reduce__()` directly in your classes is error prone.", I can't find a better option. While normally custom pickling can be defined through `__getstate__` and `__setstate__`, dictionaries (and thus things that inherit from a dictionary) have special logic that prevented this from working.

At least the solution uses the minimal form of `__reduce__`: a callable object and a tuple of  arguments for the callable object.



codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3588?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3588](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3588?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (750a2e7) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/880517228462ba01af35d94204e55fc9c47a7cac?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (8805172) will **not change** coverage.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##           master    #3588   +/-   ##
=======================================
  Coverage   99.87%   99.87%           
=======================================
  Files         314      314           
  Lines       28019    28021    +2     
=======================================
+ Hits        27983    27985    +2     
  Misses         36       36           
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3588?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/pauli/pauli\_arithmetic.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3588/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3BhdWxpL3BhdWxpX2FyaXRobWV0aWMucHk=) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index f70957925..5952eadf4 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -126,6 +126,9 @@
 
 <h3>Bug fixes</h3>
 
+* `qml.pauli.PauliWord` is now pickle-able.
+  [(#3588)](https://github.com/PennyLaneAI/pennylane/pull/3588)
+
 * Child classes of `QuantumScript` now return their own type when using `SomeChildClass.from_queue`.
   [(#3501)](https://github.com/PennyLaneAI/pennylane/pull/3501)
 
@@ -141,6 +144,7 @@ Utkarsh Azad
 Astral Cai
 Lillian M. A. Frederiksen
 Soran Jahangiri
+Christina Lee
 Albert Mitjans Coma
 Romain Moyard
 Matthew Silverman
diff --git a/pennylane/pauli/pauli_arithmetic.py b/pennylane/pauli/pauli_arithmetic.py
index 789d66714..b4fcc3a22 100644
--- a/pennylane/pauli/pauli_arithmetic.py
+++ b/pennylane/pauli/pauli_arithmetic.py
@@ -116,6 +116,13 @@ class PauliWord(dict):
                 del mapping[wire]
         super().__init__(mapping)
 
+    def __reduce__(self):
+        """Defines how to pickle and unpickle a PauliWord. Otherwise, un-pickling
+        would cause __setitem__ to be called, which is forbidden on PauliWord.
+        For more information, see: https://docs.python.org/3/library/pickle.html#object.__reduce__
+        """
+        return (PauliWord, (dict(self),))
+
     def __copy__(self):
         """Copy the PauliWord instance."""
         return PauliWord(dict(self.items()))


Test patch:
diff --git a/tests/pauli/test_pauli_arithmetic.py b/tests/pauli/test_pauli_arithmetic.py
index 487d6b6e4..a8f381314 100644
--- a/tests/pauli/test_pauli_arithmetic.py
+++ b/tests/pauli/test_pauli_arithmetic.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """Unit Tests for the PauliWord and PauliSentence classes"""
+import pickle
 import pytest
 from copy import copy, deepcopy
 
@@ -218,6 +219,13 @@ class TestPauliWord:
         with pytest.raises(ValueError, match="Can't get the Hamiltonian for an empty PauliWord."):
             pw4.hamiltonian()
 
+    def test_pickling(self):
+        """Check that pauliwords can be pickled and unpickled."""
+        pw = PauliWord({2: "X", 3: "Y", 4: "Z"})
+        serialization = pickle.dumps(pw)
+        new_pw = pickle.loads(serialization)
+        assert pw == new_pw
+
 
 class TestPauliSentence:
     def test_missing(self):
@@ -533,3 +541,13 @@ class TestPauliSentence:
             ValueError, match="Can't get the Hamiltonian for an empty PauliSentence."
         ):
             ps5.hamiltonian()
+
+    def test_pickling(self):
+        """Check that paulisentences can be pickled and unpickled."""
+        pw1 = PauliWord({2: "X", 3: "Y", 4: "Z"})
+        pw2 = PauliWord({2: "Y", 3: "Z"})
+        ps = PauliSentence({pw1: 1.5, pw2: -0.5})
+
+        serialization = pickle.dumps(ps)
+        new_ps = pickle.loads(serialization)
+        assert ps == new_ps