関数に渡された値が、数値なのか文字列なのかを確かめたい。そんな場面に出会ったことはありませんか。
Pythonは変数に型を書かなくても動く言語です。そのぶん、思っていたのと違う型の値が紛れ込んで、エラーになることがあります。
型を確かめる方法として、よく紹介されるのがtype()とisinstance()の2つです。どちらも組み込み関数なのでimportは要りませんが、実は継承したクラスの扱いで結果が変わります。
今回は、2つの関数の違いから、複数の型をまとめて判定する書き方、うっかりハマりやすいboolの落とし穴まで、手元のPython 3.11で動かした結果とともに紹介します。どちらを使えばいいのか迷っていた方は、ぜひ最後まで読んでみてください。
type()とisinstance()は何をする関数か¶
まずは、それぞれの役割をひとことで押さえておきましょう。type()は、渡した値の型そのものを返す関数です。
一方のisinstance()は、値がある型に当てはまるかどうかを、TrueかFalseで答えてくれる関数です。名前のとおり、そのクラスのインスタンスですか、と質問するイメージですね。
実際に動かすと、次のような結果になります。
x = 42
print(type(x)) # <class 'int'>
print(type(x) is int) # True
print(isinstance(x, int)) # True
print(isinstance(x, str)) # False
この例だけを見ると、どちらも同じように使えそうです。違いが出てくるのは、クラスの継承がからんだときです。
2つの関数の特徴を、表で並べてみました。
| 関数 | 返すもの | 継承した子クラス | 主な用途 |
|---|---|---|---|
| type(x) | xの型そのもの | 親クラスとは別物として扱う | 型の名前を調べる、型がぴったり一致するか確かめる |
| isinstance(x, 型) | TrueかFalse | 親クラスの仲間として扱う | 条件分岐での型チェック |
表の真ん中の列が、今回いちばん大事なポイントです。次の章で、具体的なコードを使って確かめてみましょう。
継承したクラスで結果が変わる¶
ここでは、動物を表すAnimalクラスと、それを継承したDogクラスを用意します。継承とは、親クラスの性質を引き継いで新しいクラスを作る仕組みのことです。
class Animal:
pass
class Dog(Animal):
pass
pochi = Dog()
print(type(pochi)) # <class '__main__.Dog'>
print(type(pochi) is Animal) # False
print(isinstance(pochi, Animal)) # True
type()で調べると、pochiの型はあくまでDogです。だからAnimalと比べるとFalseになります。
それに対してisinstance()は、DogはAnimalの一種だと考えてTrueを返します。犬は動物ですか、と聞かれたら、ふつうはYesと答えますよね。
子クラスも仲間として扱うのがisinstance()¶
この考え方は、オブジェクト指向の世界ではとても自然なものです。子クラスは親クラスの代わりとして使える、という前提で設計することが多いからです。
親クラスのメソッドを呼ぶ処理に、子クラスのインスタンスを渡しても問題なく動くはずです。それなのにtype()で型をぴったり比べてしまうと、せっかくの継承が台無しになってしまいます。
継承やポリモーフィズムの考え方をおさらいしたい方は、こちらの記事も参考にしてみてください。【関連記事】継承・カプセル化・ポリモーフィズム。オブジェクト指向の3大要素をPythonで理解
標準ライブラリのクラスでも同じことが起こる¶
自分で作ったクラスだけの話ではありません。標準ライブラリのOrderedDictは、dictを継承して作られたクラスです。
from collections import OrderedDict
d = OrderedDict()
print(type(d) == dict) # False
print(isinstance(d, dict)) # True
辞書を受け取る関数でtype(d) == dictと書いていると、OrderedDictを渡したときだけ弾かれてしまいます。中身は辞書として問題なく使えるのに、です。
私は10年ほどエンジニアとして開発に関わってきましたが、この書き方が原因のバグを何度か見てきました。テストでは普通の辞書しか渡していなかったので、本番で別の種類の辞書が来たときに初めて気づいた、というパターンです。
複数の型をまとめて判定する書き方¶
実務では、数値なら整数でも小数でもOK、という判定をしたいことがよくあります。isinstance()なら、こうしたケースも1回の呼び出しで書けます。
タプルで型を並べる¶
2つ目の引数に、型をタプルで並べて渡すのが昔からある書き方です。どれかひとつに当てはまれば、Trueが返ってきます。
def is_number(value):
return isinstance(value, (int, float))
print(is_number(3)) # True
print(is_number(3.14)) # True
print(is_number("3")) # False
文字列の3は、見た目こそ数字ですが型はstrです。ここでFalseになることで、計算の前に変な値を弾けるわけです。
Python 3.10からは縦棒でも書ける¶
Python 3.10からは、int | floatのように縦棒で型をつなげた書き方も、isinstance()に渡せるようになりました。これはPEP 604という提案で入った機能で、型ヒントと同じ書き方ができるのがうれしいところです。
print(isinstance(3.14, int | float)) # True
print(isinstance(None, int | None)) # True
型ヒントの書き方と見た目がそろうので、コード全体の統一感が出ます。型ヒントについて詳しく知りたい方は、こちらもあわせてどうぞ。【関連記事】Pythonの型ヒントとは?型ヒントの基礎を解説
ただし、list[int]のように中身の型まで指定した書き方は、isinstance()には渡せません。実際に試すと、次のようなエラーになりました。
isinstance([1, 2], list[int])
# TypeError: isinstance() argument 2 cannot be a parameterized generic
isinstance()が確かめられるのは、あくまで外側の型だけです。リストの中身まで調べたいときは、any()やall()と組み合わせて1つずつ確かめましょう。
boolがintとして判定される落とし穴¶
ここからが、今回いちばん伝えたい内容です。Pythonでは、TrueとFalseはintの仲間として扱われます。
なぜなら、真偽値を表すbool型は、int型を継承して作られているからです。その結果、次のような一見ふしぎな結果になります。
print(isinstance(True, int)) # True
print(issubclass(bool, int)) # True
print(True + True) # 2
issubclass()は、クラス同士の親子関係を確かめる関数です。boolがintの子クラスだということが、ここではっきりわかりますね。
年齢の入力チェックでTrueが通ってしまった話¶
この性質は、入力チェックで問題になりがちです。年齢は整数で受け取りたいので、isinstance(age, int)で確かめる関数を書いたとしましょう。
すると、age=Trueのような値も整数として通ってしまいます。JSONから読み込んだ値にtrueが入っていた、という形で起こりやすいミスです。
対策としては、boolかどうかを先に確かめてから、intの判定をするのが確実です。
def check_age(age):
if isinstance(age, bool) or not isinstance(age, int):
raise TypeError(f"年齢は整数で指定してください: {age!r}")
if age < 0:
raise ValueError(f"年齢は0以上で指定してください: {age!r}")
return age
print(check_age(30)) # 30
check_age(True) # TypeError: 年齢は整数で指定してください: True
boolの判定を先に置くのがポイントです。順番を逆にすると、intのチェックを先に通過してしまうので注意してください。
この場面だけは、type(age) is intと書く方法もあります。継承した子クラスを含めたくない、という意図がはっきりしているなら、type()を使うのも立派な選択です。
type()を使ったほうがよい場面¶
ここまで読むと、isinstance()だけ覚えておけばよさそうに思えるかもしれません。ですが、type()にもちゃんと出番があります。
場面ごとの使い分けを、表にまとめてみました。
| やりたいこと | 使うもの | 書き方の例 |
|---|---|---|
| 条件分岐で型をチェックしたい | isinstance() | isinstance(x, (int, float)) |
| 子クラスを除いて、型がぴったり一致するか確かめたい | type() | type(x) is int |
| デバッグで型の名前を表示したい | type() | print(type(x).name) |
| クラス同士の親子関係を確かめたい | issubclass() | issubclass(Dog, Animal) |
デバッグのときに、この変数はいま何の型なんだろう、と気になることはよくあります。そんなときは、print(type(x))で中身の型をさっと確かめるのが便利です。
なお、type()で型を比べるときは、==よりもisを使うのが一般的です。型のオブジェクトはプログラムの中にひとつしかないので、同じものかどうかをisで確かめれば十分だからです。
isと==の違いが気になった方は、こちらの記事で詳しく解説しています。【関連記事】Pythonのisと== は何が違う? Pythonの比較演算子で知っておくべき罠
抽象基底クラスと組み合わせると判定の幅が広がる¶
isinstance()は、具体的な型だけでなく、こういう性質を持っているか、という判定にも使えます。そのときに活躍するのが、collections.abcモジュールに用意された抽象基底クラスです。
たとえばIterableは、forループで回せるものを表すクラスです。リストでも文字列でも辞書でも、Trueが返ってきます。
from collections.abc import Iterable
print(isinstance([1, 2], Iterable)) # True
print(isinstance("ab", Iterable)) # True
print(isinstance(3, Iterable)) # False
listかtupleかsetか、と型を全部並べなくても、ループで回せるかどうかをまとめて判定できます。型の種類ではなく、できることに注目した書き方ですね。
抽象基底クラスの仕組みや、自分で作る方法については、こちらの記事で紹介しています。【関連記事】Pythonの抽象クラス(ABC)とは?実装のし忘れを防ぐ設計の基本を初心者向けに解説
型チェックに頼りすぎないという考え方¶
最後に、少しだけ視点を変えた話をさせてください。Pythonでは、そもそも型チェックを書きすぎないほうがよい、という考え方があります。
Pythonの世界には、ダックタイピングという言葉があります。アヒルのように歩いて鳴くなら、それはアヒルだ、という考え方です。
つまり、型が何かを気にするより、必要なメソッドを持っているかどうかを大切にします。とりあえず処理を試してみて、失敗したら例外で受け止める、という書き方がよく使われるのはそのためです。
関数の冒頭にisinstance()をずらりと並べると、受け取れる値の幅が必要以上に狭くなってしまいます。外から入ってくるデータの入り口では型をしっかり確かめ、内側の処理では型を信じて書く、というメリハリを付けるのがおすすめです。
まとめ¶
type()は値の型そのものを返し、isinstance()はある型に当てはまるかをTrueかFalseで答えてくれる関数です。いちばんの違いは、継承した子クラスを親クラスの仲間として扱うかどうかでした。
条件分岐で型をチェックするなら、基本はisinstance()を使うと覚えておけば大丈夫です。複数の型はタプルで並べるか、Python 3.10以降なら縦棒でつなげて書けます。
そして、TrueやFalseがintとして判定されてしまう点だけは、ぜひ頭の片隅に置いておいてください。入力チェックでは、boolを先に弾いておくのが安全です。
まずは、手元のコードでtype(x) == とぴったり比べている箇所を探して、isinstance()で書き換えられないか見直してみてはいかがでしょうか。思わぬバグの芽を、ひとつ摘み取れるかもしれません。
ここまでお読みいただきありがとうございました。