ゆくゆくは有へと

おかゆ/オカ∃/大鹿有生/彼ノ∅有生 の雑記

__new__ がわからん

メタクラス関連で__new__の使い方はよくみるけど、クラスレベルでの使い方がぐぐってもあんまりなし。

というところに、以下の記事のコメントでやってることが面白かったので、似たことを検証がてらメモ。

qiita.com

class A():
    def __new__(cls, value):
        print("generate by A.__new__")
        cls.X = 50
        return object.__new__(cls)

    def __init__(self, value):
        self.a = value

    def hello(self):
        return 'hello'


class B():
    def __new__(cls, value):
        return A.__new__(cls, value)

    def __init__(self, value):
        self.b = value


class C():
    def __new__(cls, value):
        return A(value)

    def __init__(self, value):
        self.c = value


if __name__ == '__main__':
    b = B(10)
    print("type of b :", type(b))
    print("b has a? :", hasattr(b, "a"))
    print("b has b? :", hasattr(b, "b"))
    print("B has X? :", hasattr(B, "X"))
    print("b.__dict__ :", b.__dict__)
    c = C(0)
    print("type of c :", type(c))
    print("c has a? :", hasattr(c, "a"))
    print("c has c? :", hasattr(c, "c"))
    print("c.__dict__ :", c.__dict__)
> python new_with_other.py
generate by A.__new__
type of b : <class '__main__.B'>
b has a? : False
b has b? : True
B has X? : True
b.__dict__ : {'b': 10}
generate by A.__new__
type of c : <class '__main__.A'>
c has a? : True
c has c? : False
c.__dict__ : {'a': 0}

BとCは__new__A.__new__(cls, value)で返すか、A(value)で返すかの違い。

クラスを呼び出してインスタンスを生成すると、__new__が呼ばれ、インスタンスが作られてから __init__ で初期化されるというのが典型的な流れ。

まず、b。こいつはBインスタンスで、この際呼ばれた__new__(cls, value) では、clsB が入ってるわけなので、たとえA.__new__を使おうと、Bを使ってインスタンスを作ろうとしていることに変わりはない。

持ってるアトリビュートを見る限り、呼び出したクラスの__init__だけが呼び出されている模様(つまり今回 A の __init__ は呼び出されていない)。まあこれも当然っちゃあ当然か。一方で、A.__new__ ではクラスのアトリビュート X が加えられるんだけど、そのおかげで B.X が存在する。まあこれも当然。

b.__dict__ みると分かる通り、基本的にメソッドはクラスのもんで、インスタンスから呼び出す場合も、クラスのアトリビュートとしてその関数が探索され(そして、selfインスタンスが束縛され)るわけなので、Aのコンストラクタを使ったからといって、Aのメソッドがそのインスタンスに直接登録されるわけではない(よって、b.hello()はエラーが起きる)。

続いて c。こっちは __new__の返り値で A(value)、Aクラスのインスタンスを返している。ので、クラスCの呼び出しをしたのに返ってくるインスタンスの生成元クラスはAという変なことになっている。興味深いのはCの__init__が呼び出されていないというところですかね。

で、これはなんでかというと、3. データモデル — Python 3.5.2 ドキュメント にある通りで、

__new__() が cls のインスタンスを返さない場合、インスタンスの __init__() メソッドは呼び出されません。

わけですな。だので、ちゃんとそのクラス独自の初期化をしたい場合は、object.__new__(cls, ...) なんなり、他の__new__を使ってとにかくclsインスタンスを作って返すというのが必要そう。

__new__()clsインスタンスを返さないような場合(Noneはアホらしいので除くとして)ってどういうときに使えるんだろ…?

ごく平凡な1つ目の案は、実体のあるエイリアス。Aのインスタンスを作るときに、C()が使える。*1

あるいは、Aの呼び出しに関してなにかprintとかフックしたいとき*2。クラスCになにかクラス変数をもたせておいて、それを使ってなにかフックしたりできそう*3

あるいは、ディスパッチ*4

なんか何を考えても大体「関数でよくない?」になってしまう…。

あくまで「インスタンス生成」だということを主張しつつ、でもそれ自身のインスタンスじゃないことにそこまで違和感をもたれないような使い方か…。たとえば、A がファイルオブジェクトを引数にとるけど、Cではfilenameを引数にとって、コンストラクタ内でファイルオブジェクトをつくって、それを使ってAインスタンスを生成して返してもらう。とか。

class Hoge():
    def __init__(self, file):
        # ..

class HogeWithFilename():
    def __new__(self, filename):
        with open(filename) as f:
            return Hoge(f)

みたいな。いやどうなんだ・・・・・・。

インスタンス生成しないクラス

ところで、インスタンス生成しないクラスというのをつくるときには __new__ が必要そうだ*5

class InstantiateError(Exception):
    pass

class DontInit():
    def __init__(self):
        raise InstantiateError


if __name__ == '__main__':
    try:
        DontInit()
    except InstantiateError:
        print("eh, don't instantiate.")
    d = DontInit.__new__(DontInit)
    print("type of d :", type(d))
eh, don't instantiate.
type of d : <class '__main__.DontInit'>

これは、__init__時点で例外吐いていて、一見インスタンス生成不可に見えますが、__new__ が生き残ってるので無理やり DontIntインスタンスを作ることができてしまう。ので、本当にインスタンスを作りたくなければ、__new__ で例外吐くしかない。

class DontInitAtAll():
    def __new__(cls):
        raise InstantiateError


if __name__ == '__main__':
    try:
        DontInitAtAll()
    except InstantiateError:
        print("eh, don't instantiate.")
    try:
        d = DontInitAtAll.__new__(DontInitAtAll)
    except:
        print("oh, can't really instantiate")
eh, don't instantiate.
oh, can't really instantiate

たとえば、MixInクラス(ふつうこいつのインスタンスを作ろうとは思わんよね?)であることを明示するために、こういう処置をするのはいいような気もする。

class MixIn():
    def __new__(cls):
        raise InstantiateError

という MixIn 基底クラスを用意しておいて、MixInクラスはこれを継承していけば、結構いい感じにいい感じなのでは?と思ったけど、__new__ も継承されるから、MixIn先のクラスの__new__をきちんとオーバーライドしてやらないといけないことになるから面倒か(そもそも __init__ がないって時点で MixInクラスか?ってなるだろうし、有用じゃなさが上回る…)

ううん

やっぱようわからんね

*1:関数でよくない?

*2:関数でよくない?

*3:これは関数でやるならクロージャになる。

*4:関数でよくない?

*5:そういうクラスが必要になるかはともかく