Otsuhachi’s diary

プログラミングのことや覚書などその他いろいろ

当ブログの内容を使用したり参考にした場合に生じた問題の責任は負いかねます。

Python組み込み関数7 -isinstance・issubclass編-

Pythonおさらい第7段
制約として公式ドキュメントのみ参照*1として、読む癖をつける訓練も兼ねる。

今回はisinstance関数issubclass関数の2種類について。
前回に引き続き、特定の条件を満たすかどうか判定し、boolを返す関数。

重要な関数ではあるものの、TrueFalseが返るだけなので実に地味!
とはいえ、よく使うのは間違いないのでしっかり押さえておきたい。

読み込むドキュメントはこちら[isinstance, issubclass]

構成

今回の関数は以下のようになっている。

isinstance(object, classinfo)

issubclass(class, classinfo)

classinfoとはなんぞや?
ドキュメントを読んでみると、型オブジェクト, 型オブジェクトのタプルあるいは再帰的に複数のタプルだそうだが……再帰的に複数のタプルとはなんぞや?
謎は深まるばかりであるが使ってみた段階でわかることに期待したい。

関数 内容
isinstance objectclassinfoインスタンスかどうか
issubclass classclassinfoサブクラスかどうか
クラスはそれ自身のサブクラスとみなされる

使ってみる

ひとまず、普通に単体のクラスで試してみて、classinfoについては専用の項目で見ていくこととする。
また、クラス定義などの共通部分については前提コードを使いまわす。

使ってみる-前提コード

from abc import ABC, abstractmethod


class Creature(ABC):
    def __init__(self):
        self.alive = True

    @abstractmethod
    def desperate(self):
        pass

    def death(self):
        self.desperate()
        self.alive = False


class Runner(ABC):
    def run(self):
        print(f'{self}は走った!')


class Person(Creature):
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age
        super().__init__()

    def __repr__(self) -> str:
        me = f'{self.name}({self.age})'
        return me


class Man(Person):
    def desperate(self):
        print(f'{self}: グワーッ!')


class Woman(Person):
    def desperate(self):
        print(f'{self}: キャーッ!')


class RunningMan(Runner, Man):
    pass


man = Man('Otsuhachi', 28)
rman = RunningMan('Taro', 55)
woman = Woman('Hanako', 20)
check_classes = [ABC, Creature, Person, Man, Woman, Runner, RunningMan]
member = [man, rman, woman]

どうしようもないクソコードが爆誕してしまった……。

クラスの繋がりは以下の通り。

ABC
  ├ Creature
  │      └ Person
  │          ├ Woman
  │          └ Man
  │             ├─ RunningMan
  └───────── Runner

使ってみる-isinstance

前提コードで定義したmembercheck_classesそれぞれの組み合わせでisinstanceを行い、その結果を出力する。

実行するコードは以下の通り。

# ここより上の行に前提コードがある
for m in member:
    print(f'{m}<{type(m).__name__}>')
    print('-' * 30)
    for cc in check_classes:
        print(f'{cc.__name__}: {isinstance(m,cc)}')
    print()
Otsuhachi(28)<Man>
------------------------------
ABC: True
Creature: True
Person: True
Man: True
Woman: False
Runner: False
RunningMan: False

Taro(55)<RunningMan>
------------------------------
ABC: True
Creature: True
Person: True
Man: True
Woman: False
Runner: True
RunningMan: True

Hanako(20)<Woman>
------------------------------
ABC: True
Creature: True
Person: True
Man: False
Woman: True
Runner: False
RunningMan: False

子クラスのインスタンスが親クラスをclassinfoとしてisinstanceしたときにはTrueが返っているのがわかる。
例えばmanABC->Creature->Person->Manという繋がりを持つ、Manクラスのインスタンスなので、classinfoABC, Creature, Person, ManのいずれでもTrueとなっている。
逆に同じ親クラスを持つWomanは直接継承関係にないのでFalseになるし、
Manクラスの子クラスであるRunningManFalseとなる。

objectのクラスが、どこかの段階でclassinfoクラスを継承しているかを調べる関数だということが理解できた。

使ってみる-issubclass

check_classes[i]check_classes[j]のサブクラスであるかを調べて出力する。
0 <= i,j < len(check_classes)

実行するコードは以下の通り。

# ここより上の行に前提コードがある
for subcls in check_classes:
    print(subcls.__name__)
    print('-' * 30)
    for cls in check_classes:
        print(f'{cls.__name__}: {issubclass(subcls,cls)}')
    print()
ABC
------------------------------
ABC: True
Creature: False
Person: False
Man: False
Woman: False
Runner: False
RunningMan: False

Creature
------------------------------
ABC: True
Creature: True
Person: False
Man: False
Woman: False
Runner: False
RunningMan: False

Person
------------------------------
ABC: True
Creature: True
Person: True
Man: False
Woman: False
Runner: False
RunningMan: False

Man
------------------------------
ABC: True
Creature: True
Person: True
Man: True
Woman: False
Runner: False
RunningMan: False

Woman
------------------------------
ABC: True
Creature: True
Person: True
Man: False
Woman: True
Runner: False
RunningMan: False

Runner
------------------------------
ABC: True
Creature: False
Person: False
Man: False
Woman: False
Runner: True
RunningMan: False

RunningMan
------------------------------
ABC: True
Creature: True
Person: True
Man: True
Woman: False
Runner: True
RunningMan: True

自身と、自身のクラスが継承しているクラスがclassinfoの場合Trueが返るのがわかる。

isinstanceと違うのは第一引数がclassな点くらいだろうか……。
メタプログラミングとかの際には必要になるのかもしれないが、今のところisinstanceほど魅力を感じない。

使ってみる-classinfo

2つの関数共通の第二引数であるclassinfoについて。
受け取る形式は以下の3通り。

  1. 型オブジェクト
  2. 型オブジェクトのタプル
  3. 再帰的に複数の型オブジェクトのタプル

1., 2.についてはわかりやすいのでさらっと以下のコードで確かめる。

# ここより上の行に前提コードがある
classinfo = [str, (str, Runner)]
for m in member:
    print(f'{m}<{type(m).__name__}>')
    print('-' * 30)
    for ci in classinfo:
        if type(ci) is tuple:
            c_name = tuple(map(lambda x: x.__name__, ci))
        else:
            c_name = ci.__name__
        print(f'{c_name}: {isinstance(m,ci)}')
    print()
Otsuhachi(28)<Man>
------------------------------
str: False
('str', 'Runner'): False

Taro(55)<RunningMan>
------------------------------
str: False
('str', 'Runner'): True

Hanako(20)<Woman>
------------------------------
str: False
('str', 'Runner'): False

型オブジェクトのタプルを渡した場合、いずれかのクラスがTrueを返せばTrueを返しているのがわかる。
以下のようなイメージすればわかりやすいだろう。

def tupled_isinstance(object, classinfo: tuple) -> bool:
    for ci in classinfo:
        if isinstance(object, ci):
            return True
    return False

問題なのは3.だが、再帰的に複数のタプルとはなんなのか?
確信はないが、入れ子構造のタプルであると思われる。
以下のコードで確認してみる。

# ここより上の行に前提コードがある
def convert_classnames(ct: tuple):
    res = []
    for c in ct:
        if type(c) is tuple:
            res.append(convert_classnames(c))
        else:
            res.append(c.__name__)
    return tuple(res)


classinfo = [(str, Man), (str, (Woman, (Runner, RunningMan)))]

for m in member:
    print(f'{m}<{type(m).__name__}>')
    print('-' * 60)
    for ci in classinfo:
        c_name = convert_classnames(ci)
        print(f'{c_name}: {isinstance(m,ci)}')
    print()
Otsuhachi(28)<Man>
------------------------------------------------------------
('str', 'Man'): True
('str', ('Woman', ('Runner', 'RunningMan'))): False

Taro(55)<RunningMan>
------------------------------------------------------------
('str', 'Man'): True
('str', ('Woman', ('Runner', 'RunningMan'))): True

Hanako(20)<Woman>
------------------------------------------------------------
('str', 'Man'): False
('str', ('Woman', ('Runner', 'RunningMan'))): True

これが正しい再帰的に複数のタプルであるかはわからないが、入れ子構造のタプルを渡した場合も問題なく判定していることは分かった。

締め

今回の2つの関数についてはクラスの継承がどんなものか、インスタンスとクラスオブジェクトの違いの2点がなんとなくでもわかっていれば、問題なく使うことができるだろう。

時々やらかすのが、classinfoインスタンスを指定してしまうことぐらいだろうか。
タプルの再帰に気を遣うぐらいならclassinfoインスタンスの時にtype(instance)してくれる方がよほどありがたく感じる。

タプルはともかく、再帰的なタプルは本当にわからん。

*1:わかりやすく解説している他サイトを見ない