Python 3.0 Hacks

第10回 argument"s" clinic

この記事を読むのに必要な時間:およそ 7.5 分

新たな機能

PEP 3102で提案された機能"Keyword only argument"

文法を見ても直感的ではないので,PEP3102で出されている例を見ていきましょう。

def sortwords(*wordlist, case_sensitive=False):
     ...

という関数は,

sortwords("Python", "Ruby", "Lisp", "Perl", "Java")
sortwords("Python", "Ruby", "Lisp", "Perl", "Java", case_sensiteive=True)

というような使われかたを想定しています。不定数個の引数としてwordを渡すことができます。

3.0以前では,このような書き方は許されておらず,

def sortwords(*wordlist, **kwargs):
     ...

としか書けません。ドキュメントに書いた上でkwargsから"case_sensiteive"をkeyに取り出さねばなりません。前出のXMLPaserの__init__関数の中でkeyを調べていたコードがそれに該当するでしょう。

さて,名前がついている必須の引数がある場合はどうなるでしょうか?

def compare(a, b, *, key=None):
    ...

この形式が最終的に投入された書式です。これどういうことかというと,3.0以前では次のように書かれていました。

def compare(a, b, key=None):
     ...

ただしこの形式だと,keyに位置で値を束縛することができてしまい,

compare(1, 2, 3)

と書くことができてしまい,keyに3を束縛することの意味がわかりません。ましてや

def f(*arg):
   return compare(*arg)

だったらコードの読み手を当惑させるでしょう。keyword only argumentはこのような状況に「くだらない出っぱりに指を引っかけて怪我をするような状況⁠⁠,ひいてはそれによって引き起こされてしまうかもしれないPythonのsketchのようなナンセンスな状況を未然に対処するために提案されたといっていいでしょう。

最終的な形式の覚えよくする話をすこししましょう。

def compare(a, b, *ignore, key=None)
     ...

*ignoreはa, b,より後ろに出てくるkeyと名前のついてないものをすべてtupleとしてうけとります。したがって,

def compare(a, b, *ignore, key=None):
    if ignore:  # If ignore is not empty
         raise TypeError

と書くことで,誤って呼び出し元が何かを引数を渡してしまったとき,ignoreが空にならずTypeErrorが投げられます。

*ignoreと書くことはタイプ数が多いですし,ignore tupleには通常使い道がないので,*ひとつで済ませるという書式が採用されたわけです。

具体的にkeyword only引数が3.0で使われているところを挙げておきましょう。それはbuiltinのsortedです。ビルトインのsorted()と組み込み型listオブジェクトのsortメソッドは比較関数cmpをとらなくなりました。その代わりkeyを渡します。keyとreverseはkeyword only引数です。ちなみに,keyはなんであるかというと,sequenceのitem1つを引数にとる関数で比較のキー値を返します。たとえば,key=str.lowerなどとしてあげると,小文字に変換して比較します。reverseはTrueを渡すと順序が逆になります。

任意のmapping objcetを辞書引数として渡す

記事をここまで読んでいると,**kwにユーザ定義の辞書が渡せたらいいなぁと思うのは人情です。Alexander Belopolskyのpatchによって,2.6からUserDictなどのdictのようなもの(mapping)を引数辞書として渡すことができるようになりました。patchの説明の例では,UserDictの例があげられています。

リスト14 

>>> def f(**kw):
...    print sorted(kw)
...
>>> ud=UserDict.UserDict()
>>> ud['a'] = 1
>>> ud['b'] = 'string'
>>> f(**ud)
['a', 'b']

cgiで出てくるFieldStoreageもmapping objectでしたね。

リスト15 

>>> import cgi
>>> f = cgi.FieldStorage()
>>> f
FieldStorage(None, None, [])
>>> def  foo(**kw):
...     print(kw)
... 
>>> foo(**f)
{}

この仕組みは一見ドキッとしますが,なんのことはない,実装はany mappingから組込みのdict型を作っています。

リスト16 前出のAlexander Belopolskyのpatchより

+               if (!PyDict_Check(kwdict)) {
+                       PyObject *d;
+                       d = PyDict_New();
+                       if (d == NULL)
+                               goto ext_call_fail;
+                       if (PyDict_Update(d, kwdict) != 0) {
+                               Py_DECREF(d);

それは結局,PyDict_Merge(Objects/dictobject.c)に落ちます。コメントによれば,PyMapping_Keys()とPyObject_GetItem()を持っていれば機能するようです。Pythonのレベルでは,keysと__getitem__に対応します。

リスト17

int
PyDict_Merge(PyObject *a, PyObject *b, int override)
{
  register PyDictObject *mp, *other;
  register Py_ssize_t i;
  PyDictEntry *entry;

  /* We accept for the argument either a concrete dictionary object,
   * or an abstract "mapping" object.  For the former, we can do
   * things quite efficiently.  For the latter, we only require that
   * PyMapping_Keys() and PyObject_GetItem() be supported.
   */
  if (a == NULL || !PyDict_Check(a) || b == NULL) {
    PyErr_BadInternalCall();
    return -1;
  }
  mp = (PyDictObject*)a;
  if (PyDict_Check(b)) {

いま興味があるのはmapping object(以下,bとよぶ)なので, then節に用はなくelse節に興味があります。先にでてきたコメントから想像がつきますが,べたべたなひねりない実装で,keyのシーケンスをmapping objectから取り出して,それをiterateしてb.get(key)してvalueを取り出し,aにset(key, value)しています。Py_DECREFは,参照カウンタを減らしているだけなので,実装の意味をとるときには気にしなくていいです。

リスト18

  else {
    /* Do it the generic, slower way */
    PyObject *keys = PyMapping_Keys(b);
    PyObject *iter;
    PyObject *key, *value;
    int status;

    if (keys == NULL)
      /* Docstring says this is equivalent to E.keys() so
       * if E doesn't have a .keys() method we want
       * AttributeError to percolate up.  Might as well
       * do the same for any other error.
       */
      return -1;

    iter = PyObject_GetIter(keys);
    Py_DECREF(keys);
    if (iter == NULL)
      return -1;

    for (key = PyIter_Next(iter); key; key = PyIter_Next(iter)) {
      if (!override && PyDict_GetItem(a, key) != NULL) {
        Py_DECREF(key);
        continue;
      }
      value = PyObject_GetItem(b, key);
      if (value == NULL) {
        Py_DECREF(iter);
        Py_DECREF(key);
        return -1;
      }
      status = PyDict_SetItem(a, key, value);
      Py_DECREF(key);
      Py_DECREF(value);
      if (status < 0) {
        Py_DECREF(iter);
        return -1;
      }
    }

function call with kwsが速くなった

キーワード引数は文字列比較して遅いのではないかと疑っている方は,Raymond Hettingerが最初にアイディアをだし,Antoine Pitrouが実装したpatch, issue 1819を当たられるといいと思います。このパッチの概要としては,文字列を構成しているbyteの全比較を行うのではなく,ポインタの比較によって文字列の比較とすることができるので,これにより高速化をしています(internedな文字列,さらに調べたい方はinternedで調べるといろいろわかるでしょう⁠⁠。

まとめ

Pythonでの生活を快適にしているキーワード引数に関して見てみました。3.0からさらに使い手に親切な作りになったことがご理解いただけたかと思います。

謝辞

この場を借りて記事を書く機会をくださった柴田さん,技術評論社の小坂さんに感謝します。

参考資料

パズルの答え

f(**{"class": 1})

著者プロフィール

保坂範行(ほさかのりゆき)

病気でお休みしていましたが活動再開に向けてウォームアップ中です。Life workと化しつつある趣味のゲーム(backgammon)のためのweb-siteを構築しています。局面のimageを動的に生成して返すサーバをPythonで実装&運用中です。

URLhttp://image.backgammonbase.com/

過去の仕事でいやな思いをした経験からPythonの読みやすさを重視する哲学に深く共感。

個人サイト

URLhttp://www.tonic-water.com/