Doge log

Abby CTO 雑賀 力王のオフィシャルサイトです

Python3 Advent Calendar - Pythonで2/3両方で動くコードを書く(C/API)

これは Python3 Advent Calendarの記事です。
こんにちわ、高校生です。
先日、子供(遼くん)が無事生まれましたが、毎日お世話で忙しいです。

Python3 Advent Calendar ということでPython3に関して書いてみたいと思います。
Python3対応というのはまあいろいろあるんですが、Pure Pythonで両方動くコード書こうとなると文法とかいろいろ面倒ですね。
ですがC/APIで書くとマクロで大きく処理を2系、3系と切り替えができるのでので容易に両バージョンをサポートするコードが書けます。
ということで2/3両方をサポートするコードをC/APIを中心に幾つか書いてみたいと思います。
(3系といっても3.2以降と思ってください)

Python3の判別

Python3であるかどうかは元々用意されているPythonバージョンを確認できるマクロで容易に判断できます。

例:Python3系

#if PY_MAJOR_VERSION >= 3
#define PY3
#endif

単純に3系の場合にはこんな感じですね。

PythonのC/APIをCで利用する際のAPIが3.1以降変わったため、それらを考慮するならばこのような書き方になります。

例:Python3.1以降

#if (PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 1) || PY_MAJOR_VERSION > 3
#define PY3
#endif

PY_MINOR_VERSIONの他にも、HEXバージョンで判断する方法もあります。
(多分HEXが一番楽かも)
毎回、長いマクロ名を書くのがだるいので共通ヘッダなどに短めに再定義してしまうと楽だと思います。

モジュールの定義

モジュールの作成方法も2/3で変わっています。
ここでは先程の判定マクロを使用して2と3で分岐しています。

例:書き方の例

static PyMethodDef CoreMethods[] = {
    {NULL, NULL, 0, NULL}        /* Sentinel */
};


#ifdef PY3
#define INITERROR return NULL

static struct PyModuleDef core_module_def = {
    PyModuleDef_HEAD_INIT,
    MODULE_NAME,
    NULL,
    -1,
    CoreMethods,
};

PyObject *
PyInit_core(void)
#else
#define INITERROR return

PyMODINIT_FUNC
initcore(void)
#endif
{
    PyObject *m;

#ifdef PY3
    m = PyModule_Create(&core_module_def);
#else
    m = Py_InitModule3(MODULE_NAME, CoreMethods, "");
#endif
    if(m == NULL){
        INITERROR;
    }
...
#ifdef PY3
    return m;
#endif

}

大きく異なるのはPy_InitModule3などがなくなった点です。
モジュール定義用の構造体にモジュール情報をセットしてPyModule_Createでモジュールを作成します。
またPython3以降は作成したモジュールオブジェクト返さないといけません。
(返り値が異なる。初期化メソッド名なども変更)
その他、モジュールの関数の定義などは特に変更はないです。

PyInt系関数の廃止

Python3からはPyInt_から始まる関数は排除されています。
Python3からは全てPyLongに統一する必要があります。
特に問題なければ2/3でもPyLongの関数を使ってしまってもいいかも知れませんね。
生成だけではばくチェックなどもPyLongの関数に変更になります。

#ifdef PY3
    if(!PyLong_Check(sec)){
#else
    if(!PyInt_Check(sec)){
#endif

文字列の生成

色々なところで触れられていますが、Python3では通常文字列は全てUnicode型になります。
今までのstr型と同じような動作にしたい場合にはPyBytes系(PyString)の関数を使用することになります。

    char *tmp;

...

#ifdef PY3
    return PyUnicode_FromString(tmp);
#else
    //あるいはreturn PyString_FromString(tmp);
    return PyBytes_FromString(tmp);
#endif

Python2.6以降はPyBytes系の関数が既に存在しているため、そちらを使う事を推奨します。
PyUnicode_FromStringは渡された文字列がUTF-8 encodedなものとして処理をします。多くの場合はこれで事足りると思います。

BytesとUnicode

Python3でも全てUnicodeを使えばいいのか?というとそうではありません。
生データを必要とする場合には、適時PyBytes_FromStringでBytesを返す必要があります。

例:

  1. ファイルからのデータの読込結果
  2. ソケットから読み込んだデータ

興味があればPEP-3333などを読むと使い分けに対する理解が深まるかも知れません。

char*型の参照

逆にPythonの文字列型をCで参照する場合にもUnicodeAPIを使います。

    char *ch;
    char *tmp;
....

#ifdef PY3
    PyObject *latin_item;
    latin_item = PyUnicode_AsLatin1String(ip);
    ch = PyBytes_AsString(latin_item);
    Py_DECREF(latin_item);
#else
    ch = PyBytes_AsString(ip);

#endif

上記の例はlatin1のUnicodeである前提で処理しています。
(ネットワーク系の処理ではこれで十分だったりします。)
必要に応じてPyUnicode_Decode関数でデコードし、参照する必要があります。
上記ではUnicodeかどうかのチェックをしていませんが、必要に応じてPyUnicode_Checkを呼び出して下さい。

C API の提供、およびImport

あまり使われてないようですがC/APIで書かれたモジュールは他のCモジュールでも使用できるAPIを提供することができます。
2.7および3.2以降からは以前のPyCObject_FromVoidPtrではなくPyCapsule_系のAPIを使用します。

例:提供する側

#if (PY_MAJOR_VERSION == 2 && PY_MINOR_VERSION >= 7) || (PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 1) 
#define USE_PYCAPSULE
#endif

...

        PyObject *c_api_object;
        static void *_C_API[8];
        ....

#ifdef USE_PYCAPSULE
        c_api_object = PyCapsule_New((void *) _C_API, "xxxxx._C_API", NULL);
#else
        c_api_object = PyCObject_FromVoidPtr((void *) _C_API, NULL);
#endif
        if (c_api_object != NULL)
        {
                PyModule_AddObject(m, "_C_API", c_api_object);
        }

例:使用する側

#ifdef USE_PYCAPSULE
#define C_API_Import() \
{ \
        _C_API = (void**)PyCapsule_Import("xxxxx._C_API", 0); \
}
#else
#define C_API_Import() \
{ \
        PyObject *module = PyImport_ImportModule("xxxxx"); \
        if (module != NULL) { \
                PyObject *c_api_object = PyObject_GetAttrString( \
                        module, "_C_API"); \
                if (c_api_object != NULL && PyCObject_Check(c_api_object)) { \
                        _C_API = \
                                (void **) PyCObject_AsVoidPtr(c_api_object); \
                        Py_DECREF(c_api_object); \
                } \
                Py_DECREF(module); \
        } \
}
#endif

Pure PythonでPython3を判別する

Python側でも簡単に判別することができます。
マクロ同様、短めの関数名で定義しておくと使うとき楽だと思います。

import sys

def is_py3():
    return sys.hexversion >=  0x3000000

モジュール定義時に大きく分岐させることで両方で動くモジュールを作成することが出来ます。

例:

if is_py3():

    #python3系の処理を書く

    class socket(_socket.socket):
	....
else:

    class socket(_socket.socket):
	....

とざっくり両バージョンで動かすためのコードを紹介しました。
両方をサポートするコードを書いた場合にはtoxなどでテストをすることをオススメします。
(toxについてはそのうち誰かが書くと思います)

次はid:doloopwhileさんです。