Repository: optuna/optuna

Issue:
Bug when passing multiple callbacks to `MultiObjectiveStudy.optimize`
<!-- Please write a clear and concise description of what the bug is. -->

When passing multiple callbacks to `MultiObjectiveStudy.optimize`, it uses only the last callback.

```python
import optuna


n_objectives = 2
directions = ["minimize" for _ in range(n_objectives)]
study = optuna.multi_objective.create_study(directions)


def objective(trial):
    return [trial.suggest_uniform("v{}".format(i), 0, 5) for i in range(n_objectives)]


callbacks = [
    lambda a, b: print("first callback"),
    lambda a, b: print("second callback"),
]

study.optimize(objective, n_trials=10, callbacks=callbacks)
```

This code outputs:

```
[I 2020-05-06 12:56:02,205] Finished trial#0 with values: [0.030503339013888686, 0.457606610954287] with parameters: {'v0': 0.030503339013888686, 'v1': 0.457606610954287}.
second callback
second callback
[I 2020-05-06 12:56:02,243] Finished trial#1 with values: [3.7186248349736672, 1.8148814699741478] with parameters: {'v0': 3.7186248349736672, 'v1': 1.8148814699741478}.
second callback
second callback
[I 2020-05-06 12:56:02,283] Finished trial#2 with values: [4.386752523520264, 1.180154220379264] with parameters: {'v0': 4.386752523520264, 'v1': 1.180154220379264}.
second callback
second callback
...
```

Colab notebook that reproduces this bug:
https://colab.research.google.com/drive/1YeT8ItU9IO65wXsqY9k-_ATCrCBW0BRZ?usp=sharing


## Expected behavior

The output is expected to be:

```
[I 2020-05-06 12:56:02,205] Finished trial#0 with values: [0.030503339013888686, 0.457606610954287] with parameters: {'v0': 0.030503339013888686, 'v1': 0.457606610954287}.
first callback
second callback
[I 2020-05-06 12:56:02,243] Finished trial#1 with values: [3.7186248349736672, 1.8148814699741478] with parameters: {'v0': 3.7186248349736672, 'v1': 1.8148814699741478}.
first callback
second callback
[I 2020-05-06 12:56:02,283] Finished trial#2 with values: [4.386752523520264, 1.180154220379264] with parameters: {'v0': 4.386752523520264, 'v1': 1.180154220379264}.
first callback
second callback
...
```
## Environment

- Optuna version: master branch
- Python version: 3.6.9
- OS: linux
- (Optional) Other libraries and their versions:

## Error messages, stack traces, or logs

```
# error messages, stack traces, or logs
```

## Steps to reproduce

1.
2.
3.

## Reproducible examples (optional)


## Additional context (optional)

<!-- Please add any other context or screenshots about the problem here. -->

I found this bug on https://lgtm.com/projects/g/optuna/optuna/?mode=list

Discussion:

Patch:
diff --git a/optuna/multi_objective/study.py b/optuna/multi_objective/study.py
index db1f70402..0d9755d25 100644
--- a/optuna/multi_objective/study.py
+++ b/optuna/multi_objective/study.py
@@ -16,6 +16,7 @@ from optuna import multi_objective
 from optuna.storages import BaseStorage
 from optuna.study import Study
 from optuna.study import StudyDirection
+from optuna.trial import FrozenTrial
 from optuna.trial import Trial
 from optuna.trial import TrialState
 
@@ -233,15 +234,17 @@ class MultiObjectiveStudy(object):
             mo_trial._report_complete_values(values)
             return 0.0  # Dummy value.
 
-        mo_callbacks = None
-        if callbacks is not None:
-            mo_callbacks = [
-                lambda study, trial: callback(
-                    MultiObjectiveStudy(study),
-                    multi_objective.trial.FrozenMultiObjectiveTrial(self.n_objectives, trial),
-                )
-                for callback in callbacks
-            ]
+        # Wraps a multi-objective callback so that we can pass it to the `Study.optimize` method.
+        def wrap_mo_callback(callback: CallbackFuncType) -> Callable[[Study, FrozenTrial], None]:
+            return lambda study, trial: callback(
+                MultiObjectiveStudy(study),
+                multi_objective.trial.FrozenMultiObjectiveTrial(self.n_objectives, trial),
+            )
+
+        if callbacks is None:
+            wrapped_callbacks = None
+        else:
+            wrapped_callbacks = [wrap_mo_callback(callback) for callback in callbacks]
 
         self._study.optimize(
             mo_objective,
@@ -249,7 +252,7 @@ class MultiObjectiveStudy(object):
             n_trials=n_trials,
             n_jobs=n_jobs,
             catch=catch,
-            callbacks=mo_callbacks,
+            callbacks=wrapped_callbacks,
             gc_after_trial=gc_after_trial,
             show_progress_bar=show_progress_bar,
         )


Test patch:
diff --git a/tests/multi_objective/test_study.py b/tests/multi_objective/test_study.py
index cc0aa1a60..ab8c83c60 100644
--- a/tests/multi_objective/test_study.py
+++ b/tests/multi_objective/test_study.py
@@ -120,3 +120,23 @@ def test_enqueue_trial() -> None:
         return [0, 0]
 
     study.optimize(objective, n_trials=2)
+
+
+def test_callbacks() -> None:
+    study = optuna.multi_objective.create_study(["minimize", "maximize"])
+
+    def objective(trial: optuna.multi_objective.trial.MultiObjectiveTrial) -> List[float]:
+        x = trial.suggest_float("x", 0, 10)
+        y = trial.suggest_float("y", 0, 10)
+        return x, y
+
+    list0 = []
+    list1 = []
+    callbacks = [
+        lambda study, trial: list0.append(trial.number),
+        lambda study, trial: list1.append(trial.number),
+    ]
+    study.optimize(objective, n_trials=2, callbacks=callbacks)
+
+    assert list0 == [0, 1]
+    assert list1 == [0, 1]