Repository: pybamm-team/PyBaMM (python)

Issue:
Define `__iter__` and/or `__contains__` for `pybamm.ParameterValues`
### Description

The `pybamm.ParameterValues` is dict-like, and through its `__getitem__` and `__setitem__` methods allows dict-like key access. However, it does not support the `in` keyword, which for a dict will treat the keys as an iterable. This means that natural code like:

```python
if "Negative particle radius [m]" in parameter_values:
    # do something
```

will not work (generally raises a `KeyError`). It is currently necessary to instead write explicitly:

```python
if "Negative particle radius [m]" in parameter_values.keys():
    # do something
```

Defining the missing method `__iter__` (and `__contains__` if preferred) would allow `in` to work in logical test and for-loop contexts.

### Motivation

Code is cleaner if `pybamm.ParameterValues` looks like a souped-up dict always, not just sometimes. Key-value accessors are expected to iterate over their keys and to allow logical test on the presence/absence of keys. PyBaMM-containing code looks 'asymmetric' compared to code working with actual `dict` objects due to the requirement to call into `pybamm.ParameterValues.keys()` specifically.

Was there any historic reason for the decision to exclude these methods?

### Possible Implementation

_No response_

### Additional context

_No response_

Pull request:
Added __iter__ and __contains__ method to pybamm.ParameterValues
# Description

Defined `__iter__ `and `__contains__ `method to `pybamm.ParameterValues` which would allow `in` to work in logical test and for-loop contexts. 

Fixes #4438 

## Type of change

- [x] New feature (non-breaking change which adds functionality)


# Key checklist:

- [x] No style issues: `$ pre-commit run` (or `$ nox -s pre-commit`) (see [CONTRIBUTING.md](https://github.com/pybamm-team/PyBaMM/blob/develop/CONTRIBUTING.md#installing-and-using-pre-commit) for how to set this up to run automatically when committing locally, in just two lines of code)
- [x] All tests pass: `$ python run-tests.py --all` (or `$ nox -s tests`)
- [ ] The documentation builds: `$ python run-tests.py --doctest` (or `$ nox -s doctests`)

You can run integration tests, unit tests, and doctests together at once, using `$ python run-tests.py --quick` (or `$ nox -s quick`).

## Further checks:

- [x] Code is commented, particularly in hard-to-understand areas
- [x] Tests added that prove fix is effective or that feature works


Patch:
diff --git a/src/pybamm/parameters/parameter_values.py b/src/pybamm/parameters/parameter_values.py
index 43c1ea17c..d5d5878cf 100644
--- a/src/pybamm/parameters/parameter_values.py
+++ b/src/pybamm/parameters/parameter_values.py
@@ -930,3 +930,9 @@ class ParameterValues:
                     file.write((s + " : {:10.4g}\n").format(name, value))
                 else:
                     file.write((s + " : {:10.3E}\n").format(name, value))
+
+    def __contains__(self, key):
+        return key in self._dict_items
+
+    def __iter__(self):
+        return iter(self._dict_items)


Test patch:
diff --git a/tests/unit/test_parameters/test_parameter_values.py b/tests/unit/test_parameters/test_parameter_values.py
index 4086abea6..543d3a6b4 100644
--- a/tests/unit/test_parameters/test_parameter_values.py
+++ b/tests/unit/test_parameters/test_parameter_values.py
@@ -17,6 +17,7 @@ from pybamm.input.parameters.lithium_ion.Marquis2019 import (
 )
 from pybamm.expression_tree.exceptions import OptionError
 import casadi
+from pybamm.parameters.parameter_values import ParameterValues
 
 
 class TestParameterValues:
@@ -1029,3 +1030,23 @@ class TestParameterValues:
             match="referring to the reaction at the surface of a lithium metal electrode",
         ):
             parameter_values.evaluate(param)
+
+    def test_contains_method(self):
+        """Test for __contains__ method to check the functionality of 'in' keyword"""
+        parameter_values = ParameterValues(
+            {"Negative particle radius [m]": 1e-6, "Positive particle radius [m]": 2e-6}
+        )
+        assert (
+            "Negative particle radius [m]" in parameter_values
+        ), "Key should be found in parameter_values"
+        assert (
+            "Invalid key" not in parameter_values
+        ), "Non-existent key should not be found"
+
+    def test_iter_method(self):
+        """Test for __iter__ method to check if we can iterate over keys"""
+        parameter_values = ParameterValues(
+            values={"Negative particle radius [m]": 1e-6}
+        )
+        pv = [i for i in parameter_values]
+        assert len(pv) == 5, "Should have 5 keys"