要約
- Python を書く時、変数は再代入をしないほうが良い。 (この記事内容に関わらず)
- 変数の使い回し (再代入の繰り返し) をすると、ときに型ヒントは無力になる。
- この現象を VSCode + Pylance の環境で確認した。
イントロ
業務や趣味などで実装を行っていると、気づけばコード量は 3 桁、時には 4 桁に差し掛かるような python ファイルが出来てしまう。
こういったファイルにおいて処理の追加やバグ修正など行っている時に、ふと VSCode + Pylance 上で補完がうまくできないケースが発生していた。
例えば a: Foo = Foo()
といった型ヒントを書いているものの、次の行あたりで VSCode 上では a:Bar()
と認識されている。
一見すると、変数 a
では Foo クラスに実装したメソッド Foo.hoge()
のようなものが使えないように見えてしまっていた。
直前に a:Foo = Foo()
と型ヒントを明示的に書いているのに、次の行では VSCode 上ではエラーの表示。
明らかに意図していない状況ではあるが、ざっと同ファイルを読むと以下のような状況になっていることがわかった。
- 同変数
a
を for 文や if 文などを使いながら、何度も再代入をしていた。 - 変数
a
の型ヒントの誤認識は複数箇所で起きている。 -
- と同じように何度も再代入をしている変数、例えば
b
も同じく型ヒントの誤認識が発生している。
- と同じように何度も再代入をしている変数、例えば
そのとき作業していた環境では、「for 文によって似たような処理を繰り返すので、可読性を考えて似た処理については同変数名で統一しておこう」といった考えを過去に持っていたので、このような使い回し (再代入の繰り返し) が発生していた。
とはいえ、この現象は単にファイルの記述量が多すぎるがゆえに、適切な処理ができなかったのでは?と考えられる部分もある。
もし行数が起因するなら、適切なファイル分割をすれば回避できる問題かもしれない。
そうであれば、どうせリファクタリングも予定しているし、Pylance の型ヒントがどれほど壊れやすいか検証してみよう。
実験
ひとまず、条件分岐を使うことで変数 a
に何度も違う型ヒントを与えつつ、再代入を繰り返すとどうなるか検証
環境
簡単なコードによる検証
コード自体は無意味だが、やりたいことの最小限の再現は以下。
condition: bool = False
a:int = 0
if condition:
a:str = "0"
a
if condition:
a: list = [0]
a
結果
上記コードにおいて、それぞれの型が何かを VSCode 上で確認してみる。
思っていたより、型ヒントはすぐ無意味になってしまうんだな…
追加検証
前述のスニペットでは、最初の変数 a:int = 0
の時点で a:list
と判定されてしまっている。
型ヒントがすぐ無意味になってしまうのはわかったが、なぜ a:list
なのだろうか?
人間の目から判断すると、直近で int
以外の変更をしている a:str = "0"
のほうが影響しそうな気もするが…
Pylance はどのような parse をしているか具体的には不明であるが、コードの最下部の方から処理をしているんだろうか?
ということで、最後の行に追加で a
に dict 型を入れてみる
condition: bool = False
a:int = 0
if condition:
a:str = "0"
a
if condition:
a: list = [0]
a
a:dict = {"0": 0}
結果
スクリーンショットでは一部の変数を見ているが、実際にはすべての a
が a:dict
として判定されていた。
まとめ
そもそもとして、「変数の再代入を行うのは参照透過性が壊れる」といったような話は既に多くの場所でされており、この行為自体は良くないものである。
とはいえ、今回の自分のケースでは
- 条件分岐などで、そのブロック内でしか使わない変数であった。
- 毎回、そのブロックに入った時点で最初に宣言するので、異なるデータが入ってこないという状況にはしていた。
といった点で、「可読性を上げたいしなぁ」ととりあえず書いていた。
しかし、型ヒントが意味を成さなくなる状況は、前述したスニペットの更に一部の中で発生していた。
思ったより容易に、この現象に似たような再現はできるのである。
(今回の検証が、完全に同一の現象を再現しているかは置いておいて)
可読性の高さを求めることも大事だが、手の抜きすぎも思わぬトラブルを引き起こすものである(自戒)