Python の __new__ と __init__ ってなに?
__init__ の解説を省略して __new__ の解説に飛ぶ
1. __init__ ってなに?
__init__ 自体は、かなり難しくて、つまずきどころかなと思います。 なぜ難しいかというと「名前空間」「スコープ」といった難しい概念がしれっと入っていたり、裏でいろんなことを自動的にやっているからです。
伝わるかは厳しいのですが __init__ について、以下に説明させていただきます。 そのため理解する必要はなく、まず __init__ の動作を覚えてしまうことが大事かなと感じたりもします。
Step 1. オブジェクトってなに?
例えば猫 cat には名前があり、鳴くこと say ができます。
>>> cat.name 'たま' >>> >>> cat.type '猫科' >>> >>> cat.say() 'にゃー' >>>
Step 2. クラスってなに?
例えば猫 cat はみな '猫科' の動物で、みな鳴くこと say ができます。
もし Python で書けば次のようになります。
class Cat: type = '猫科' def say(self): print('にゃー')
Step 3. オブジェクトをインスタンス化する。
あるクラスからオブジェクトを作ることをインスタンス化と言います。
オブジェクト = クラス名()
# コピペで動きます。 class Cat: type = '猫科' def say(self): print('にゃー') # 猫クラスをインスタンス化 tama = Cat() tama.name = "たま" print(tama.name)
>>> print(tama.name) たま >>>
Step 4. インスタンス化する時に実行する処理を追加する
オブジェクトをインスタンス化する時に、ちょっと改造したいことがあります。 例えば猫クラス Cat をインスタンス化してから名前をつけるのは面倒なので、 インスタンス化する時に名前をつけたいとします。
実はインスタンス化する時に __init__ が、自動的に呼び出されるのでこれを使います。 ここで大事なのは普通の関数とは違い return 文を使わなくても自動的に self が返されるということです。
# コピペで動きます。 class Cat: type = '猫科' def say(self): print('にゃー') def __init__(self): # 1. 第一引数 self には名前のない猫オブジェクトが # 自動的に代入されています。 self.name = "たま" # 2. self は return しない # return self tama = Cat() print(tama.name) # たま
>>> tama = Cat() # <- Cat クラスをインスタンス化しています。 >>> print(tama.name) # たま たま >>>
吾輩わがはいは猫である。名前はまだ無い。
夏目漱石 - 吾輩は猫である
(注意)頭に self をつける。
頭に self をつけていない変数は、あとから参照できません。
class Cat: def __init__(self): name = "たま" tama = Cat() print(tama.name) # AttributeError
>>> print(tama.name) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Cat' object has no attribute 'name' >>>
(余談)インスタンス変数とローカル変数
self をつけた変数をインスタンス変数と言い、後から確認することができます。 self をつけなかった変数をローカル変数といい、後から確認することはできません。
class Cat: def __init__(self): # インスタンス変数 # そこで名前をつけてあげる。 # その時、必ず self をつけないといけません。 self.name = "たま" # ローカル変数 # self をつけない変数は参照できません。 age = 0
自分は Python を習いたての頃、self をつけ忘れて辛い思いをしました笑 「インスタンス変数」と「ローカル変数」というのは、「スコープ」、「名前空間」という、とても深い内容と密接に関わっていたりします。
Step 5. インスタンス化をさらにカスタマイズする。
全ての猫の名前が「たま」では不都合があるので、
名前を変えられるようにしましょう。
ここで大事なのは __init__(self, name)
メソッドと、
Cat("ドラえもん")
の引数の数が違うことです。
# コピペで動きます。 class Cat: type = '猫科' def say(self): print('にゃー') def __init__(self, name): self.name = name tama = Cat("たま") print(tama.name) # たま dora = Cat("ドラえもん") print(dora.name) # ドラえもん
◯「Python __init__」で、検索して来られた方へ
__init__ の説明は以上になります。ここから先は __new__ について、説明します。 ただ __new__ については、いますぐ知る必要はないかなと思います。 なぜなら __new__ はもっと難しい上に、あまり使う機会がないからです。
__init__ 自体結構難しいと思います。なんで難しいかというと、次のような概念を説明しないといけないからです。
- インスタンス変数とローカル変数の違い。
- 関数とは違い、第一引数 self に 自動的に インスタンスが代入される
- 関数とは違い、インスタンス化した時に 自動的に __init__ が呼び出される
- 関数とは違い、return していないのに 自動的に self が返される
でも上の説明ではそれを全て割愛しています...
自動的に やってくれるというのは、便利にはなるのですが、
説明すること以上に理解することが結構難しくなったりします。
明示的であることは、暗黙的であるより良い。(下手に自動化させるよりも面倒でもベタ書きしてもらった方がわかりやすいコードになるよ。的な意味だと個人的に思っています。)
Explicit is better than implicit.
PEP 20 - The Zen of Python
◯ self ってなに?
むしろ __new__ よりも self がなにかについて、押さえておいた方がいいかなと感じたりもします。self とは実際には特別なものではありません。そのことについては、こちらで記載させていただきました。
2. __new__ ってなに?
2.1. __new__ と __init__ の違い
3つの違いがあります。
インスタンスオブジェクトが 生成される前に呼ばれます。 |
インスタンスオブジェクトが 生成された後に呼ばれます。 |
|
オブジェクト self を インスタンス化します。 |
オブジェクト self を 初期化します。 |
|
第一引数 cls に クラスオブジェクトが 代入されます。 |
第一引数 self に インスタンスオブジェクトが 代入されます。 |
class Cls(object): def __new__(cls): self = super().__new__(cls) print('__new__ :', str(id(self))) return self def __init__(self): print('__init__:', str(id(self))) self.attr = 'Hello, world!' obj = Cls()
>>> obj = Cls() __new__ : 4525721080 __init__: 4525721080 >>>
2.2. __new__ は、例えば、いつ使うの?
答え:
- immutable を初期化したい(本稿で解説)
- singleton を実装したい(Python で良い感じのシングルトンを書く)
- 引数をもとにクラスを切り替えたいとき(いつか解説したい)
この記事では immutable なオブジェクトを初期化する際に __new__ の使い方について説明させていただきます。
immutable については以下の記事で説明させていただきました。
Python の immutable と mutable の違い
◯ 具体例
例えば namedtuple 関数を使って immutable なオブジェクトを生成した時、値を初期化するための処理が実行できません。
class Region(namedtuple('Region', ('x1', 'y1', 'x2', 'y2', 'is_rectangle', 'is_line', 'is_dot'))): def __init__(self, x1, y1, x2, y2): width_0 = (x1 - x2 == 0) height_0 = (y1 - y2 == 0) self.is_rectangle = (not width_0 and not height_0) # 0 0 self.is_line = (width_0 != height_0) # 0 1 or 1 0; xor self.is_dot = (width_0 and height_0) # 1 1 # TypeError Region(0, 0, 10, 10)
先に親クラスの __new__ が呼ばれている。オブジェクトを生成する前に全ての値を用意する必要がある。
>>> Region(0, 0, 10, 10) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: __new__() missing 3 required positional arguments: 'is_rectangle', 'is_line', and 'is_dot' >>>
仮に __init__ が呼び出せたとしても、self に代入されたオブジェクトは immutable なので属性に代入することはできません。
◯ 改善策
import collections class Region(collections.namedtuple('ImmutableRegion', ('x1', 'y1', 'x2', 'y2', 'is_rectangle', 'is_line', 'is_dot'))): def __new__(cls, x1, y1, x2, y2): # point 1. __init__ ではなく、__new__ を使う。 width_0 = (x1 - x2 == 0) height_0 = (y1 - y2 == 0) is_rectangle = (not width_0 and not height_0) # 0 0 is_line = (width_0 != height_0) # 0 1 or 1 0; xor is_dot = (width_0 and height_0) # 1 1 args = (x1, y1, x2, y2, is_rectangle, is_line, is_dot) # point 2. 必要なオブジェクトが揃ったところで # オブジェクトを生成する。 self = super().__new__(cls, *args) # point 3. 生成したオブジェクトを return return self Region(0, 0, 1, 1)
なお typing.NamedTuple を継承したクラスは __new__ を上書きできません。
>>> # はじかれました。 >>> python smaple.py ... AttributeError: Cannot overwrite NamedTuple attribute __new__ >>>
おわりに
mutable と immutable の違いについて説明していたページで、何気なく「値」という言葉を使っていました。では、この「値」という言葉は何者でしょうか?
従って immutable であること (immutability) は、厳密に言えば "値が変更できないこと" と同義ではなく、もう少し複雑です。
So, immutability is not strictly the same as having an unchangeable value, it is more subtle.
3.1. Objects, values and types - The Python Language Reference