ฉันต้องการที่จะหลั่งน้อยบิตแสงเพิ่มเติมเกี่ยวกับอิทธิพลซึ่งกันและกันของiter
, __iter__
และ__getitem__
และสิ่งที่เกิดขึ้นหลังม่าน ด้วยความรู้นั้นคุณจะสามารถเข้าใจได้ว่าทำไมสิ่งที่ดีที่สุดที่คุณสามารถทำได้คือ
try:
iter(maybe_iterable)
print('iteration will probably work')
except TypeError:
print('not iterable')
ฉันจะแสดงรายการข้อเท็จจริงก่อนแล้วติดตามด้วยการเตือนความจำอย่างรวดเร็วว่าเกิดอะไรขึ้นเมื่อคุณจ้างfor
ลูปในไพ ธ อนตามด้วยการอภิปรายเพื่ออธิบายข้อเท็จจริง
ข้อเท็จจริง
คุณสามารถรับตัววนซ้ำจากวัตถุใด ๆo
โดยการเรียกiter(o)
ถ้าอย่างน้อยหนึ่งในเงื่อนไขต่อไปนี้ถือเป็นจริง:
a) o
มี__iter__
วิธีการที่ส่งกลับวัตถุตัววนซ้ำ ตัววนซ้ำเป็นวัตถุใด ๆ ที่มี__iter__
และ__next__
(Python 2:) next
วิธีการ
b) o
มี__getitem__
วิธีการ
การตรวจสอบอินสแตนซ์ของIterable
หรือSequence
หรือการตรวจสอบแอตทริบิวต์__iter__
ไม่เพียงพอ
หากวัตถุo
ดำเนินการเพียง__getitem__
แต่ไม่ได้__iter__
, iter(o)
จะสร้าง iterator ที่พยายามที่จะดึงข้อมูลรายการจากo
ดัชนีจำนวนเต็มเริ่มต้นที่ดัชนี 0 iterator จะจับใด ๆIndexError
( แต่ไม่มีข้อผิดพลาดอื่น ๆ ) ที่ถูกยกขึ้นแล้วยกStopIteration
ตัวเอง
โดยทั่วไปแล้วไม่มีทางที่จะตรวจสอบว่าตัววนซ้ำที่ส่งคืนมาiter
นั้นมีเหตุผลอย่างอื่นนอกเหนือจากการลองใช้หรือไม่
หากวัตถุo
ดำเนิน__iter__
การiter
ฟังก์ชั่นจะทำให้แน่ใจว่าวัตถุที่ส่งกลับโดย__iter__
เป็นตัววนซ้ำ __getitem__
ไม่มีการตรวจสอบสุขภาพจิตดีคือถ้าวัตถุเพียงการดำเนินการ
__iter__
ชนะ หากวัตถุo
การดำเนินการทั้งใน__iter__
และ__getitem__
, จะเรียกiter(o)
__iter__
หากคุณต้องการทำให้วัตถุของคุณเป็นตัวกำหนดให้ใช้__iter__
วิธีการนี้เสมอ
for
ลูป
ในการติดตามคุณจำเป็นต้องมีความเข้าใจในสิ่งที่เกิดขึ้นเมื่อคุณใช้for
ลูปใน Python อย่าลังเลที่จะข้ามไปยังส่วนถัดไปหากคุณรู้แล้ว
เมื่อคุณใช้for item in o
สำหรับวัตถุที่วนซ้ำบางตัวo
หลามจะเรียกiter(o)
และคาดว่าวัตถุตัววนซ้ำเป็นค่าที่ส่งคืน ตัววนซ้ำเป็นวัตถุใด ๆ ที่ใช้เมธอด__next__
(หรือnext
ใน Python 2) และ__iter__
เมธอด
ตามแบบแผน__iter__
วิธีการวนซ้ำควรส่งคืนวัตถุเอง (เช่นreturn self
) Python จะเรียกnext
ใช้ตัววนซ้ำจนกว่าStopIteration
จะเพิ่มขึ้น ทั้งหมดนี้เกิดขึ้นโดยปริยาย แต่การสาธิตต่อไปนี้ทำให้มองเห็นได้:
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
ย้ำกว่า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
การอภิปรายและภาพประกอบ
ในวันที่ 1 และ 2: รับ iterator และการตรวจสอบที่ไม่น่าเชื่อถือ
พิจารณาคลาสต่อไปนี้:
class BasicIterable(object):
def __getitem__(self, item):
if item == 3:
raise IndexError
return item
โทรiter
กับอินสแตนซ์ของBasicIterable
จะกลับ iterator โดยไม่มีปัญหาใด ๆ เพราะการดำเนินการBasicIterable
__getitem__
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
อย่างไรก็ตามสิ่งสำคัญคือต้องทราบว่าb
ไม่มี__iter__
แอตทริบิวต์และไม่ถือว่าเป็นตัวอย่างของIterable
หรือSequence
:
>>> from collections import Iterable, Sequence
>>> hasattr(b, '__iter__')
False
>>> isinstance(b, Iterable)
False
>>> isinstance(b, Sequence)
False
นี่คือเหตุผลที่Fluent Pythonโดย Luciano Ramalho แนะนำให้โทรiter
และจัดการศักยภาพTypeError
เป็นวิธีที่แม่นยำที่สุดในการตรวจสอบว่าวัตถุนั้นสามารถใช้การได้หรือไม่ การอ้างอิงโดยตรงจากหนังสือ:
ตั้งแต่ Python 3.4 วิธีที่แม่นยำที่สุดในการตรวจสอบว่าวัตถุx
นั้นสามารถทำซ้ำได้คือการเรียกiter(x)
และจัดการTypeError
ข้อยกเว้นหากไม่มี วิธีนี้มีความแม่นยำมากกว่าการใช้isinstance(x, abc.Iterable)
เพราะiter(x)
ยังคำนึงถึง__getitem__
วิธีการดั้งเดิมในขณะที่Iterable
ABC ไม่ทำเช่นนั้น
ในจุดที่ 3: วนซ้ำวัตถุที่จัดเตรียมไว้ให้เท่านั้น__getitem__
แต่ไม่ใช่__iter__
วนซ้ำของอินสแตนซ์ของBasicIterable
งานตามที่คาดไว้: Python สร้างตัววนซ้ำที่พยายามดึงรายการตามดัชนีเริ่มต้นที่ศูนย์จนกระทั่งมีการIndexError
ยกขึ้น การสาธิตของวัตถุ__getitem__
วิธีการเพียงแค่ส่งกลับitem
ซึ่งได้รับการจัดเป็นอาร์กิวเมนต์__getitem__(self, item)
โดย iterator 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
โปรดทราบว่าตัววนซ้ำจะยกขึ้นStopIteration
เมื่อไม่สามารถส่งคืนรายการถัดไปและสิ่งIndexError
ที่ถูกยกขึ้นสำหรับitem == 3
ได้รับการจัดการภายใน นี่คือเหตุผลที่การวนซ้ำBasicIterable
กับ a for
loop ทำงานอย่างที่คาดไว้:
>>> for x in b:
... print(x)
...
0
1
2
นี่คืออีกตัวอย่างหนึ่งในการผลักดันแนวคิดของตัววนซ้ำที่ส่งคืนโดยiter
พยายามเข้าถึงไอเท็มตามดัชนี WrappedDict
ไม่สืบทอดdict
ซึ่งหมายความว่าอินสแตนซ์จะไม่มี__iter__
วิธีการ
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
โปรดทราบว่าการเรียกไป__getitem__
ยังได้รับการมอบหมายให้dict.__getitem__
ทำเครื่องหมายรูปสี่เหลี่ยมจัตุรัสเป็นเพียงการจดชวเลข
>>> w = WrappedDict({-1: 'not printed',
... 0: 'hi', 1: 'StackOverflow', 2: '!',
... 4: 'not printed',
... 'x': 'not printed'})
>>> for x in w:
... print(x)
...
hi
StackOverflow
!
ที่จุด 4 และ 5: iter
ตรวจสอบตัววนซ้ำเมื่อมีการเรียก__iter__
:
เมื่อiter(o)
เรียกว่าสำหรับวัตถุo
, iter
จะให้แน่ใจว่าค่าตอบแทนของ__iter__
ถ้าวิธีการที่เป็นปัจจุบันเป็น iterator ซึ่งหมายความว่าวัตถุที่ส่งกลับมาจะต้องดำเนินการ__next__
(หรือnext
ในหลาม 2) __iter__
และ iter
ไม่สามารถทำการตรวจสอบความถูกต้องของวัตถุที่มีให้__getitem__
ได้เพราะไม่มีวิธีการตรวจสอบว่ารายการของวัตถุนั้นสามารถเข้าถึงได้โดยดัชนีจำนวนเต็มหรือไม่
class FailIterIterable(object):
def __iter__(self):
return object() # not an iterator
class FailGetitemIterable(object):
def __getitem__(self, item):
raise Exception
โปรดทราบว่าการสร้าง iterator จากFailIterIterable
กรณีล้มเหลวทันทีในขณะที่การสร้าง iterator จากFailGetItemIterable
ประสบความสำเร็จ __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
เมื่อวันที่ 6: __iter__
ชนะ
อันนี้ตรงไปตรงมา หากมีการดำเนินการวัตถุ__iter__
และ__getitem__
, จะเรียกiter
__iter__
พิจารณาคลาสต่อไปนี้
class IterWinsDemo(object):
def __iter__(self):
return iter(['__iter__', 'wins'])
def __getitem__(self, item):
return ['__getitem__', 'wins'][item]
และเอาต์พุตเมื่อวนลูปมากกว่าอินสแตนซ์:
>>> iwd = IterWinsDemo()
>>> for x in iwd:
... print(x)
...
__iter__
wins
ในวันที่ 7: คลาส iterable ของคุณควรใช้ __iter__
คุณอาจถามตัวเองว่าทำไมลำดับในตัวส่วนใหญ่เช่นlist
ใช้__iter__
วิธีการเมื่อ__getitem__
มีเพียงพอ
class WrappedList(object): # note: no inheritance from list!
def __init__(self, lst):
self._list = lst
def __getitem__(self, item):
return self._list[item]
ท้ายที่สุดการวนซ้ำของอินสแตนซ์ของคลาสด้านบนซึ่งผู้รับมอบสิทธิ์การเรียกไป__getitem__
ที่list.__getitem__
(โดยใช้สัญกรณ์วงเล็บเหลี่ยม) จะทำงานได้ดี:
>>> wl = WrappedList(['A', 'B', 'C'])
>>> for x in wl:
... print(x)
...
A
B
C
สาเหตุที่กำหนดเอง iterables ของคุณควรใช้__iter__
มีดังนี้:
- หากคุณใช้
__iter__
อินสแตนซ์จะ iterables พิจารณาและจะกลับมาisinstance(o, collections.abc.Iterable)
True
- หากวัตถุที่ส่งกลับโดย
__iter__
ไม่ได้เป็น iterator, จะล้มเหลวทันทีและเพิ่มiter
TypeError
- มีการจัดการพิเศษ
__getitem__
สำหรับเหตุผลด้านความเข้ากันได้ย้อนหลัง การอ้างอิงอีกครั้งจาก Fluent Python:
นั่นคือเหตุผลใด ๆ ลำดับงูหลามเป็น iterable: __getitem__
พวกเขาทั้งหมดใช้ ในความเป็นจริงแล้วลำดับมาตรฐานก็ใช้__iter__
เช่นกันและคุณก็ควรใช้เช่นกันเนื่องจากมีการจัดการพิเศษ__getitem__
สำหรับเหตุผลด้านความเข้ากันได้แบบย้อนหลังและอาจหายไปในอนาคต (แม้ว่ามันจะไม่คัดค้านในขณะที่ฉันเขียน)
__getitem__
ก็เพียงพอที่จะทำให้วัตถุ iterable