LoginSignup
3
2

More than 5 years have passed since last update.

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

Posted at

はじめに

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

Python 3.7 で PEP 563 -- Postponed Evaluation of Annotations が導入され、前方参照もできるようになってますます使いやすくなった Python typing ですが、まだまだ課題も残っています。
今日はそんな Python typing の課題のうち、外部モジュールによってAnyの侵略が始まってしまう話をします。

Any型のこと

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

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

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

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

例を出しましょう。

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

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

出現! 侵略者Any

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

UntypedModule.py
class UntypedClass:
    def get_some_integer(self):
        return 42
    ...
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()とした時点でbAnyになってしまい、b.foo()という行がエラーなく通ってしまいます。その戻り値であるfoo2も当然Anyなので、以降この関数スコープが終わるまで、プログラマは延々Anyの汚染に頭を抱えることになってしまいます。

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

なんとかする方法

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

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()内で呼ぶときにはbintと推論され、Anyの汚染をmake_a_and_get_integer()関数内に抑えることができました。

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

おわりに

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

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

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

 

 

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

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2