Fonction de générateur vide Python


99

En python, on peut facilement définir une fonction itérateur, en mettant le mot-clé yield dans le corps de la fonction, comme:

def gen():
    for i in range(100):
        yield i

Comment puis-je définir une fonction de générateur qui ne donne aucune valeur (génère 0 valeurs), le code suivant ne fonctionne pas, car python ne peut pas savoir qu'il est censé être un générateur et non une fonction normale:

def empty():
    pass

Je pourrais faire quelque chose comme

def empty():
    if False:
        yield None

Mais ce serait très moche. Existe-t-il un moyen intéressant de réaliser une fonction d'itérateur vide?

Réponses:


133

Vous pouvez utiliser returnune fois dans un générateur; il arrête l'itération sans rien donner, et fournit ainsi une alternative explicite à laisser la fonction s'exécuter hors de portée. Utilisez donc yieldpour transformer la fonction en générateur, mais faites-la précéder de returnpour terminer le générateur avant de donner quoi que ce soit.

>>> def f():
...     return
...     yield
... 
>>> list(f())
[]

Je ne suis pas sûr que ce soit tant mieux que ce que vous avez - cela remplace simplement une ifinstruction no-op par une instruction no-op yield. Mais c'est plus idiomatique. Notez que la simple utilisation yieldne fonctionne pas.

>>> def f():
...     yield
... 
>>> list(f())
[None]

Pourquoi ne pas simplement utiliser iter(())?

Cette question concerne spécifiquement une fonction de générateur vide . Pour cette raison, je considère que c'est une question sur la cohérence interne de la syntaxe de Python, plutôt qu'une question sur la meilleure façon de créer un itérateur vide en général.

Si la question porte en fait sur la meilleure façon de créer un itérateur vide, vous pourriez être d'accord avec Zectbumo sur l'utilisation à la iter(())place. Cependant, il est important de noter que iter(())cela ne renvoie pas de fonction! Il renvoie directement un itérable vide. Supposons que vous travaillez avec une API qui attend un appelable qui renvoie un itérable. Vous devrez faire quelque chose comme ceci:

def empty():
    return iter(())

(Le mérite revient à Unutbu d' avoir donné la première version correcte de cette réponse.)

Maintenant, vous pouvez trouver ce qui précède plus clair, mais je peux imaginer des situations dans lesquelles ce serait moins clair. Considérez cet exemple d'une longue liste de définitions de fonctions de générateur (artificielles):

def zeros():
    while True:
        yield 0

def ones():
    while True:
        yield 1

...

À la fin de cette longue liste, je préfère voir quelque chose avec un yielddedans, comme ceci:

def empty():
    return
    yield

ou, dans Python 3.3 et supérieur (comme suggéré par DSM ), ceci:

def empty():
    yield from ()

La présence du yieldmot - clé montre clairement au plus bref coup d'œil qu'il ne s'agit que d'une autre fonction de générateur, exactement comme toutes les autres. Il faut un peu plus de temps pour voir que la iter(())version fait la même chose.

C'est une différence subtile, mais je pense honnêtement que les yieldfonctions basées sont plus lisibles et maintenables.


C'est en effet mieux que if False: yieldmais toujours un peu déroutant pour les personnes qui ne connaissent pas ce modèle
Konstantin Weitz

1
Ew, quelque chose après le retour? Je m'attendais à quelque chose comme itertools.empty().
Grault

1
@Jesdisciple, eh bien, returnsignifie quelque chose de différent à l'intérieur des générateurs. C'est plus comme break.
senderle

J'aime cette solution parce qu'elle est (relativement) concise et qu'elle ne fait aucun travail supplémentaire comme la comparaison avec False.
Pi Marillion

La réponse d'Unutbu n'est pas une "vraie fonction de générateur" comme vous l'avez mentionné puisqu'elle renvoie un itérateur.
Zectbumo

71
iter(())

Vous n'avez pas besoin d' un générateur. Allez les gars!


3
J'aime définitivement cette réponse. C'est rapide, facile à écrire, rapide dans l'exécution et plus attrayant pour moi que iter([])pour le simple fait qu'il ()s'agit d'une constante alors que []peut instancier un nouvel objet liste en mémoire à chaque fois qu'il est appelé.
Mumbleskates

2
En revenant sur ce fil, je me sens obligé de souligner que si vous voulez un véritable remplacement instantané pour une fonction de générateur, vous auriez écrit quelque chose comme empty = lambda: iter(())ou def empty(): return iter(()).
senderle

Si vous devez avoir un générateur, vous pouvez aussi bien utiliser (_ for _ in ()) que d'autres l'ont suggéré
Zectbumo

1
@Zectbumo, ce n'est toujours pas une fonction de générateur . C'est juste un générateur. Une fonction de générateur renvoie un nouveau générateur à chaque fois qu'elle est appelée.
senderle

1
Cela renvoie a tuple_iteratorau lieu de a generator. Si vous avez un cas où votre générateur ne doit rien renvoyer, n'utilisez pas cette réponse.
Boris

53

Python 3.3 (parce que je suis sur un yield fromcoup de pied, et parce que @senderle m'a volé ma première pensée):

>>> def f():
...     yield from ()
... 
>>> list(f())
[]

Mais je dois admettre que je suis un moment difficile à venir avec un cas d'utilisation de ce pour lequel iter([])ou (x)range(0)ne fonctionnerait pas aussi bien.


J'aime vraiment cette syntaxe. le rendement est génial!
Konstantin Weitz

2
Je pense que c'est beaucoup plus lisible pour un novice que l'un return; yieldou l' autre if False: yield None.
abarnert

1
C'est la solution la plus élégante
Maksym Ganenko

"Mais je dois admettre que j'ai du mal à trouver un cas d'utilisation pour cela pour lequel iter([])ou (x)range(0)ne fonctionnerait pas aussi bien." -> Je ne sais pas ce que (x)range(0)c'est, mais un cas d'utilisation peut être une méthode censée être remplacée par un générateur complet dans certaines des classes héritées. Pour des raisons de cohérence, vous voudriez que même le générateur de base, dont d'autres héritent, renvoie un générateur comme ceux qui le remplacent.
Vedran Šego

19

Une autre option est:

(_ for _ in ())

Contrairement à d'autres options, Pycharm considère que cela est cohérent avec les indices de type standard utilisés pour les générateurs, commeGenerator[str, Any, None]
Michał Jabłoński

3

Doit-il être une fonction de générateur? Sinon, que diriez-vous

def f():
    return iter(())

2

La manière "standard" de créer un itérateur vide semble être iter ([]). J'ai suggéré de faire de [] l'argument par défaut de iter (); cela a été rejeté avec de bons arguments, voir http://bugs.python.org/issue25215 - Jurjen


2

Comme @senderle l'a dit, utilisez ceci:

def empty():
    return
    yield

J'écris cette réponse principalement pour partager une autre justification.

Une des raisons de choisir cette solution au-dessus des autres est qu'elle est optimale en ce qui concerne l'interprète.

>>> import dis
>>> def empty_yield_from():
...     yield from ()
... 
>>> def empty_iter():
...     return iter(())
... 
>>> def empty_return():
...     return
...     yield
...
>>> def noop():
...     pass
...
>>> dis.dis(empty_yield_from)
  2           0 LOAD_CONST               1 (())
              2 GET_YIELD_FROM_ITER
              4 LOAD_CONST               0 (None)
              6 YIELD_FROM
              8 POP_TOP
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE
>>> dis.dis(empty_iter)
  2           0 LOAD_GLOBAL              0 (iter)
              2 LOAD_CONST               1 (())
              4 CALL_FUNCTION            1
              6 RETURN_VALUE
>>> dis.dis(empty_return)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE
>>> dis.dis(noop)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE

Comme nous pouvons le voir, le empty_returna exactement le même bytecode qu'une fonction vide régulière; les autres effectuent un certain nombre d'autres opérations qui ne changent pas le comportement de toute façon. La seule différence entre empty_returnet noopest que le premier a le jeu d'indicateurs de générateur:

>>> dis.show_code(noop)
Name:              noop
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, NOFREE
Constants:
   0: None
>>> dis.show_code(empty_return)
Name:              empty_return
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, GENERATOR, NOFREE
Constants:
   0: None

Bien sûr, la force de cet argument est très dépendante de l'implémentation particulière de Python en cours d'utilisation; un interprète alternatif suffisamment intelligent peut remarquer que les autres opérations ne sont pas utiles et les optimiser. Cependant, même si de telles optimisations sont présentes, elles obligent l'interpréteur à passer du temps à les exécuter et à se prémunir contre la rupture des hypothèses d'optimisation, comme le iterrebond de l' identifiant à portée globale vers autre chose (même si cela indiquerait très probablement un bogue s'il effectivement arrivé). Dans le cas où empty_returnil n'y a tout simplement rien à optimiser, même le CPython relativement naïf ne perdra pas de temps sur des opérations parasites.


0
generator = (item for item in [])

0

Je veux donner un exemple basé sur la classe puisque nous n'avons encore eu aucune suggestion. Il s'agit d'un itérateur appelable qui ne génère aucun élément. Je pense que c'est une manière simple et descriptive de résoudre le problème.

class EmptyGenerator:
    def __iter__(self):
        return self
    def __next__(self):
        raise StopIteration

>>> list(EmptyGenerator())
[]

Pouvez-vous ajouter une explication pourquoi / comment cela fonctionne pour résoudre le problème du PO?
SherylHohman

Veuillez ne pas publier uniquement le code comme réponse, mais également expliquer ce que fait votre code et comment il résout le problème de la question. Les réponses avec une explication sont généralement plus utiles et de meilleure qualité, et sont plus susceptibles d'attirer des votes positifs.
SherylHohman le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.