Repository: scikit-hep/pyhf

Issue:
Simultaneous to have magic iter functions (make it look like a list)
# Description

We will soon be using Simultaneous as if it was a list, accessing via `self.make_pdf(pars).pdfobjs[n]` basically.

## Is your feature request related to a problem? Please describe.

No.

### Describe the solution you'd like

I prefer `self.make_pdf(pars)[n]` instead, since it's cleaner/user-friendly. Simultaneous is just a list of PDF objects.

### Describe alternatives you've considered

None.

# Relevant Issues and Pull Requests

- #558

# Additional context

 Nah


Discussion:

Resolving patch:
diff --git a/src/pyhf/pdf.py b/src/pyhf/pdf.py
--- a/src/pyhf/pdf.py
+++ b/src/pyhf/pdf.py
@@ -567,7 +567,7 @@ def _create_nominal_and_modifiers(self, config, spec):
         return mega_mods, _nominal_rates
 
     def expected_auxdata(self, pars):
-        return self.make_pdf(pars).pdfobjs[1].expected_data()
+        return self.make_pdf(pars)[1].expected_data()
 
     def _modifications(self, pars):
         return self.main_model._modifications(pars)
@@ -577,20 +577,20 @@ def nominal_rates(self):
         return self.main_model.nominal_rates
 
     def expected_actualdata(self, pars):
-        return self.make_pdf(pars).pdfobjs[0].expected_data()
+        return self.make_pdf(pars)[0].expected_data()
 
     def expected_data(self, pars, include_auxdata=True):
         tensorlib, _ = get_backend()
         pars = tensorlib.astensor(pars)
         if not include_auxdata:
-            return self.make_pdf(pars).pdfobjs[0].expected_data()
+            return self.make_pdf(pars)[0].expected_data()
         return self.make_pdf(pars).expected_data()
 
     def constraint_logpdf(self, auxdata, pars):
-        return self.make_pdf(pars).pdfobjs[1].log_prob(auxdata)
+        return self.make_pdf(pars)[1].log_prob(auxdata)
 
     def mainlogpdf(self, maindata, pars):
-        return self.make_pdf(pars).pdfobjs[0].log_prob(maindata)
+        return self.make_pdf(pars)[0].log_prob(maindata)
 
     def make_pdf(self, pars):
         """
diff --git a/src/pyhf/probability.py b/src/pyhf/probability.py
--- a/src/pyhf/probability.py
+++ b/src/pyhf/probability.py
@@ -202,9 +202,34 @@ def __init__(self, pdfobjs, tensorview, batch_size=None):
 
         """
         self.tv = tensorview
-        self.pdfobjs = pdfobjs
+        self._pdfobjs = pdfobjs
         self.batch_size = batch_size
 
+    def __iter__(self):
+        """
+        Iterate over the constituent pdf objects
+
+        Returns:
+            pdfobj (`Distribution`): A constituent pdf object
+
+        """
+        for pdfobj in self._pdfobjs:
+            yield pdfobj
+
+    def __getitem__(self, index):
+        """
+        Access the constituent pdf object at the specified index
+
+        Args:
+
+            index (`int`): The index to access the constituent pdf object
+
+        Returns:
+            pdfobj (`Distribution`): A constituent pdf object
+
+        """
+        return self._pdfobjs[index]
+
     def expected_data(self):
         """
         Compute mean data of the density
@@ -213,7 +238,7 @@ def expected_data(self):
             data (`tensor`): The expected data
 
         """
-        tostitch = [p.expected_data() for p in self.pdfobjs]
+        tostitch = [p.expected_data() for p in self]
         return self.tv.stitch(tostitch)
 
     def sample(self, sample_shape=()):
@@ -227,7 +252,7 @@ def sample(self, sample_shape=()):
             samples (`tensor`): The samples
 
         """
-        return self.tv.stitch([p.sample(sample_shape) for p in self.pdfobjs])
+        return self.tv.stitch([p.sample(sample_shape) for p in self])
 
     def log_prob(self, value):
         """
@@ -241,7 +266,7 @@ def log_prob(self, value):
 
         """
         constituent_data = self.tv.split(value)
-        pdfvals = [p.log_prob(d) for p, d in zip(self.pdfobjs, constituent_data)]
+        pdfvals = [p.log_prob(d) for p, d in zip(self, constituent_data)]
         return Simultaneous._joint_logpdf(pdfvals, batch_size=self.batch_size)
 
     @staticmethod


Test patch:
diff --git a/tests/test_probability.py b/tests/test_probability.py
--- a/tests/test_probability.py
+++ b/tests/test_probability.py
@@ -1,4 +1,5 @@
 from pyhf import probability
+import numpy as np
 
 
 def test_poisson(backend):
@@ -63,3 +64,14 @@ def test_independent(backend):
     assert tb.tolist(probability.Simultaneous._joint_logpdf([p1, p2]))[0] == tb.tolist(
         result
     )
+    assert tb.tolist(probability.Simultaneous._joint_logpdf([p1, p2]))[0] == tb.tolist(
+        result
+    )
+
+
+def test_simultaneous_list_ducktype():
+    myobjs = np.random.randint(100, size=10).tolist()
+    sim = probability.Simultaneous(myobjs, None)
+    assert sim[3] == myobjs[3]
+    for simobj, myobj in zip(sim, myobjs):
+        assert simobj == myobj