¿Es una buena idea tener una instalación de lenguaje generador como `rendir`?

9

PHP, C #, Python y probablemente algunos otros lenguajes tienen una yieldpalabra clave que se usa para crear funciones generadoras.

En PHP: http://php.net/manual/en/language.generators.syntax.php

En Python: https://www.pythoncentral.io/python-generators-and-yield-keyword/

En C #: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield

Me preocupa que, como característica / facilidad del lenguaje, yieldrompa algunas convenciones. Uno de ellos es a lo que me referiría es "certeza". Es un método que devuelve un resultado diferente cada vez que lo llama. Con una función normal que no sea de generador, puede llamarla y si se le da la misma entrada, devolverá la misma salida. Con el rendimiento, devuelve resultados diferentes, en función de su estado interno. Por lo tanto, si llama aleatoriamente a la función generadora, sin conocer su estado anterior, no puede esperar que devuelva un determinado resultado.

¿Cómo encaja una función como esta en el paradigma del lenguaje? ¿Rompe realmente alguna convención? ¿Es una buena idea tener y usar esta función? (para dar un ejemplo de lo que es bueno y lo que es malo, gotoalguna vez fue una característica de muchos idiomas y todavía lo es, pero se considera dañino y, como tal, se erradicó de algunos idiomas, como Java). ¿Los compiladores / intérpretes de lenguaje de programación tienen que romper alguna de las convenciones para implementar tal característica, por ejemplo, un lenguaje tiene que implementar multi-threading para que esta característica funcione, o puede hacerse sin tecnología de threading?

Dennis
fuente
44
yieldEs esencialmente un motor de estado. No está destinado a devolver el mismo resultado cada vez. Lo que hará con absoluta certeza es devolver el siguiente elemento de forma enumerable cada vez que se invoque. No se requieren hilos; necesita un cierre (más o menos) para mantener el estado actual.
Robert Harvey
1
En cuanto a la calidad de la "certeza", considere que, dada la misma secuencia de entrada, una serie de llamadas al iterador producirá exactamente los mismos elementos en exactamente el mismo orden.
Robert Harvey
44
No estoy seguro de dónde provienen la mayoría de sus preguntas, ya que C ++ no tiene una yield palabra clave como Python. Tiene un método estático std::this_thread::yield(), pero esa no es una palabra clave. Por lo tanto this_thread, antepondría casi cualquier llamada, por lo que es bastante obvio que es una función de biblioteca solo para generar subprocesos, no una función de lenguaje sobre el flujo de control en general.
Ixrec
enlace actualizado a C #, uno para C ++ eliminado
Dennis

Respuestas:

16

Advertencias primero: C # es el lenguaje que mejor conozco, y aunque tiene un yieldaspecto que parece ser muy similar al de otros lenguajes yield, puede haber diferencias sutiles que desconozco.

Me preocupa que, como característica / facilidad del lenguaje, el rendimiento rompe algunas convenciones. Uno de ellos es a lo que me referiría es "certeza". Es un método que devuelve un resultado diferente cada vez que lo llama.

Majaderías. ¿ Realmente espera Random.Nexto Console.ReadLine devuelve el mismo resultado cada vez que los llama? ¿Qué tal llamadas de descanso? ¿Autenticación? ¿Obtener un artículo de una colección? Hay todo tipo de funciones (buenas, útiles) que son impuras.

¿Cómo encaja una función como esta en el paradigma del lenguaje? ¿Rompe realmente alguna convención?

Sí, yieldjuega muy mal try/catch/finallyy no está permitido ( https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/ para más información).

¿Es una buena idea tener y usar esta función?

Sin duda, es una buena idea tener esta característica. Cosas como LINQ de C # es realmente agradable: evaluar perezosamente las colecciones proporciona un gran beneficio de rendimiento y yieldpermite que se haga ese tipo de cosas en una fracción del código con una fracción de los errores que haría un iterador manual.

Dicho esto, no hay muchos usos yieldfuera del procesamiento de la colección de estilo LINQ. Lo he usado para el procesamiento de validación, la generación de programas, la aleatorización y algunas otras cosas, pero espero que la mayoría de los desarrolladores nunca lo hayan usado (o lo hayan usado mal).

¿Los compiladores / intérpretes de lenguaje de programación tienen que romper alguna de las convenciones para implementar tal característica, por ejemplo, un lenguaje tiene que implementar multi-threading para que esta característica funcione, o puede hacerse sin tecnología de threading?

No exactamente. El compilador genera un iterador de máquina de estado que realiza un seguimiento de dónde se detuvo para que pueda comenzar allí nuevamente la próxima vez que se lo llame. El proceso para la generación de código hace algo similar al estilo de paso de continuación, donde el código después de que yieldse extrae en su propio bloque (y si tiene algún yields, otro subbloque, etc.). Ese es un enfoque bien conocido que se usa con mayor frecuencia en la programación funcional y también aparece en la compilación async / wait de C #.

No se necesita subprocesamiento, pero requiere un enfoque diferente para la generación de código en la mayoría de los compiladores, y tiene algún conflicto con otras características del lenguaje.

Sin embargo, en general, yieldes una característica de impacto relativamente bajo que realmente ayuda con un subconjunto específico de problemas.

Telastyn
fuente
Nunca he usado C # en serio, pero esta yieldpalabra clave es similar a las corutinas, sí, o algo diferente. Si es así, ¡desearía tener uno en C! Puedo pensar en al menos algunas secciones de código decentes que habrían sido mucho más fáciles de escribir con una función de lenguaje de este tipo.
2
@DrunkCoder: similar, pero con algunas limitaciones, según tengo entendido.
Telastyn
1
Tampoco querrás ver el rendimiento mal utilizado. Cuantas más características tenga un idioma, es más probable que encuentre un programa mal escrito en ese idioma. No estoy seguro de si el enfoque correcto para escribir un lenguaje accesible es tirarlo todo a ti y ver qué queda.
Neil
1
@DrunkCoder: es una versión limitada de semi-coroutines. En realidad, el compilador lo trata como un patrón sintáctico que se expande en una serie de llamadas a métodos, clases y objetos. (Básicamente, el compilador genera un objeto de continuación que captura el contexto actual en los campos). La implementación predeterminada para las colecciones es una semi-corutina, pero al sobrecargar los métodos "mágicos" que usa el compilador, puede personalizar el comportamiento. Por ejemplo, antes async/ awaitse agregó al idioma, alguien lo implementó usando yield.
Jörg W Mittag
1
@Neil En general, es posible hacer un mal uso de prácticamente cualquier función del lenguaje de programación. Si lo que dices es cierto, entonces sería mucho más difícil programar mal usando C que Python o C #, pero este no es el caso ya que esos lenguajes tienen muchas herramientas que protegen a los programadores de muchos de los errores que son muy fáciles. hacer con C. En realidad, la causa de los malos programas son los malos programadores: es un problema bastante independiente del lenguaje.
Ben Cottrell
12

¿Tener una instalación de lenguaje generador es yielduna buena idea?

Me gustaría responder esto desde una perspectiva de Python con un rotundo , es una gran idea .

Comenzaré abordando algunas preguntas y suposiciones en su pregunta primero, luego demostraré la omnipresencia de los generadores y su utilidad irrazonable en Python más tarde.

Con una función normal que no sea de generador, puede llamarla y si se le da la misma entrada, devolverá la misma salida. Con el rendimiento, devuelve resultados diferentes, en función de su estado interno.

Esto es falso Los métodos sobre los objetos pueden considerarse funciones en sí mismas, con su propio estado interno. En Python, dado que todo es un objeto, en realidad puede obtener un método de un objeto y pasar ese método (que está vinculado al objeto del que proviene, por lo que recuerda su estado).

Otros ejemplos incluyen funciones deliberadamente aleatorias, así como métodos de entrada como la red, el sistema de archivos y el terminal.

¿Cómo encaja una función como esta en el paradigma del lenguaje?

Si el paradigma del lenguaje admite cosas como funciones de primera clase, y los generadores admiten otras características del lenguaje como el protocolo Iterable, entonces encajan perfectamente.

¿Rompe realmente alguna convención?

No. Dado que está integrado en el lenguaje, las convenciones están construidas alrededor e incluyen (¡o requieren!) El uso de generadores.

¿Los compiladores / intérpretes de lenguaje de programación tienen que romper cualquier convención para implementar tal característica?

Al igual que con cualquier otra función, el compilador simplemente debe diseñarse para admitir la función. En el caso de Python, las funciones ya son objetos con estado (como los argumentos predeterminados y las anotaciones de funciones).

¿Tiene que implementar un lenguaje multihilo para que esta característica funcione, o puede hacerse sin tecnología de subprocesamiento?

Dato curioso: la implementación predeterminada de Python no admite subprocesos en absoluto. Cuenta con un Bloqueo de intérprete global (GIL), por lo que nada se está ejecutando simultáneamente a menos que haya acelerado un segundo proceso para ejecutar una instancia diferente de Python.


nota: los ejemplos están en Python 3

Más allá del rendimiento

Si bien la yieldpalabra clave se puede usar en cualquier función para convertirla en un generador, no es la única forma de crear una. Python presenta Generator Expressions, una forma poderosa de expresar claramente un generador en términos de otro iterable (incluidos otros generadores)

>>> pairs = ((x,y) for x in range(10) for y in range(10) if y >= x)
>>> pairs
<generator object <genexpr> at 0x0311DC90>
>>> sum(x*y for x,y in pairs)
1155

Como puede ver, la sintaxis no solo es limpia y legible, sino que también incluye funciones incorporadas como los sumgeneradores de aceptación.

Con

Consulte la propuesta de mejora de Python para la instrucción With . Es muy diferente de lo que cabría esperar de una declaración With en otros idiomas. Con un poco de ayuda de la biblioteca estándar, los generadores de Python funcionan maravillosamente como administradores de contexto para ellos.

>>> from contextlib import contextmanager
>>> @contextmanager
def debugWith(arg):
        print("preprocessing", arg)
        yield arg
        print("postprocessing", arg)


>>> with debugWith("foobar") as s:
        print(s[::-1])


preprocessing foobar
raboof
postprocessing foobar

Por supuesto, imprimir cosas es lo más aburrido que puedes hacer aquí, pero muestra resultados visibles. Las opciones más interesantes incluyen la administración automática de recursos (abrir y cerrar archivos / flujos / conexiones de red), bloquear la concurrencia, ajustar o reemplazar temporalmente una función y descomprimir y luego volver a comprimir los datos. Si llamar a funciones es como inyectar código en su código, entonces con declaraciones es como envolver partes de su código en otro código. Independientemente de cómo lo use, es un ejemplo sólido de un enlace fácil a una estructura de lenguaje. Los generadores basados ​​en rendimiento no son la única forma de crear gestores de contexto, pero ciertamente son convenientes.

Por y agotamiento parcial

Los bucles en Python funcionan de manera interesante. Tienen el siguiente formato:

for <name> in <iterable>:
    ...

Primero, la expresión que llamé <iterable>se evalúa para obtener un objeto iterable. En segundo lugar, el iterable lo ha __iter__llamado y el iterador resultante se almacena detrás de escena. Posteriormente, __next__se llama en el iterador para obtener un valor que se vincule con el nombre que ingresó <name>. Este paso se repite hasta que la llamada a __next__arroja a StopIteration. La excepción es tragada por el bucle for, y la ejecución continúa desde allí.

Volviendo a los generadores: cuando llamas __iter__a un generador, simplemente vuelve.

>>> x = (a for a in "boring generator")
>>> id(x)
51502272
>>> id(x.__iter__())
51502272

Lo que esto significa es que puedes separar la iteración sobre algo de lo que quieres hacer con él y cambiar ese comportamiento a mitad de camino. A continuación, observe cómo se usa el mismo generador en dos bucles, y en el segundo comienza a ejecutarse desde donde se quedó desde el primero.

>>> generator = (x for x in 'more boring stuff')
>>> for letter in generator:
        print(ord(letter))
        if letter > 'p':
                break


109
111
114
>>> for letter in generator:
        print(letter)


e

b
o
r
i
n
g

s
t
u
f
f

Evaluación perezosa

Una de las desventajas de los generadores en comparación con las listas es que lo único que puede acceder en un generador es lo siguiente que sale de él. No puede retroceder y en cuanto a un resultado anterior, o avanzar a uno posterior sin pasar por los resultados intermedios. El lado positivo de esto es que un generador puede ocupar casi ninguna memoria en comparación con su lista equivalente.

>>> import sys
>>> sys.getsizeof([x for x in range(10000)])
43816
>>> sys.getsizeof(range(10000000000))
24
>>> sys.getsizeof([x for x in range(10000000000)])
Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    sys.getsizeof([x for x in range(10000000000)])
  File "<pyshell#10>", line 1, in <listcomp>
    sys.getsizeof([x for x in range(10000000000)])
MemoryError

Los generadores también se pueden encadenar perezosamente.

logfile = open("logs.txt")
lastcolumn = (line.split()[-1] for line in logfile)
numericcolumn = (float(x) for x in lastcolumn)
print(sum(numericcolumn))

La primera, segunda y tercera líneas solo definen un generador cada una, pero no hacen ningún trabajo real. Cuando se llama a la última línea, sum solicita un valor a la columna numérica, la columna numérica necesita un valor de la última columna, la última columna solicita un valor del archivo de registro, que en realidad lee una línea del archivo. Esta pila se desenrolla hasta que sum obtiene su primer entero. Luego, el proceso ocurre nuevamente para la segunda línea. En este punto, la suma tiene dos enteros y los suma. Tenga en cuenta que la tercera línea aún no se ha leído del archivo. Suma continúa solicitando valores de la columna numérica (totalmente ajena al resto de la cadena) y agregándolos, hasta que se agota la columna numérica.

La parte realmente interesante aquí es que las líneas se leen, se consumen y se descartan individualmente. En ningún momento está todo el archivo en la memoria de una vez. ¿Qué sucede si este archivo de registro es, digamos, un terabyte? Simplemente funciona, porque solo lee una línea a la vez.

Conclusión

Esta no es una revisión completa de todos los usos de los generadores en Python. Notablemente, salté infinitos generadores, máquinas de estado, pasando valores nuevamente y su relación con las rutinas.

Creo que es suficiente demostrar que puedes tener generadores como una función de lenguaje útil y perfectamente integrada.

Joel Harmon
fuente
6

Si está acostumbrado a los lenguajes clásicos de OOP, los generadores yieldpueden parecer discordantes porque el estado mutable se captura en el nivel de función en lugar del nivel de objeto.

Sin embargo, la cuestión de la "certeza" es un arenque rojo. Generalmente se llama transparencia referencial , y básicamente significa que la función siempre devuelve el mismo resultado para los mismos argumentos. Tan pronto como tenga un estado mutable, pierde la transparencia referencial. En OOP, los objetos a menudo tienen un estado mutable, lo que significa que el resultado de la llamada al método no solo depende de los argumentos, sino también del estado interno del objeto.

La pregunta es dónde capturar el estado mutable. En una OOP clásica, el estado mutable existe a nivel de objeto. Pero si se cierra un soporte de idioma, es posible que tenga un estado mutable en el nivel de función. Por ejemplo en JavaScript:

function getCounter() {
   var cnt = 1;
   return function(){ return cnt++; }
}
var counter = getCounter();
counter() --> 1
counter() --> 2

En resumen, yieldes natural en un lenguaje que admite cierres, pero estaría fuera de lugar en un lenguaje como la versión anterior de Java donde el estado mutable solo existe en el nivel de objeto.

JacquesB
fuente
Supongo que si las características del lenguaje tuvieran un espectro, el rendimiento estaría tan lejos de lo funcional como podría ser. Eso no es necesariamente algo malo. OOP estuvo una vez muy de moda, y nuevamente más tarde con programación funcional. Supongo que el peligro de esto es realmente mezclar y combinar características como el rendimiento con un diseño funcional que hace que su programa se comporte de maneras inesperadas.
Neil
0

En mi opinión, no es una buena característica. Es una mala característica, principalmente porque necesita ser enseñada con mucho cuidado, y todos lo enseñan mal. La gente usa la palabra "generador", confundiendo entre la función del generador y el objeto generador. La pregunta es: ¿quién o qué está haciendo el rendimiento real?

Esta no es simplemente mi opinión. Incluso Guido, en el boletín PEP en el que dictamina sobre esto, admite que la función del generador no es un generador sino una "fábrica de generadores".

Eso es algo importante, ¿no te parece? Pero si lee el 99% de la documentación, tendrá la impresión de que la función del generador es el generador real, y tienden a ignorar el hecho de que también necesita un objeto generador.

Guido consideró reemplazar "def" por "gen" para estas funciones y dijo que no. Pero diría que de todos modos no habría sido suficiente. Realmente debería ser:

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
usuario320927
fuente