Pythonってジェネリクス必要なの？→静的型解析するなら使ったほうがいい

## はじめに

最近デコレータを使って[フレームワークを作ってみる趣旨の記事](https://qiita.com/nikawamikan/items/74978c3a81e2a2e2a92c)を書いたのだが、Pylanceを利用して静的型チェックをするとうまく型推論できないことに気づいた。

```py
import sqlite3
from typing import Callable, Any

DB_PATH = 'test.db'


def db_connect(auto_commit: bool = False):
    # デコレータ
    def decorator(func: Callable[[sqlite3.Connection, sqlite3.Cursor], Any]):
        # ラッパー
        def wrapper(*args, **kwargs):
            connection = sqlite3.Connection(DB_PATH)
            cursor = connection.cursor()
            try:
                print('open')
                result = func(connection, cursor, *args, **kwargs)
            except Exception as e:
                connection.rollback()
                raise e
            else:
                # デコレータ生成時のauto_commitによってコミットするか決定する
                if auto_commit:
                    connection.commit()
                return result
            finally:
                print('close')
                connection.close()

        return wrapper
    return decorator


@db_connect(auto_commit=True)
def create_table(connection: sqlite3.Connection, cursor: sqlite3.Cursor):
    cursor.execute(
        'CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)')


create_table() # ここが型推論できない状態になる。
```

以下のようにcreate_tableには引数が必須であることについて怒られている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/83e5a943-2891-f8a8-524f-11b56d89b68c.png)

これを解決する方法こそ**ジェネリクス**だった。

## そもそもジェネリクスとは？

ジェネリクスとは主に静的型付け言語で必要になる概念で、静的型付け言語では任意のデータ型を返却させるだとか、任意のデータ型を戻り値にするだとかといった関数を書く場合に利用することを前提とした概念だ。

有名な例だとJavaやC#のコレクションの型を決定するはジェネリクスだ。

```java
// <> の中に任意の型を入れる事で任意の配列(List)が生成できる。
List<String> hogeList = new ArrayList<>();
```

```cs
// 受け取った型を確認し、そのまま返却する関数(型は何でもよい)
T Hoge<T>(T fuga)
{
    var typeOfFuga = typeof(T);
    Console.WriteLine($"fugaのタイプは　{typeOfFuga}");
    return fuga;    
}
```

これらはジェネリクスがない場合は全ての型を列挙し実装していかなければならないので、非常に便利だ。

今回はPythonなのでそもそも`Any`、つまり「何が返ってくるかわからない。」という状態から、「どの型が返ってくるか？」がわかるようになるので毎回どんな型が返ってきてるかをデバッグコンソールなどで確かめる手間が減り見通しの良いコードとなる。

## このコードの問題点

Pylanceの解析ではこの`create_table`関数を呼び出した際、デコレータでラッピングした`wrapper`関数を呼ぶのではなく、`create_table`を呼び出している状態となっている。

そのため、明示的にこれはラップされた関数であることを示すために`functools`の`wraps`関数デコレータを利用する。

```py
def db_connect(auto_commit: bool = False):
    # デコレータ
    def decorator(func: Callable[[sqlite3.Connection, sqlite3.Cursor], Any]):
        # ラッパー
        @functools.wraps(func) # このデコレータを追加
        def wrapper(*args, **kwargs):
            ... # 省略
```

それによりに`Callable(..., Any)`の型アノテーションがついた状態と同一の関数として扱われるようになる。
(前半の`(Connection, Cursor), Any`までがデコレートされる前の関数の型で、後半の`Callable(..., Any)`が今現在の関数の型)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/8e31ee3c-25ba-1e56-93d0-ca2c3bf77ab3.png)


これでも利用は可能だが、静的型言語出身の私にとっては結構気持ち悪い感じになっている。

なんせ、引数はゼロなのに何かが入る余地がありそうだし、返却される型はNoneなのにAnyとなにかを返す余地がありそうになっている。


::: note info
`@functools.wraps(func)`デコレータは引数となる関数の属性を引き継ぐ事ができます。
```py
from functools import wraps

def my_decorator(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        print("Before the function call")
        result = f(*args, **kwargs)
        print("After the function call")
        return result
    return wrapper

@my_decorator
def say_hello():
    """This function says hello"""
    print("Hello!")

say_hello()

print(say_hello.__name__)  # 出力: say_hello
print(say_hello.__doc__)  # 出力: This function says hello
```
:::

## ジェネリクスの実装

Python3.12から登場した構文らしいが、良い感じにジェネリクスを定義できるようになっている。


#### 今までのジェネリクス

`typing`モジュールの`TypeVar`を利用する必要があり、他の言語と違いひと手間あり面倒な感じであった。

```py
from typing import TypeVar

T = TypeVar('T')

# 引数の型で返却する。
def hoge(obj: T) -> T:
    return obj
```

上記の構文の通りにすることで、Pylanceは以下のようにint返却されることを推論できている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/c26312b4-9eae-6f39-87a4-5fd91773ae87.png)

#### 3.12からのジェネリクス

他の言語のジェネリクスのように関数名の前に`[T]`のように明記することでジェネリクスを実装することが可能になった。

```py
# 引数の型で返却する。
def hoge[T](obj: T) -> T:
    return obj
```

こちらでも同様の型推論が行われる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/5f8ab9a3-c971-49c7-4320-dc734fe8f04a.png)


### DB接続でのジェネリクスを実装

先ほどのコードでは`Callable`の戻り値が`Any`となってしまっていたことが原因なので、`Callable[[sqlite3.Connection, sqlite3.Cursor], T]`としてやることでラップ対象の関数と同じ戻り値が定義できる。

```py
def db_connect(auto_commit: bool = False):
    # 受け取った関数の戻り値がラッパーの戻り値になるようにジェネリクスを指定
    def decorator[T](func: Callable[[sqlite3.Connection, sqlite3.Cursor], T]):
        # ラッパー
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            ... # 省略
```

このようにジェネリクスを指定することで明示的に何が返ってくるか？がわかるようになる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/74555446-1c1f-8865-4a63-4391ee40acfa.png)

しかし、他のソースでは追加の引数を用意して、その引数のみで呼び出すような使われ方もしていたはずだ。

## 可変長なパラメータを許容するために必要なジェネリクス

追加の引数が必要なコードを確認する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/6d1393f9-f378-b1d9-835b-b88d8b19f4e3.png)

Pylanceで指摘されているのは`user`引数がデコレータの引数として存在しないからだ。

```py
def decorator[T](
    func: Callable[
        # このアノテーションがConnectionとCursor しか存在しないため。
        [sqlite3.Connection, sqlite3.Cursor],
        T
    ]):
```

そのため、既定の`Connection`, `Cursor`以外に任意のパラメーターを許容できる必要がある。
それを実現するために`typing`モジュールの`Concatenate`と`ParamSpec`を利用する。

`ParamSpec`に関して言えば先程のジェネリクスの話と同様にPython3.12からimportしなくても利用できるようになっている

#### 今までのジェネリクス

`ParamSpec`も`TypeVar`同様省略可能になっており、3.12より前は以下のような構文だった。

```py
import functools
from typing import ParamSpec, Callable

# パラメーターの型を定義
P = ParamSpec('P')


def hoge(func: Callable[P, int]):
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> int:
        print('hoge')
        return func(*args, **kwargs)
    return wrapper


@hoge
# ここでは任意の型の引数を受け取る関数を定義している
def fuga(a: int, b: int) -> int:
    return a + b


print(fuga(1, 2))
```

これでPylanceはfuga型の引数をそのままwrapper関数で利用されるよう認識してくれる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/4076b911-3f53-9e92-6e89-fcfe12035ec0.png)

#### 3.12からのジェネリクス

ParamSpecの場合は`**P`をつける必要があるが、これもほかの言語のように関数名と`()`の間にジェネリクスを記述するだけで記述可能となっている。

```py
import functools
from typing import Callable


def hoge[**P](func: Callable[P, int]):
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> int:
        print('hoge')
        return func(*args, **kwargs)
    return wrapper
```

### DB接続でのジェネリクスを実装

`typing`モジュールの`Concatenate`をimportし、`ParamSpec`以外の引数はラッパーが直接渡す事を明記するようにする。

```py
from typing import Concatenate


def db_connect(auto_commit: bool = False):
    def decorator[T, **P](func: Callable[
            # Concatenateで既定引数と任意引数を結合する
            Concatenate[sqlite3.Connection, sqlite3.Cursor, P],
            T]):
        # ラッパー
        @functools.wraps(func)
        # ラッパーの引数の型をP.args, P.kwargsにすることで、任意の引数を受け取れるようにする
        def wrapper(*args: P.args, **kwargs: P.kwargs):
            ... # 省略
```

こうすることでPylanceは正しく型推論を行う事ができるので`create_user`関数では`User`の型のみを受け取ることが可能だ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/8914fdac-8311-b8a5-98eb-cb63b03b3797.png)

## さいごに

前回書いた記事にこの情報も突っ込むべきかと思ったが、思った以上に書くことが多かったため、別の記事とした。

なお、同じOrganizationのメンバーのこの記事を見て「そういえばTypingモジュールあたり読み直すかー」となってなければ補足としてこの記事を書くことはなかった気がするので宣伝しておこうと思う。

https://qiita.com/CinnamonSea2073/items/2cae34c3dd7ff86d0950

## 参考

https://docs.python.org/ja/3/library/typing.html