循環する型
いろいろな事情があり(どんな事情だ?)、Pythonのオブジェクトシステムについて調べていました。そうすると、結構知らないなぁ。ということがあったので、まとめます。
割と自明な話ですが、pythonでは値の型を調べるのにtype
関数を使います。
>>> a=1
>>> type(a)
<class 'int'>
ここで1
の型がint型
だと分かりました。では、type(1)
の型は何でしょうか?
>>> ta = type(a) #type(a)の結果を代入
>>> type(ta)
<class 'type'>
ここで、int型
の型はtype型
であることが分かりました。ここで1つ謎ポイントだと思います。int型
にも型が存在し、その型はtype型
であるということです。では、type型
の型は何でしょうか?
>>> tta = type(ta) #type(ta)の結果を代入
>>> type(tta)
<class 'type'>
ここで、type型
の型はtype型
であることが分かりました。多分、ゲシュタルト崩壊してくると思うので、図示してみます。
1
に対し、typeを実行することで、型はint型
だとわかります。
int型
に対し、typeを実行することで、型はtype型
だとわかります。
type型
に対し、typeを実行することで、型はtype型
だとわかります。
(ここでなぜint
をint型
と表記していたのかが分かると思いますが、type型
をtype
と表記してしまうと、type
関数と混ざって脳みそが崩壊するので、あえて型には型と表記しています。)
objectとは
では、今度は、基底クラスについて考えてみます。ここでは簡単な例を考えてみます。
>>> class A:
... def __init__(self):
... print("A")
...
>>> class B(A):
... def __init__(self):
... super().__init__()
... print("B")
...
>>> B()
A
B
クラスAを定義し、それを継承したクラスとしてBを定義します。当たり前のことを言いますが、Aを継承したクラスBを定義しようとすると、Aを先に定義する必要があります。
クラスの基底クラス(継承元)を取得するには__base__
を見ればわかります。
>>> B.__base__
<class '__main__.A'>
では、1
の基底クラスは何でしょうか?
>>> a=1
>>> a.__base__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute '__base__'. Did you mean: '__hash__'?
1はクラスではないため、基底クラスは取得できません。では、int型
の基底クラスは?
>>> ta=type(a)
>>> ta.__base__
<class 'object'>
int型
の基底クラスはobject型
だそうです。では、type型
の基底クラスは?
>>> tta=type(ta)
>>> tta.__base__
<class 'object'>
type型
の基底クラスはobject型
だそうです。では、object型
の基底クラスは?
>>> tab = ta.__base__
>>> tab
<class 'object'>
>>> tab.__base__
>>> tab.__base__ is None
True
こから、object型
の基底クラスは存在しないことが分かりました。
ここまでの事実を図示するとこうなります。
int型
の基底クラスはobject型
type型
の基底クラスはobject型
object型
の基底クラスは存在しない。
object型の型?
では、最後にobject型
の型について調べてみます。
>>> tab = ta.__base__
>>> type(tab)
<class 'type'>
object型
の型はtype型
です。これらを図示すると以下のようになります。
objectと型の相互循環
たぶん混乱がすごいと思うので、混乱の元凶部分だけ書き出します。
事実を整理します。
事実1. インスタンスを生成するためにはクラスの定義が必要である。
事実2. 継承したクラスを定義するためには、基底クラスを事前に定義する必要がある。
例えば、1というint型のインスタンスを生成するためには、int型の定義が必要です(事実1)
int型はobject型を継承したクラスなので、基底クラスであるobject型の定義が必要です(事実2)
ここから、2つのパターンを見てみます。
パターン1. object型を先に定義する
object型を定義しようとしたとき、object型は基底クラスがないため、そのまま定義できそうです。(事実2はクリア)
しかし、object型自身はtype型のインスタンスとなっています。object型をtypeしたものがtype型であり、isinstanceでも、object型がtype型のインスタンスであることが確認できます。
>>> tta
<class 'type'>
>>> tab
<class 'object'>
>>> isinstance(tab,tta)
True
ということは、object型の生成には、type型が必要になります。(事実1)
では、type型を定義することを試みます。type型を定義しようとすると、基底クラスであるobjectが必要なります。しかし、今はobject型を定義しようとして、議論していたため、現在はobject型が未定義であるため、typeが定義できません。
パターン2. type型を先に定義する
type型を定義しようとすると、基底クラスであるobject型を定義する必要があります。object型はtype型のインスタンスです。そうすると、object型の定義にtype型が必要になります。ここで、循環が発生してしまい、定義できません。
ChatGPTさんに聞いてみる
どうも解せないので、ChatGPTさんに聞いてみます。
型とobject?
実は、ChatGPTさんによくよく聞いてみるとpythonの型がキモイということが分かりました。では、typescriptの型取得はどうなっているでしょうか?
$ npx ts-node
> const a=1
undefined
> typeof a
'number'
> typeof (typeof a)
'string'
typescriptでは型の取得はtypeof
を使いますが、このtypeof
で返ってくる値は"string"型になっています。したがって、上記のような型とオブジェクトの循環定義問題のようなものは起きないようです。
では、Javaの場合は?途中の過程は端折りますが、循環定義問題は起きてます。
型を表すjava.lang.Classは自分自身の型をjava.lang.Classとしており、その基底クラスをjava.lang.Objectとしています。java.lang.Objectの基底クラスはnullですが、型はjava.lang.Classとなっています。これはpythonのtype型とobject型の構図とほぼ同じです。
すべてはオブジェクトである。
オブジェクト指向のプログラミング言語でいいがちな話ですが、「すべてがオブジェクトである。」ということです。Pythonにはこういう記述があります。
Python における オブジェクト (object) とは、データを抽象的に表したものです。Python プログラムにおけるデータは全て、オブジェクトまたはオブジェクト間の関係として表されます。(ある意味では、プログラムコードもまたオブジェクトとして表されます。これはフォン・ノイマン: Von Neumann の "プログラム記憶方式コンピュータ: stored program computer" のモデルに適合します。)
こう聞くと割と納得ではありますが、型自身すらobjectとなっていると割と面食らうものがあります。ChatGPTのいうことだけ聞いていると、pythonの型ってのは特殊な実装なんだなーぐらいで済んだのですが、どうやら割と純なオブジェクト指向を標榜する言語はどれでも起こってそうです。検証してないですがC#も恐らくそう。
もともとオブジェクト指向を調べており、型を生成する型とかを考えると、そもそもクラスとインスタンスの境界があいまいになってきて、自分自身が迷宮に入ってきたのでまとめました。実際、クラスとインスタンスの境界が私にはわからなくなりました。
私自身わかっていないですが、このようなオブジェクト指向の型システムを作ることを考えたとき、objectを定義するとき、その型は未定義にする。type型を定義する。objectの型をtype型と上書き定義する。みたいな、ちょっとした手続きによる工夫とそれができる実装が必要なんだろうなぁ。という感触があります。そこの確信をするには多分言語の実装まで見ないといけないので辛いですが・・・その辺もまたどっかで調べたいです。