PHP, C #, Python y probablemente algunos otros lenguajes tienen una yield
palabra 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, yield
rompa 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, goto
alguna 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?
yield
Es 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.yield
palabra clave como Python. Tiene un método estáticostd::this_thread::yield()
, pero esa no es una palabra clave. Por lo tantothis_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.Respuestas:
Advertencias primero: C # es el lenguaje que mejor conozco, y aunque tiene un
yield
aspecto que parece ser muy similar al de otros lenguajesyield
, puede haber diferencias sutiles que desconozco.Majaderías. ¿ Realmente espera
Random.Next
oConsole.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.Sí,
yield
juega muy maltry/catch/finally
y 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).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
yield
permite 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
yield
fuera 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).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
yield
se extrae en su propio bloque (y si tiene algúnyield
s, 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,
yield
es una característica de impacto relativamente bajo que realmente ayuda con un subconjunto específico de problemas.fuente
yield
palabra 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.async
/await
se agregó al idioma, alguien lo implementó usandoyield
.Me gustaría responder esto desde una perspectiva de Python con un sí 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.
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.
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.
No. Dado que está integrado en el lenguaje, las convenciones están construidas alrededor e incluyen (¡o requieren!) El uso de generadores.
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).
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
yield
palabra 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)Como puede ver, la sintaxis no solo es limpia y legible, sino que también incluye funciones incorporadas como los
sum
generadores 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.
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:
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 aStopIteration
. 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.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.
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.
Los generadores también se pueden encadenar perezosamente.
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.
fuente
Si está acostumbrado a los lenguajes clásicos de OOP, los generadores
yield
pueden 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:
En resumen,
yield
es 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.fuente
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:
fuente