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年で成長したんだなあという気持ちになります(しみじみ)。
ではまた。