Repository: sktime/sktime

Issue:
[BUG] make_forecasting_error is not working
**Describe the bug**
When calling make_forecasting_error I get - AttributeError: '_BaseForecastingScoreMetric' object has no attribute 'func'

**To Reproduce**
```python
from sktime.performance_metrics.forecasting import mean_absolute_error, make_forecasting_scorer
make_forecasting_scorer(func=mean_absolute_error, name='mae')
```

**Versions**
System:
    python: 3.8.5 (default, Sep  4 2020, 02:22:02)  [Clang 10.0.0 ]
executable: /opt/anaconda3/bin/python
   machine: macOS-10.16-x86_64-i386-64bit

Python dependencies:
          pip: 20.2.4
   setuptools: 50.3.1.post20201107
      sklearn: 0.24.2
       sktime: 0.7.0
  statsmodels: 0.12.2
        numpy: 1.19.2
        scipy: 1.4.1
       Cython: 0.29.21
       pandas: 1.3.1
   matplotlib: 3.3.2
       joblib: 0.17.0
        numba: 0.51.2
     pmdarima: 1.8.2
      tsfresh: None


Discussion:
@RNKuhns, can you kindly have a look at this, since you wrote the function/classes involved?
@RNKuhns?
@eyalshafran and @fkiraly sorry for having missed this. Looking into it now.
I believe I've isolated what I think the issue being referenced is. 

Note that I don't get an error when executing `make_forecasting_scorer` to create a forecasting scorer or executing the returned scorer. The code snippet below runs as expected (returns 0.0) through that point. However, when trying to print the returned scorer, I'm seeing an error being raised.

```
from sktime.datasets import load_airline
from sktime.performance_metrics.forecasting import mean_absolute_error, make_forecasting_scorer
y = load_airline()

mae = make_forecasting_scorer(mean_absolute_error, name="mae") # This works
print(mae(y, y)) # This works

print(mae) # This does not work
```

Based on the error message, I think the problem is that I've stored the input parameter `func` in the attribute `self._func`, when the expectation is it is stored in `self.func`. I believe we had it as `self.func` and then we made it non-public. 

I'm up for switching it back to `self.func` if that works for @fkiraly and @mloning. If so, I'll make that change after #1472 gets reviewed and merged.
Ah, yes, I should have spotted this!

`sklearn`'s pretty-print method (`__str__` or `__repr__`) assumes that arguments of the `__init__` are stored as an attribute of the same name - otherwise any printing will break, since it tries to access attributes of the same name as the `__init__` arguments that don't exist.

I think I wasn't involved in the discussion that led to renaming the parameter, but all descendants of `BaseObject` must obey the simple rule that `__init__` sets attributes with the same name as the inputs, just like `sklearn` estimators.
perhaps we should add a simple `BaseObject` extension template?
The other extension templates are very explicit about this rule.
and, we should probably add a test. Might be a good thing to add to `estimator_checks`.
I opened an issue here #1495

Resolving patch:
diff --git a/sktime/performance_metrics/base/_base.py b/sktime/performance_metrics/base/_base.py
--- a/sktime/performance_metrics/base/_base.py
+++ b/sktime/performance_metrics/base/_base.py
@@ -16,7 +16,7 @@ class BaseMetric(BaseObject):
     """
 
     def __init__(self, func, name=None):
-        self._func = func
+        self.func = func
         self.name = name if name is not None else func.__name__
 
     def __call__(self, y_true, y_pred, **kwargs):
diff --git a/sktime/performance_metrics/forecasting/_classes.py b/sktime/performance_metrics/forecasting/_classes.py
--- a/sktime/performance_metrics/forecasting/_classes.py
+++ b/sktime/performance_metrics/forecasting/_classes.py
@@ -106,7 +106,7 @@ def __call__(self, y_true, y_pred, **kwargs):
         loss : float
             Calculated loss metric.
         """
-        return self._func(y_true, y_pred, multioutput=self.multioutput, **kwargs)
+        return self.func(y_true, y_pred, multioutput=self.multioutput, **kwargs)
 
 
 class _BaseForecastingScoreMetric(_BaseForecastingErrorMetric):
@@ -151,7 +151,7 @@ def __call__(self, y_true, y_pred, **kwargs):
         loss : float
             Calculated loss metric.
         """
-        return self._func(
+        return self.func(
             y_true,
             y_pred,
             multioutput=self.multioutput,
@@ -191,7 +191,7 @@ def __call__(self, y_true, y_pred, **kwargs):
         loss : float
             Calculated loss metric.
         """
-        return self._func(
+        return self.func(
             y_true,
             y_pred,
             multioutput=self.multioutput,
@@ -233,7 +233,7 @@ def __call__(self, y_true, y_pred, **kwargs):
         loss : float
             Calculated loss metric.
         """
-        return self._func(
+        return self.func(
             y_true,
             y_pred,
             multioutput=self.multioutput,
@@ -266,7 +266,7 @@ def __call__(self, y_true, y_pred, **kwargs):
         loss : float
             Calculated loss metric.
         """
-        return self._func(
+        return self.func(
             y_true,
             y_pred,
             multioutput=self.multioutput,
@@ -300,7 +300,7 @@ def __call__(self, y_true, y_pred, **kwargs):
         loss : float
             Calculated loss metric.
         """
-        return self._func(
+        return self.func(
             y_true, y_pred, a=self.a, b=self.b, multioutput=self.multioutput, **kwargs
         )
 
@@ -328,7 +328,7 @@ def __call__(self, y_true, y_pred, **kwargs):
         loss : float
             Calculated loss metric.
         """
-        return self._func(
+        return self.func(
             y_true,
             y_pred,
             multioutput=self.multioutput,


Test patch: