Python: Queue に型を付ける

皆さん、普段 Python でコードを書くときはちゃんと型を書いてますよね?
まだ型の書き方を知らない場合は Software Design 2020年 5月号の『Python でも型チェックしよう』を読むといいと思いますよ (宣伝)。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">レビューのお手伝いをした『Python でも型チェックしよう』(by <a href="https://twitter.com/t2y?ref_src=twsrc%5Etfw">@t2y</a>)が掲載されているSDが発売されました。型ヒントを使ってみたい方におすすめです。また、3.8で導入された Protocolなども紹介されており、すでに使っている方にもお勧め！ / Software Design 2020年5月号 <a href="https://t.co/JL5GgO0mcq">https://t.co/JL5GgO0mcq</a></p>&mdash; tk0miya (@tk0miya) <a href="https://twitter.com/tk0miya/status/1251494114446569474?ref_src=twsrc%5Etfw">April 18, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

私は[コードに型をつけ始めておおよそ 3年ぐらい](https://github.com/sphinx-doc/sphinx/pull/3144)になるのですが、いまでは型がないと落ち着かなくなっています。
コード規模がある程度大きくなるとコードを書くときにツッコミをくれたり、バグを未然に防いだりしてくれるので、プログラム開発強制ギプス的な役割として非常に助かっています。
慣れるまではしばらく時間がかかりますが、とてもおすすめです。

## キューの型を書きたい

そんなある日、ふとデータ型としてキューを使おうとしたときにこんなことに気づきました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ふむー、typing.Queue って存在しないのかー</p>&mdash; tk0miya (@tk0miya) <a href="https://twitter.com/tk0miya/status/1253997834656870400?ref_src=twsrc%5Etfw">April 25, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

`queue.Queue` クラスを使って型を書くことはできますが、`typing.Queue` に相当する型がないのです。

```python
q: queue.Queue = queue.Queue()
q.put(1)
value = q.get()
```

この状態でも型チェッカーによるサポートは得られるのですが、もう少し詳細な型をつけたくなります。
たとえば、キューでやり取りするデータが数値である、文字列であるといった情報も指定できると嬉しいですよね。

リストの場合は `List[int]` や `List[str]` とアノテーションしますが、Queue の場合はどうするとよいのでしょうか。試しに `Queue[int]` とアノテーションしてみたところエラーになってしまいました。

```python
>>> from queue import Queue
>>> q: Queue[int] = Queue()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'type' object is not subscriptable
```

## 数値を扱うキューの型ってどう書くのか

どのように書くべきか調べてみたところ、 [bpo-33315](https://bugs.python.org/issue33315) や [python 3.6 - What is the correct way to type hint a homogenous Queue in Python3.6 (especially for PyCharm)? - Stack Overflow](https://stackoverflow.com/questions/48956422/what-is-the-correct-way-to-type-hint-a-homogenous-queue-in-python3-6-especially) に解決方法が書かれていました。

>I think this issue appeared previously on typing tracker. The current recommendation is to escape problematic annotations with quotes:
>
>q: 'Queue[int]'

現状の回避策としては文字列として型を記述するとよい、ということのようです。
実際に Python インタプリタで指定してみるとエラーにはなりません。

```python
>>> from queue import Queue
>>> q: "Queue[int]" = Queue()
>>>
```

そして、mypy も正しく判定してくれるようです。

```python:example.py
from queue import Queue


q: "Queue[int]" = Queue()
q.put(1)
i: int = q.get()
print(i)

q.put("2")
s: str = q.get()
print(s)
```

```bash
$ mypy example.py
example.py:9: error: Argument 1 to "put" of "Queue" has incompatible type "str"; expected "int"
example.py:10: error: Incompatible types in assignment (expression has type "int", variable has type "str")
Found 2 errors in 1 file (checked 1 source file)
```

## 仕組み

この動作は Python インタプリタと mypy で型アノテーションの扱いが異なるために発生します。

### Queue[int] の場合

Python インタプリタでは `Queue[int]` は Queue クラスの辞書アクセスとして解釈され、`Queue.__getitem__(int)` を呼び出そうとします。しかし、 Queue クラスには `__getitem__()` メソッドが存在しないためエラーになります。

一方、mypy の方では、Queue クラスはジェネリッククラスとして認識されており、`Queue[int]` は 「int の値を持つキュー」として扱われます。typeshed[^1] の [Queue クラスの型定義](https://github.com/python/typeshed/blob/aa06f3bc7b9511f1fad4f8456c2cc5389838f2e4/stdlib/3/queue.pyi#L14-L36) を見てみると、Queue クラスは Generic を継承していることがわかります。

[^1]: Python の標準ライブラリの型データベース

### "Queue[int]" (文字列)の場合

今度は `"Queue[int]"` という文字列表記の型アノテーションについて見てみましょう。
Python インタプリタでは単なる文字列として扱うため、「文字列でアノテーションされた変数」として扱います。Python インタプリタは型アノテーションを評価しないため、中身は気にせず先に進みます。

一方、mypy は文字列表記されている型アノテーションは中身を展開して型ヒントとして扱います。そのため、`Queue[int]` が指定されたものと同様に扱います。

ということで、文字列で `"Queue[int]"` と書くとうまくいくのでした。

||`Queue[int]`|`"Queue[int]"`|
|---|---|---|
|Pythonインタプリタ|エラーになる(評価しようとする)|エラーにならない(単なる文字列扱い)|
|mypy|型チェックできる|型チェックできる|

## Python3.7 の遅延評価機能を使おう

型アノテーションの評価について説明したところでピンときた方もいるかも知れません。お気づきの方はこのブロックは読み飛ばしていただいて結構です。

先ほどは回避方法として文字列でアノテーションする方法をお伝えしましたが、Python3.7 を使っている場合はもうひとつ別のアプローチがあります。Python3.7 で先行導入された機能のひとつである [PEP 563 -- Postponed Evaluation of Annotations](https://www.python.org/dev/peps/pep-0563/) を利用する方法です。この機能は読んで字の如し、型アノテーションの遅延評価機能です。

この機能を利用すると、型アノテーションは次のように扱われます。

* Python インタプリタでは型アノテーションを評価しない
* mypy などの型チェックツールでは型アノテーションを評価する

先ほどの表に照らし合わせると次のようになります。

||`Queue[int]`|`"Queue[int]"`|
|---|---|---|
|Pythonインタプリタ|エラーになる(評価しようとする)|エラーにならない(単なる文字列扱い)|
|PEP 563 (`__future__`)|無視される (評価しない)|無視される (評価しない)|
|mypy|型チェックできる|型チェックできる|


この機能を利用するには、コードの先頭に future 文を書きます(参考: [__future__ --- future 文の定義](https://docs.python.org/ja/3/library/__future__.html))。

```python
from __future__ import annotations
from queue import Queue


q: Queue[int] = Queue()
q.put(1)
i: int = q.get()
print(i)

q.put("2")
s: str = q.get()
print(s)
```

この機能を利用すると、型アノテーションを文字列にせずにすむため、スッキリ書けますね。

## 余談

以前の記事([PEP 585 (Type Hinting Generics In Standard Collections) を読んだよメモ - Qiita](https://qiita.com/tk0miya/items/65c00d7026f69690f791)) で紹介しましたが、[PEP 585](https://www.python.org/dev/peps/pep-0585/) の導入が決定した場合 `Queue[int]` と書けるようになるはずです。記事の途中でリンクを張った [bpo-33315](https://bugs.python.org/issue33315) でも PEP 585 に言及されています。

いずれはこうした小手先のテクニックを使わずに自然なアノテーションができるようになることが期待されます。

追記:
いつのまにか PEP 585 の採用が決まっており(Accepted)、3.9.0a6 で使えるようになっていました。以下、少し前にインストールした 3.9-dev での様子です。

```bash
$ python3.9
Python 3.9.0a6+ (heads/master:7f7e706, May  9 2020, 13:35:20)
[Clang 11.0.3 (clang-1103.0.32.59)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from queue import Queue
>>> Queue[int]
queue.Queue[int]
```

そのため、遅延評価をせずに型付けできます。いいですね。

```python
>>> q: Queue[int] = Queue()
>>> q
<queue.Queue object at 0x10533deb0>
>>> __annotations__
{'q': queue.Queue[int]}
```

## まとめ

* int のキューを書きたい場合は文字列で `"Queue[int]"` と書きましょう
* Python 3.7 以降に利用している場合は `from __future__ import annotations` のほうがよさそう
* 将来的には単に `Queue[int]` で良くなると思われる


## 謝辞

この記事を書くに当たり、雑談 & ボヤキに付き合ってくれた @t2y, @masahito に感謝します。