2016-03-14 28 views
6

Questo è il comportamento osservato:Comportamento imprevisto di 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] 

Il risultato atteso di list(y[1]) è [0,1,2,3,4,5,6,7,8,9]

cosa sta succedendo qui?

Ho osservato questo su cpython 3.4.2, ma altri hanno visto questo con cpython 3.5 e IronPython 2.9.9a0 (2.9.0.0) on Mono 4.0.30319.17020 (64-bit).

il comportamento osservato sul Jython 2.7.0 e 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]) 
[] 

risposta

6

itertools.groupby documentazione dice che

itertools.groupby(iterable, key=None)

[...]

L'operazione di groupby() è simile al filtro uniq in Unix. Genera un'interruzione o un nuovo gruppo ogni volta che cambia il valore della funzione del tasto (che è il motivo per cui di solito è necessario aver ordinato i dati utilizzando la stessa funzione chiave). Questo comportamento differisce da GROUP BY di SQL che aggrega elementi comuni indipendentemente dal loro ordine di input.

Il gruppo restituito è esso stesso un iteratore che condivide il iterabile sottostante con groupby(). Poiché la sorgente è condivisa, quando l'oggetto `groupby() è avanzato, il gruppo precedente non è più visibile. Quindi, se è necessario che i dati più tardi, deve essere conservata come un elenco [-]

Così il presupposto dall'ultimo comma è che che l'elenco generato sarebbe la lista vuota [], da quando l'iteratore è già avanzato e ha incontrato StopIteration; ma invece in CPython il risultato è sorprendente [9].


Questo perché la _grouper iterator ritardo rispetto un elemento dietro l'iteratore originale, che è perché groupby ha bisogno di sbirciare un elemento in anticipo per vedere se appartiene alla corrente o il prossimo gruppo, tuttavia devono essere in grado di seguito cede questo articolo come primo elemento del nuovo gruppo.

Tuttavia le currkey e currvalue attributi del groupby sono non resettato quando il original iterator is exhausted, in modo currvalue ancora punti per l'ultimo elemento del iteratore.

La documentazione CPython contiene in realtà il codice equivalente, che ha anche lo stesso comportamento esattamente come il codice della versione 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) 

In particolare il __next__ trova il primo elemento del gruppo successivo, e lo memorizza la chiave in self.currkey e il suo valore a self.currvalue. Ma la chiave è la linea

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

Quando next tiri StopItertion il self.currvalue contiene ancora l'ultima chiave del gruppo precedente. Ora, quando y[1] viene convertito in ,, il primo produce il valore di self.currvalue e solo in seguito viene eseguito next() sull'iteratore sottostante (e soddisfa nuovamente StopIteration).


Anche se c'è Python equivalente nella documentazione, che si comporta esattamente come l'attuazione del codice C autorevole CPython, IronPython, Jython e PyPy dare risultati diversi.

2

Il problema è che si gruppo tutti loro in un unico gruppo così dopo il primo tutto next chiamata è già raggruppati:

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

ma il elements è un generatore, quindi è necessario passarlo immediatamente in qualche struttura assumendo un iterabile per "stamparlo" o "salvarlo", ovvero uno list:

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

e poi il tuo range(10) è vuota e la Groupy-generatore è finito:

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