Python の __new__ と __init__ の違い

1. 違い

3つの違いがあります。

__new__
__init__
1
インスタンスオブジェクトが
生成される前に呼ばれます。
インスタンスオブジェクトが
生成された後に呼ばれます。
2
インスタンスオブジェクトを
生成 new して返します。
self の属性に値を
設定し初期化 init します。
3
第一引数に
クラスオブジェクトが
代入されます。
第一引数に
インスタンスオブジェクトが
代入されます。


class A(object):
    pass


class B(A):
    # 最初に呼ばれる
    def __new__(cls):
        # super class の __new__ を読んでオブジェクトを生成
        self = super(A, cls).__new__(cls)
        return self

    # 2番目に呼ばれる
    def __init__(self):
        pass

2. __new__ は、例えば、いつ使うの?

答え:

  1. immutable なオブジェクトを初期化したい時とか(本稿で解説)
  2. シングルトンを実装したい時とか(__new__とか__init__とかを再整理

◯ immutable って何?

◯ 具体例

例えば 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


先に親クラスの __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 なので代入することはできません。

◯ 改善策

class Region(namedtuple('Region', 
      ['x1', 'y1', 'x2', 'y2',
       'is_rectangle', 'is_line', 'is_dot'])):
    # point 1. __init__ ではなく、__new__ を使う。
    def __new__(cls, x1, y1, x2, y2):
        # オブジェクトが生成される前に呼び出される.
        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

        # point 2. 必要なオブジェクトが揃ったところで
        # オブジェクトを生成する。
        super_cls = cls.__bases__[0]
        argugments = (x1, y1, x2, y2, is_rectangle, is_line, is_dot)
        self = super(super_cls, cls).__new__(cls, argugments)

        """引数が違う
        # super を使った場合
        # namedtuple で生成したクラスではなく tuple クラスの __new__ が呼ばれている様子
        # Calling __new__ when making a subclass of tuple [duplicate]
        # http://bit.ly/2rqeTm3
        self = super(super_cls, cls).__new__(cls, argugments)

        # 直接呼び出した場合
        # namedtuple で生成したクラスの __new__ を読んでいる。
        self = super_cls.__new__(cls + arguments)
        """

        # point 3. 生成したオブジェクトを return
        return self

3. immutable デコレータ


# クラス定義文で immutable であることを明示
@immutable('x1', 'y1', 'x2', 'y2', 'is_rectangle', 'is_line', 'is_dot')
class Region(object):
    # __init__ の代わりに __new__ を使う。
    def __new__(cls, x1, y1, x2, y2):
        """オブジェクトが生成される前に呼び出される."""
        ...

神様サイト


上記のサイトの劣化版です。上記のサイトのコードを参考にして @immutable と言う劣化版デコレータを作りました。

実は self が immutable のため
__init__ メソッドの中で属性に代入できません。

@immutable('a', 'b', 'c')
class C:
  def __init__(self, a, b):
    # できない。
    self.c = a + b


そのため、神様のサイトでは Builder パターンを実装しています。

builder = C(a=1, b=2)
builder.c(1 + 2)
obj = builder.build()


下記の劣化版では、劣化版では builder パターンを実装していません。
__new__ を使って、__init__ の代替としています。

@immutable('a', 'b', 'c')
class C:
  def __new__(cls, a, b):
    c = a + b
    # call super class's __new__
    super_cls = cls.__bases__[0]
    self = super(super_cls, cls).__new__(cls, (a, b, c))
    return self

◯ 劣化版コード

矩形を表現する Region クラスを immutable にしました。

from collections import namedtuple


def sample_code():
    # @immutable('x1', 'y1', 'x2', 'y2', 'is_line', 'is_dot')
    # class Region(object):
    #     ...

    region1 = Region(0, 0, 1, 1)
    print(region1)

    # 代入はエラーになる。
    try:
        region1.x1 = 100
    except AttributeError as attribute_error:
        print(AttributeError, attribute_error)

    # def __init__ の代替もできてる。
    #   self.is_rectangle = value とはできない。
    # self は immutable だから
    print(region1.is_rectangle)
    print(region1.is_line)
    print(region1.is_dot)

    region2 = Region(0, 0, 0, 0)
    print(region2)
    print(region2.is_rectangle)
    print(region2.is_line)
    print(region2.is_dot)

    # メソッドもちゃんと動作する。
    print('region1 == region2: ', region1 == region2)


def immutable(*attributes):
    def decorate(Class):
        ClassImmutableAttributes = namedtuple(Class.__name__, attributes)
        bases = tuple(filter(lambda cls: cls is not object,
                             Class.__bases__ + (ClassImmutableAttributes, )))
        ImmutableClass = type(Class.__name__, bases, dict(Class.__dict__))
        return ImmutableClass
    return decorate


# point 1. 属性はデコレータに文字列で与える。
@immutable('x1', 'y1', 'x2', 'y2', 'is_rectangle', 'is_line', 'is_dot')
class Region(object):
    # point 2. __init__ ではなく、__new__ を使う。
    def __new__(cls, x1, y1, x2, y2):
        # オブジェクトが生成される前に呼び出される.
        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

        # point 3. 必要なオブジェクトが揃ったところで
        # オブジェクトを生成する。
        super_cls = cls.__bases__[0]
        argugments = (x1, y1, x2, y2, is_rectangle, is_line, is_dot)
        self = super(super_cls, cls).__new__(cls, argugments)

        """引数が違う
        # super を使った場合
        # namedtuple で生成したクラスではなく tuple クラスの __new__ が呼ばれている様子
        # Calling __new__ when making a subclass of tuple [duplicate]
        # http://bit.ly/2rqeTm3
        self = super(super_cls, cls).__new__(cls, argugments)

        # 直接呼び出した場合
        # namedtuple で生成したクラスの __new__ を読んでいる。
        self = super_cls.__new__(cls + arguments)
        """

        # point 4. 生成したオブジェクトを return
        return self

    """
    def __init__(self, x1, y1, x2, y2):
        # オブジェクトが生成された後に呼び出される
        # self は immutable なので代入できない。
        self.is_rectangle = False
    """

    def __eq__(self, other):
        return all((self.x1 == other.x1,
                    self.y1 == other.y1,
                    self.x2 == other.x2,
                    self.y2 == other.y2))


if __name__ == '__main__':
    sample_code()