`_sk_visual_block` can be executed before parameter validation

I found that `_sk_visual_block` can be executed before parameter validation. It can lead to a not really informative error message:

```python
from sklearn.ensemble import StackingClassifier
StackingClassifier(estimators=[])
```

```pytb
ValueError                                Traceback (most recent call last)
File ~/miniconda3/envs/dev/lib/python3.8/site-packages/IPython/core/formatters.py:973, in MimeBundleFormatter.__call__(self, obj, include, exclude)
    970     method = get_real_method(obj, self.print_method)
    972     if method is not None:
--> 973         return method(include=include, exclude=exclude)
    974     return None
    975 else:

File ~/Documents/packages/scikit-learn/sklearn/base.py:596, in BaseEstimator._repr_mimebundle_(self, **kwargs)
    594 output = {"text/plain": repr(self)}
    595 if get_config()["display"] == "diagram":
--> 596     output["text/html"] = estimator_html_repr(self)
    597 return output

File ~/Documents/packages/scikit-learn/sklearn/utils/_estimator_html_repr.py:410, in estimator_html_repr(estimator)
    396 fallback_msg = (
    397     "In a Jupyter environment, please rerun this cell to show the HTML"
    398     " representation or trust the notebook. <br />On GitHub, the"
    399     " HTML representation is unable to render, please try loading this page"
    400     " with nbviewer.org."
    401 )
    402 out.write(
    403     f"<style>{style_with_id}</style>"
    404     f'<div id="{container_id}" class="sk-top-container">'
   (...)
    408     '<div class="sk-container" hidden>'
    409 )
--> 410 _write_estimator_html(
    411     out,
    412     estimator,
    413     estimator.__class__.__name__,
    414     estimator_str,
    415     first_call=True,
    416 )
    417 out.write("</div></div>")
    419 html_output = out.getvalue()

File ~/Documents/packages/scikit-learn/sklearn/utils/_estimator_html_repr.py:144, in _write_estimator_html(out, estimator, estimator_label, estimator_label_details, first_call)
    142 """Write estimator to html in serial, parallel, or by itself (single)."""
    143 if first_call:
--> 144     est_block = _get_visual_block(estimator)
    145 else:
    146     with config_context(print_changed_only=True):

File ~/Documents/packages/scikit-learn/sklearn/utils/_estimator_html_repr.py:107, in _get_visual_block(estimator)
    105 """Generate information about how to display an estimator."""
    106 with suppress(AttributeError):
--> 107     return estimator._sk_visual_block_()
    109 if isinstance(estimator, str):
    110     return _VisualBlock(
    111         "single", estimator, names=estimator, name_details=estimator
    112     )

File ~/Documents/packages/scikit-learn/sklearn/ensemble/_stacking.py:685, in StackingClassifier._sk_visual_block_(self)
    683 else:
    684     final_estimator = self.final_estimator
--> 685 return super()._sk_visual_block_(final_estimator)

File ~/Documents/packages/scikit-learn/sklearn/ensemble/_stacking.py:360, in _BaseStacking._sk_visual_block_(self, final_estimator)
    359 def _sk_visual_block_(self, final_estimator):
--> 360     names, estimators = zip(*self.estimators)
    361     parallel = _VisualBlock("parallel", estimators, names=names, dash_wrapped=False)
    363     # final estimator is wrapped in a parallel block to show the label:
    364     # 'final_estimator' in the html repr

ValueError: not enough values to unpack (expected 2, got 0)
StackingClassifier(estimators=[])
```

Here, `estimators` should be a list of estimators and it will raise an informative message when calling `fit`:

```python
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
StackingClassifier(estimators=[]).fit(X, y)
```

```pytb
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [10], in <cell line: 1>()
----> 1 StackingClassifier(estimators=[]).fit(X, y)

File ~/Documents/packages/scikit-learn/sklearn/ensemble/_stacking.py:597, in StackingClassifier.fit(self, X, y, sample_weight)
    595 self._le = LabelEncoder().fit(y)
    596 self.classes_ = self._le.classes_
--> 597 return super().fit(X, self._le.transform(y), sample_weight)

File ~/Documents/packages/scikit-learn/sklearn/ensemble/_stacking.py:180, in _BaseStacking.fit(self, X, y, sample_weight)
    176 self._validate_params()
    178 # all_estimators contains all estimators, the one to be fitted and the
    179 # 'drop' string.
--> 180 names, all_estimators = self._validate_estimators()
    181 self._validate_final_estimator()
    183 stack_method = [self.stack_method] * len(all_estimators)

File ~/Documents/packages/scikit-learn/sklearn/ensemble/_base.py:258, in _BaseHeterogeneousEnsemble._validate_estimators(self)
    256 def _validate_estimators(self):
    257     if self.estimators is None or len(self.estimators) == 0:
--> 258         raise ValueError(
    259             "Invalid 'estimators' attribute, 'estimators' should be a list"
    260             " of (string, estimator) tuples."
    261         )
    262     names, estimators = zip(*self.estimators)
    263     # defined by MetaEstimatorMixin

ValueError: Invalid 'estimators' attribute, 'estimators' should be a list of (string, estimator) tuples.
```

@thomasjpfan Do you have an idea to make the error message more informative?

COMMENT:
Issues around invalid parameters before fit's validation has popped up when dealing with `get_params` and `set_params` in meta-estimators such as `ColumnTransformer`. We hack around it by catching the exception:

https://github.com/scikit-learn/scikit-learn/blob/79c21c57aa622b76a0174e852c7806d6e8c11e43/sklearn/compose/_column_transformer.py#L229-L237

For the case of `_sk_visual_block_`, I'll prefer to have `_get_visual_block` have a good default when `_sk_visual_block_` fails. I opened #24015 with this solution.

Labels: Needs Triage