Tôi muốn đổ một chút cắn nhẹ thêm về sự tương tác của iter, __iter__và __getitem__và những gì xảy ra đằng sau rèm cửa. Được trang bị kiến thức đó, bạn sẽ có thể hiểu tại sao điều tốt nhất bạn có thể làm là
try:
iter(maybe_iterable)
print('iteration will probably work')
except TypeError:
print('not iterable')
Tôi sẽ liệt kê các sự kiện trước và sau đó theo dõi nhanh chóng về những gì xảy ra khi bạn sử dụng một forvòng lặp trong python, sau đó là một cuộc thảo luận để minh họa các sự kiện.
Sự kiện
Bạn có thể nhận được một trình vòng lặp từ bất kỳ đối tượng nào obằng cách gọi iter(o)nếu ít nhất một trong các điều kiện sau đây đúng:
a) ocó một __iter__phương thức trả về một đối tượng lặp. Một iterator là bất kỳ đối tượng nào có phương thức __iter__và __next__(Python 2 next:).
b) ocó __getitem__phương pháp.
Kiểm tra một thể hiện của Iterablehoặc Sequence, hoặc kiểm tra thuộc tính __iter__là không đủ.
Nếu một đối tượng ochỉ thực hiện __getitem__, nhưng không __iter__, iter(o)sẽ xây dựng một trình vòng lặp cố gắng tìm nạp các mục từ ochỉ số nguyên, bắt đầu từ chỉ số 0. Trình lặp sẽ bắt bất kỳ IndexError(nhưng không có lỗi nào khác) được đưa ra và sau đó StopIterationtự tăng .
Theo nghĩa chung nhất, không có cách nào để kiểm tra xem trình lặp được trả về itercó lành mạnh hay không ngoài việc dùng thử.
Nếu một đối tượng othực hiện __iter__, iterhàm sẽ đảm bảo rằng đối tượng được trả về __iter__là một trình vòng lặp. Không có kiểm tra độ tỉnh táo nếu một đối tượng chỉ thực hiện __getitem__.
__iter__chiến thắng Nếu một đối tượng othực hiện cả hai __iter__và __getitem__, iter(o)sẽ gọi __iter__.
Nếu bạn muốn làm cho các đối tượng của riêng mình lặp lại, luôn luôn thực hiện __iter__phương thức.
for vòng lặp
Để làm theo, bạn cần hiểu về những gì xảy ra khi bạn sử dụng một forvòng lặp trong Python. Hãy bỏ qua ngay phần tiếp theo nếu bạn đã biết.
Khi bạn sử dụng for item in ocho một số đối tượng lặp lại o, Python gọi iter(o)và mong đợi một đối tượng lặp là giá trị trả về. Trình lặp là bất kỳ đối tượng nào thực hiện phương thức __next__(hoặc nexttrong Python 2) và __iter__phương thức.
Theo quy ước, __iter__phương thức của một trình vòng lặp sẽ trả về chính đối tượng đó (tức là return self). Python sau đó gọi nextiterator cho đến khi StopIterationđược nâng lên. Tất cả điều này xảy ra ngầm, nhưng trình diễn sau đây cho thấy nó:
import random
class DemoIterable(object):
def __iter__(self):
print('__iter__ called')
return DemoIterator()
class DemoIterator(object):
def __iter__(self):
return self
def __next__(self):
print('__next__ called')
r = random.randint(1, 10)
if r == 5:
print('raising StopIteration')
raise StopIteration
return r
Lặp lại qua một DemoIterable:
>>> di = DemoIterable()
>>> for x in di:
... print(x)
...
__iter__ called
__next__ called
9
__next__ called
8
__next__ called
10
__next__ called
3
__next__ called
10
__next__ called
raising StopIteration
Thảo luận và minh họa
Ở điểm 1 và 2: nhận được một vòng lặp và kiểm tra không đáng tin cậy
Hãy xem xét các lớp sau:
class BasicIterable(object):
def __getitem__(self, item):
if item == 3:
raise IndexError
return item
Gọi itervới một thể hiện BasicIterablesẽ trả về một iterator mà không có bất kỳ vấn đề nào vì BasicIterablethực hiện __getitem__.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
Tuy nhiên, điều quan trọng cần lưu ý là bkhông có __iter__thuộc tính và không được coi là một thể hiện của Iterablehoặc Sequence:
>>> from collections import Iterable, Sequence
>>> hasattr(b, '__iter__')
False
>>> isinstance(b, Iterable)
False
>>> isinstance(b, Sequence)
False
Đây là lý do tại sao Fluent Python của Luciano Ramalho khuyên bạn nên gọi itervà xử lý tiềm năng TypeErrorlà cách chính xác nhất để kiểm tra xem một đối tượng có thể lặp lại được không. Trích dẫn trực tiếp từ cuốn sách:
Kể từ Python 3.4, cách chính xác nhất để kiểm tra xem một đối tượng xcó thể lặp được hay không là gọi iter(x)và xử lý một TypeErrorngoại lệ nếu không. Điều này chính xác hơn so với sử dụng isinstance(x, abc.Iterable), vì iter(x)cũng xem xét __getitem__phương pháp kế thừa , trong khi IterableABC thì không.
Ở điểm 3: Lặp lại các đối tượng chỉ cung cấp __getitem__, nhưng không__iter__
Lặp lại một thể hiện của các BasicIterablecông việc như mong đợi: Python xây dựng một trình vòng lặp cố gắng tìm nạp các mục theo chỉ mục, bắt đầu từ 0, cho đến khi IndexErrorđược nâng lên. __getitem__Phương thức của đối tượng demo chỉ đơn giản trả về itemcái được cung cấp làm đối số cho __getitem__(self, item)iterator trả về iter.
>>> b = BasicIterable()
>>> it = iter(b)
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Lưu ý rằng iterator tăng StopIterationkhi nó không thể trả về mục tiếp theo và mục IndexErrorđược nâng lên item == 3được xử lý bên trong. Đây là lý do tại sao việc lặp qua a BasicIterablevới một forvòng lặp hoạt động như mong đợi:
>>> for x in b:
... print(x)
...
0
1
2
Đây là một ví dụ khác để lái xe về nhà khái niệm về cách trình lặp được trả về bằng cách itercố gắng truy cập các mục theo chỉ mục. WrappedDictkhông kế thừa từ dictđó, có nghĩa là các trường hợp sẽ không có __iter__phương thức.
class WrappedDict(object): # note: no inheritance from dict!
def __init__(self, dic):
self._dict = dic
def __getitem__(self, item):
try:
return self._dict[item] # delegate to dict.__getitem__
except KeyError:
raise IndexError
Lưu ý rằng các cuộc gọi __getitem__được ủy quyền dict.__getitem__mà ký hiệu dấu ngoặc vuông chỉ đơn giản là một tốc ký.
>>> w = WrappedDict({-1: 'not printed',
... 0: 'hi', 1: 'StackOverflow', 2: '!',
... 4: 'not printed',
... 'x': 'not printed'})
>>> for x in w:
... print(x)
...
hi
StackOverflow
!
Trên điểm 4 và 5: iterkiểm tra một trình vòng lặp khi nó gọi__iter__ :
Khi iter(o)được gọi cho một đối tượng o, itersẽ đảm bảo rằng giá trị trả về của __iter__, nếu phương thức có mặt, là một trình vòng lặp. Điều này có nghĩa là đối tượng trả về phải thực hiện __next__(hoặc nexttrong Python 2) và __iter__. iterkhông thể thực hiện bất kỳ kiểm tra độ tỉnh táo nào cho các đối tượng chỉ cung cấp __getitem__, vì nó không có cách nào để kiểm tra xem các mục của đối tượng có thể truy cập được bằng chỉ số nguyên hay không.
class FailIterIterable(object):
def __iter__(self):
return object() # not an iterator
class FailGetitemIterable(object):
def __getitem__(self, item):
raise Exception
Lưu ý rằng việc xây dựng một trình vòng lặp từ các FailIterIterabletrường hợp không thành công ngay lập tức, trong khi việc xây dựng một trình vòng lặp từ FailGetItemIterablethành công, nhưng sẽ ném Ngoại lệ vào lệnh gọi đầu tiên __next__.
>>> fii = FailIterIterable()
>>> iter(fii)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: iter() returned non-iterator of type 'object'
>>>
>>> fgi = FailGetitemIterable()
>>> it = iter(fgi)
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/path/iterdemo.py", line 42, in __getitem__
raise Exception
Exception
Trên điểm 6: __iter__thắng
Điều này là đơn giản. Nếu một đối tượng thực hiện __iter__và __getitem__, itersẽ gọi __iter__. Hãy xem xét các lớp sau
class IterWinsDemo(object):
def __iter__(self):
return iter(['__iter__', 'wins'])
def __getitem__(self, item):
return ['__getitem__', 'wins'][item]
và đầu ra khi lặp qua một thể hiện:
>>> iwd = IterWinsDemo()
>>> for x in iwd:
... print(x)
...
__iter__
wins
Ở điểm 7: các lớp lặp của bạn sẽ thực hiện __iter__
Bạn có thể tự hỏi tại sao hầu hết các chuỗi dựng sẵn như listtriển khai một __iter__phương thức khi __getitem__nào là đủ.
class WrappedList(object): # note: no inheritance from list!
def __init__(self, lst):
self._list = lst
def __getitem__(self, item):
return self._list[item]
Rốt cuộc, việc lặp lại qua các thể hiện của lớp ở trên, mà ủy nhiệm các cuộc gọi __getitem__đến list.__getitem__(sử dụng ký hiệu dấu ngoặc vuông), sẽ hoạt động tốt:
>>> wl = WrappedList(['A', 'B', 'C'])
>>> for x in wl:
... print(x)
...
A
B
C
Những lý do lặp lại tùy chỉnh của bạn nên thực hiện __iter__như sau:
- Nếu bạn thực hiện
__iter__, các thể hiện sẽ được coi là lặp lại và isinstance(o, collections.abc.Iterable)sẽ trả về True.
- Nếu đối tượng được trả về
__iter__không phải là một trình vòng lặp, itersẽ thất bại ngay lập tức và tăng a TypeError.
- Việc xử lý đặc biệt
__getitem__tồn tại vì lý do tương thích ngược. Trích dẫn một lần nữa từ Python thông thạo:
Đó là lý do tại sao bất kỳ chuỗi Python nào cũng có thể lặp lại: tất cả chúng đều thực hiện __getitem__. Trong thực tế, các trình tự tiêu chuẩn cũng thực hiện__iter__ và bạn cũng vậy, vì việc xử lý đặc biệt __getitem__tồn tại vì lý do tương thích ngược và có thể không còn trong tương lai (mặc dù nó không bị phản đối khi tôi viết bài này).
__getitem__cũng đủ để tạo một đối tượng có thể lặp lại