Saya ingin menumpahkan sedikit yang sedikit lebih terang pada interaksi iter
, __iter__
dan __getitem__
dan apa yang terjadi di balik tirai. Berbekal pengetahuan itu, Anda akan dapat memahami mengapa yang terbaik yang bisa Anda lakukan adalah
try:
iter(maybe_iterable)
print('iteration will probably work')
except TypeError:
print('not iterable')
Saya akan mendaftar fakta terlebih dahulu dan kemudian menindaklanjuti dengan pengingat cepat tentang apa yang terjadi ketika Anda mempekerjakan seorang for
loop dalam python, diikuti dengan diskusi untuk menggambarkan fakta-fakta.
Fakta
Anda bisa mendapatkan iterator dari objek apa pun o
dengan memanggil iter(o)
jika setidaknya salah satu dari kondisi berikut ini berlaku:
a) o
memiliki __iter__
metode yang mengembalikan objek iterator. Iterator adalah objek apa pun dengan metode __iter__
dan __next__
(Python 2 next
:).
b) o
punya __getitem__
metode.
Memeriksa instance Iterable
atau Sequence
, atau memeriksa atribut __iter__
tidak cukup.
Jika suatu objek o
hanya mengimplementasikan __getitem__
, tetapi tidak __iter__
, iter(o)
akan membangun iterator yang mencoba mengambil item dari o
dengan indeks integer, mulai dari indeks 0. Iterator akan menangkap setiap IndexError
(tetapi tidak ada kesalahan lain) yang dinaikkan dan kemudian memunculkan StopIteration
dirinya sendiri.
Dalam arti yang paling umum, tidak ada cara untuk memeriksa apakah iterator yang dikembalikan oleh iter
waras selain untuk mencobanya.
Jika suatu objek o
mengimplementasikan __iter__
, iter
fungsi akan memastikan bahwa objek yang dikembalikan oleh __iter__
iterator. Tidak ada pemeriksaan kewarasan jika suatu objek hanya mengimplementasikan __getitem__
.
__iter__
menang. Jika suatu objek o
mengimplementasikan keduanya __iter__
dan __getitem__
, iter(o)
akan memanggil __iter__
.
Jika Anda ingin membuat objek Anda sendiri dapat diubah, selalu terapkan __iter__
metode ini.
for
loop
Untuk mengikuti, Anda perlu pemahaman tentang apa yang terjadi ketika Anda menggunakan for
loop dengan Python. Jangan ragu untuk langsung ke bagian selanjutnya jika Anda sudah tahu.
Ketika Anda menggunakan for item in o
untuk beberapa objek iterable o
, Python memanggil iter(o)
dan mengharapkan objek iterator sebagai nilai balik. Iterator adalah objek apa pun yang mengimplementasikan metode __next__
(dan next
dengan Python 2) dan __iter__
metode.
Dengan konvensi, __iter__
metode iterator harus mengembalikan objek itu sendiri (yaitu return self
). Python kemudian memanggil next
iterator hingga StopIteration
dinaikkan. Semua ini terjadi secara implisit, tetapi demonstrasi berikut membuatnya terlihat:
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
Iterasi atas 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
Diskusi dan ilustrasi
Pada poin 1 dan 2: mendapatkan iterator dan cek tidak dapat diandalkan
Pertimbangkan kelas berikut:
class BasicIterable(object):
def __getitem__(self, item):
if item == 3:
raise IndexError
return item
Menelepon iter
dengan instance of BasicIterable
akan mengembalikan iterator tanpa masalah karena BasicIterable
implementasinya __getitem__
.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
Namun, penting untuk dicatat bahwa b
tidak memiliki __iter__
atribut dan tidak dianggap sebagai contoh Iterable
atau Sequence
:
>>> from collections import Iterable, Sequence
>>> hasattr(b, '__iter__')
False
>>> isinstance(b, Iterable)
False
>>> isinstance(b, Sequence)
False
Inilah sebabnya mengapa Fluent Python oleh Luciano Ramalho merekomendasikan pemanggilan iter
dan penanganan potensi TypeError
sebagai cara paling akurat untuk memeriksa apakah suatu objek dapat diubah. Mengutip langsung dari buku:
Pada Python 3.4, cara paling akurat untuk memeriksa apakah suatu objek x
dapat diubah adalah dengan memanggil iter(x)
dan menangani TypeError
pengecualian jika tidak. Ini lebih akurat daripada menggunakan isinstance(x, abc.Iterable)
, karena iter(x)
juga mempertimbangkan __getitem__
metode warisan , sedangkan Iterable
ABC tidak.
Pada poin 3: Iterasi objek yang hanya menyediakan __getitem__
, tetapi tidak__iter__
Iterasi atas instance BasicIterable
karya seperti yang diharapkan: Python membangun iterator yang mencoba mengambil item menurut indeks, mulai dari nol, hingga sebuah IndexError
dinaikkan. Metode objek demo __getitem__
hanya mengembalikan item
yang diberikan sebagai argumen __getitem__(self, item)
oleh iterator yang dikembalikan oleh 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
Perhatikan bahwa iterator meningkat StopIteration
ketika tidak dapat mengembalikan item berikutnya dan item IndexError
yang dinaikkan item == 3
ditangani secara internal. Inilah sebabnya mengapa pengulangan BasicIterable
dengan sebuah for
pengulangan bekerja seperti yang diharapkan:
>>> for x in b:
... print(x)
...
0
1
2
Berikut adalah contoh lain untuk mengembalikan konsep bagaimana iterator dikembalikan dengan iter
mencoba mengakses item berdasarkan indeks. WrappedDict
tidak mewarisi dari dict
, yang berarti instance tidak akan memiliki __iter__
metode.
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
Perhatikan bahwa panggilan untuk __getitem__
didelegasikan ke dict.__getitem__
mana notasi braket persegi hanyalah sebuah singkatan.
>>> w = WrappedDict({-1: 'not printed',
... 0: 'hi', 1: 'StackOverflow', 2: '!',
... 4: 'not printed',
... 'x': 'not printed'})
>>> for x in w:
... print(x)
...
hi
StackOverflow
!
Pada poin 4 dan 5: iter
memeriksa iterator ketika memanggil__iter__
:
Ketika iter(o)
dipanggil untuk suatu objek o
, iter
akan memastikan bahwa nilai balik dari __iter__
, jika metode ini ada, adalah iterator. Ini berarti bahwa objek yang dikembalikan harus mengimplementasikan __next__
(atau next
dengan Python 2) dan __iter__
. iter
tidak dapat melakukan pemeriksaan kewarasan untuk objek yang hanya menyediakan __getitem__
, karena tidak memiliki cara untuk memeriksa apakah item objek dapat diakses oleh indeks integer.
class FailIterIterable(object):
def __iter__(self):
return object() # not an iterator
class FailGetitemIterable(object):
def __getitem__(self, item):
raise Exception
Perhatikan bahwa membangun iterator dari FailIterIterable
instance gagal dengan segera, saat membangun iterator dari FailGetItemIterable
berhasil, tetapi akan membuang Pengecualian pada panggilan pertama ke __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
Pada poin 6: __iter__
menang
Yang ini mudah. Jika suatu objek mengimplementasikan __iter__
dan __getitem__
, iter
akan memanggil __iter__
. Pertimbangkan kelas berikut
class IterWinsDemo(object):
def __iter__(self):
return iter(['__iter__', 'wins'])
def __getitem__(self, item):
return ['__getitem__', 'wins'][item]
dan output ketika mengulang contoh:
>>> iwd = IterWinsDemo()
>>> for x in iwd:
... print(x)
...
__iter__
wins
Pada poin 7: kelas iterable Anda harus diimplementasikan __iter__
Anda mungkin bertanya pada diri sendiri mengapa sebagian besar urutan builtin seperti list
mengimplementasikan suatu __iter__
metode padahal __getitem__
sudah cukup.
class WrappedList(object): # note: no inheritance from list!
def __init__(self, lst):
self._list = lst
def __getitem__(self, item):
return self._list[item]
Bagaimanapun, iterasi atas instance dari kelas di atas, yang mendelegasikan panggilan __getitem__
ke list.__getitem__
(menggunakan notasi braket persegi), akan berfungsi dengan baik:
>>> wl = WrappedList(['A', 'B', 'C'])
>>> for x in wl:
... print(x)
...
A
B
C
Alasan iterables kustom Anda harus menerapkan __iter__
adalah sebagai berikut:
- Jika Anda menerapkan
__iter__
, instance akan dianggap iterables, dan isinstance(o, collections.abc.Iterable)
akan kembali True
.
- Jika objek yang dikembalikan oleh
__iter__
bukan iterator, iter
akan gagal segera dan menaikkan a TypeError
.
- Penanganan khusus
__getitem__
ada untuk alasan kompatibilitas ke belakang. Mengutip lagi dari Fluent Python:
Itulah sebabnya setiap urutan Python dapat diubah: mereka semua mengimplementasikan __getitem__
. Bahkan, urutan standar juga diterapkan __iter__
, dan Anda harus melakukannya juga, karena penanganan khusus __getitem__
ada karena alasan kompatibilitas ke belakang dan mungkin hilang di masa depan (meskipun tidak ditinggalkan saat saya menulis ini).
__getitem__
juga cukup untuk membuat objek dapat diubah