私が見たい Python - The Python I Would Like To See


概要
The Python I Would Like To See という記事の翻訳です。この記事は Flask の開発者である Armin Ronacher によって、2014/08/16 に書かれました。CPython の実装について、批判しています。

批判している内容は、大きく2つに分けられます。1つ目は、CPython で書かれた組込型の実装と Python で書かれたユーザ定義型の実装が異なること。2つ目は、インタープリタが、ひとつのインタープリタしか起動できず、複数のインタープリタを同時に起動できないこと。

前提知識として、 CPython のオブジェクトの実装の概要を押さえておいた方が理解しやすいです。以下の記事を参考にしてください。
Step1. Python のオブジェクトって何?
Step2. Pythonの内部構造::PyObject - POSTD

本文中で slots という言葉が出てきます。特殊メソッドを高速に参照するために、特殊メソッドを CPython 構造体のメンバに割り当てるような実装を指しています(あるいは構造体のメンバそのものを指しています)。

// クラスオブジェクトの CPython 実装(かなり省略)
// 構造体の各メンバに特殊メソッドを割り当てる。
typedef struct _typeobject {
    PyNumberMethods *tp_as_number;       // __add__, __sub__, ...
    PySequenceMethods *tp_as_sequence;   // __getitem__, __len__, ...
    PyMappingMethods *tp_as_mapping;     // __getitem__, __len__, ...

    getiterfunc tp_iter;                 // __iter__
    iternextfunc tp_iternext;            // __next__

    initproc tp_init;                    // __init__
    newfunc tp_new;                      // __new__

} PyTypeObject;

cpython/Include/object.h - GitHub


上のコードを、ごくさわりだけ、読んでみます。

tp_as_number には、数値型プロトコル に関連する関数が保存された構造体が保存されます。数値型とは例えば int, bool, floot, complex などを指します。この構造体には足し算、引き算などの四則演算などを表す関数が、保存されています。本文中でも tp_as_number について言及していますので、なんとなく押さえておいてください。

ちなみに tp_as_sequence には シーケンス型プロトコル に関連する関数が保存された構造体が保存されます。シーケンス型とは例えば range, list, tuple などをさします。tp_as_mapping には マップ型プロトコル に関連する関数が保存された構造体が保存されます。マップ型とは例えば dict を指します。

プロトコルが実装するべきメソッドの一覧を Python/C API リファレンスマニュアル で見ても、いまいちピンと来ないかもしれません。下記のサイトでシーケンスとマッピングが実装するメソッドを見るとわかりやすいかもしれません。
8.4. collections.abc — コレクションの抽象基底クラス



本文


私が Python 3 のファンではないし、あるいは Python 3 が現在目指しているところが好きでもないことを隠すつもりはありません。このような態度は、Python が何をしたら私が気にいるのかという質問について、ここ2、3ヶ月の間にたくさんのメールが、飛び交いました。そのため私は、私の考えを広く共有して将来の言語設計者の参考になればと思いました :)

It's no secret that I'm not a fan of Python 3 or where the language is currently going. This has led to a bunch of emails flying my way over the last few months about questions about what exactly I would prefer Python would do. So I figured I might share some of my thoughts publicly to maybe leave some food for thought for future language designers :)


Python は、完璧というには程遠い言語です。しかしながら、 Python について私をイライラさせるものは、大部分は、インタープリタのごくわずかな仕様について、個々に対応していかなければいけないという問題であり、言語仕様そのものはそこまで大きい要因ではありません。しかしながら、これらのインタープリタの実装の詳細は、言語仕様の一部となりますし、そうであるが故にインタープリタの実装の詳細は重要でもあります

Python is definitely a language that is not perfect. However I think what frustrates me about the language are largely problems that have to do with tiny details in the interpreter and less the language itself. These interpreter details however are becoming part of the language and this is why they are important.


私は Pythonインタープリタ(slots)に関する小さな違和感から初めて、最終的には Python の言語設計上の最大の誤りについて、説明していきたいと思います。もし、この記事が受け入れられるなら、きっと今後も似たような投稿がなされるでしょう。

I want to take you on a journey that starts with a small oddity in the interpreter (slots) and ends up with the biggest mistake in the language design. If the reception is good there will be more posts like this.


それでも、おそらく将来投稿される記事は大抵インタープリタの設計について議論するものになるだろうし、その結果として得られるものは、インタープリタに関する知見と、それにインタープリタに関する議論した結果から得られる言語仕様そのものに関する知見になるでしょう。私は、この記事が、たんに Python が今後どうあるべきかというよりも、より一般的に言語設計という観点から面白い記事になると信じています。

In general though these posts will be an exploration about design decisions in the interpreter and what consequences they have on both the interpreter and the resulting language. I believe this is more interesting from a general language design point of view than as a recommendation about how to go forward with Python.


言語 対 実装 - Language vs Implementation

最初にこの記事を書き上げた後に、私はこの節を書き加えました。なぜなら言語としての Pythonインタープリタとしての CPython が、開発者が思っているほど分かれていないということが、まったく見過ごされていると思っているからです。Python の言語仕様というものは、確かに存在します、しかし大抵の場合は、インタープリタの実装を書き起こしただけですし、あるいは説明不足だったりします。

I added this particular paragraph after I wrote the initial version of this article because I think it has been largely missed that Python as a language and CPython as the interpreter are not nearly as separate as developers might believe. There is a language specification but in many cases it just codifies what the interpreter does or is even lacking.

Python の list.remove メソッド(公式ドキュメントの説明不足の例)


この件については、この不明瞭なインタープリタの実装の詳細が、言語設計を変更したり影響を与えたりしました、するとまた、他の Python の実装に採用するように強制しました。例えば PyPy は slots についてまったく何も知りませんが(と私は推測していますが)、PyPy は slots がインタープリタの一部であるかのように振舞わなければなりません。

In this particular case this obscure implementation detail of the interpreter changed or influenced the language design and also forced other Python implementations to adopt. For instance PyPy does not know anything about slots (I presume) but it still has to operate as if slots were part of the interpreter.


Slots

私にとって Python の最大の問題は、愚かな slot システムです。私は特殊属性である __slots__ のことを言っているのではありません、特殊メソッドのための slots について述べています。これらの slots は Python という言語の "機能" です、大抵の場合は見過ごされます、何故なら slots は開発者がほとんど気にする必要がないからです。そうは言っても、私の主張としては、slots が存在することが Python という言語の最大の問題点なのです。

By far my biggest problem with the language is the stupid slot system. I do not mean the __slots__ but the internal type slots for special methods. These slots are a "feature" of the language which is largely missed because it is something you rarely need to be concerned with. That said, the fact that slots exist is in my opinion the biggest problem of the language.


では slot とは何でしょうか?slot とはインタープリタがどのように内部で実装されているかという副作用です。全ての Python プログラマ"dunder methods" について知っています: __add__ のようなものです。これらのメソッドは2つのアンダースコアから始まります、そして特殊メソッド名、再度2つのアンダースコアと続きます。開発者が個々に知っているように a + b は a.__add__(b) と同じです。

So what's a slot? A slot is the side effect of how the interpreter is implemented internally. Every Python programmer knows about "dunder methods": things like __add__. These methods start with two underscores, the name of the special method, and two underscores again. As each developer knows, a + b is something like a.__add__(b).


残念なことにこれは嘘です。

Unfortunately that is a lie.


Python は実際そのようには動作しません。 Python は、内部では実際にそのようには全く動作していません(今日では)。代わりに、だいたいどのようにインタープリタが動作するか示します:

Python does not actually work that way. Python internally does actually not work that way at all (nowadays). Instead here is roughly how the interpreter works:


  1. 型が生成された時、インタープリタは、クラスの全てのディスクリプタを探し、__add__ のような特殊メソッドを探します。
  2. 個々の特殊メソッドに対して、インタープリタは、事前に定義した型オブジェクトの slot に格納されたディスクリプタへの参照を探します。
  3. 例えば特殊メソッド __add__ は2つの slot に対応している: tp_as_number->nb_add と tp_as_sequence->sq_concat です。
  4. インタープリタが a + b を評価したいときは TYPE_OF(a)->tp_as_number->nb_add(a, b) を呼びます(実際にはこれより複雑です、__add__ は複数の slot が対応しているからです)。
  1. When a type gets created the interpreter finds all descriptors on the class and will look for special methods like __add__.
  2. For each special method the interpreter finds it puts a reference to the descriptor into a predefined slot on the type object.
  3. For instance the special method __add__ corresponds to two internal slots: tp_as_number->nb_add and tp_as_sequence->sq_concat.
  4. When the interpreter wants to evaluate a + b it will invoke something like TYPE_OF(a)->tp_as_number->nb_add(a, b) (more complicated than that because __add__ actually has multiple slots).


そのため表面的には a + b は type(a).__add__(a, b) のようにも見えますが、しかし slot の取り扱いから Python の動作を見たように、実際には正しくありません。あなたはこれを簡単に確認することができます、メタクラスで __getattribute__ を実装して自分で実装した __add__ をフックして見てください。自分で実装した __add__ は決して呼び出されないことに、気づくでしょう。

So on the surface a + b does something like type(a).__add__(a, b) but even that is not correct as you can see from the slot handling. You can easily verify that yourself by implementing __getattribute__ on a metaclass and attempting to hook a custom __add__ in. You will notice that it's never invoked.


私の考えでは slot システムは、完全に誤っています。slot システムは、インタープリタのごくごく特定の型(int のような)に対する最適化でありますが、他の型に対しては、実際全く効果がありません。

The slot system in my mind is absolutely ridiculous. It's an optimization that helps for some very specific types in the interpreter (like integers) but it actually makes no sense for other types.


これを示すために、この全く意味のない型 A について考えて見ます(x.py):

To demonstrate this, consider this completely pointless type (x.py):

class A(object):
    def __add__(self, other):
        return 42


__add__ メソッドを持っているのでインタープリタは slot にこれを格納します。これはどれほど早いでしょうか?a + b を行う時に slot が使われます、ここに時間がどれほどのものか記します。

Since we have an __add__ method the interpreter will set this up in a slot. So how fast is it? When we do a + b we will use the slots, so here is what it times it as:

$ python3 -m timeit -s 'from x import A; a = A(); b = A()' 'a + b'
1000000 loops, best of 3: 0.256 usec per loop


しかしながら、もし a.__add__(b) を実行すれば、slot システムを回避できます。代わりに、インタープリタは、まずインスタンスの辞書 a.__dict__ の中を探しますが、そこではメソッドを見つけられないので、次に型の辞書 type(a).__dict__ の中を探して、メソッドを見つけ出します。以下で時間を計測しています:

If we do however a.__add__(b) we bypass the slot system. Instead the interpreter is looking in the instance dictionary (where it will not find anything) and then looks in the type's dictionary where it will find the method. Here is where that clocks in at:

$ python3 -m timeit -s 'from x import A; a = A(); b = A()' 'a.__add__(b)'
10000000 loops, best of 3: 0.158 usec per loop


信じられますか?slots を使わない方が実際には速いのです。なぜこのようなことが起こっているのでしょうか?私はこのような挙動の理由について、完全に理解している訳ではありませんが、本当に長いこと、いままでこのように動作してきました。事実、slots を持っていなかった古いスタイルのクラスは、新しいスタイルのクラスよりも処理が速く、より多くの機能を有していました。

Can you believe it: the version without slots is actually faster. What magic is that? I'm not entirely sure what the reason for this is, but it has been like this for a long, long time. In fact, old style classes (which did not have slots) where much faster than new style classes for operators and had more features.


より多くの機能?そうです、何故なら古いスタイルのクラスではこれができました(Python 2.7):

More features? Yes, because old style classes could do this (Python 2.7):

>>> original = 42
>>> class FooProxy:
...  def __getattr__(self, x):
...   return getattr(original, x)
...
>>> proxy = FooProxy()
>>> proxy
42
>>> 1 + proxy
43
>>> proxy + 1
43

Python 2 と 3 の違い - __coerce__ と __getattr__


そうです。私たちは今日、より複雑な型システムのために Python 2 よりも少ない機能を有しています。上記のコードは新しいスタイルのクラスでは実行できません。実際、新しいスタイルでは実行できないということよりも、いかに古いクラスが軽量であったかについて考えると、気が重くなります。

Yes. We have less features today than we had in Python 2 for a more complex type system. Because the code above cannot be done with new style classes and more. It's actually worse than that if you consider how lightweight oldstyle classes were:

>>> # Python 2
>>> import sys
>>> class OldStyleClass:
...  pass
...
>>> class NewStyleClass(object):
...  pass
...
>>> sys.getsizeof(OldStyleClass)
104
>>> sys.getsizeof(NewStyleClass)
904


Slots はどこから来たのか? - Where do Slots Come From?

このことから slots が何故存在するのか、という疑問が生じます。私が slot システムの存在について可能な限り言えることは、遺産(注釈: 昔からそうやって実装されていたからということ)以外の何者でもありません。初めて Pythonインタープリタが実装された時、文字列やその他の組込型は、型が必要とする全てのメソッドを保持した、グローバルで静的に確保された構造体でした。これは特殊メソッドの __add__ が存在する前の話です。もし Python を 1990 年から確認すれば、オブジェクトがどのように構築されたかを振り返ることができるでしょう。

This raises the question why slots exist. As far as I can tell the slot system exists because of legacy more than anything else. When the Python interpreter was created initially, builtin types like strings and others were implemented as global and statically allocated structs which held all the special methods a type needs to have. This was before __add__ was a thing. If you check out a Python from 1990 you can see how objects were built back then.


例えば、これは int がどのようなものかを示しています。

This for instance is how integers looked:

static number_methods int_as_number = {
    intadd, /*tp_add*/
    intsub, /*tp_subtract*/
    intmul, /*tp_multiply*/
    intdiv, /*tp_divide*/
    intrem, /*tp_remainder*/
    intpow, /*tp_power*/
    intneg, /*tp_negate*/
    intpos, /*tp_plus*/
};

typeobject Inttype = {
    OB_HEAD_INIT(&Typetype)
    0,
    "int",
    sizeof(intobject),
    0,
    free,       /*tp_dealloc*/
    intprint,   /*tp_print*/
    0,          /*tp_getattr*/
    0,          /*tp_setattr*/
    intcompare, /*tp_compare*/
    intrepr,    /*tp_repr*/
    &int_as_number, /*tp_as_number*/
    0,          /*tp_as_sequence*/
    0,          /*tp_as_mapping*/
};


見て分かる通り、かつてリリースされた最初のバージョンの Python でさえも、tp_as_number は存在しました。1点、残念なことに古いバージョンのリポジトリが壊れてしまっているので、Python のとても古いリリースに関しては、重要なこと(例えば実際のインタープリタ)は、行方不明になっています、そのため私たちはいくらかそれよりも後にリリースされたリポジトリを見てオブジェクトがどのように実装されていたかを調べてみます。1993 年までにインタープリタの add opcode callback は、次のようでした:

As you can see, even in the first version of Python that was ever released, tp_as_number was a thing. Unfortunately at one point the repo probably got corrupted for old revisions so in those very old releases of Python important things (such as the actual interpreter) are missing so we need to look at little bit into the future to see how these objects were implemented. By 1993 this is what the interpreter's add opcode callback looked like:

static object *
add(v, w)
    object *v, *w;
{
    if (v->ob_type->tp_as_sequence != NULL)
        return (*v->ob_type->tp_as_sequence->sq_concat)(v, w);
    else if (v->ob_type->tp_as_number != NULL) {
        object *x;
        if (coerce(&v, &w) != 0)
            return NULL;
        x = (*v->ob_type->tp_as_number->nb_add)(v, w);
        DECREF(v);
        DECREF(w);
        return x;
    }
    err_setstr(TypeError, "bad operand type(s) for +");
    return NULL;
}


__add__ とそのほかの演算子はどこで実装されているのでしょうか?私が見ることができた範囲で言えばバージョン 1.1 から登場します。私は実際になんとか Python 1.1 を取得して、いくらか操作しながら OS X 10.9 でコンパイルしました。

So when were __add__ and others implemented? From what I can see they appear in 1.1. I actually managed to get a Python 1.1 to compile on OS X 10.9 with a bit of fiddling:

$ ./python -v
Python 1.1 (Aug 16 2014)
Copyright 1991-1994 Stichting Mathematisch Centrum, Amsterdam


確かに、それはクラッシュしていて全てが動くわけではありませんでした。しかし、Python が過去にどのようなものであったかについてのアイディアを与えてくれます。例えば C で実装された型と Python で実装された型に大きな隔たりがありました。

Sure. It likes to crash and not everything works, but it gives you an idea of how Python was like back then. For instance there was a huge split between types implemented in C and Python:

$ ./python test.py
Traceback (innermost last):
  File "test.py", line 1, in ?
    print dir(1 + 1)
TypeError: dir() argument must have __dict__ attribute


見て分かる通り、int のような組込型には、introspection がありません(注釈: オブジェクトの属性を参照するようなことはできません)。事実、カスタムクラスに対して __add__ が実装されたとしても、全てカスタムクラスに対する機能でした。

As you can see, no introspection of builtin types such as integers. In fact, while __add__ was supported for custom classes, it was a whole feature of custom classes:

>>> (1).__add__(2)
Traceback (innermost last):
  File "<stdin>", line 1, in ?
TypeError: attribute-less object


すなわちこれは、私たちが Python の中に持っている遺産なのです。Python の型に対する大まかな設計は、変更されたことがありません、しかし、細かい修正を長年くわえられてきました。

So this is the heritage we even today have in Python. The general layout of a Python type has not changed but it was patched on top for many, many years.


最新の PyObject - A Modern PyObject

そのため、今日多くの人が C のインタープリタで実装された Python のオブジェクトと Python のコードで実装された Python のオブジェクトの違いは、とても小さいと主張します。Python 2.7 では最大の違いは既定の __repr__ の動作でした、Python で実装された型に対して class と表示するのに対して、C で実装された型に対しては type と表示します。実際、この repr における違いは、静的に領域を確保された (type) なのか、動的にヒープ上で確保された (class) なのかの違いを示してくれます。これは実質的な違いがあったわけではありませんし、Python 3 では違いは完全に消えて無くなりました(注釈: 両方とも class と表示されるようになりました)。特殊メソッドは、slots で表現されているし、その逆もまたそうであります。ほとんどの場合、Python と C のクラスの違いは消えてしまったようです。

So today many would argue the difference between a Python object implemented in the C interpreter and a Python object implemented in actual Python code is very minimal. In Python 2.7 the biggest difference seemed to be that the __repr__ that was provided by default reported class for types implemented in Python and type for types implemented in C. In fact this difference in the repr indicated if a type was statically allocated (type) or on dynamically on the heap (class). It did not make a practical difference and is entirely gone in Python 3. Special methods are replicated to slots and vice versa. For the most part, the difference between Python and C classes seems to have disappeared.

Python 2 と 3 の違い - __repr__


しかしながら残念なことに、2つのものは全く異なるものです。早速見てみましょう。

However they are still very different unfortunately. Let's have a look.


全ての Python 開発者が知っているように Python のクラスは "公開" されています。あなたはクラスの中身を見たり、またクラスが保持している状態を全て見たり、クラス定義を終えた後でさえメソッドを外したり、もう一度つけたりもできます。この動的性質は、インタープリタに対するクラス(注釈: C で実装されたクラス)では使えません。なぜでしょうか?

As every Python developer knows, Python classes as "open". You can look into them, see all the state they store, detach and reattach method on them even after the class declaration finished. This dynamic nature is not available for interpreter classes. Why is that?


例えば dict 型にメソッドを付けたり外したりすること自体に、技術的制限があるわけではありません。インタープリタがメソッドの付け外しをあなたにさせないのは、まず組込型がヒープにないという事実を前提にしたプログラマの健全さと、実際にはほとんど関係がありません(注釈: 組込型はメソッドの付け外しを動的にできないようにしていますが、それは組込型が動的に領域を変更できないからという訳ではありません)。このことについて広範にわたる因果関係を理解するために、Python がどうやってインタープリタを起動させているかを理解する必要があります。

There is no technical restriction in itself of why you could not attach another method to, say, the dict type. The reason the interpreter does not let you do that actually has very little to do with programmer sanity in the first place as the fact that builtin types are not on the heap. To understand the wide ranging consequences of this you need to understand how the Python language starts the interpreter.

ヒープとスタック - 学校では教えてくれないこと


いまいましいインタープリタ - The Damn Interpreter

Python ではインタープリタの起動は非常に重たい処理です。Python の実行ファイルを起動すると、なんでも実行してくれる大きな装置が呼び出されます。この大きな装置は、組込型を起動して、import 機構をセットアップして、必要なモジュールを import して、 OS と協調してシグナルを処理しコマンドライン引数を受け取って、内部状態をセットアップしたりします、この他にも色々と実行しています。それが最終的に終わると、Pythonインタープリタは、あなたのコードを走らせて、シャットダウンします。これは Python は今日に至る 25 年間もの間やっていたことです。

In Python the intepreter startup is a very expensive process. Whenever you start the Python executable you invoke a huge machinery that does pretty much everything. Among other things it will bootstrap the internal types, it will setup the import machinery, it will import some required modules, work with the OS to handle signals and to accept the command line parameters, setup internal state etc. When it's finally done it will run your code and shut down. This is also something that Python is doing like this for 25 years now.

Pythonアプリの起動を高速化する - DSAS開発者の部屋


疑似コードで示せば、これは次のようになります。

In pseudocode this is how this looks like:

/* called once */
bootstrap()

/* these three could be called in a loop if you prefer */
initialize()
rv = run_code()
finalize()

/* called once */
shutdown()


このことに関する問題は、Pythonインタープリタは多くのグローバルな状態を保持しているということです。事実、あなたは1つのインタープリタしか持つことができません。より良い設計では、インタープリタをセットアップし、何かをそのインタープリタの上で走らせることです:

The problem with this, is that Python's interpreter has a huge amount of global state. In fact, you can only have one interpreter. A much better design would be to setup the interpreter and run something on it:

interpreter *iptr = make_interpreter();
interpreter_run_code(iptr):
finalize_interpreter(iptr);


事実これは、多くの他の動的言語がこのように動作しています。例えば、これは lua の実装がどのように処理しているか、javascript エンジンが動作するか.. などを示しています。明確な利点は、2つのインタープリタを持てるということです。これは、新しい概念です。

This is in fact how many other dynamic languages work. For instance this is how lua implementations operate, how javascript engines work etc. The clear advantage is that you can have two interpreters. What a novel concept.


なぜ 複数のインタープリタ が必要なのでしょうか?あなたは驚くでしょう。Python でさえ 複数のインタープリタ が必要ですし、少なくとも 複数のインタープリタ は役に立ちます。例えば、Python を埋め込んだアプリケーションがインタープリタを個々に実行できるようにするために、複数のインタープリタ は存在します(例えば mod_python で実装されたウェブアプリケーションについて考えて見てください)。なので Python にはサブインタープリタがあります。サブインタープリタインタープリタの中で動作します、しかし多くのグローバルな状態を保持しています。グローバルな状態の最も大きな部分は、また最も議論のある部分でもあります: global lock interpreterPython はすでにこの 1つのインタープリタ というコンセプトを採用することを決めたので、サブインタープリタ間では多くのデータが共有されています。ロックという機能を 1つのインタープリタ の上で実現するためには、データは共有される全てのサブインタープリタ間でロックが必要になります。どんなデータが共有されているのでしょうか?

Who needs multiple interpreters? You would be surprised. Even Python needs them or at least thought they are useful. For instance those exist so that an application embedding Python can have things run independently (for instance think web applications implemented in mod_python. They want to run in isolation). So in Python there are sub interpreters. They work within the interpreter but because there is so much global state. The biggest piece of global state is also the most controversial one: the global interpreter lock. Python already decided on this one interpreter concept so there is lots of data shared between subinterpreters. As those are shared there needs to be a lock around all of them, so that lock is on the actual interpreter. What data is shared?


私が上の方で貼ったコードを見ると、大きな構造体が書かれたコードがありますよね。これらの構造体は、実際、グローバル変数として存在しています。事実、インタープリタは、これらの型の構造体を、直接 Python のコードから見えるようにしています。OB_HEAD_INIT(&Typetype) マクロによって、これをPython のコードから見えるようにしています、OB_HEAD_INIT(&Typetype) マクロは、インタープリタと協調できるようにするために、構造体に必要なヘッダを与えています。例えば、その型への参照カウントがあります。

If you look at the code I pasted above you can see these huge structs sitting around. These structs are actually sitting around as global variables. In fact the interpreter exposes those type structs directly to the Python code. This is enabled by the OB_HEAD_INIT(&Typetype) macro which gives this struct the necessary header so that the interpreter can work with it. For instance in there is the refcount of the type.


いまこのような実装がどこに向かっているのかを知ることができます。これらのオブジェクトはサブインタープリタ間で共有されます。なので、Python コードの中にあるこのオブジェクトを変更できることを想像して見てください。お互いに全く関連のない、2つの完全に独立した Python コードが、お互いに状態を変更することができてしまいます。同じことを JavaScript 考えて見てください、 Facebook を開いたタブは組込配列型の実装を変更することができます、そして Google を開いたタブでも即座にその影響を確認することができます。

Now you can see where this is going. These objects are shared between sub interpreters. So imagine you could modify this object in your Python code. Two completely independent pieces of Python code that have nothing to do with each other could change each other's state. Imagine this was in JavaScript and the Facebook tab would be able to change the implementation of the builtin array type and the Google tab would immediately see the effects of this.


1990年ごろからの、このような設計を採用したことは、今日でも感じることができる、さざ波ような影響を持っています。

This design decision from 1990 or so still has ripples that can be felt today.


前向きに捉えれば、組込型が immutabile であることは、コミュニティで良い機能であると一般に受け入れられています。mutable な組込型の問題は、これまでも他の言語でも示されてきましたし、私たちも、あまり見てこなかったというものでもありません。

On the bright side, the immutability of builtin types has generally been accepted as a good feature by the community. The problems of mutable builtin types has been demonstrated by other programming languages and it's not something we missed much.


他にもまだ考えることがあります。

There is more though.


VTable って何? - What's a VTable?

C で実装された Python の型は、ほとんどが immutable です。他の違いは何でしょうか?他の大きな違いも、またクラスの公開性と関連があります。Python で実装されたクラスは仮想としてメソッドを持ちます。C++ にあるような vtable が無い一方で、全てのメソッドはクラス辞書に保存されていて、どのように探索するかアルゴリズムが定められています、結局は C++ の vtable と全く同じ動作になります。このような実装による影響は明白です。あるクラスを何かのサブクラスにしてメソッド a をオーバーライドしたとします、するとその過程の中で他のメソッド b が間接的に修正される良い機会が生じます、何故ならメソッド b はメソッド a を呼び出してているからです。

So Python types coming from C are largely immutable. What else is different though? The other big difference also has to do with the open nature of classes in Python. Classes implemented in Python have their methods as "virtual". While there is no "real" C++ style vtable, all methods are stored on the class dictionary and there is a lookup algorithm, it boils down to pretty much the same. The consequences are quite clear. When you subclass something and you override a method, there is a good chance another method will be indirectly modified in the process because it's calling into it.


良い例はコレクションでしょう。多くのコレクションは、便利なメソッドを提供しています。例としては Python の辞書のは、辞書からオブジェクトを取得するために、2つのメソッドを実装しています: __getitem__() と get() です。Python でクラスを実装した時、通常、他のメソッドを通してメソッドを実装するでしょう、ちょうど get(key) メソッドの定義の中で return self.__getitem__(key) を呼び出すように。

A good example are collections. Lots of collections have convenience methods. As an example a dictionary in Python has two methods to retrieve an object from it: __getitem__() and get(). When you implement a class in Python you will usually implement one through the other by doing something like return self.__getitem__(key) in get(key).


インタープリタによって実装された型にとっては、これは違います。理由は、またもや、slots と辞書の違いです。いま、インタープリタ側で辞書を実装したいとしましょう。目標はコードを再利用することです、すなわち get の定義の中で __getitem__ を呼び出すことです。どうやってこれを実装しますか?

For types implemented by the interpreter that is different. The reason is again the difference between slots and the dictionary. Say you want to implement a dictionary in the interpreter. Your goal is to reuse code still, so you want to call __getitem__ from get. How do you go about this?


C における Python のメソッドは、特定のシグネチャがついた C の関数です。これが第一の問題です。関数の目標は、Python レベルで引数を受け取り、C のレベルで使えるように変換することです。少なくとも、Python の tuple, dict (args, kwargs) から個々の引数を取り出し、ローカル変数に移し替えなければなりません。dict__getitem__ は内部的に引数を構文解析して、それから実際の引数と共に dict_do_getitem を呼び出すというのが、一般的なパターンです。あなたはこの話がどこに向かっているか、わかるはずです。dict__getitem__ と dict_get は共に内部の静的関数である dict_get を呼び出します。あなたは、これをオーバーライドできません。

A Python method in C is just a C function with a specific signature. That is the first problem. That function's first purpose is to handle the Python level parameters and convert them into something you can use on the C layer. At the very least you need to pull the individual arguments from a Python tuple or dict (args and kwargs) into local variables. So a common pattern is that dict__getitem__ internally does just the argument parsing and then calls into something like dict_do_getitem with the actual parameters. You can see where this is going. dict__getitem__ and dict_get both would call into dict_get which is an internal static function. You cannot override that.


これに関しては、全く良い方法がありません。これに関する理由は、slot システムと関係があります。かなり狂ったようなことでもしない限り、インタープリタから vtable を経由して内部で呼び出しを行う良い方法はありません。これに関する理由は、global interpreter lock と関係があります。 あなたが辞書であるとき、あなたの外の世界に対する API 契約は、あなたの処理が原始的なものであるということです(注釈: 訳が怪しい)。内部の呼び出しが vtable を経由して行われると、契約は完全に窓から出ていきます(注釈: 訳が怪しい)。どうしてでしょうか?何故なら、呼び出しは Python コードを経由していきます、global interpreter lock そのものを管理する必要があるか、大きな問題に直面するでしょう。

There really is no good way around this. The reason for this is related to the slot system. There is no good way from the interpreter internally issue a call through the vtable without going crazy. The reason for this is related to the global interpreter lock. When you are a dictionary your API contract to the outside world is that your operations are atomic. That contract completely goes out of the window when your internal call goes through a vtable. Why? Because that call might now go through Python code which needs to manage the global interpreter lock itself or you will run into massive problems.


辞書のサブクラスが、内部の dict_get をオーバーライドしたことによって遅延 import が起動します、これによって生じる、痛みを想像して見てください。窓から全ての保証を投げてしまいます。また一方で、私たちはだいぶ、昔にそれをするべきだったのかもしれません。

Imagine the pain of a dictionary subclass overriding an internal dict_get which would kick off a lazy import. You throw all your guarantees out of the window. Then again, maybe we should have done that a long time ago.


今後のために - For Future Reference

近年は Python を言語として、より複雑なものにしようという明白な傾向があります。私は、それとは反対の傾向を見たいです。

In recent years there is a clear trend of making Python more complex as a language. I would like to see the inverse of that trend.


JavaScript が動作しているのと同じような互いに独立して動作するインタープリタをもとにした内部のインタープリタの設計が見たいです。これは、埋め込みとメッセージのやりとりをもとにした同時並行性へのドアを急に開けるでしょう。CPU は、もう速くはならないでしょう :)

I would like to see an internal interpreter design could be based on interpreters that work independent of each other, with local base types and more, similar to how JavaScript works. This would immediately open up the door again for embedding and concurrency based on message passing. CPUs won't get any faster :)


vtable として slots と辞書を持つ代わりに、辞書だけで実験して見ましょう。言語としての Objective-C は完全にメッセージベースで、このことは呼び出し素早くすることにおいて利点を生んでいます。文字列は Python において興味深いです、比較をとても高速に行なっています。私はあなたに遅くするようなことはしないでほしい、たとえ少しであっても、文字列はとてもシンプルなシステムで簡単に最適化できます。

Instead of having slots and dictionaries as a vtable thing, let's experiment with just dictionaries. Objective-C as a language is entirely based on messages and it has made huge advances in making their calls fast. Their calls are from what I can see much faster than Python's calls in the best case. Strings are interned anyways in Python, making comparisons very fast. I bet you it's not slower and even if it was a tiny bit slower, it's a much simpler system that would be easier to optimize.


あなたは Python のコードベースが、slot システムを扱うために、どれだけ多くの余分なロジックを必要としているか見るべきです。それはとても信じられないものです。

You should have a look through the Python codebase how much extra logic is required to handle the slot system. It's pretty incredible.


私は slot システムが悪いアイディアであり、だいぶ前に削除されるべきであったと確信しています。slot システムを削除することは PyPy にとっても利益があります、何故なら、PyPy は、互換性を獲得するために CPython のインタープリタのように動作するようなインタープリタになんとしてでも制限する必要があるからです。

I am very much convinced the slot system was a bad idea and should have been ripped out a long ago. The removal might even have benefited PyPy because I'm pretty sure they need to go out of the way to restrict their interpreter to work like the CPython one to achieve compatibility.




Armin Ronacher が書いた記事の翻訳。この記事のパート2に当たります。
Revenge of the Types: 型の復讐