2016-03-14 44 views
6

To Obserwowane zachowanie:Nieoczekiwane zachowanie itertools.groupby

In [4]: x = itertools.groupby(range(10), lambda x: True) 

In [5]: y = next(x) 

In [6]: next(x) 
--------------------------------------------------------------------------- 
StopIteration        Traceback (most recent call last) 
<ipython-input-6-5e4e57af3a97> in <module>() 
----> 1 next(x) 

StopIteration: 

In [7]: y 
Out[7]: (True, <itertools._grouper at 0x10a672e80>) 

In [8]: list(y[1]) 
Out[8]: [9] 

Oczekiwany wyjście list(y[1]) jest [0,1,2,3,4,5,6,7,8,9]

Co tu się dzieje?

Zauważyłem to na cpython 3.4.2, ale inni widzieli to z cpython 3.5 i IronPython 2.9.9a0 (2.9.0.0) on Mono 4.0.30319.17020 (64-bit).

zaobserwowane zachowanie na Jython 2.7.0 i pypy:

Python 2.7.10 (5f8302b8bf9f, Nov 18 2015, 10:46:46) 
[PyPy 4.0.1 with GCC 4.8.4] 

>>>> x = itertools.groupby(range(10), lambda x: True) 
>>>> y = next(x) 
>>>> next(x) 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
StopIteration 
>>>> y 
(True, <itertools._groupby object at 0x00007fb1096039a0>) 
>>>> list(y[1]) 
[] 

Odpowiedz

6

itertools.groupby dokumentacja mówi, że

itertools.groupby(iterable, key=None)

[...]

Działanie groupby() jest podobny do filtra uniq w systemie Unix. Generuje przerwę lub nową grupę za każdym razem, gdy zmienia się wartość funkcji klucza (dlatego zazwyczaj konieczne jest posortowanie danych przy użyciu tej samej funkcji klawiszy). To zachowanie różni się od GROUP BY grupy SQL, która agreguje wspólne elementy, niezależnie od ich kolejności wprowadzania.

Zwrócona grupa sama jest iteratorem, który dzieli obiekt bazowy z wartością iteracji przy pomocy groupby(). Ponieważ źródło jest współużytkowane, gdy obiekt `groupby() jest zaawansowany, poprzednia grupa nie jest już widoczna. Tak więc, jeśli dane są potrzebne później, to powinny być przechowywane w postaci listy [-]

Więc założenie z ostatnim akapicie jest to, że wygenerowany lista będzie pusta lista [], ponieważ iterator już się rozwinął i poznał StopIteration; ale zamiast tego w CPython wynik jest zaskakujący: [9].


To dlatego, że _grouper iterator pozostaje jedną pozycję za oryginalnym iterator, który jest dlatego groupby potrzeby zaglądać jedną pozycję do przodu, aby zobaczyć, czy należy on do prądu lub następnej grupy, ale to musi być w stanie później wydaj ten element jako pierwszy element nowej grupy.

Jednak currkey i currvalue atrybuty groupbynie zresetowane gdy original iterator is exhausted, więc currvalue nadal wskazuje na ostatniej pozycji z iteracyjnej.

Dokumentacja CPython faktycznie zawiera ten równoważny kod, który ma również dokładnie takie samo zachowanie jak kod wersji C:

class groupby: 
    # [k for k, g in groupby('AAAABBBCCDAABBB')] --> A B C D A B 
    # [list(g) for k, g in groupby('AAAABBBCCD')] --> AAAA BBB CC D 
    def __init__(self, iterable, key=None): 
     if key is None: 
      key = lambda x: x 
     self.keyfunc = key 
     self.it = iter(iterable) 
     self.tgtkey = self.currkey = self.currvalue = object() 
    def __iter__(self): 
     return self 
    def __next__(self): 
     while self.currkey == self.tgtkey: 
      self.currvalue = next(self.it) # Exit on StopIteration 
      self.currkey = self.keyfunc(self.currvalue) 
     self.tgtkey = self.currkey 
     return (self.currkey, self._grouper(self.tgtkey)) 
    def _grouper(self, tgtkey): 
     while self.currkey == tgtkey: 
      yield self.currvalue 
      try: 
       self.currvalue = next(self.it) 
      except StopIteration: 
       return 
      self.currkey = self.keyfunc(self.currvalue) 

szczególności tablice __next__ odnajduje pierwszy element z następnej grupy i zapisuje to jego klucz do self.currkey i jego wartość do self.currvalue. Ale kluczem jest linia

self.currvalue = next(self.it) # Exit on StopIteration 

Kiedy next rzuca StopItertionself.currvalue nadal zawiera ostatni klucz poprzedniej grupy. Teraz, gdy y[1] zostanie przekształcony w list, to najpierw najpierw daje wartość self.currvalue, a następnie uruchamia next() na podstawowym iteratorze (i znów spotyka się z StopIteration).


Chociaż istnieje Python równoważne w dokumentacji, że zachowuje się dokładnie tak autorytatywnego realizacji kodu C w CPython, IronPython, Jython i pypy dają różne wyniki.

2

Problemem jest to, że grupa ich wszystkich w jedną grupę, więc po pierwszym next wezwanie wszystko jest już pogrupowane:

import itertools 
x = itertools.groupby(range(10), lambda x: True) 
key, elements = next(x) 

ale elements to generator, więc musisz go natychmiast przekazać do jakiejś struktury, wykonując iterację, aby "wydrukować" lub "zapisać", tj. list:

print('Key: "{}" with value "{}"'.format(key, list(elements))) 

i wówczas range(10) jest pusta i Groupy-generator jest zakończona:

Key: True with value [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]