2016-03-14 32 views
6

Đây là hành vi quan sát:hành vi bất ngờ của 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] 

Sản lượng dự kiến ​​của list(y[1])[0,1,2,3,4,5,6,7,8,9]

gì đang xảy ra ở đây?

Tôi quan sát điều này trên cpython 3.4.2, nhưng những người khác đã thấy điều này với cpython 3.5IronPython 2.9.9a0 (2.9.0.0) on Mono 4.0.30319.17020 (64-bit).

Các hành vi quan sát được trên Jython 2.7.0 và 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]) 
[] 

Trả lời

6

itertools.groupby tài liệu nói rằng

itertools.groupby(iterable, key=None)

[...]

Hoạt động của groupby() tương tự như bộ lọc uniq trong Unix. Nó tạo ra một break hoặc nhóm mới mỗi khi giá trị của các chức năng chính thay đổi (đó là lý do tại sao nó thường là cần thiết để có được sắp xếp dữ liệu bằng cách sử dụng cùng một chức năng quan trọng). Hành vi đó khác với GROUP BY của SQL, tập hợp các phần tử phổ biến bất kể thứ tự đầu vào của chúng.

Nhóm được trả về tự là một trình lặp có chia sẻ lặp lại cơ bản với groupby(). Vì nguồn được chia sẻ, khi đối tượng `groupby() được nâng cao, nhóm trước đó không còn nhìn thấy được nữa. Vì vậy, nếu dữ liệu là cần thiết sau, nó nên được lưu trữ như một danh sách [-]

Vì vậy, các giả định từ đoạn cuối cùng là rằng danh sách được tạo ra sẽ là danh sách trống [], kể từ khi trình vòng lặp được nâng cao, và đã đáp ứng StopIteration; nhưng thay vì trong CPython kết quả là đáng ngạc nhiên [9].


Điều này là do _grouper iterator chậm một mục phía sau iterator gốc, đó là bởi vì groupby nhu cầu để nhìn trộm một mục trước để xem nếu nó thuộc về hiện tại hoặc các nhóm tiếp theo, nhưng nó phải có khả năng sau tạo mục này làm mục đầu tiên của nhóm mới.

Tuy nhiên currkeycurrvalue thuộc tính của groupbykhông reset khi original iterator is exhausted, vì vậy currvalue vẫn trỏ tới mục cuối cùng từ iterator.

Các tài liệu CPython thực sự chứa mã này tương đương, mà còn có hành vi chính xác giống như mã số phiên bản 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) 

Đáng chú ý là các __next__ tìm thấy mục đầu tiên của nhóm tiếp theo, và lưu trữ nó quan trọng của nó vào self.currkey và giá trị của nó là self.currvalue. Nhưng quan trọng là dòng

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

Khi next ném StopItertion các self.currvalue vẫn chứa chìa khóa cuối cùng của nhóm trước. Bây giờ, khi y[1] được tạo thành list, đầu tiên mang lại giá trị self.currvalue và chỉ sau đó chạy next() trên trình lặp cơ bản (và đáp ứng lại StopIteration).


Mặc dù có Python tương đương trong tài liệu, mà ứng xử chính xác như việc thực hiện mã C có thẩm quyền trong CPython, IronPython, Jython và PyPy cho kết quả khác nhau.

2

Vấn đề là bạn nhóm tất cả chúng vào một nhóm như vậy sau khi các next cuộc gọi đầu tiên tất cả mọi thứ đã được theo nhóm:

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

nhưng elements là một máy phát điện, vì vậy bạn cần phải chuyển nó ngay lập tức vào một số cấu trúc có thể lặp lại để "in" hoặc "lưu" nó, tức là một list:

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

và sau đó range(10) của bạn là trống rỗng và groupy-máy phát điện được hoàn thành:

Key: True with value [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 
Các vấn đề liên quan