はじめに
皆さん、きちんと型付けしてますか?
Python 3.7 で PEP 563 -- Postponed Evaluation of Annotations が導入され、前方参照もできるようになってますます使いやすくなった 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になります。
例を出しましょう。
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 のソースコードを書いていると結構頻繁に現れます。
どういう時に現れるのかというと、外部モジュールをインポートするときです。
class UntypedClass:
def get_some_integer(self):
return 42
...
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の汚染を最小限に留めることができます。
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 や PEP 544 -- Protocols: Structural subtyping (static duck typing)、PEP 560 -- Core support for typing module and generic types など、Python の型付けサポートは日々手厚くなっており、Python typing は今後どんどん便利になっていくと考えられます。
あらゆる Python module に適切な型が付いた幸せな世界になることを切望しています。
でもこれ、もう Python じゃなくてもよくない?