¿Tener una instalación de lenguaje generador es yield
una buena idea?
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.
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 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)
>>> 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 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.
>>> 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.
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.