【Python】型ヒントのtyping.Anyとobjectの違いについて

Pythonで型ヒントを書くとき、`typing.Any`と`object`、どっちを使えばいいの？とわからなくなってしまったので、両者の違いについてまとめてみました。
~~1年以上放置してた下書きの供養なんて言えない~~

※PyCharm環境での作業を想定しています。

# 更新履歴
2023/11/17 : 初版


# 環境
* Windows10
* Python3.10
* PyCharm Community Edition 2021.2.2


# 1. 結局何が違うの？

有識者からは「全然違うだろ」と言われそうですが、まとめるとこんな感じです。

## typing.Any
* <font color="red">**型チェックしないよ**</font>！
* コード補完もしないよ！自分で全部頑張ってね！

## object
* <font color="red">**型チェックするよ**</font>！
* コード補完はするけど、あんまり有用じゃないよ


# 2. 具体例

具体例でそれぞれ（＋ 一般人代表`int`）の特徴を見ていきます。

※`Redeclared 'hoge' defined above without usage`の警告は今回無視します。

まず`int`を指定した変数に、色々な値を代入してみます。

```Python: sample_int.py
int_: int
int_ = 0          # 警告なし
int_ = 'A'        # intじゃねえぞ！
int_ = [0, 1, 2]  # intじゃねえぞ！
int_ = None       # intじゃねえぞ！
```

`int`である`0`を代入したときは警告は出ませんでしたが、`int`以外の値を代入したときは怒られてしまいました。
まあ、これが一般的な型チェックですね。

続いて`typing.Any`の場合を見てみましょう。

```Python: sample_any.py
from typing import Any

any_: Any
any_ = 0          # 警告なし
any_ = 'A'        # 警告なし
any_ = [0, 1, 2]  # 警告なし
any_ = None       # 警告なし
```

`typing.Any`を指定した変数は、何を代入しても警告は出ませんでした。

最後に、`object`の場合を見てみましょう。

```Python: sample_object.py
object_: object
object_ = 0          # 警告なし
object_ = 'A'        # 警告なし
object_ = [0, 1, 2]  # 警告なし
object_ = None       # 警告なし
```

`object`を指定した変数も、何を代入しても警告は出ません。

......`typing.Any`と同じじゃん。
とまあ、この場合両者の挙動が一致してしまいます。
これによって、何が違うの？と混乱してしまうのかなあと思います。
**挙動が一致するのは偶然**です。ここポイントです。


# 3.解説

まず`typing.Any`で警告が出ないのは、<font color="red">**そもそも`typing.Any`に対してはチェックなどしていない**</font>ことが理由です。
チェックしていないものに対して警告も何もありません。
`int`だろうが`str`だろうが`好きに代入してくれ、その後のことは知らん、というスタンスです。

ですが`object`で警告が出ないのは、<font color="red">**チェックをした上で、全部判定OKだった**</font>ことが理由です。
判定OKなら警告出せませんもんね。

といった感じで、理由は違えども警告の表示という点では同じ挙動になってしまうのでした。

.....え？`object`を指定した変数に`int`の値を代入したのに警告が出ないのはおかしい？型が違うじゃないかって？
それを説明するにはクラスの継承を理解する必要があります。

Pythonでは、あらゆる変数が何らかのクラスのインスタンスになっています。
`0`(`int`)も`True`(`bool`)も`"ABC"`(`str`)も`None`(`NoneType`)も、更には関数だって何らかのクラスのインスタンスです。
また、クラス間には親子関係が存在します。
既存クラスをベースにしてカスタマイズしたい！といった場合に、既存クラスを親、新しいクラスを子供にして実装します。
身近な例では、`bool`は`int`の子供です。
一方、`int`と`str`の間に親子関係はありません。
そして実は、`object`クラスは全てのクラスの親玉になっているのです。

```text:クラス関係図
object
 └ int
 │  └ bool
 └ str
 └ NoneType
```

この親子関係を**継承**と言い、「`bool`は`int`を継承している」などと言います。
そして、「全てのクラスは`object`を継承している」ということが言えます。
（ここでは「親子関係」と書きましたが、「直系かどうか」の方が正しいです。`bool`と`object`は少し離れていますが、`bool`->`int`->`object`と一直線に遡ることができるので`bool`は`object`を継承している、と言います。）

さて、`object`の正体についてはわかりました。
次に、リスコフの置換原則というものについて解説します。
といっても全部は解説できないのでざっくり言うと、「子供は親の代わりをできるようにしようね！」ということです。
つまり、`int`にできることは`bool`でもできないといけないし、`object`にできることは全てのクラスでできないといけない、ということです。
あくまで「原則」なので守られないこともありますが、守った方がメリットあるよね！ということで、広く一般的に受け入れられています。

ここまできて、ようやく謎の挙動の説明ができます。
もう一度コードを表示します。

```Python: sample_object.py
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`の値を代入しても警告が出ないんですよね。（逆は出ます。）

```Python: sample_int_float.py
float_: float
float_ = 0  # 警告なし

int_: int
int_ = 0.0  # 警告あり
```

数学的には整数(`int`)は実数(`float`)の部分集合なので、整数を実数と同じように扱えるよう特殊処理が入っているのだと思います。
ですが本文中で触れても話が脱線するだけなので、おまけとして最後に書くことにしました。

ということで、`typing.Any`と`object`の違いについて解説してきました。
冒頭に書いた通りこの記事の下書きを書いたのは1年前で、当時かなり悩んでいた記憶があります。
そう考えるとこの1年で成長したんだなあという気持ちになります（しみじみ）。
ではまた。