Pythonのクラス定義のselfはなぜ必要なのか、Pythonインタプリタの気持ちで考察・図解してみた

## はじめに

@free_jinji2020 さんの「[Pythonのselfはなぜ必要なのかを初学者なりに考察してみた ](https://qiita.com/free_jinji2020/items/93a45102995648ad06f1)」を拝読しました。
私は別のアプローチで、Pythonインタプリタの内部構造や内部動作を想像し、実際にPythonインタプリタで動作検証することで理解が深まったので、その方法を書き残すことにいたします。

なお、動作確認は Python3系で行っています。Python2系では違う表示結果になりますのでご注意ください。


## 関数定義： 関数オブジェクト生成

Pythonインタプリタは、Pythonスクリプトを読み込んで「`def`定義」を見つけると「`function`クラスのインスタンス」を生成し、処理を呼び出せる「**関数オブジェクト**」にして、関数名と同じ名前の**グローバル変数**に**代入**（グローバル変数辞書に格納）します。
関数内の処理は[バイトコード](https://docs.python.org/ja/3/library/dis.html#bytecodes)にコンパイルし、関数オブジェクトに代入します。バイトコードを[逆アセンブル](https://docs.python.org/ja/3.5/library/dis.html)することもできます。

- 関数定義はオブジェクトに変換されて関数名と同じ名前の変数に代入される
- 変数辞書は `vars()` 関数で確認できる
- 関数名（変数名）だけ書くと関数オブジェクトを確認できる
- 関数名（変数名）に括弧を付けて引数リストを指定すると関数を呼び出す

```py
>>> def hello():
...     print("Hello, world!")
...
>>> vars()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class'_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
 'hello': <function hello at 0x6fffffdcda60>}
>>> hello
<function hello at 0x6fffffd0dae8>
>>> type(hello)
<class'function'>
>>> hello()
Hello, world!
>>> hello.__name__
'hello'
>>> type(hello.__code__)
<class'code'>
>>> hello.__code__.co_code.hex()
'740064018301010064005300'
>>> import dis
>>> dis.dis(hello)
  2           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 ('Hello, world!')
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29070/302f971b-252f-226d-de4f-d87b1aaa9a58.png)

## クラス定義： クラスオブジェクト生成

Pythonインタプリタは、「`class`定義」を見つけると `type`クラスのインスタンスを生成し、「**クラスオブジェクト**」にして、クラス名と同じ名前の変数に代入します。
クラスオブジェクトは**名前空間**（**変数空間**）を作り、クラスオブジェクトの中に自由に **変数** を定義して **値** や **関数** を代入できます。
オブジェクトの中の変数は、[`vars`関数](https://docs.python.org/ja/3/library/functions.html#vars)で見ることができます。[`dir`関数](https://docs.python.org/ja/3/library/functions.html#dir)でオブジェクト内の名前一覧を見ることもできます。変数以外にも[いろいろな情報](https://docs.python.org/ja/3.5/library/stdtypes.html#special-attributes)を抱えていることがわかります。

```py
>>> class Sample:
...     pass
...
>>> vars()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class'_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
 'Sample': <class'__main__.Sample'>}
>>> type(Sample)
<class'type'>
>>> Sample.value = 123
>>> Sample.value * 3
369
>>> Sample.data = [1, 2, 3]
>>> vars(Sample)
mappingproxy({'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Sample' objects>, '__weakref__': <attribute '__weakref__' of 'Sample' objects>, '__doc__': None,
 'value': 123,
 'data': [1, 2, 3]})
>>> dir(Sample)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> Sample.__name__
'Sample'
>>> Sample.data.__len__()
3
>>> Sample.data[0]
1
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29070/10f2f2b6-ad64-eea1-084b-fd5a19bf7a86.png)

なお、関数は呼び出さないと実行されませんが、クラス定義直下に書いた変数代入（クラスオブジェクトへの代入）や関数定義（関数オブジェクトの代入）はクラス定義時（Pythonインタプリタがクラス定義を読み込んでクラスオブジェクトを生成するとき）に実行されます。

```py
>>> class Sample:
...     name = "Taro"
...     print("My name is", name)
...
My name is Taro
>>> Sample.name
'Taro'
```

## クラス内関数定義： 関数オブジェクトをクラスオブジェクトに代入

`class`定義の中に `def`定義（関数定義）があると、クラスオブジェクトの中に「関数オブジェクト」を代入します。
`クラス名.関数名()` で呼び出せます。

```py
>>> class Sample:
...     def greet():
...         print("Hey, guys!")
...
>>> Sample
<class'__main__.Sample'>
>>> type(Sample)
<class'type'>
>>> Sample.greet
<function Sample.greet at 0x6fffffd0dd08>
>>> type(Sample.greet)
<class'function'>
>>> Sample.greet()
Hey, guys!
```

`greet`関数オブジェクト (`function greet`) が 変数`greet` に代入されていることが確認できます。

```py
>>> vars(Sample)
mappingproxy({'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Sample' objects>, '__weakref__': <attribute '__weakref__' of 'Sample' objects>, '__doc__': None, 
'greet': <function Sample.greet at 0x6fffffd0dd08>})
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29070/0ff9f16b-7ba8-727c-6029-01f686965dbc.png)

## コンストラクタ： インスタンス生成

クラスオブジェクトを作る際、「コンストラクタ」と呼ばれる処理を[`__call__`メソッド](https://docs.python.org/ja/3.6/reference/datamodel.html#object.__call__)として自動設定し、「`クラス名()`」でコンストラクタを呼び出せるようになります。
コンストラクタを呼び出すと、「インスタンス」と呼ばれる「データ領域」を作り出します。

## インスタンス： データ

インスタンスも名前空間（変数空間）を作り、自由に変数を定義して値などを代入できます。
初期状態のインスタンスは、変数がない空の状態です。

```py
>>> data = Sample()
>>> vars(data)
{}
>>> data.name = "Taro"
>>> vars(data)
{'name': 'Taro'}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29070/097efc85-3654-f0d7-418f-a349845cbac1.png)

インスタンスに代入した`name`を表示する「関数」を定義して、`name`を表示してみます。
どのインスタンスの `name` かを指示する引数が必要です。

```py
>>> def introduce(data):
...     print("My name is", data.name)
...
>>> introduce(data)
My name is Taro
```

## クラスオブジェクトに関数を代入

上述のクラス外で定義した `introduce`関数 を クラスオブジェクト に代入して呼び出すこともできます。

```py
>>> introduce
<function introduce at 0x6fffffd0dc80>
>>> Sample.introduce = introduce
>>> Sample.introduce
<function introduce at 0x6fffffd0dc80>
>>> Sample.introduce(data)
My name is Taro
```

## メソッド： methodオブジェクト

クラス生成時に「コンストラクタ」を自動設定しましたが、インスタンス生成時には「メソッド」を自動設定します。
「メソッド」はmethodクラスのインスタンスです。
メソッドには「インスタンスメソッド」、「クラスメソッド」、「スタティックメソッド」の3種類あります。
インスタンスのメソッドを呼び出すと、「インスタンスメソッド」は第一引数にインスタンスを自動挿入し、「クラスメソッド」は第一引数をクラスオブジェクトを自動挿入し、「スタティックメソッド」は引数は変更せずにクラスオブジェクト内の関数を呼び出します。

```py
>>> Sample.introduce
<function introduce at 0x6fffffd0dc80>
>>> type(Sample.introduce)
<class'function'>
>>> data.introduce
<bound method introduce of <__main__.Sample object at 0x6fffffd16518>>
>>> type(data.introduce)
<class'method'>
>>> data.introduce()
My name is Taro
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29070/ec86ac1d-b94e-151f-81f0-65ac6a029dca.png)

## インスタンスメソッド

クラス外で定義した`introduce`関数 を クラスオブジェクト に代入しましたが、クラス内に関数定義を書いた方がまとまっていて分かりやすいですよね。

```py
>>> class Person:
...     def initialize(data, name):
...         data.name = name
...     def introduce(data):
...         print("My name is", data.name)
...
>>> taro = Person()
>>> vars(taro)
{}
>>> Person.initialize(taro, "Taro")
>>> vars(taro)
{'name': 'Taro'}
>>> Person.introduce(taro)
My name is Taro
```

インスタンスメソッド経由でクラス内関数を呼び出すこともできます。
先ほど書いたように、メソッドがインスタンス自身を第一引数にしてクラスオブジェクト内の関数を呼び出します。

```py
>>> hanako = Person()
>>> hanako.inittialize("Hanako")
>>> hanako.introduce()
My name is Hanako
```

## 初期化メソッド： `__init__`メソッド

インスタンス生成後に`initialize`メソッドを呼び出して名前設定していますが、コンストラクタの引数で初期値を指定できると便利ですよね。
それが、[`__init__`メソッド](https://docs.python.org/ja/3.6/reference/datamodel.html#object.__init__)です。
コンストラクタがインスタンス生成したあと、インスタンス自身を第一引数にして`__init__`メソッドを呼び出します。コンストラクタに渡された引数と一緒に。

```py
>>> class Person:
...     def __init__(data, name):
...         self.name = name
...     def introduce(data):
...         print("My name is", self.name)
...
>>> ichiro = Person("Ichiro")
>>> ichiro.introduce()
My name is Ichiro
```

## インスタンス自身： `self`

「[PEP8: Pythonコードのスタイルガイド](https://pep8-ja.readthedocs.io/ja/latest/)」に次のように書いてあります。
> インスタンスメソッドのはじめの引数の名前は常に `self` を使ってください。
> クラスメソッドのはじめの引数の名前は常に `cls` を使ってください。

引数名は何でも構わないのですが、スタイルガイドに合わせて上記コードの 引数名 `data` を `self` に変更しましょう。
それで、一般的なPythonコードの書き方になります。

```py
>>> class Person:
...     def __init__(self, name):
...         self.name = name
...     def introduce(self):
...         print("My name is", self.name)
...
>>> ichiro = Person("Ichiro")
>>> ichiro.introduce()
My name is Ichiro
```

## クラスメソッド: `@classmethod`

クラス内関数定義時に `@classmethod`デコレータを付けると、第一引数をクラスオブジェクトにして呼び出すメソッドに置き換わります。
インスタンスに左右されない、クラス毎の処理を定義できます。

```py
>>> class Sample:
...     @classmethod
...     def class_method(cls):
...         print("called:", cls)
...
>>> Sample.class_method
<bound method Sample.class_method of <class'__main__.Sample'>>
>>> Sample.class_method()
called: <class'__main__.Sample'>
>>> s = Sample()
>>> s.class_method
<bound method Sample.class_method of <class'__main__.Sample'>>
>>> s.class_method()
called: <class'__main__.Sample'>
```

## スタティックメソッド： `@staticmethod`

クラス内関数定義時に `@staticmethod`デコレータを付けると、第一引数を追加せずに関数を直接呼び出します。
普通の関数定義と同じですが、クラス名前空間に入れることで、同じ名前の関数をあちこちのクラスに定義できるので、名前衝突を気にしなくて済みます。

```py
>>> class Sample:
...     @staticmethod
...     def static_method():
...         print("Hello, world!")
...
>>> Sample.static_method
<function Sample.static_method at 0x6fffffd1e268>
>>> Sample.static_method()
Hello, world!
>>> s = Sample()
>>> s.static_method
<function Sample.static_method at 0x6fffffd1e268>
>>> s.static_method()
Hello, world!
```

## さいごに

Pythonでの処理はすべて関数オブジェクトが担っていて、どのデータ＝インスタンス＝オブジェクトに対して処理するかを指定する引数が必要で、インスタンスが渡る第一引数名は`self`で統一しようという運用ルールがPEP8に明記されていることがわかりました。

Pythonインタプリタのソースコードを深く追ったわけではないので、正確ではない部分もあると思います。指摘などコメントを頂けたら有難いです。
私は、Pythonインタプリタの気持ちを考えることで理解できることが沢山ありました。
みなさんも、Pythonインタプリタの気持ちを考えてみませんか？