2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2023-11-17

Pythonで型ヒントを書くとき、typing.Anyobject、どっちを使えばいいの?とわからなくなってしまったので、両者の違いについてまとめてみました。
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を指定した変数に、色々な値を代入してみます。

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

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

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

sample_any.py
from typing import Any

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

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

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

sample_object.py
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)も、更には関数だって何らかのクラスのインスタンスです。
また、クラス間には親子関係が存在します。
既存クラスをベースにしてカスタマイズしたい!といった場合に、既存クラスを親、新しいクラスを子供にして実装します。
身近な例では、boolintの子供です。
一方、intstrの間に親子関係はありません。
そして実は、objectクラスは全てのクラスの親玉になっているのです。

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

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

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

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

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

変数object_は、objectクラスとして振る舞うことが期待されています。
objectクラスが持つプロパティは全部持っているし、同様にメソッドは全て実行可能であるはずです。
......でも、intだってobjectクラスのプロパティやメソッドは全部持ってるよね。
intに限らずboolstrNoneType、ありとあらゆるクラスはobjectクラスのプロパティやメソッドを全部持ってるよね。
だって、「子供は親の代わりができる(リスコフの置換原則)」んだから。
それなら変数object_0(int)を代入しても、objectクラスでできることは全部できるんだから問題ないよね。
問題が起きないなら警告も要らないよね。

以上が、obejctを指定した変数への代入に関する説明です。

4.最後に

今回の説明はPython3を前提にしています。
Python2だとクラス周りの仕様が違うのですが、まさかまだ使ってる人なんて......いないからいいよね?

あと、今回の説明ではあえてfloatについては触れませんでした。
というのも、intfloatの間には親子関係はないのですが、floatを指定した変数にintの値を代入しても警告が出ないんですよね。(逆は出ます。)

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

int_: int
int_ = 0.0  # 警告あり

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

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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?