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 for
vò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 o
bằ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) o
có 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) o
có __getitem__
phương pháp.
Kiểm tra một thể hiện của Iterable
hoặc Sequence
, hoặc kiểm tra thuộc tính __iter__
là không đủ.
Nếu một đối tượng o
chỉ 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ừ o
chỉ 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 đó StopIteration
tự 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ề iter
có lành mạnh hay không ngoài việc dùng thử.
Nếu một đối tượng o
thực hiện __iter__
, iter
hà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 o
thự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 for
vò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 o
cho 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 next
trong 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 next
iterator 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 iter
với một thể hiện BasicIterable
sẽ trả về một iterator mà không có bất kỳ vấn đề nào vì BasicIterable
thự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à b
không có __iter__
thuộc tính và không được coi là một thể hiện của Iterable
hoặ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 iter
và xử lý tiềm năng TypeError
là 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 x
có thể lặp được hay không là gọi iter(x)
và xử lý một TypeError
ngoạ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 Iterable
ABC 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 BasicIterable
cô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ề item
cá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 StopIteration
khi 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 BasicIterable
với một for
vò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 iter
cố gắng truy cập các mục theo chỉ mục. WrappedDict
khô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: iter
kiể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
, iter
sẽ đả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 next
trong Python 2) và __iter__
. iter
khô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 FailIterIterable
trườ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ừ FailGetItemIterable
thà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__
, iter
sẽ 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ư list
triể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, iter
sẽ 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