Pythonのfunctools.partial(部分適用)を解説する

## 動機

うちの会社で[JDLA E資格](https://www.jdla.org/certificate/engineer/)対策のために使わせて頂いている教材に特になんの説明も無く`functools.partial`が出てきていて、歴代の受験者が

>なんだろう、コレ...?

となっていたので解説記事を作ることにした。

## やりたいこと

単純な線形回帰問題を最急降下法か何かで実装しようとしているとする。

線形回帰の仮説(`hypothesis`)をこんな感じで実装した。

```python
import numpy as np

def hypothesis(X, W, b):
    """仮説関数

    線形回帰の仮説(WX + b)を計算して結果を返す 

    Args:
       X (numpy.ndarray): 学習データ(データ数, 項目数)
       W (numpy.ndarray): 重み(データの項目数,1)
       b (numpy.ndarray): バイアス(1,1)

    Returns:
       numpy.ndaray: 仮説の計算結果(データ数,)
    """

    # WX+bを行列の積で計算するためにXの列にバイアス項（全て1）を繋げる
    X_with_bias = np.c_[X, np.ones((X.shape[0],1))]
    # バイアス項を繋げたXとW,bを行方向に繋げた行列の積を取る
    return X_with_bias.dot(np.r_[W,b])
```

例えば、このように使用する。

```python
X = np.array([[7,5],[8,10],[8.5,1]]) #学習データ（テキトーです）
W = np.random.randn(X.shape[1]) #重みの初期化
b = np.zeros(1) #バイアスの初期化

y = hypothesis(X, W, b)
```

なんやかんやで学習が収束して、精度高く予想できる重みとバイアスが決まったとする。

```python
learned_W = np.array([0.8,-0.05]) #学習済みの重み（テキトー）
learned_b = np.array([-0.8]) #学習済みのバイアス（テキトー）
```

学習が終わったので、このモデルを使って実際に予測を行いたい。予測したい入力`X`を`X_to_predict`と置いたとき、

```python
X_to_predict = np.array([[7,10],[10,1]]) #予測したい入力X(テキトー)
```

予測値(`prediction`)は`hypothesis`関数と学習済みの`learned_W`や`learned_b`を使ってこのように取得することができる。

```python
prediction = hypothesis(X_to_predict, learned_W, learned_b)
```

しかし、`learned_W`や`learned_b`は学習済みの重み/バイアスでもはや**固定値**なので、毎回予測をするたびに引数に与えるのは面白くない。そこで、`learned_W`や`learned_b`を**引数に渡さず**に入力`X`だけを渡して予測値を返す`predict`関数を作りたい。

```python
# こんな関数が欲しい!
prediction = predict(X_to_predict)
```


## 意外と実装が難しい

もちろん、こんな実装は**よろしくない**。

```python
def predict(X):
    """予測関数

    学習済みの重み(learned_W)と学習済みのバイアス(learned_b)を使用した予測値を返す

    Args:
       X (numpy.ndarray): 予測したいデータ(データ数, 項目数)

    Returns:
       numpy.ndaray: 予測結果(データ数,)
    """

    # WX+bを行列の積で計算するためにXの列にバイアス項（全て1）を繋げる
    X_with_bias = np.c_[X, np.ones((X.shape[0],1))]
    # バイアス項を繋げたXとW,bを行方向に繋げた行列の積を取る
    return X_with_bias.dot(np.r_[learned_W,learned_b])
```

明らかに[DRY原則](https://xn--97-273ae6a4irb6e2hsoiozc2g4b8082p.com/%E3%82%A8%E3%83%83%E3%82%BB%E3%82%A4/DRY%E5%8E%9F%E5%89%87/)に反しているので、もう少しなんとかしたい。

せめて、先に実装した`hypothesis`関数を使って、

```python
def predict(X):
    """予測関数

    学習済みの重み(learned_W)と学習済みのバイアス(learned_b)を使用した予測値を返す

    Args:
       X (numpy.ndarray): 予測したいデータ(データ数, 項目数)

    Returns:
       numpy.ndaray: 予測結果(データ数,)
    """
    # learned_Wとlearned_bを決め打ちしてhypothesis関数を呼び出す
    return hypothesis(X, learned_W, learned_b)
```

こんな感じでどうだろうか。

『なかなかシンプルな実装でいいんじゃなかろうか』などと思っていると、後ろに怖い先輩が立っている。

> お前、グローバル変数に依存してるのはどういうことだ！？結合度って概念知ってるか？
> この関数どうやってユニットテストする気だ？えぇ！？

...なるほど。どうやって解決しようか。
`learned_W`や`learned_b`を`predict`関数の引数に戻してしまったら意味が無いわけで。。

そうだ！Pythonは[広い意味での関数型言語](https://qiita.com/tetsuya-zama/items/6c88f26302a3fe9b28f3)なので、関数型のテクニックを使って解決しよう。
[クロージャ](https://qiita.com/hennin/items/806bc2633556dea85ee2)を作って`learned_W`や`learned_b`のスコープを分離してしまえばいい。

```python
def create_predict_func(learned_W, learned_b):
    """予測関数を作成する関数

    hypothesis関数に学習済みの重み(learned_W)と学習済みのバイアス(learned_b)を適用して、
    predict(予測)関数を作成する

    Args:
       learned_W (numpy.ndarray): 学習済みの重み(データの項目数,1)
       learned_b (numpy.ndarray): 学習済みのバイアス(1,1)

    Returns:
       func : 予測関数
           Args:
              X: 予測したいデータ(データ数, 項目数)
           Returns:
              numpy.ndaray: 予測結果(データ数,)
    """
    # Xを引数にとってhypothesisの戻り値を返す関数を返す
    return lambda X: hypothesis(X, learned_W, learned_b)

    # (参考) lambda式を使わない別解
    #def predict(X):
    #    return hypothesis(X, learned_W, learned_b)
    #
    #return predict
```

これをこのように使う。

```python
# learned_Wとlearned_bを指定してpredict関数を作る
predict = create_predict_func(learned_W, learned_b)
# 一度作ったpredict関数は今後何度でもXのみで（learned_Wやlearned_bをいちいち指定せず）呼べる
prediction = predict(X_to_predict)

# (参考)
# 今回は解説のためにわざわざcreate_predict_func関数を作ったが、以下でも良い
#
# # learned_Wとlearned_bを指定してpredict関数を作る
# predict = lambda X: hypothesis(X, learned_W, learned_b)
# # 一度作ったpredict関数は今後何度でもXのみで（learned_Wやlearned_bをいちいち指定せず）呼べる
# prediction = predict(X_to_predict)
```

こんなんでどうでしょう？グローバル先輩（あだ名）！

> いや、hypothesisもグローバル関数だから！

...わかりましたよ。。じゃあhypothesisも引数にとって、

```python
def create_predict_func(hypothesis, learned_W, learned_b):
    """予測関数を作成する関数

    hypothesis関数に学習済みの重み(learned_W)と学習済みのバイアス(learned_b)を適用して、
    predict(予測)関数を作成する

    Args:
       hypothesis (func): 重みとバイアスを適用する予測関数
       learned_W (numpy.ndarray): 学習済みの重み(データの項目数,1)
       learned_b (numpy.ndarray): 学習済みのバイアス(1,1)

    Returns:
       func : 予測関数
           Args:
              X: 予測したいデータ(データ数, 項目数)
           Returns:
              numpy.ndaray: 予測結果(データ数,)
    """
    # Xを引数にとってhypothesisの戻り値を返す関数を返す
    return lambda X: hypothesis(X, learned_W, learned_b)

# hypothesisにlearned_Wとlearned_bを適用してpredict関数を作る
predict = create_predict_func(hypothesis, learned_W, learned_b)
# 一度作ったpredict関数は今後何度でもXのみで（learned_Wやlearned_bをいちいち指定せず）呼べる
prediction = predict(X_to_predict)
```

これで文句ないだろ！グロ先（あだ名）!!

> いやお前さ、この実装、hypothesisの引数の順番が(X,W,b)であることに依存してんじゃん！
> (W,b,X)になったらどうする気？

...もうイヤ。
あ〜あ、誰か**任意の関数の引数の一部を固定した新たな関数を作ってくれる関数**を用意してくれないかなぁ。。。

## そこで部分適用

ということで、

> ある関数の引数の一部を固定して新たな関数を作る

ことを**部分適用(partial application)**といい、Pythonでは`functools.partial`という関数が用意されている。

先ほどから苦労していた`predict`関数の実装はこの`functools.partial`を使って以下のように書くことができる。

```python
from functools import partial
# hypothesis関数の引数W,bにそれぞれlearned_W,learned_bを部分適用してpredict関数を作る
predict = partial(hypothesis, W=learned_W, b=learned_b)
# 一度作ったpredict関数は引数にXのみを渡して呼ぶことができる
prediction = predict(X_to_predict)
```

`functools.partial`は便利なことに`W=learned_W`,`b=learned_b`のように順番関係なく引数名で部分適用する引数を指定できるので、先ほどのグロ先の要望も満たすことができる。

## 関数型のテクニックでサクッとスコープ管理を

正直なところ、`functools.partial`を紹介する前の段階、クロージャを作り出したあたりからすでに実質hypothsisに対する**部分適用**をしており、関数型のテクニックを使ってグローバル変数への参照を避けてスコープ管理をしていた。

では、**部分適用**という考え方を知らず、それを用いないで今回の要件を実現するにはどうしたら良いか。もちろん、**DRY**でかつ**スコープ管理**もきちんと行った上でだ。

**オブジェクト指向**的な考え方をする人であれば、以下のように実現するかも知れない。

```python
class Predictor:
    def __init__(self, hypothesis, learned_W, learned_b):
        self._hypothesis = hypothesis
        self._W = learned_W
        self._b = learned_b

    def predict(self, X):
        return self._hypothesis(X, self._W, self._b)

predictor = Predictor(hypothesis, learned_W, learned_b)

prediction = predictor.predict(X_to_predict)
```

もちろん、この方法自体は全く問題無い。ただ、このオブジェクト指向的な解決方法の欠点をあげるとすれば、**この程度のスコープ管理のためにわざわざclassを宣言しなければいけない**ことだろうか。

`Java6`からそれ以前あたりのレガシーなJavaで本格的に開発したことがある方なら共感いただけると思うが、**オブジェクト指向のみで真面目にスコープ管理・カプセル化をしようとすると、とにかくたくさんのclassやinterfaceを宣言しなくてはならない**。

今回取り上げた`Python`や最近の`Java`を含め、現在のメジャーな言語のうちほとんどは程度の差こそあれ**オブジェクト指向と関数型のハイブリッド**ができるようになっているので、ケースバイケースで適切な方法でスコープ管理ができるようになると生産性が高くかつ安全にコードが組めるようになるのではなかろうか。グローバル先輩もきっとそれを伝えたかったのだろう。

それにしても、本職がプログラマでは無い人もたくさん受講するE資格対策の教材でいきなり**部分適用**を説明も無しに持ってくるのは流石にエグいと思う。。正直、本職のプログラマでも知らん人多いのに。。。