Pythonで型ヒントを書くとき、typing.Any
とobject
、どっちを使えばいいの?とわからなくなってしまったので、両者の違いについてまとめてみました。
1年以上放置してた下書きの供養なんて言えない
※PyCharm環境での作業を想定しています。
更新履歴
2023/11/17 : 初版
環境
- Windows10
- Python3.10
- PyCharm Community Edition 2021.2.2
1. 結局何が違うの?
有識者からは「全然違うだろ」と言われそうですが、まとめるとこんな感じです。
typing.Any
- 型チェックしないよ!
- コード補完もしないよ!自分で全部頑張ってね!
object
- 型チェックするよ!
- コード補完はするけど、あんまり有用じゃないよ
2. 具体例
具体例でそれぞれ(+ 一般人代表int
)の特徴を見ていきます。
※Redeclared 'hoge' defined above without usage
の警告は今回無視します。
まずint
を指定した変数に、色々な値を代入してみます。
int_: int
int_ = 0 # 警告なし
int_ = 'A' # intじゃねえぞ!
int_ = [0, 1, 2] # intじゃねえぞ!
int_ = None # intじゃねえぞ!
int
である0
を代入したときは警告は出ませんでしたが、int
以外の値を代入したときは怒られてしまいました。
まあ、これが一般的な型チェックですね。
続いてtyping.Any
の場合を見てみましょう。
from typing import Any
any_: Any
any_ = 0 # 警告なし
any_ = 'A' # 警告なし
any_ = [0, 1, 2] # 警告なし
any_ = None # 警告なし
typing.Any
を指定した変数は、何を代入しても警告は出ませんでした。
最後に、object
の場合を見てみましょう。
object_: object
object_ = 0 # 警告なし
object_ = 'A' # 警告なし
object_ = [0, 1, 2] # 警告なし
object_ = None # 警告なし
object
を指定した変数も、何を代入しても警告は出ません。
......typing.Any
と同じじゃん。
とまあ、この場合両者の挙動が一致してしまいます。
これによって、何が違うの?と混乱してしまうのかなあと思います。
挙動が一致するのは偶然です。ここポイントです。
3.解説
まずtyping.Any
で警告が出ないのは、そもそもtyping.Any
に対してはチェックなどしていないことが理由です。
チェックしていないものに対して警告も何もありません。
int
だろうがstr
だろうが`好きに代入してくれ、その後のことは知らん、というスタンスです。
ですがobject
で警告が出ないのは、チェックをした上で、全部判定OKだったことが理由です。
判定OKなら警告出せませんもんね。
といった感じで、理由は違えども警告の表示という点では同じ挙動になってしまうのでした。
.....え?object
を指定した変数にint
の値を代入したのに警告が出ないのはおかしい?型が違うじゃないかって?
それを説明するにはクラスの継承を理解する必要があります。
Pythonでは、あらゆる変数が何らかのクラスのインスタンスになっています。
0
(int
)もTrue
(bool
)も"ABC"
(str
)もNone
(NoneType
)も、更には関数だって何らかのクラスのインスタンスです。
また、クラス間には親子関係が存在します。
既存クラスをベースにしてカスタマイズしたい!といった場合に、既存クラスを親、新しいクラスを子供にして実装します。
身近な例では、bool
はint
の子供です。
一方、int
とstr
の間に親子関係はありません。
そして実は、object
クラスは全てのクラスの親玉になっているのです。
object
└ int
│ └ bool
└ str
└ NoneType
この親子関係を継承と言い、「bool
はint
を継承している」などと言います。
そして、「全てのクラスはobject
を継承している」ということが言えます。
(ここでは「親子関係」と書きましたが、「直系かどうか」の方が正しいです。bool
とobject
は少し離れていますが、bool
->int
->object
と一直線に遡ることができるのでbool
はobject
を継承している、と言います。)
さて、object
の正体についてはわかりました。
次に、リスコフの置換原則というものについて解説します。
といっても全部は解説できないのでざっくり言うと、「子供は親の代わりをできるようにしようね!」ということです。
つまり、int
にできることはbool
でもできないといけないし、object
にできることは全てのクラスでできないといけない、ということです。
あくまで「原則」なので守られないこともありますが、守った方がメリットあるよね!ということで、広く一般的に受け入れられています。
ここまできて、ようやく謎の挙動の説明ができます。
もう一度コードを表示します。
object_: object
object_ = 0 # 警告なし
object_ = 'A' # 警告なし
object_ = [0, 1, 2] # 警告なし
object_ = None # 警告なし
変数object_
は、object
クラスとして振る舞うことが期待されています。
object
クラスが持つプロパティは全部持っているし、同様にメソッドは全て実行可能であるはずです。
......でも、int
だってobject
クラスのプロパティやメソッドは全部持ってるよね。
int
に限らずbool
、str
、NoneType
、ありとあらゆるクラスはobject
クラスのプロパティやメソッドを全部持ってるよね。
だって、「子供は親の代わりができる(リスコフの置換原則)」んだから。
それなら変数object_
に0
(int
)を代入しても、object
クラスでできることは全部できるんだから問題ないよね。
問題が起きないなら警告も要らないよね。
以上が、obejct
を指定した変数への代入に関する説明です。
4.最後に
今回の説明はPython3を前提にしています。
Python2だとクラス周りの仕様が違うのですが、まさかまだ使ってる人なんて......いないからいいよね?
あと、今回の説明ではあえてfloat
については触れませんでした。
というのも、int
とfloat
の間には親子関係はないのですが、float
を指定した変数にint
の値を代入しても警告が出ないんですよね。(逆は出ます。)
float_: float
float_ = 0 # 警告なし
int_: int
int_ = 0.0 # 警告あり
数学的には整数(int
)は実数(float
)の部分集合なので、整数を実数と同じように扱えるよう特殊処理が入っているのだと思います。
ですが本文中で触れても話が脱線するだけなので、おまけとして最後に書くことにしました。
ということで、typing.Any
とobject
の違いについて解説してきました。
冒頭に書いた通りこの記事の下書きを書いたのは1年前で、当時かなり悩んでいた記憶があります。
そう考えるとこの1年で成長したんだなあという気持ちになります(しみじみ)。
ではまた。