ฉันต้องการที่จะหลั่งน้อยบิตแสงเพิ่มเติมเกี่ยวกับอิทธิพลซึ่งกันและกันของ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__วิธีการดั้งเดิมในขณะที่IterableABC ไม่ทำเช่นนั้น
ในจุดที่ 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 forloop ทำงานอย่างที่คาดไว้:
>>> 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, จะล้มเหลวทันทีและเพิ่มiterTypeError
- มีการจัดการพิเศษ
__getitem__สำหรับเหตุผลด้านความเข้ากันได้ย้อนหลัง การอ้างอิงอีกครั้งจาก Fluent Python:
นั่นคือเหตุผลใด ๆ ลำดับงูหลามเป็น iterable: __getitem__พวกเขาทั้งหมดใช้ ในความเป็นจริงแล้วลำดับมาตรฐานก็ใช้__iter__เช่นกันและคุณก็ควรใช้เช่นกันเนื่องจากมีการจัดการพิเศษ__getitem__สำหรับเหตุผลด้านความเข้ากันได้แบบย้อนหลังและอาจหายไปในอนาคต (แม้ว่ามันจะไม่คัดค้านในขณะที่ฉันเขียน)
__getitem__ก็เพียงพอที่จะทำให้วัตถุ iterable