はじめに
@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
クラス定義: クラスオブジェクト生成
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
なお、関数は呼び出さないと実行されませんが、クラス定義直下に書いた変数代入(クラスオブジェクトへの代入)や関数定義(関数オブジェクトの代入)はクラス定義時(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>})
コンストラクタ: インスタンス生成
クラスオブジェクトを作る際、「コンストラクタ」と呼ばれる処理を__call__
メソッドとして自動設定し、「クラス名()
」でコンストラクタを呼び出せるようになります。
コンストラクタを呼び出すと、「インスタンス」と呼ばれる「データ領域」を作り出します。
インスタンス: データ
インスタンスも名前空間(変数空間)を作り、自由に変数を定義して値などを代入できます。
初期状態のインスタンスは、変数がない空の状態です。
>>> data = Sample()
>>> vars(data)
{}
>>> data.name = "Taro"
>>> vars(data)
{'name': 'Taro'}
インスタンスに代入した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
インスタンスメソッド
クラス外で定義した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
を使ってください。
引数名は何でも構わないのですが、スタイルガイドに合わせて上記コードの 引数名 data
を self
に変更しましょう。
それで、一般的な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インタプリタの気持ちを考えてみませんか?