jsからpythonに翻訳する過程で気づいた__new__の使いみち(追記:ダメだった)
http://pod.hatenablog.com/entry/2017/09/14/020740気づいたのだけれど。
jsのちょっとした記述をpythonに直す時に、今まであまり使わなかった__new__()
が使える箇所があるかもしれないと思った。
具体的な話
例えば、以下の様なコードがあるとする。これはnpmで使われているsemverのコードを簡略化したものなのだけれど。これをpythonのコードに翻訳したい。
function Range(range, loose) { if (range instanceof Range) { if (range.loose === loose) { return range; // b. } else { return new Range(range.range, loose) } } if (!(this instanceof Range)) return new Range(range, loose); // c. // d. this.loose = loose; this.raw = range; // do something }
上のコードでやっていることは以下の様なこと
a.
Rangeの引数には,rangeとlooseが与えられるb.
rangeがRangeのインスタンスで使いまわせそうだったら、rangeをそのまま返すc.
関数として呼ばれた場合にも、適切にオブジェクトが作られるようにするd.
通常のコンストラクタとしての利用
b.
はflyweight的な感じだし。c.
はjs固有の事情。
js固有の事情のおさらい
そういえば、と思いだしたけれど。pythonではクラスはオブジェクトを生成するファクトリー関数とみなすことができて、オブジェクトの生成は単にクラスを関数の実行と同様に呼び出すだけだけれど。jsでは関数をオブジェクトのコンストラクターとして利用する場合と通常の通りに関数として呼び出す場合の2種類の方法がある。
newについては以下の様な形。これはd.
の経路を辿り、thisはrange object。
const r = new Range(">=1.2.0", true);
一方で関数呼び出しのように読んでしまった場合には、callerはglobalになる。これはc.
の経路をたどる。thisはRangeのインスタンスではないので。
const r = Range(">=1.2.0", true);
オブジェクトを使いまわしたいときは、b.
の経路をたどる。あんまり最近見ない気がするけれど。
const r = Range(Range(">=1.2.0", true), true);
過去の対応
過去の対応、というか現時点でのpythonでは、関数とクラスに分けていた。以下のような感じ。
class Range: def __init__(self, range_, loose): self.range = range_ self.loose = loose def make_range(range_, loose): if isinstance(range_, Range): if range_.loose == loose: return ranse_ else: Range(range_.range, loose) return Range(range_, loose)
js固有のコードは要らないので消している。__init__()
の段階で既にオブジェクトのインスタンスが生成済みなので困るということでmake_range()
という関数を作り、常にこの関数を経由してオブジェクトを生成するように書いていた。悩ましいのはそれを強制する方法が全く存在しないこと。
実のところ
実のところ、オブジェクトの生成前のフックというのは、__new__()
そのものなのでこれを使ってあげれば良い。
こうかけば良かったことに気づいた。
class Range: def __new__(cls, range_, loose): if isinstance(range_, Range): if range_.loose == loose: return range_ else: return Range(range_.range, loose) return super().__new__(cls) def __init__(self, range_, loose): self.range = range_ self.loose = loose
オブジェクトの生成方法が1つだけになるのでこちらのほうが良さそう。
追記:この方法はダメです
この方法はダメです。
なぜダメかと言うと、まず、__new__()
でオブジェクトが生成されたあと自分自身のクラスと同じクラスのオブジェクトが返された場合に必ず__init__()
が呼ばれます。これがまず無駄だし気持ち悪い。その上、self.range
がRangeオブジェクトになってしまいます。
そして、渡されたrangeがRangeオブジェクトだった場合のところでも、結局、__init__()
で渡されたrangeが使われてしまうので、やっぱりself.range
がRangeオブジェクトになってしまいます。
追記2:ムキになって対応しようとしてみた
ムキになって対応してみようとした結果。デコレーターはisinstanceを壊すからだめだし。メタクラスでどうにかできることはわかっているけれど。どう考えてもオーバースペックな感じ。
class RangeMeta(type): def __call__(cls, range_, loose): if isinstance(range_, cls): if range_.loose == loose: return range_ else: return cls(range_.range, loose) return super().__call__(range_, loose) class Range(metaclass=RangeMeta): def __init__(self, range_, loose): self.range = range_ self.loose = loose r = Range(">=1.2.0", True) print(r.range) r2 = Range(r, True) print(r2.range) r3 = Range(r, False) print(r3.range) print(isinstance(r3, Range))