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


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

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




前半の文章を読むための前提知識として、 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 には シーケンス型プロトコル に関連する関数が保存された構造体が保存されます。シーケンス型とは例えば list, tuple, str, range などをさします。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 の文法はいいけど CPython が一部の属性に対して slot を設けてくれたものだから、そう言ったものに対しては個々に対応していかなくて辛い.. ということかな)。しかしながら、これらのインタープリタの実装の詳細は、言語仕様の一部となりますし、そうであるが故にインタープリタの実装の詳細は重要でもあります

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 です (ワイの注釈: tp_as_number->nb_add は 数値プロトコルの __add__, tp_as_sequence->sq_concat は シーケンスプロトコルの __add__ )

  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.

# ワイのサンプルコード その1

def main():
    a = Cls()
    b = Cls()

    print('1) a + b')
    print(a + b)
    # 0

    print('2) a.__add__(b)')
    print(a.__add__(b))
    # __add__ is called.
    # 0

    """
    次のことから何がわかるか?

    1)  a + b を実行したときに
        普通なら必ず呼ばれるはずの
        __getattribute__ が呼ばれなかった

    2)  直接 slot に対応した
        メソッドが呼ばれた

    ->  一貫性の無い特殊な属性参照の仕方が
        実装がされてしまっている。
    """


class Cls(object):
    def __add__(self, other):
        return 0

    def __getattribute__(self, name):
        print(name + ' is called.')
        return super().__getattribute__(name)


if __name__ == '__main__':
    main()
# ワイのサンプルコード その2
#     JavaScript の undefined のような定数があったらなと思って
#     値が評価されたら NotImplementedError を出す例外を
#     ふと作って見て失敗しました笑

def main():
    # x = None
    x = UnDefined
    
    if x:
        ...
    # Hello, world! 
    # -> __getattribute__ は呼び出されない。    
    
    if x.__bool__():
        ...
    # NotImplementedError
    # -> __getattribute__ が呼び出された。    
    
    # -> ここでも一貫性の無い特殊な属性参照の仕方が
    #    実装がされてしまっている。


class UnDefinedClass(object):
    def __bool__(self):
        print('Hello, world!')
        return True
    
    def __getattribute__(self, name):
        raise NotImplementedError


UnDefined = UnDefinedClass()

if __name__ == '__main__':
    main()

(ワイの注釈: Armin Ronacher はなぜこんな細かい言語仕様に文句を言っているのでしょうか?なぜならこのような挙動をされると PyPy 側の実装に問題を越してしまうからだと思われます。この記事の続編である「型の復讐」という記事から引用します。「以前に書いた slot システムに関する記事を読んでいたら、Python の型が C 言語側もしくは Python 側で実装されるかに依存して異なる意味論をもつことを覚えているでしょう。これはこの言語のかなり珍しい機能であり、通常、多くの他の言語では見られません。... 中略 ... このことは実際に PyPy で相当数の問題を引き起こしています。これらの違いが目立たない類似の API を実現するため、できるだけオリジナルの型を模倣する必要があるからです。C 言語レベルのインタープリターのコードと言語の残りの部分との間にある、この雑多な違いが意味することを理解するのはとても重要です。」)


私の考えでは slot システムは、完全に誤っています。slot システムは、インタープリタのごくごく特定の型(int のような)に対する最適化でありますが、他の型に対しては、実際全く効果がありません(ワイの注釈: 高速化のために slot を使って一貫性を犠牲にして、特殊な実装をしているのに、実際にはそんなに効果がないと怒っています。)

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 システムの存在について可能な限り言えることは、遺産以外の何者でもありません(ワイの注釈: 昔からそうやって実装されていたからということ, len がなぜメソッドではなく関数なのかも 公式 FAQ では "歴史" と表現されていました。)。初めて 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.





(ワイの注釈: このあたりから少しずつ slot からインタープリタに、話題が替わります。)


最新の 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 にはサブインタープリタがあります。サブインタープリタインタープリタの中で動作します、しかし多くのグローバルな状態を保持しています。グローバルな状態の最も大きな部分は、また最も議論のある1つでもあります: global interpreter lock。Python はすでにこの 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 と全く同じ動作になります(ワイの注釈: vtable というのは virtual table の略称でオブジェクトが持つメソッドを保存したテーブルのことです。オブジェクトからメソッドが呼び出されたとき、このテーブルを参照して実際のメソッドを取得する。)。このような実装による影響は明白です。あるクラスを何かのサブクラスにしてメソッド 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) から個々の引数を取り出し、C のローカル変数に移し替えなければなりません。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 と関係があります。 ここで、あなたは辞書になったとしましょう、辞書であるあなたから見た外の世界に対する 事前条件 は、あなたの処理がごく小さいものに限定されているということです。 内部の呼び出しが vtable を経由して行われるとき、そのごく小さいものに限定された事前条件は完全に窓から放りだされます。どうしてでしょうか? 何故なら、その呼び出しは(ワイの注釈: 辞書では到底対処しきれないほど大きな) global interpreter lock そのものを管理する必要がある 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.


(ワイの注釈: API contract を事前条件, all your guarantees を事後条件, 不変条件と訳しました。いずれも契約による設計 (Design By Contract) の用語です。 契約による設計に基づいたコードでは、事前条件を満たした引数をあるオブジェクトのメソッドに与えた場合、 オブジェクトとメソッドの返り値は事後条件を満たし、かつオブジェクトはメソッドの実行前後で不変条件を満たさなければなりません。 ざっくり言えば引数と返り値を型だけを検査するのではなく、もっと色んな条件を検査しようという設計あるいは考えです。 事前条件、事後条件、不変条件は Python の型アノテーションのように書きます。 自分は書籍 達人プログラマ で勉強しました。12~13P そこそこで問題もついてるのでオススメです。 いまはあまり使われていない機能らしいですが、そう言った条件の書き方を形式化しているのは、とても魅力的に見えました。 いまはあまり使われていないのは、型を書くのが面倒なように、契約を書くのが面倒だったということなのでしょうか)

今後のために - 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 は完全にメッセージという考えを元に実装されいて、このことは呼び出し素早くすることにおいて利点を生んでいます(ワイの注釈: Objective-C ではメソッドのことをメッセージと読んでいるようです。ここでいう "Objective-C は完全にメッセージという考えを元に実装されいて"と言うのは Objective-C は、Python の slot のような固定されたフィールドを持っていない、Python で言えば全てのフィールドが辞書で実装されているということかな。CPython にも、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.




◯ まとめ

  1. slot は、やめて欲しい。CPython の実装が汚くなる割に、効果がない。ユーザ定義クラスと同じように全部、辞書のようにして欲しい。

  2. ひとつだけのインタープリタは、やめて欲しい。インタープリタは複数実行できるようにして欲しい。

◯ 感想

slot は、たしかに改善が必要かもしれないと感じました。 CPython のコードをちょこっとだけ覗くと slot に対する処理の記述が、すこし複雑で違和感があります。 反面 GIL、ひとつだけのインタープリタとして実装されているには、賛否が色々あるようです 英語の記事ですが、とてもわかりやすいです。
What is the Python Global Interpreter Lock (GIL)? - Real Python

◯ 続編

型の復讐 - Qiita
Armin Ronacher が書いた記事の翻訳。この記事のパート2に当たります。 CPython の型の実装に対する不満が述べられていますが、理解できていません... orz

◯ 続編の参考文献(殴り書き)

漸進的型付け言語の時代に必要なもの
ttp://mizchi.hatenablog.com/entry/2018/07/05/180219

型に関する記事は、この記事もすごい。と言うか感動しました。 結局、型の要否は、開発するものの規模や堅牢性によるという結論に個人的に達しました。

初めて Python を知った時、あれだけ世の中、デスマやらバグで苦しんでるのに、 可読性を重視しているとうたっていながら、なぜ動的型付けを採用したのか、わかりませんでした。 self を明示するのに、なんで型は明示しないの?みたいな感じでした。

Explicit is better than implicit.


そして、最近になって型をアノテーションが採用された理由は、さらにわかりませんでした。 「あればいいものは、無くていい。」が Python の考えだと思っていたので、 こんな、型を明示してもしなくてもいいというオプショナルな機能は Python には許されないものだと思っていました。

しかし、この記事を読んで色々と納得できました。 規模が小さいものの時は、型は明示しない方が小綺麗に書けるし、反対に規模が大きくなると型がないと、 何をやってるのかさっぱりわからなくなってきます。

個人的には Python の言語仕様として、型アノテーションが採用されて欲しくなかったかなと思います。 なぜなら処理系、CPython の挙動に直接影響を与えないからです。

PyCharm などが採用していた 型コメント が、ベストな解だと感じました。 コメントは人間が読むものですし、コメントの書き方に規約を設けることは、良いことだと感じました。 なぜなら 1. 新しい機能を設けないから(一貫性) 2. たった1つのやり方を定めること に繋がると感じたからです。

Special cases aren't special enough to break the rules.
There should be one-- and preferably only one --obvious way to do it.


なんで Guido が型コメントではなく、型アノテーションとして文法レベルで採用したのかはメールも PEP も見てないのでわからないのですが、 全体的に型付けの流れが大きかったことと mypyc みたいに型付けしたら高速化もする処理系を念頭に置いてたのかなと思ったりもします。

「あればいいものは、無くていい。」精神でいけば、型コメントも本来は設けてはいけないことですが やはり規模が大きくなり Go などの他言語に書き換えないまでも、 あるいは一時的な逃げ方としては、採用されてもいいのではないかと感じました。

Armin Ronacher は型に関する記事で、型アノテーションは筋が通らないと言っていましたが。 人間が読むアノテーションは必要かなと思いました。 Armin Ronacher みたいな超スーパープログラマーは要らんやろと思われると思うのですが、 ワイみたいなのには型を明示してくれないと理解しにくかったりするので。

この土台の上に型アノテーションを置くことはほとんど筋が通らないことだと私は思います。
型の復讐

自分の未熟さをカバーするために、型システムがあるんですよ id: yug1224 。未熟だからミスが起きた、根性でテスト書けばなんとかなる、 という一部の動的型付け言語ユーザーの精神論はまさにブラック企業の体質と同じ。
megmin1 氏のブコメ

バカでプログラミング能力も低いぼくはすぐに飛びつきました。 確かに簡単にプログラミングが出来ます。 しかし、規模が大きくなるにつれてバカ向けでは無い事が分かって来たのです。
非バカ向け言語 Ruby


Armin Ronacher は Rust に軸足を移してるらしいです。 これだけ型について大きな不満を持っていると、Go ではなくて、よりきっちりした型システムのある Rust を選んだのは、なんかわかる気がします。 もともと Python をやっていた人は Go に移動している多い気配を感じます。自分の狭い観測範囲の中の話ですが。 Go はいい意味でいろんな書き方ができない言語なので Python を使っている人には移りやすいのかなと思ったりします。


Armin Ronacher が不満に感じていることは、動作が未定義なオブジェクトを実引数として受けることを許容していることです。 結果的に CPython 以外の処理系、 例えば PyPy が、その未定義な処理を模倣するという拷問のような作業をしなければならないことに不満を持っています。 Armin Ronacher は PyPy が大好きです。文章を読んでて感じます。確かに PyPy の実装は綺麗だなと、ちょっだけみて思いました。

これを解消するには、組み込み型については、厳密に引数の型をチェックする実装をしないといけないかなと感じました。 処理が定義されていない型を受け付けないようにするためです。

"厳密に" 型チェックをするには nominal subtyping しかないかなと感じます。 structual subtyping では、未定義の型を引数に取り込んでしまう恐れがあります。 でも Python では nominal subtyping はできないですし、解決策ってあるのかなと思ったりします。

動作を保証しないという訳には行かないのだろうかとも思いました。 型に対する処理が未定義の関数やメソッドにオブジェクトを代入するプログラマが悪いんやろってことで (事前条件を満たしていないプログラマが悪いということで)。 しかし、この文章の最後の最後に、こんなこと言ってるし、そういう訳にも行かないのか...

何故なら、PyPy は、互換性を獲得するために CPython のインタープリタのように動作するよう インタープリタをなんとしてでも制限する必要があるからです。


打つ手なしかと思ったのですが、そういう機能のない JavaScript は上手く対処しているようです。

JavaScript はその点においてかなりうまくやっています。奇妙ではあっても組み込み型の全ての意味論が明確に定義されています。 これは一般的には良いことだと私は思います。その意味論がどう作用するかを明確に定義したなら、最適化したり、後から選択的静的型付け (optional static typing) を行う余地があります。


これは具体的に何を指してるんだろう... 関数の引数側で型を指定しないで、未定義のクラスのインスタンスが代入されたときに弾くことなんてできるのかな... あるいは完璧に引数の型チェックを関数側で実装してるのかな。