1. 程式人生 > >Python Cookbook(第3版)中文版:15.14 傳遞Unicode字符串給C函數庫

Python Cookbook(第3版)中文版:15.14 傳遞Unicode字符串給C函數庫

pre 緩存 標準 解決 pep 存儲 nal body clas

15.14 傳遞Unicode字符串給C函數庫?

問題?

你要寫一個擴展模塊,需要將一個Python字符串傳遞給C的某個庫函數,但是這個函數不知道該怎麽處理Unicode。

解決方案?

這裏我們需要考慮很多的問題,但是最主要的問題是現存的C函數庫並不理解Python的原生Unicode表示。
因此,你的挑戰是將Python字符串轉換為一個能被C理解的形式。

為了演示的目的,下面有兩個C函數,用來操作字符串數據並輸出它來調試和測試。
一個使用形式為 char *, int 形式的字節,
而另一個使用形式為 wchar_t *, int 的寬字符形式:

void print_chars(char *s, int len) {
  int n = 0;

  while (n < len) {
    printf("%2x ", (unsigned char) s[n]);
    n++;
  }
  printf("\n");
}

void print_wchars(wchar_t *s, int len) {
  int n = 0;
  while (n < len) {
    printf("%x ", s[n]);
    n++;
  }
  printf("\n");
}

對於面向字節的函數 print_chars() ,你需要將Python字符串轉換為一個合適的編碼比如UTF-8.
下面是一個這樣的擴展函數例子:

static PyObject *py_print_chars(PyObject *self, PyObject *args) {
  char *s;
  Py_ssize_t  len;

  if (!PyArg_ParseTuple(args, "s#", &s, &len)) {
    return NULL;
  }
  print_chars(s, len);
  Py_RETURN_NONE;
}

對於那些需要處理機器本地 wchar_t

類型的庫函數,你可以像下面這樣編寫擴展代碼:

static PyObject *py_print_wchars(PyObject *self, PyObject *args) {
  wchar_t *s;
  Py_ssize_t  len;

  if (!PyArg_ParseTuple(args, "u#", &s, &len)) {
    return NULL;
  }
  print_wchars(s,len);
  Py_RETURN_NONE;
}

下面是一個交互會話來演示這個函數是如何工作的:

>>> s = ‘Spicy Jalape\u00f1o‘
>>> print_chars(s)
53 70 69 63 79 20 4a 61 6c 61 70 65 c3 b1 6f
>>> print_wchars(s)
53 70 69 63 79 20 4a 61 6c 61 70 65 f1 6f
>>>

仔細觀察這個面向字節的函數 print_chars() 是怎樣接受UTF-8編碼數據的,
以及 print_wchars() 是怎樣接受Unicode編碼值的

討論?

在繼續本節之前,你應該首先學習你訪問的C函數庫的特征。
對於很多C函數庫,通常傳遞字節而不是字符串會比較好些。要這樣做,請使用如下的轉換代碼:

static PyObject *py_print_chars(PyObject *self, PyObject *args) {
  char *s;
  Py_ssize_t  len;

  /* accepts bytes, bytearray, or other byte-like object */
  if (!PyArg_ParseTuple(args, "y#", &s, &len)) {
    return NULL;
  }
  print_chars(s, len);
  Py_RETURN_NONE;
}

如果你仍然還是想要傳遞字符串,
你需要知道Python 3可使用一個合適的字符串表示,
它並不直接映射到使用標準類型 char *wchar_t * (更多細節參考PEP 393)的C函數庫。
因此,要在C中表示這個字符串數據,一些轉換還是必須要的。
PyArg_ParseTuple() 中使用”s#” 和”u#”格式化碼可以安全的執行這樣的轉換。

不過這種轉換有個缺點就是它可能會導致原始字符串對象的尺寸增大。
一旦轉換過後,會有一個轉換數據的復制附加到原始字符串對象上面,之後可以被重用。
你可以觀察下這種效果:

>>> import sys
>>> s = ‘Spicy Jalape\u00f1o‘
>>> sys.getsizeof(s)
87
>>> print_chars(s)
53 70 69 63 79 20 4a 61 6c 61 70 65 c3 b1 6f
>>> sys.getsizeof(s)
103
>>> print_wchars(s)
53 70 69 63 79 20 4a 61 6c 61 70 65 f1 6f
>>> sys.getsizeof(s)
163
>>>

對於少量的字符串對象,可能沒什麽影響,
但是如果你需要在擴展中處理大量的文本,你可能想避免這個損耗了。
下面是一個修訂版本可以避免這種內存損耗:

static PyObject *py_print_chars(PyObject *self, PyObject *args) {
  PyObject *obj, *bytes;
  char *s;
  Py_ssize_t   len;

  if (!PyArg_ParseTuple(args, "U", &obj)) {
    return NULL;
  }
  bytes = PyUnicode_AsUTF8String(obj);
  PyBytes_AsStringAndSize(bytes, &s, &len);
  print_chars(s, len);
  Py_DECREF(bytes);
  Py_RETURN_NONE;
}

而對 wchar_t 的處理時想要避免內存損耗就更加難辦了。
在內部,Python使用最高效的表示來存儲字符串。
例如,只包含ASCII的字符串被存儲為字節數組,
而包含範圍從U+0000到U+FFFF的字符的字符串使用雙字節表示。
由於對於數據的表示形式不是單一的,你不能將內部數組轉換為 wchar_t * 然後期望它能正確的工作。
你應該創建一個 wchar_t 數組並向其中復制文本。
PyArg_ParseTuple() 的”u#”格式碼可以幫助你高效的完成它(它將復制結果附加到字符串對象上)。

如果你想避免長時間內存損耗,你唯一的選擇就是復制Unicode數據懂啊一個臨時的數組,
將它傳遞給C函數,然後回收這個數組的內存。下面是一個可能的實現:

static PyObject *py_print_wchars(PyObject *self, PyObject *args) {
  PyObject *obj;
  wchar_t *s;
  Py_ssize_t len;

  if (!PyArg_ParseTuple(args, "U", &obj)) {
    return NULL;
  }
  if ((s = PyUnicode_AsWideCharString(obj, &len)) == NULL) {
    return NULL;
  }
  print_wchars(s, len);
  PyMem_Free(s);
  Py_RETURN_NONE;
}

在這個實現中,PyUnicode_AsWideCharString() 創建一個臨時的wchar_t緩沖並復制數據進去。
這個緩沖被傳遞給C然後被釋放掉。
但是我寫這本書的時候,這裏可能有個bug,後面的Python問題頁有介紹。

如果你知道C函數庫需要的字節編碼並不是UTF-8,
你可以強制Python使用擴展碼來執行正確的轉換,就像下面這樣:

static PyObject *py_print_chars(PyObject *self, PyObject *args) {
  char *s = 0;
  int   len;
  if (!PyArg_ParseTuple(args, "es#", "encoding-name", &s, &len)) {
    return NULL;
  }
  print_chars(s, len);
  PyMem_Free(s);
  Py_RETURN_NONE;
}

最後,如果你想直接處理Unicode字符串,下面的是例子,演示了底層操作訪問:

static PyObject *py_print_wchars(PyObject *self, PyObject *args) {
  PyObject *obj;
  int n, len;
  int kind;
  void *data;

  if (!PyArg_ParseTuple(args, "U", &obj)) {
    return NULL;
  }
  if (PyUnicode_READY(obj) < 0) {
    return NULL;
  }

  len = PyUnicode_GET_LENGTH(obj);
  kind = PyUnicode_KIND(obj);
  data = PyUnicode_DATA(obj);

  for (n = 0; n < len; n++) {
    Py_UCS4 ch = PyUnicode_READ(kind, data, n);
    printf("%x ", ch);
  }
  printf("\n");
  Py_RETURN_NONE;
}

在這個代碼中,PyUnicode_KIND()PyUnicode_DATA()
這兩個宏和Unicode的可變寬度存儲有關,這個在PEP 393中有描述。
kind 變量編碼底層存儲(8位、16位或32位)以及指向緩存的數據指針相關的信息。
在實際情況中,你並不需要知道任何跟這些值有關的東西,
只需要在提取字符的時候將它們傳給 PyUnicode_READ() 宏。

還有最後幾句:當從Python傳遞Unicode字符串給C的時候,你應該盡量簡單點。
如果有UTF-8和寬字符兩種選擇,請選擇UTF-8.
對UTF-8的支持更加普遍一些,也不容易犯錯,解釋器也能支持的更好些。
最後,確保你仔細閱讀了 關於處理Unicode的相關文檔

艾伯特(http://www.aibbt.com/)國內第一家人工智能門戶

Python Cookbook(第3版)中文版:15.14 傳遞Unicode字符串給C函數庫