Python の __new__ と __init__ ってなに?


__init__ の解説を省略して __new__ の解説に飛ぶ



1. __init__ ってなに?

__init__ 自体は、かなり難しくて、つまずききどころかなと思います。 なぜ難いいかというと「名前空間」「スコープ」と言った難しい概念がしれっと入っていたり、裏でいろんなことを自動的にやっているからです。

伝わるかは厳しいのですが __init__ について、以下に説明させていただきます。 そのため理解する必要はなく、まず __init__ の動作を覚えてしまうことが大事かなと感じたりもします。

Step 1. クラスを作る。

# 猫クラスを作る
class Cat:
    pass  # pass 文は何もしない。
(余談)pass 文ってなに?

pass 文は何もしません。

>>> # pass 文は何も実行しません。
>>> pass
>>> 


pass 文を書いた理由は、 pass 文をいれないとエラーになるからです。

>>> class Cat:
...   
... 
  File "<stdin>", line 3
    
    ^
IndentationError: expected an indented block
>>> 

Step 2. オブジェクトをインスタンス化する。

あるクラスからオブジェクトを作ることをインスタンス化と言います。

オブジェクト = クラス名()
# 猫クラスを作る
class Cat:
    pass

# 猫クラスをインスタンス化
tama = Cat()
tama.name = "たま"
print(tama.name)
>>> print(tama.name)
たま
>>> 

Step 3. インスタンス化を改造する。

オブジェクトをインスタンス化する時に、ちょっと改造したいことがあります。 例えば猫クラス Cat をインスタンス化してから名前をつけるのは面倒なので、 インスタンス化する時に名前をつけたいとします。

実はインスタンス化する時に __init__ が、自動的に呼び出されるのでこれを使います。 ここで大事なのは普通の関数とは違い return 文を使わなくても自動的に self が返されるということです。

class Cat:
    def __init__(self):
        # 1. 第一引数 self には名前のない猫オブジェクトが
        #    自動的に代入されています。
        self.name = "たま"
        
        # 2. self は return しない        
        # return self

tama = Cat()
print(tama.name)  # たま
>>> tama = Cat()  # <- Cat クラスをインスタンス化しています。
>>> print(tama.name)  # たま
たま
>>> 

Step 4. 頭に 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:
    def __init__(self, name):
        self.name = name

tama = Cat("たま")
print(tama.name)  # たま

dora = Cat("ドラえもん")
print(dora.name)  # ドラえもん






◯「Python __init__」で、検索して来られた方へ

__init__ の説明は以上になります。ここから先は __new__ について、説明します。 ただ __new__ については、いますぐ知る必要はないかなと思います。 なぜなら __new__ はもっと難しい上に、あまり使う機会がないからです。

__init__ 自体結構難しいと思います。なんで難しいかというと、次のような概念を説明しないといけないからです。でも上の説明ではそれを全て割愛しています... 自動的に やってくれるというのは、便利にはなるのですが、理解するのは結構難しくなったりします。


  1. インスタンス変数とローカル変数の違い。
  2. 関数とは違い、第一引数 self に 自動的に インスタンスが代入される
  3. 関数とは違い、インスタンス化した時に 自動的に __init__ が呼び出される
  4. 関数とは違い、return していないのに 自動的に self が返される


明示的であることは、暗黙的であるより良い。(下手に自動化させるよりも面倒でもベタ書きしてもらった方がわかりやすいコードになるよ。的な意味だと個人的に思っています。)
Explicit is better than implicit.
PEP 20 - The Zen of Python

◯ self ってなに?

むしろ __new__ よりも self がなにかについて、押さえておいた方がいいかなと感じたりもします。self とは実際には特別なものではありません。そのことについては、こちらで記載させていただきました。
Python の関数とメソッドの違いってなに?











2. __new__ ってなに?

2.1. __new__ と __init__ の違い




3つの違いがあります。

__new__
__init__
1
インスタンスオブジェクトが
生成されるに呼ばれます。
インスタンスオブジェクトが
生成されたに呼ばれます。
2
オブジェクト self を
インスタンスします。
オブジェクト self を
初期化します。
3
第一引数 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__ は、例えば、いつ使うの?

答え:

  1. immutable を初期化したい(本稿で解説)
  2. singleton を実装したい(Python で良い感じのシングルトンを書く)
  3. 引数をもとにクラスを切り替えたいとき(いつか解説したい)


この記事では 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


f:id:domodomodomo:20180514165854j:plain