Función de generador vacío de Python

99

En Python, uno puede definir fácilmente una función de iterador, poniendo la palabra clave yield en el cuerpo de la función, como por ejemplo:

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

¿Cómo puedo definir una función generadora que no produce ningún valor (genera 0 valores), el siguiente código no funciona, ya que Python no puede saber que se supone que es un generador y no una función normal:

def empty():
    pass

Podría hacer algo como

def empty():
    if False:
        yield None

Pero eso sería muy feo. ¿Hay alguna forma agradable de realizar una función de iterador vacía?

Konstantin Weitz
fuente

Respuestas:

133

Puede usarlo returnuna vez en un generador; detiene la iteración sin producir nada y, por lo tanto, proporciona una alternativa explícita a dejar que la función se ejecute fuera de alcance. Por lo tanto, use yieldpara convertir la función en un generador, pero preceda con returnpara terminar el generador antes de generar nada.

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

No estoy seguro de que sea mucho mejor que lo que tiene, simplemente reemplaza una ifdeclaración de no operación con una declaración de no operación yield. Pero es más idiomático. Tenga en cuenta que el solo uso yieldno funciona.

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

¿Por qué no usar iter(())?

Esta pregunta se refiere específicamente a una función de generador vacía . Por esa razón, lo considero una pregunta sobre la consistencia interna de la sintaxis de Python, en lugar de una pregunta sobre la mejor manera de crear un iterador vacío en general.

Si la pregunta es sobre la mejor manera de crear un iterador vacío, entonces podría estar de acuerdo con Zectbumo sobre usarlo en su iter(())lugar. Sin embargo, es importante observar que iter(())no devuelve una función. Devuelve directamente un iterable vacío. Suponga que está trabajando con una API que espera un invocable que devuelve un iterable. Tendrás que hacer algo como esto:

def empty():
    return iter(())

(El crédito debe ir a Unutbu por dar la primera versión correcta de esta respuesta).

Ahora, puede encontrar lo anterior más claro, pero puedo imaginar situaciones en las que sería menos claro. Considere este ejemplo de una larga lista de definiciones de funciones de generador (artificiales):

def zeros():
    while True:
        yield 0

def ones():
    while True:
        yield 1

...

Al final de esa larga lista, prefiero ver algo con un yielden él, como esto:

def empty():
    return
    yield

o, en Python 3.3 y superior (como sugiere DSM ), esto:

def empty():
    yield from ()

La presencia de la yieldpalabra clave deja claro a simple vista que esta es solo otra función generadora, exactamente como todas las demás. Se necesita un poco más de tiempo para ver que la iter(())versión está haciendo lo mismo.

Es una diferencia sutil, pero honestamente creo que las yieldfunciones basadas en son más legibles y fáciles de mantener.

remitente
fuente
De hecho, esto es mejor que, if False: yieldpero sigue siendo un poco confuso para las personas que no conocen este patrón
Konstantin Weitz
1
Ew, ¿algo después del regreso? Esperaba algo como itertools.empty().
Grault
1
@Jesdisciple, bueno, returnsignifica algo diferente dentro de los generadores. Es más como break.
remitente
Me gusta esta solución porque es (relativamente) concisa y no hace ningún trabajo adicional como comparar False.
Pi Marillion
La respuesta de Unutbu no es una "función generadora verdadera" como mencionaste, ya que devuelve un iterador.
Zectbumo
71
iter(())

No necesita un generador. ¡Vamos chicos!

Zectbumo
fuente
3
Definitivamente me gusta más esta respuesta. Es rápido, fácil de escribir, rápido en ejecución y más atractivo para mí que iter([])por el simple hecho de que ()es una constante mientras []puede instanciar un nuevo objeto de lista en la memoria cada vez que se llama.
Mumbleskates
2
Volviendo a este hilo, me siento obligado a señalar que si desea un verdadero reemplazo directo para una función de generador, debe escribir algo como empty = lambda: iter(())o def empty(): return iter(()).
remitente
Si debe tener un generador, también puede usar (_ for _ in ()) como otros han sugerido
Zectbumo
1
@Zectbumo, todavía no es una función generadora . Es solo un generador. Una función de generador devuelve un nuevo generador cada vez que se llama.
remitente
1
Esto devuelve a en tuple_iteratorlugar de a generator. Si tiene un caso en el que su generador no necesita devolver nada, no use esta respuesta.
Boris
53

Python 3.3 (porque estoy en una yield frompatada, y porque @senderle robó mi primer pensamiento):

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

Pero tengo que admitir que me está costando encontrar un caso de uso para esto para el cual iter([])o (x)range(0)no funcionaría igualmente bien.

DSM
fuente
Realmente me gusta esta sintaxis. el rendimiento es impresionante!
Konstantin Weitz
2
Creo que esto es mucho más legible para un novato que return; yieldo if False: yield None.
abarnert
1
Esta es la solución más elegante
Maksym Ganenko
"Pero tengo que admitir que me está costando encontrar un caso de uso para esto para el cual iter([])o (x)range(0)no funcionaría igualmente bien". -> No estoy seguro de qué (x)range(0)es, pero un caso de uso puede ser un método destinado a ser anulado con un generador completo en algunas de las clases heredadas. Por motivos de coherencia, querrá que incluso el base, del que otros heredan, devuelva un generador como los que lo anulan.
Vedran Šego
19

Otra opcion es:

(_ for _ in ())
Ben Reynwar
fuente
A diferencia de otras opciones, Pycharm considera que esto es coherente con las sugerencias de tipo estándar utilizadas para los generadores, comoGenerator[str, Any, None]
Michał Jabłoński
3

¿Debe ser una función de generador? Si no, que tal

def f():
    return iter(())
unutbu
fuente
2

La forma "estándar" de hacer un iterador vacío parece ser iter ([]). Sugerí hacer [] el argumento predeterminado para iter (); esto fue rechazado con buenos argumentos, ver http://bugs.python.org/issue25215 - Jurjen

Jurjen Bos
fuente
2

Como dijo @senderle, use esto:

def empty():
    return
    yield

Escribo esta respuesta principalmente para compartir otra justificación.

Una razón para elegir esta solución por encima de las demás es que es óptima en lo que respecta al intérprete.

>>> 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

Como podemos ver, empty_returntiene exactamente el mismo código de bytes que una función vacía normal; el resto realiza una serie de otras operaciones que no cambian el comportamiento de todos modos. La única diferencia entre empty_returny noopes que el primero tiene activada la bandera del generador:

>>> 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

Por supuesto, la fuerza de este argumento depende mucho de la implementación particular de Python en uso; un intérprete alternativo suficientemente inteligente puede notar que las otras operaciones no son útiles y optimizarlas. Sin embargo, incluso si tales optimizaciones están presentes, requieren que el intérprete dedique tiempo a realizarlas y a protegerse contra las suposiciones de optimización que se rompen, como que el iteridentificador en el alcance global se rebote a otra cosa (aunque eso probablemente indicaría un error si realmente sucedió). En el caso de empty_returnque simplemente no hay nada que optimizar, incluso el relativamente ingenuo CPython no perderá el tiempo en operaciones falsas.

user3840170
fuente
0
generator = (item for item in [])

fuente
0

Quiero dar un ejemplo basado en una clase, ya que aún no hemos recibido ninguna sugerencia. Este es un iterador invocable que no genera elementos. Creo que esta es una forma sencilla y descriptiva de resolver el problema.

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

>>> list(EmptyGenerator())
[]
Zectbumo
fuente
¿Puede agregar alguna explicación de por qué / cómo funciona esto para resolver el problema del OP?
SherylHohman
No publique solo el código como respuesta, sino que también proporcione una explicación de lo que hace su código y cómo resuelve el problema de la pregunta. Las respuestas con una explicación suelen ser más útiles y de mejor calidad, y es más probable que atraigan votos positivos.
SherylHohman