#
3 例外

Python プログラマは、特定のエラー処理が必要なときだけしか例外を扱う 必要はありません; 処理しなかった例外は、処理の呼び出し側、そのまた 呼び出し側、といった具合に、トップレベルのインタプリタ層まで自動的に 伝播します。インタプリタ層は、スタックトレースバックと合わせて 例外をユーザに報告します。

ところが、 C プログラマの場合、エラーチェックは常に明示的に行わねば なりません。Python/C API の全ての関数は、関数のドキュメントで明確に 説明がない限り例外を発行する可能性があります。 一般的な話として、ある関数が何らかのエラーに遭遇すると、関数は 例外を送出して、関数内における参照の所有権を全て放棄し、 エラー指標 (error indicator) -- 通常は `NULL` または `-1` を返します。いくつかの関数ではブール型で真/偽を返し、偽はエラーを 示します。きわめて少数の関数では明確なエラー指標を返さなかったり、 あいまいな戻り値を返したりするので、 `PyErr_Occurred()` で 明示的にエラーテストを行う必要があります。

例外時の状態情報 (exception state)は、スレッド単位に用意された 記憶領域 (per-thread storage) 内で管理されます (この記憶領域は、 スレッドを使わないアプリケーションではグローバルな記憶領域と 同じです)。 一つのスレッドは二つの状態のどちらか: 例外が発生したか、まだ発生 していないか、をとります。 関数 `PyErr_Occurred()` を使うと、この状態を調べられます: この関数は例外が発生した際にはその例外型オブジェクトに対する 借用参照 (borrowed reference) を返し、そうでないときには `NULL` を返します。例外状態を設定する関数は数多くあります: `PyErr_SetString()` はもっとも よく知られている (が、もっとも汎用性のない) 例外を設定するための 関数で、`PyErr_Clear()` は 例外状態情報を消し去る関数です。

完全な例外状態情報は、3 つのオブジェクト: 例外の型、例外の値、 そしてトレースバック、からなります (どのオブジェクトも `NULL`を取り得ます)。これらの情報は、 Python オブジェクトの `sys.exc_type`, `sys.exc_value`, および `sys.exc_traceback` と同じ意味を持ちます; とはいえ、 C と Python の例外状態情報は全く同じではありません: Python における 例外オブジェクトは、Python の `try` ... `except` 文で最近処理したオブジェクトを表す一方、 C レベルの例外状態情報が存続するのは、 渡された例外情報を `sys.exc_type`その他に転送するよう取り計らう Python のバイトコードインタプリタのメインループに到達するまで、 例外が関数の間で受け渡しされている間だけです。

Python 1.5 からは、Python で書かれたコードから例外状態情報にアクセス する方法として、推奨されていてスレッドセーフな方法は `sys.exc_info()` になっているので注意してください。 この関数は Python コードの実行されているスレッドにおける 例外状態情報を返します。 また、これらの例外状態情報に対するアクセス手段は、両方とも 意味づけ (semantics) が変更され、ある関数が例外を捕捉すると、 その関数を実行しているスレッドの例外状態情報を保存して、呼び出し側の 呼び出し側の例外状態情報を維持するようになりました。 この変更によって、無害そうに見える関数が現在扱っている例外を上書き することで引き起こされる、例外処理コードでよくおきていたバグを 抑止しています; また、トレースバック内のスタックフレームで 参照されているオブジェクトがしばしば不必要に寿命を永らえて いたのをなくしています。

一般的な原理として、ある関数が別の関数を呼び出して何らかの作業を させるとき、呼び出し先の関数が例外を送出していないか調べなくては ならず、もし送出していれば、その例外状態情報は呼び出し側に 渡されなければなりません。 呼び出し元の関数はオブジェクト参照の所有権をすべて放棄し、 エラー指標を返さなくてはなりませんが、余計に例外を設定 する必要は ありません -- そんなことをすれば、たった今 送出されたばかりの例外を上書きしてしまい、エラーの原因そのもの に関する重要な情報を失うことになります。

例外を検出して渡す例は、上の `sum_sequence()` で示しています。 偶然にも、この例ではエラーを検出した際に何ら参照を放棄する必要が ありません。以下の関数の例では、エラーに対する後始末について示して います。まず、どうして Python で書くのが好きか思い出してもらうために、 等価な Python コードを示します:

    def incr_item(dict, key):
        try:
            item = dict[key]
        except KeyError:
            item = 0
        dict[key] = item + 1

以下は対応するコードを C で完璧に書いたものです:

    int
    incr_item(PyObject *dict, PyObject *key)
    {
        /* Py_XDECREF 用に全てのオブジェクトを NULL で初期化 */
        PyObject *item = NULL, *const_one = NULL, *incremented_item = NULL;
        int rv = -1; /* 戻り値の初期値を -1 (失敗) に設定しておく */

        item = PyObject_GetItem(dict, key);
        if (item == NULL) {
            /* KeyError だけを処理: */
            if (!PyErr_ExceptionMatches(PyExc_KeyError))
                goto error;

            /* エラーを無かったことに (clear) してゼロを使う: */
            PyErr_Clear();
            item = PyInt_FromLong(0L);
            if (item == NULL)
                goto error;
        }
        const_one = PyInt_FromLong(1L);
        if (const_one == NULL)
            goto error;

        incremented_item = PyNumber_Add(item, const_one);
        if (incremented_item == NULL)
            goto error;

        if (PyObject_SetItem(dict, key, incremented_item) < 0)
            goto error;
        rv = 0; /* うまくいった場合 */
        /* 後始末コードに続く */

     error:
        /* 成功しても失敗しても使われる後始末コード */

        /* NULL を参照している場合は無視するために Py_XDECREF() を使う */
        Py_XDECREF(item);
        Py_XDECREF(const_one);
        Py_XDECREF(incremented_item);

        return rv; /* エラーなら -1 、 成功なら 0 */
    }

なんとこの例は C で `goto` 文を使うお勧めの方法まで示して いますね! この例では、特定の例外を処理するために `PyErr_ExceptionMatches()` および `PyErr_Clear()` をどう使うかを 示しています。また、所有権を持っている参照で、値が `NULL` に なるかもしれないものを捨てるために `Py_XDECREF()` をどう使うかも示しています (関数名に "`X`" が付いていることに 注意してください; `Py_DECREF()` は `NULL` 参照に出くわすと クラッシュします)。正しく動作させるためには、所有権を持つ参照を 保持するための変数を `NULL` で初期化することが重要です; 同様に、 あらかじめ戻り値を定義する際には値を `-1` (失敗) で初期化して おいて、最後の関数呼び出しまでうまくいった場合にのみ `0` (成功) に設定します。