2017-05-11 10 views
2

今日私が書いたan answerで発見した奇妙なものを理解しようとしています。基本的には、itertools.groupbyをラップするジェネレータ関数からグループを生成していました。私が発見した興味深いのは、ジェネレータを割り当ての左側でアンパックすると、ジェネレータの最後の要素が残ることです。例えば:`groupby`と引数展開の実装固有の振る舞い

$ python3 ~/sandbox/test_gb.py 
[] 
[(True, 9)] 

このCPython2.7とCPython3.5両方のケースである:CPythonのオン

# test_gb.py 
from itertools import groupby 
from operator import itemgetter 

inputs = ((x > 5, x) for x in range(10)) 

def make_groups(inputs): 
    for _, group in groupby(inputs, key=itemgetter(0)): 
     yield group 

a, b = make_groups(inputs) 
print(list(a)) 
print(list(b)) 

、これはもたらします。

PyPyで

、それは、その結果:いずれの場合も

$ pypy ~/sandbox/test_gb.py 
[] 
[] 

、最初の空のリスト(「a」)が説明するのはとても簡単です - itertoolsからのグループは、すぐに次の要素として消費されます必要とされている。これらの値はどこにも保存されていなかったので、エーテルに失われてしまいました。

取り出すときPythonはことを確認するために後に何を探すためにを必要とするため、あまりにも...、私たちは(あまりにもbを消費PyPyバージョンが第二空のリスト(「b」)のために理にかなっているように私には思えます解凍するアイテムの数が間違っているためにValueErrorを投げるべきではありません)。しかし何らかの理由で、CPythonのバージョンでは、入力iterableの最後の要素が保持されています...誰でもこの理由が説明できますか?

編集

これはおそらく多かれ少なかれ明白ですが、我々はまた、としてそれを書くことができますので、groupbyそれだ

inputs = ((x > 5, x) for x in range(10)) 
(_, a), (_, b) = groupby(inputs, key=itemgetter(0)) 
print(list(a)) 
print(list(b)) 

と同じ結果を得る...

+1

PyPyをCPythonのように変更しました。私はこれが本当に「なぜ」という質問に本当に答えるものではないことを知っています.CPythonからPyPyに正確なアルゴリズムをコピーするだけで、ドキュメントから再構築されたバージョンを使用するのではなく、同じ結果が得られます。 –

+1

動作は少し壊れているようです。 CPythonの問題報告でこの質問をしたいと思うかもしれません。おそらく、それは次の3.7リリースで修正されるでしょう。あるいは、現在の行動がより良い理由が誰かに見つかるかもしれません。またはあなたの問題は永遠に開いたままになります。 –

+1

興味のある人は、[このコミット](https://bitbucket.org/pypy/pypy/commits/6093ff1a44e6b17f09db83aa80aea562a738c286)でpypyの変更が行われたようです。 – mgilson

答えて

2

オブジェクトは簿記を処理し、オブジェクトはkeyと親groupbyオブジェクトを参照するだけです。

typedef struct { 
    PyObject_HEAD 
    PyObject *it;   /* iterator over the input sequence */ 
    PyObject *keyfunc;  /* the second argument for the groupby function */ 
    PyObject *tgtkey;  /* the key for the current "grouper" */ 
    PyObject *currkey;  /* the key for the current "item" of the iterator*/ 
    PyObject *currvalue; /* the plain value of the current "item" */ 
} groupbyobject; 

typedef struct { 
    PyObject_HEAD 
    PyObject *parent;  /* the groupby object */ 
    PyObject *tgtkey;  /* the key value for this grouper object. */ 
} _grouperobject; 

groupbyオブジェクトをアンパックするときにgrouperオブジェクトを反復しているわけではないので、今は無視します。それでは、興味深いのは、あなたがそれにnextを呼び出したときgroupbyで何が起こるかです:

static PyObject * 
groupby_next(groupbyobject *gbo) 
{ 
    PyObject *newvalue, *newkey, *r, *grouper; 

    /* skip to next iteration group */ 
    for (;;) { 
     if (gbo->currkey == NULL) 
      /* pass */; 
     else if (gbo->tgtkey == NULL) 
      break; 
     else { 
      int rcmp; 

      rcmp = PyObject_RichCompareBool(gbo->tgtkey, gbo->currkey, Py_EQ); 
      if (rcmp == 0) 
       break; 
     } 

     newvalue = PyIter_Next(gbo->it); 
     if (newvalue == NULL) 
      return NULL; /* just return NULL, no invalidation of attributes */ 
     newkey = PyObject_CallFunctionObjArgs(gbo->keyfunc, newvalue, NULL); 

     gbo->currkey = newkey; 
     gbo->currvalue = newvalue; 
    } 
    gbo->tgtkey = gbo->currkey; 

    grouper = _grouper_create(gbo, gbo->tgtkey); 
    r = PyTuple_Pack(2, gbo->currkey, grouper); 
    return r; 
} 

は、私はすべての無関係な例外処理コードおよび削除または簡略化された純粋な参照カウントのものを削除しました。ここで興味深いのは、イテレータの最後に到達したときにgbo->currkey,gbo->currvalueおよびgbo->tgtkeyNULLに設定されていない場合は、PyIter_Next(gbo->it) == NULLの場合はちょうどreturn NULLなので、最後に検出された値(イテレータの最後のアイテム) 。

これが完了したら、2つのgrouperオブジェクトがあります。最初のものはtgtvalueFalseで、2番目がTrueです。

static PyObject * 
_grouper_next(_grouperobject *igo) 
{ 
    groupbyobject *gbo = (groupbyobject *)igo->parent; 
    PyObject *newvalue, *newkey, *r; 
    int rcmp; 

    if (gbo->currvalue == NULL) { 
     /* removed because irrelevant. */ 
    } 

    rcmp = PyObject_RichCompareBool(igo->tgtkey, gbo->currkey, Py_EQ); 
    if (rcmp <= 0) 
     /* got any error or current group is end */ 
     return NULL; 

    r = gbo->currvalue; /* this accesses the last value of the groupby object */ 
    gbo->currvalue = NULL; 
    gbo->currkey = NULL; 

    return r; 
} 

のでcurrvalueNULLではないので、最初のif分岐は興味深いものではありません覚えている:あなたはこれらのgrouperの上nextを呼び出すときに何が起こるか見てみましょう。あなたの最初のグルーパーについては、groupergroupbyオブジェクトのtgtkeyを比較し、それらが異なっていると見て、それはすぐにreturn NULLになります。だからあなたは空リストを持っています。 tgtkey Sが同一であるので、それは、groupbyオブジェクトcurrvalue戻ります(イテレータの最後の遭遇値である!)が、今回はそれがcurrvaluecurrkeyのを設定します第二イテレータについては

groupbyのオブジェクトはNULLです。

バックのpythonに切り替え

:あなたのgroupbyで最後のグループと同じtgtkeygrouperを持っている場合は本当に面白い癖が起こる:

import itertools 

>>> inputs = [(x > 5, x) for x in range(10)] + [(False, 10)] 
>>> (_, g1), (_, g2), (_, g3) = itertools.groupby(inputs, key=lambda x: x[0]) 
>>> list(g1) 
[(False, 10)] 
>>> list(g3) 
[] 

g1内の1つの要素が属していなかったこと最初のグルーパーオブジェクトのtgtkeyFalseであり、最後のtgtkeyFalseであるため、最初のグループはそれが最初のグループに属すると考えました。また、groupbyオブジェクトを無効にして、3番目のグループが空になるようにしました。


すべてのコードはthe Python source codeから取得されていますが、短縮されています。