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 forboucle en python, suivi d'une discussion pour illustrer les faits.
Faits
Vous pouvez obtenir un itérateur à partir de n'importe quel objet oen appelant iter(o)si au moins une des conditions suivantes est remplie:
a) opossè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) oa une __getitem__méthode.
La vérification d'une instance de Iterableou Sequence, ou la vérification de l'attribut __iter__ne suffit pas.
Si un objet oimplémente uniquement __getitem__, mais pas __iter__, iter(o)il construira un itérateur qui essaiera d'extraire des éléments à partir od'un index entier, en commençant à l'index 0. L'itérateur interceptera toutes les IndexErrorerreurs (mais aucune autre erreur) qui seront levées, puis se lèvera StopIterationlui-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 iterest sain d'esprit autre que de l'essayer.
Si un objet oimplémente __iter__, la iterfonction 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 oimplé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 forboucle en Python. N'hésitez pas à passer directement à la section suivante si vous le savez déjà.
Lorsque vous utilisez for item in opour 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 nexten 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 nextl'itérateur jusqu'à ce qu'il StopIterationsoit 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 iteravec une instance de BasicIterableretournera un itérateur sans aucun problème car BasicIterableimplémente __getitem__.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
Cependant, il est important de noter qu'il bn'a pas l' __iter__attribut et n'est pas considéré comme une instance de Iterableou 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 iteret de gérer le potentiel TypeErrorcomme 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 xest itérable est d'appeler iter(x)et de gérer une TypeErrorexception 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 , Iterablecontrairement à l' ABC.
Au point 3: itération sur des objets qui ne fournissent que __getitem__, mais pas__iter__
Itération sur une instance de BasicIterabletravaux 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 IndexErrorsoit levé. La __getitem__méthode de l'objet de démonstration renvoie simplement celui itemqui 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 StopIterationlorsqu'il ne peut pas retourner l'élément suivant et que celui IndexErrorqui est déclenché item == 3est géré en interne. C'est pourquoi le bouclage sur un BasicIterableavec une forboucle 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 itertente d'accéder aux éléments par index. WrappedDictn'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: iterrecherche un itérateur lorsqu'il appelle__iter__ :
Quand iter(o)est appelé pour un objet o, iters'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 nexten Python 2) et __iter__. iterne 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' FailIterIterableinstances échoue immédiatement, tandis que la construction d'un itérateur à partir de FailGetItemIterableré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__, iterappellera __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 listimplé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