LoginSignup
43
45

More than 1 year has passed since last update.

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

Last updated at Posted at 2020-05-23

はじめに

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

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

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

Pythonインタプリタは、Pythonスクリプトを読み込んで「def定義」を見つけると「functionクラスのインスタンス」を生成し、処理を呼び出せる「関数オブジェクト」にして、関数名と同じ名前のグローバル変数代入(グローバル変数辞書に格納)します。
関数内の処理はバイトコードにコンパイルし、関数オブジェクトに代入します。バイトコードを逆アセンブルすることもできます。

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

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

Pythonインタプリタは、「class定義」を見つけると typeクラスのインスタンスを生成し、「クラスオブジェクト」にして、クラス名と同じ名前の変数に代入します。
クラスオブジェクトは名前空間変数空間)を作り、クラスオブジェクトの中に自由に 変数 を定義して 関数 を代入できます。
オブジェクトの中の変数は、vars関数で見ることができます。dir関数でオブジェクト内の名前一覧を見ることもできます。変数以外にもいろいろな情報を抱えていることがわかります。

>>> 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

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

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

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

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

>>> 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 に代入されていることが確認できます。

>>> 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

コンストラクタ: インスタンス生成

クラスオブジェクトを作る際、「コンストラクタ」と呼ばれる処理を__call__メソッドとして自動設定し、「クラス名()」でコンストラクタを呼び出せるようになります。
コンストラクタを呼び出すと、「インスタンス」と呼ばれる「データ領域」を作り出します。

インスタンス: データ

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

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

image.png

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

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

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

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

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

メソッド: methodオブジェクト

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

>>> 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

インスタンスメソッド

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

>>> 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

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

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

初期化メソッド: __init__メソッド

インスタンス生成後にinitializeメソッドを呼び出して名前設定していますが、コンストラクタの引数で初期値を指定できると便利ですよね。
それが、__init__メソッドです。
コンストラクタがインスタンス生成したあと、インスタンス自身を第一引数にして__init__メソッドを呼び出します。コンストラクタに渡された引数と一緒に。

>>> 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

インスタンス自身: self

PEP8: Pythonコードのスタイルガイド」に次のように書いてあります。

インスタンスメソッドのはじめの引数の名前は常に self を使ってください。
クラスメソッドのはじめの引数の名前は常に cls を使ってください。

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

クラスメソッド: @classmethod

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

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

>>> 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インタプリタの気持ちを考えてみませんか?

43
45
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
43
45