Je voudrais jeter un peu plus de lumière sur l'interaction entre iter
, __iter__
et __getitem__
et ce qui se passe derrière les rideaux. Armé de ces connaissances, vous serez en mesure de comprendre pourquoi le mieux que vous puissiez faire est
try:
iter(maybe_iterable)
print('iteration will probably work')
except TypeError:
print('not iterable')
Je vais d'abord énumérer les faits et ensuite faire un rappel rapide de ce qui se passe lorsque vous utilisez une for
boucle en python, suivi d'une discussion pour illustrer les faits.
Faits
Vous pouvez obtenir un itérateur à partir de n'importe quel objet o
en appelant iter(o)
si au moins une des conditions suivantes est remplie:
a) o
possède une __iter__
méthode qui renvoie un objet itérateur. Un itérateur est un objet avec une méthode __iter__
et une méthode __next__
(Python 2 next
:).
b) o
a une __getitem__
méthode.
La vérification d'une instance de Iterable
ou Sequence
, ou la vérification de l'attribut __iter__
ne suffit pas.
Si un objet o
implémente uniquement __getitem__
, mais pas __iter__
, iter(o)
il construira un itérateur qui essaiera d'extraire des éléments à partir o
d'un index entier, en commençant à l'index 0. L'itérateur interceptera toutes les IndexError
erreurs (mais aucune autre erreur) qui seront levées, puis se lèvera StopIteration
lui-même.
Dans le sens le plus général, il n'y a aucun moyen de vérifier si l'itérateur retourné par iter
est sain d'esprit autre que de l'essayer.
Si un objet o
implémente __iter__
, la iter
fonction s'assurera que l'objet retourné par __iter__
est un itérateur. Il n'y a pas de contrôle d'intégrité si un objet est uniquement implémenté __getitem__
.
__iter__
gagne. Si un objet o
implémente les deux __iter__
et __getitem__
, iter(o)
appellera __iter__
.
Si vous souhaitez rendre vos propres objets itérables, implémentez toujours la __iter__
méthode.
for
boucles
Pour suivre, vous devez comprendre ce qui se passe lorsque vous utilisez une for
boucle en Python. N'hésitez pas à passer directement à la section suivante si vous le savez déjà.
Lorsque vous utilisez for item in o
pour un objet itérable o
, Python appelle iter(o)
et attend un objet itérateur comme valeur de retour. Un itérateur est tout objet qui implémente une méthode __next__
(ou next
en Python 2) et une __iter__
méthode.
Par convention, la __iter__
méthode d'un itérateur doit retourner l'objet lui-même (ie return self
). Python appelle ensuite next
l'itérateur jusqu'à ce qu'il StopIteration
soit levé. Tout cela se produit implicitement, mais la démonstration suivante le rend visible:
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
Itération sur un 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
Discussion et illustrations
Aux points 1 et 2: obtenir un itérateur et des contrôles peu fiables
Considérez la classe suivante:
class BasicIterable(object):
def __getitem__(self, item):
if item == 3:
raise IndexError
return item
L'appel iter
avec une instance de BasicIterable
retournera un itérateur sans aucun problème car BasicIterable
implémente __getitem__
.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
Cependant, il est important de noter qu'il b
n'a pas l' __iter__
attribut et n'est pas considéré comme une instance de Iterable
ou Sequence
:
>>> from collections import Iterable, Sequence
>>> hasattr(b, '__iter__')
False
>>> isinstance(b, Iterable)
False
>>> isinstance(b, Sequence)
False
C'est pourquoi Fluent Python de Luciano Ramalho recommande d'appeler iter
et de gérer le potentiel TypeError
comme le moyen le plus précis de vérifier si un objet est itérable. Citant directement du livre:
Depuis Python 3.4, la façon la plus précise de vérifier si un objet x
est itérable est d'appeler iter(x)
et de gérer une TypeError
exception s'il ne l'est pas. Ceci est plus précis que l'utilisation isinstance(x, abc.Iterable)
, car iter(x)
il considère également la __getitem__
méthode héritée , Iterable
contrairement à l' ABC.
Au point 3: itération sur des objets qui ne fournissent que __getitem__
, mais pas__iter__
Itération sur une instance de BasicIterable
travaux comme prévu: Python construit un itérateur qui essaie de récupérer des éléments par index, en commençant à zéro, jusqu'à ce que an IndexError
soit levé. La __getitem__
méthode de l'objet de démonstration renvoie simplement celui item
qui a été fourni comme argument __getitem__(self, item)
par l'itérateur renvoyé par 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
Notez que l'itérateur déclenche StopIteration
lorsqu'il ne peut pas retourner l'élément suivant et que celui IndexError
qui est déclenché item == 3
est géré en interne. C'est pourquoi le bouclage sur un BasicIterable
avec une for
boucle fonctionne comme prévu:
>>> for x in b:
... print(x)
...
0
1
2
Voici un autre exemple afin de ramener à la maison le concept de la façon dont l'itérateur renvoyé par iter
tente d'accéder aux éléments par index. WrappedDict
n'hérite pas de dict
, ce qui signifie que les instances n'auront pas de __iter__
méthode.
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
Notez que les appels à __getitem__
sont délégués dict.__getitem__
pour lesquels la notation entre crochets est simplement un raccourci.
>>> w = WrappedDict({-1: 'not printed',
... 0: 'hi', 1: 'StackOverflow', 2: '!',
... 4: 'not printed',
... 'x': 'not printed'})
>>> for x in w:
... print(x)
...
hi
StackOverflow
!
Aux points 4 et 5: iter
recherche un itérateur lorsqu'il appelle__iter__
:
Quand iter(o)
est appelé pour un objet o
, iter
s'assurera que la valeur de retour de __iter__
, si la méthode est présente, est un itérateur. Cela signifie que l'objet renvoyé doit implémenter __next__
(ou next
en Python 2) et __iter__
. iter
ne peut effectuer aucun contrôle d'intégrité pour les objets qui fournissent uniquement __getitem__
, car il n'a aucun moyen de vérifier si les éléments de l'objet sont accessibles par index entier.
class FailIterIterable(object):
def __iter__(self):
return object() # not an iterator
class FailGetitemIterable(object):
def __getitem__(self, item):
raise Exception
Notez que la construction d'un itérateur à partir d' FailIterIterable
instances échoue immédiatement, tandis que la construction d'un itérateur à partir de FailGetItemIterable
réussit, mais lèvera une exception lors du premier appel à __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
Au point 6: __iter__
gagne
Celui-ci est simple. Si un objet implémente __iter__
et __getitem__
, iter
appellera __iter__
. Considérez la classe suivante
class IterWinsDemo(object):
def __iter__(self):
return iter(['__iter__', 'wins'])
def __getitem__(self, item):
return ['__getitem__', 'wins'][item]
et la sortie lors du bouclage sur une instance:
>>> iwd = IterWinsDemo()
>>> for x in iwd:
... print(x)
...
__iter__
wins
Au point 7: vos classes itérables devraient implémenter __iter__
Vous pourriez vous demander pourquoi la plupart des séquences intégrées comme list
implémenter une __iter__
méthode __getitem__
sont suffisantes.
class WrappedList(object): # note: no inheritance from list!
def __init__(self, lst):
self._list = lst
def __getitem__(self, item):
return self._list[item]
Après tout, l'itération sur les instances de la classe ci-dessus, qui délègue les appels __getitem__
à list.__getitem__
(en utilisant la notation entre crochets), fonctionnera correctement:
>>> wl = WrappedList(['A', 'B', 'C'])
>>> for x in wl:
... print(x)
...
A
B
C
Les raisons pour lesquelles vos itérables personnalisés doivent implémenter __iter__
sont les suivantes:
- Si vous implémentez
__iter__
, les instances seront considérées comme itérables et isinstance(o, collections.abc.Iterable)
reviendront True
.
- Si l'objet renvoyé par
__iter__
n'est pas un itérateur, iter
échouera immédiatement et déclenchera a TypeError
.
- La gestion spéciale de
__getitem__
existe pour des raisons de compatibilité descendante. Citant à nouveau de Fluent Python:
C'est pourquoi toute séquence Python est itérable: ils implémentent tous __getitem__
. En fait, les séquences standard implémentent également __iter__
, et la vôtre devrait aussi, car la gestion spéciale de __getitem__
existe pour des raisons de compatibilité descendante et peut être supprimée à l'avenir (bien qu'elle ne soit pas déconseillée au moment où j'écris ceci).
__getitem__
est également suffisant pour rendre un objet itérable