【Python】外部モジュールで始まるAnyの汚染

# はじめに

皆さん、きちんと型付けしてますか？

Python 3.7 で [PEP 563 -- Postponed Evaluation of Annotations](https://www.python.org/dev/peps/pep-0563/) が導入され、前方参照もできるようになってますます使いやすくなった Python typing ですが、まだまだ課題も残っています。
今日はそんな Python typing の課題のうち、外部モジュールによって`Any`の侵略が始まってしまう話をします。


# `Any`型のこと

動的型付け言語である Python には、`Any`という型が存在しています。
`Any`は「型情報がない」ことを表しており、`Any`型のインスタンスはあらゆる型にアップキャスト・ダウンキャストすることができます。

サラッと書きましたが、これはとんでもないことです。

`Any`は`int`になれます。すなわち、`Any`型のインスタンスを`int`しか受け取らない引数に渡すことができます。
それと同時に、`Any`は`str`にもなれるので、`str`しか受け取らない引数に渡しても問題ありません。
`Callable`にもなれるので、`()`オペレータを適用しても怒られません。`foo()`メソッドを持つ任意の型にもなれるので、突然`any_instance.foo()`としても誰も文句を言いません。

逆に、`Any`を受け取る引数には何を渡しても構いません。`int`を渡そうが`str`を渡そうが、自前の型`T`を渡しても何の文句も言われませんし、渡した瞬間それらは全て`Any`になります。

例を出しましょう。

```python:WTFAny.py
from typing import Any

any_instance: Any = 42  # No error
also_any_instance = any_instance + 4  # No error
ofcourse_any_instance = also_any_instance()  # No error (this line will raise error at runtime)
lol = ofcourse_any_instance.foo('bar')['baz']  # No error
```

本来`42`は`int`であり、`Any`のインスタンスではありません。しかし任意の型は暗黙的に`Any`になれるので、`any_instance: Any = 42`はエラーなく静的型チェックを通過し、`any_instance`は`Any`と推論されます。
また、`also_any_instance`は値としては`int`の`46`なので、本来`also_any_instance()`などと呼ぶことはできません。しかし、`any_instance`を`Any`と推論したことで連鎖的に`also_any_instance`も`Any`となっており、この行は静的型チェックをエラーなく通過します。
その戻り値`ofcourse_any_instance`も当然`Any`と推論されるため、`ofcourse_any_instance.foo('bar')['baz']`というでたらめな行もエラーを吐いてくれません。

# 出現！ 侵略者`Any`

このように、型情報の一切を蹂躙しながらソースコードを侵略する`Any`ですが、実は Python のソースコードを書いていると結構頻繁に現れます。
どういう時に現れるのかというと、外部モジュールをインポートするときです。

```python:UntypedModule.py
class UntypedClass:
    def get_some_integer(self):
        return 42
    ...
```

```python:WTFAny2.py
from UntypedModule import UntypedClass

a = UntypedClass()  # type is UntypedClass
# foo1 = a.foo()  # Error! "UntypedClass" has no attribute "foo"
b = a.get_some_integer()  # Here b is Any, not int, because there is no type hint in get_some_integer()
foo2 = b.foo()  # No error, return type is Any
```

静的型検査機は`UntypedClass`がどのような型であるかまではチェックできるので、`UntypedClass`である`a`に対する`a.foo()`という呼び出しは正しくエラーになります。しかしながら、そのインスタンスメソッドである`get_some_integer()`が何を返すかまでは知ったこっちゃないので、`b = a.get_some_integer()`とした時点で`b`は`Any`になってしまい、`b.foo()`という行がエラーなく通ってしまいます。その戻り値である`foo2`も当然`Any`なので、以降この関数スコープが終わるまで、プログラマは延々`Any`の汚染に頭を抱えることになってしまいます。

なお、頭を抱えられるのはまだ良い方です。多くの場合、`b`の型が`Any`になっていることにプログラマは気づけません。型検査機でも明らかに見つけられるはずの実行時エラーに遭遇して初めて、その変数がずっと前から`Any`だったことに気付くのです。

# なんとかする方法

完全な対処方法はありませんが、一番良いのは関数を細かく分けることです。
自分で型のある関数を細かく書くことで、`Any`の汚染を最小限に留めることができます。

```python:minimal_contaminated_with_Any.py
from UntypedModule import UntypedClass

def make_a_and_get_integer() -> int:
    a = UntypedClass()
    return a.get_some_integer()  # Here the return type will be int without any errors because Any can be int

def say_foo() -> None:
    b = make_a_and_get_integer()  # b is int
    b.foo()  # Error! "int" has no attribute "foo"

if __name__ == '__main__':
    say_foo()
```

`UntypedClass.get_some_integer()`は型ヒントが付いていないため、`a.get_some_integer()`は一度`Any`と推論されてしまいます。
しかし、`Any`は当然`int`にもなれるので、関数`make_a_and_get_integer()`は戻り値の型である`int`にエラーなくキャストされ、`int`として返ります。これにより、`say_foo()`内で呼ぶときには`b`は`int`と推論され、`Any`の汚染を`make_a_and_get_integer()`関数内に抑えることができました。

`numpy`や`django`、`torch`といった著名なモジュールが気軽に`Any`を返してくることがあるので（無名なモジュールは言うまでもない）、外部モジュールを使うときは常に気が抜けません。
私としては、`Any`を見たら常にエラーを吐くくらい厳しくしてくれたっていいんですけどね。

# おわりに

`Any`型の恐怖と発生例、その抑え方について軽くお話ししました。

Python typing は漸進的型付けなので、型の立場はまだまだ弱いように思います。
しかしながら、[PEP 526 -- Syntax for Variable Annotations](https://www.python.org/dev/peps/pep-0526/) や [PEP 544 -- Protocols: Structural subtyping (static duck typing)](https://www.python.org/dev/peps/pep-0544/)、[PEP 560 -- Core support for typing module and generic types](https://www.python.org/dev/peps/pep-0560/) など、Python の型付けサポートは日々手厚くなっており、Python typing は今後どんどん便利になっていくと考えられます。

あらゆる Python module に適切な型が付いた幸せな世界になることを切望しています。


　

　

でもこれ、もう Python じゃなくてもよくない？