Python 3.0 Hacks

第10回argument"s" clinic

はじめに

Pythonでは関数に引数を不定個数渡し、それらをタプルや辞書として参照できる機能があります。かなり便利な機能で、すっきり書く上で使いでがあります。しかしながら、引数を順番で指定する機能とともに利用されることで、人間が間違えてバグを埋め込む可能性を増やしていました。これに対して3.0では引数の名前を明示的に指定しなければ束縛できないkeyword only argumentという機能が導入されました。

Pythonにおける引数の渡し方

まずは知識の確認と、それを持ち合わせていない方のための下準備です。⁠引数をタプルや辞書として参照する、タプルや辞書を渡し引数に展開させる」ことについて、2.xのPythonを使って説明したいと思います。

引数をタプルや辞書として参照する

「タプル(tuple)として参照する」とは、リスト1に挙げる例のようなものを指します。2つ以上の引数をfooに渡した場合、それらはargsという名前のtupleとして参照され、fooの内部で使うことができます。引数は前から順番に使われていって、余ったものが全てargsに行く(tupleに参照される)という作りになっています。

リスト1 tuple sample
In [1]: def foo(a, *args):
   ...:     print args
   ...:     
   ...:     

In [2]: foo(1, 2, 3, 4)
(2, 3, 4)

In [3]: foo(1)
()

同様に「辞書として参照する」場合は、リスト2のようにkeyを指定して引数を渡すと、それらが全て辞書に格納されます。注意すべき点は最後の a=1 を渡したケースで、仮引数のaによって使われてしまい、**kwに残らない点です。

リスト2 dict sample
In [4]: def bar(a, **kw):
   ...:     print kw
   ...:     
   ...:     

In [5]: bar(1)
{}

In [6]: bar(1, b=2, c=3, d=4)
{'c': 3, 'b': 2, 'd': 4}

In [7]: bar(b=2, c=3, d=4, a=1)
{'c': 3, 'b': 2, 'd': 4}

タプルや辞書を渡し引数に展開させる

今度は逆にtupleが展開されて仮引数に束縛されるケースです。リスト3の例で、tuple xsの内容が前から順番にa, b, cに束縛されているのがわかると思います。

リスト3
In [9]: def buzz(a, b, c):
   ...:     print a, b, c
   ...:     
   ...:     

In [10]: xs = (1, 2, 3,)

In [11]: buzz(*xs)
1 2 3

最後は辞書が展開されて仮引数に束縛されるケースですリスト4⁠。リスト3の例で出てきたbuzz関数に辞書ysを渡してみます。

リスト4
In [12]: ys = {"c":1, "b":2, "a": 3}

In [13]: buzz(**ys)
3 2 1

組み合わせてみる

これらを組み合わせて使うことができます。

リスト5
In [14]: foo(*xs)
(2, 3)

この場合はxsの先頭から順番に要素が引数に割り当てられ、残りがargsに詰められるので、このような結果になります。

リスト6
In [15]: bar(**ys)
{'c': 1, 'b': 2}

同様に辞書ysの場合は、辞書の中からkeyにマッチする仮引数が割り当てられ、残ったitemが辞書kwに詰められます。

辞書とtupleの両方渡すことも可能ですが、仮引数はまず先にtupleを用いて填めていくのでxs, ysの両方をbuzzに渡すことはできません。リスト7のようにaが衝突していしまいます。

リスト7
In [16]: buzz(*xs, **ys)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

TypeError: buzz() got multiple values for keyword argument 'a'

ここまでのおさらいのための簡単なパズル

次のfを、assertに成功するように呼んでください。

def f(**kw):
	assert kw["class"] == 1
 pythonでclassは予約語です。答えはこの記事の一番最後に。

Pythonでありがちな*args/**kwsの使いかた

標準ライブラリの中にあるものをいくつか見ていきましょう。

オブジェクトのメンバを作るのに使う

このケースでは、辞書引数**kwで渡した名前と値の組が、インスタンス変数に代入されます(self.__dict__.updateの部分⁠⁠。

この場合は、インスタンス変数の名前をいちいちタイプしなくてよい、何かをwrapしたりインスタンスのユーザが好きなものを代入できる(下記の例の場合⁠⁠、というメリットがあります。デメリットとしては、初期化時ではなく実行時に問題が顕在化することで、外から渡したキーとインスタンス変数の名前の衝突や、名前が正しくタイプされていなかったために指定した値が処理に渡らない、などのバグを埋め込む可能性があげられます。

リスト8  _threading_local.pyからの抜粋
You can create custom local objects by subclassing the local class:

  >>> class MyLocal(local):
  ...     number = 2
  ...     initialized = False
  ...     def __init__(self, **kw):
  ...         if self.initialized:
  ...             raise SystemError('__init__ called too many times')
  ...         self.initialized = True
  ...         self.__dict__.update(kw)
  ...     def squared(self):
  ...         return self.number ** 2

今度はXMLParserでの例です。辞書kwのkeyをチェックしてインスタンス変数に代入しています。

リスト9 xmllib.pyから抜粋
class XMLParser:
    attributes = {}                     # default, to be overridden
    elements = {}                       # default, to be overridden

    # parsing options, settable using keyword args in __init__
    __accept_unquoted_attributes = 0
    __accept_missing_endtag_name = 0
    __map_case = 0
    __accept_utf8 = 0
    __translate_attribute_references = 1

    # Interface -- initialize and reset this instance
    def __init__(self, **kw):
        self.__fixed = 0
        if 'accept_unquoted_attributes' in kw:
            self.__accept_unquoted_attributes = kw['accept_unquoted_attributes']
        if 'accept_missing_endtag_name' in kw:
            self.__accept_missing_endtag_name = kw['accept_missing_endtag_name']
        if 'map_case' in kw:
            self.__map_case = kw['map_case']
        if 'accept_utf8' in kw:
            self.__accept_utf8 = kw['accept_utf8']
        if 'translate_attribute_references' in kw:
            self.__translate_attribute_references = kw['translate_attribute_references']
        self.reset()

このように、必要とあればキーワード引数のvalueだけでなく、keyも使うことができます。⁠あるkeywordをもった引数を渡したこと」を利用できるのです。

下位にそのまま渡す

前項で説明した「タプル・辞書で受け取る」「タプル・辞書を展開して渡す」を組み合わせて次のように使われています。

リスト10 trace.pyから抜粋

    def runfunc(self, func, *args, **kw):
        result = None
        if not self.donothing:
            sys.settrace(self.globaltrace)
        try:
            result = func(*args, **kw)
        finally:
            if not self.donothing:
                sys.settrace(None)
        return result

decoratorを実装する場合も、decorateされる関数にdecorateされる前と同じように引数を渡すために使用します。

リスト11 test/test_struct.pyから抜粋
def with_warning_restore(func):                                        
    @wraps(func)                                                       
    def decorator(*args, **kw):                                        
        with warnings.catch_warnings():                                
            # We need this function to warn every time, so stick an    
            # unqualifed 'always' at the head of the filter list       
            warnings.simplefilter("always")                            
            warnings.filterwarnings("error", category=DeprecationWarning)
            return func(*args, **kw)                                   
    return decorator

処理が不定数個引数をとるケース

substituteが実装されている時点では不定個の引数です。引数に名前をつけてそのkeyを用いてtemplateへの代入を指定します。

リスト12 http://docs.python.org/library/string.html#template-strings から
>>> from string import Template
>>> s = Template('$who likes $what')
>>> s.substitute(who='tim', what='kung pao')
'tim likes kung pao'

heapqのmerge関数です。sort済みの入力を不定複数受け取り、1つのソート済み出力にマージします。

リスト13 lib/heapq.py
def merge(*iterables):
    '''Merge multiple sorted inputs into a single sorted output.       

    Similar to sorted(itertools.chain(*iterables)) but returns a generator,
    does not pull the data into memory all at once, and assumes that each of
    the input streams is already sorted (smallest to largest).         
    
    >>> list(merge([1,3,5,7], [0,2,4,8], [5,10,15,20], [], [25]))      
    [0, 1, 2, 3, 4, 5, 5, 7, 8, 10, 15, 20, 25]                        
    
    '''
    _heappop, _heapreplace, _StopIteration = heappop, heapreplace, StopIteration
        
    h = []  
    h_append = h.append
    for itnum, it in enumerate(map(iter, iterables)):                  
        try:
            next = it.__next__                                         
            h_append([next(), itnum, next])                            
        except _StopIteration:                                         
            pass                                                       
    heapify(h)
                
    while 1:                                                           
        try:    
            while 1: 
                v, itnum, next = s = h[0]   # raises IndexError when h is empty
                yield v
                s[0] = next()               # raises StopIteration when exhausted
                _heapreplace(h, s)          # restore heap condition   
        except _StopIteration:                                         
            _heappop(h)                     # remove empty iterator    
        except IndexError:                                             
            return

新たな機能

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 

function call with kwsが速くなった

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

まとめ

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

謝辞

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

参考資料

パズルの答え

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

おすすめ記事

記事・ニュース一覧